Having problems with my ViewModel being initialized twice when using the new Observation framework for my @Observable class ViewModel, when setting an enum variable. Tried changing it back to the older class ViewModel: ObservableObject and everything worked as expected. Does anyone have an explanation?
This example doesn't work. The ViewModel.init() and AuthManager.init() get called twice when the ViewModel.loginStatus enum is set when using the @Observable macro on the ViewModel:
import SwiftUI
struct ContentView: View {
@State private var vm = ViewModel()
var body: some View {
Button {
vm.login()
} label: {
Text("Login")
}
switch vm.loginStatus {
case .unknown, .loggedOut:
Text("Login Screen")
case .loggedIn:
Text("Home Screen")
}
}
}
-----------------------------------------------
import Foundation
import Observation
@Observable class ViewModel {
private let auth = AuthManager()
enum LoginStatus {
case unknown, loggedIn, loggedOut
}
var loginStatus = .unknown
init() {
print(#function)
auth.delegate = self
}
func login() {
auth.login()
}
}
extension ViewModel: AuthManagerDelegate {
func authStateDidChange(isLoggedIn: Bool) {
logginStatus = isLoggedIn ? .loggedIn : .loggedOut
}
}
-----------------------------------------------
protocol AuthManagerDelegate: AnyObject {
func authStateDidChange(isLoggedIn: Bool)
}
class AuthManager {
weak var delegate: AuthManagerDelegate?
private let auth = Dependency()
init() {
print(#function)
}
var user: User? {
didSet {
delegate?.authStateDidChange(isLoggedIn: user != nil)
}
}
func login() {
auth.signIn { [weak self] result in
guard let self else { return }
user = result.user
}
}
}
However, when converting this back to the old way before using the @Observable macro, the ViewModel.init() and AuthManager.init() only get called once as expected:
struct ContentView: View {
@StateObject private var vm = ViewModel()
var body: some View {
...same as above...
}
}
-----------------------------------------------
class ViewModel: ObservableObject {
private let auth = AuthManager()
enum LoginStatus {
case unknown, loggedIn, loggedOut
}
@Published var loginStatus = .unknown
ini() {
print(#function)
auth.delegate = self
}
func login() {
auth.login()
}
}
extension ViewModel: AuthManagerDelegate {
func authStateDidChange(isLoggedIn: Bool) {
logginStatus = isLoggedIn ? .loggedIn : .loggedOut
}
}
-----------------------------------------------
protocol AuthManagerDelegate: AnyObject {
...same as above...
}
class AuthManager {
...same as above...
}
It is an unfortunate accident of history (I believe) that the
Stateinitializer has this signature:while the
StateObjectinitializer has this signature:Notice that
StateObjecttakes an@autoclosure, whileStatedoes not.So each time your program creates an instance of your old-style (
StateObject-based)ContentView, it initializes theStateObject-wrappedvmproperty with a closure that calls theViewModelinitializer. SwiftUI only then calls that closure one time, the first time theContentViewappears in your view hierarchy. On subsequent updates, it reuses the already-createdViewModelinstead of calling the closure to create another.But each time your program creates an instance of your new-style (
Observation-based)ContentView, it initializes theState-wrappedvmproperty with a newly createdViewModel. Then, on updates, SwiftUI discards the newViewModelin favor of the old one.