Login / Logout Flow: SwiftUI and EnvironmentObject

Search for a command to run...

No comments yet. Be the first to comment.
The TDD (Test Driven Development) approach is important when it comes to testing small portions of business logic code in isolation. In this blog, I'll cover how I would use TDD to build a clean architecture To-do SwiftUI application I'll cover imp...
CompositionLocal is useful when you want to create a dependency in a higher node of the layout tree and use it on a lower node without having to pass it down the tree through every child Composable. Here we will use it to direct our application when ...

The TDD (Test Driven Development) approach is important when it comes to testing small portions of business logic code in isolation. In this blog, I'll cover how I would use TDD to build a clean architecture To-do SwiftUI application I'll cover imp...

The TDD (Test Driven Development) approach is important when it comes to testing small portions of business logic code in isolation. In this blog, I'll cover how I would use TDD to build a clean architecture To-do Jetpack Compose Android application ...

The Room persistence library provides an abstraction layer over SQLite. To use Room in your app, add the following dependencies to your app’s build.gradle file: dependencies { .... kapt "org.xerial:sqlite-jdbc:3.34.0" // Room def roo...

EnvironmentObject is useful when you want to create a dependency in a higher component of the layout tree and use it on a lower component without having to pass it down the tree through every child component.
We will now use EnvironmentObject to monitor when a user logs in and out.

We need the following components:
The user state view model tracks and broadcasts the user status. We store this view model in an EnvironmentObject.
import Foundation
enum UserStateError: Error{
case signInError, signOutError
}
@MainActor
class UserStateViewModel: ObservableObject {
@Published var isLoggedIn = false
@Published var isBusy = false
func signIn(email: String, password: String) async -> Result<Bool, UserStateError> {
isBusy = true
do{
try await Task.sleep(nanoseconds: 1_000_000_000)
isLoggedIn = true
isBusy = false
return .success(true)
}catch{
isBusy = false
return .failure(.signInError)
}
}
func signOut() async -> Result<Bool, UserStateError> {
isBusy = true
do{
try await Task.sleep(nanoseconds: 1_000_000_000)
isLoggedIn = false
isBusy = false
return .success(true)
}catch{
isBusy = false
return .failure(.signOutError)
}
}
}
We make our view model instance available to all child views, starting from the ApplicationSwitcher view
import SwiftUI
@main
struct LoginFlowApp: App {
@StateObject var userStateViewModel = UserStateViewModel()
var body: some Scene {
WindowGroup {
NavigationView{
ApplicationSwitcher()
}
.navigationViewStyle(.stack)
.environmentObject(userStateViewModel)
}
}
}
struct ApplicationSwitcher: View {
@EnvironmentObject var vm: UserStateViewModel
var body: some View {
if (vm.isLoggedIn) {
HomeScreen()
} else {
LoginScreen()
}
}
}
The Login screen uses the UserStateViewModel to invoke signIn
import SwiftUI
struct LoginScreen: View {
@EnvironmentObject var vm: UserStateViewModel
@State var email = ""
@State var password = ""
fileprivate func EmailInput() -> some View {
TextField("Email", text: $email)
.keyboardType(.emailAddress)
.disableAutocorrection(true)
.autocapitalization(.none)
.textFieldStyle(.roundedBorder)
}
fileprivate func PasswordInput() -> some View {
SecureField("Password", text: $password)
.textFieldStyle(.roundedBorder)
}
fileprivate func LoginButton() -> some View {
Button(action: {
Task {
await vm.signIn(
email: email,
password:password
)
}
}) {
Text("Sign In")
}
}
var body: some View {
VStack{
if(vm.isBusy){
ProgressView()
}else{
Text("Login Screen").font(.title)
EmailInput()
PasswordInput()
LoginButton()
}
}.padding()
}
}
The Home screen uses the UserStateViewModel to invoke signOut
import SwiftUI
struct HomeScreen: View {
@EnvironmentObject var vm: UserStateViewModel
var body: some View {
if(vm.isBusy){
ProgressView()
}else{
Text("Home Screen")
.navigationTitle("Home")
.toolbar {
Button {
Task{
await vm.signOut()
}
} label: {
Image(systemName: "rectangle.portrait.and.arrow.right")
}
}
}
}
}
