I would like to refactor the following code using @Observable instead of @ObservableObject.
import Foundation
import FirebaseAuth
enum AuthenticationState {
case unauthenticated
case authenticating
case authenticated
}
enum AuthenticationFlow {
case login
case signUp
}
@MainActor
class AuthenticationViewModel: ObservableObject {
@Published var email = ""
@Published var password = ""
@Published var confirmPassword = ""
@Published var flow: AuthenticationFlow = .login
@Published var isValid = false
@Published var authenticationState: AuthenticationState = .unauthenticated
@Published var errorMessage = ""
@Published var user: User?
@Published var displayName = ""
init() {
registerAuthStateHandler()
$flow
.combineLatest($email, $password, $confirmPassword)
.map { flow, email, password, confirmPassword in
flow == .login
? !(email.isEmpty || password.isEmpty)
: !(email.isEmpty || password.isEmpty || confirmPassword.isEmpty)
}
.assign(to: &$isValid)
}
private var authStateHandler: AuthStateDidChangeListenerHandle?
func registerAuthStateHandler() {
if authStateHandler == nil {
authStateHandler = Auth.auth().addStateDidChangeListener { auth, user in
self.user = user
self.authenticationState = user == nil ? .unauthenticated : .authenticated
self.displayName = user?.displayName ?? user?.email ?? ""
}
}
}
func switchFlow() {
flow = flow == .login ? .signUp : .login
errorMessage = ""
}
private func wait() async {
do {
print("Wait")
try await Task.sleep(nanoseconds: 1_000_000_000)
print("Done")
}
catch {
print(error.localizedDescription)
}
}
func reset() {
flow = .login
email = ""
password = ""
confirmPassword = ""
}
}
I have refactored as follows with the aid of apples documentation about "Migrating from the Observable Object protocol to the Observable macro" .
import Foundation
import FirebaseAuth
enum AuthenticationState {
case unauthenticated
case authenticating
case authenticated
}
enum AuthenticationFlow {
case login
case signUp
}
@MainActor
@Observable class AuthenticationViewModel {
var email = ""
var password = ""
var confirmPassword = ""
var flow: AuthenticationFlow = .login
var isValid = false
var authenticationState: AuthenticationState = .unauthenticated
var errorMessage = ""
var user: User?
var displayName = ""
init() {
registerAuthStateHandler()
$flow
.combineLatest($email, $password, $confirmPassword)
.map { flow, email, password, confirmPassword in
flow == .login
? !(email.isEmpty || password.isEmpty)
: !(email.isEmpty || password.isEmpty || confirmPassword.isEmpty)
}
.assign(to: &$isValid)
}
private var authStateHandler: AuthStateDidChangeListenerHandle?
func registerAuthStateHandler() {
if authStateHandler == nil {
authStateHandler = Auth.auth().addStateDidChangeListener { auth, user in
self.user = user
self.authenticationState = user == nil ? .unauthenticated : .authenticated
self.displayName = user?.displayName ?? user?.email ?? ""
}
}
}
func switchFlow() {
flow = flow == .login ? .signUp : .login
errorMessage = ""
}
private func wait() async {
do {
print("Wait")
try await Task.sleep(nanoseconds: 1_000_000_000)
print("Done")
}
catch {
print(error.localizedDescription)
}
}
func reset() {
flow = .login
email = ""
password = ""
confirmPassword = ""
}
}
The part which is challenging is
$flow
.combineLatest($email, $password, $confirmPassword)
.map { flow, email, password, confirmPassword in
flow == .login
? !(email.isEmpty || password.isEmpty)
: !(email.isEmpty || password.isEmpty || confirmPassword.isEmpty)
}
.assign(to: &$isValid)
What is the proper way to refactor this code?
Xcode complains that it cannot find $flow in scope.
That Combine pipeline that just uses View data can actually be implemented in SwiftUI using value semantics where body is called when the state values change. Then for the async action on the data, normally you would refactor from ObservableObject to .task e.g.
.task(id: loginConfig) { // runs when any value in the config changes.
if loginConfig.isValid { // passwords match
user = await loginController.login(loginConfig)
}
}
Where loginConfig is a struct that contains the username and password fields and the controller is an Environment struct with the async funcs so it can be mocked for previews.
Same pattern as Apple ID login: https://developer.apple.com/documentation/authenticationservices/authorizationcontroller