Reputation: 115
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...
}
Upvotes: 1
Views: 246
Reputation: 386018
It is an unfortunate accident of history (I believe) that the State
initializer has this signature:
init(wrappedValue value: Value)
while the StateObject
initializer has this signature:
init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType)
Notice that StateObject
takes an @autoclosure
, while State
does not.
So each time your program creates an instance of your old-style (StateObject
-based) ContentView
, it initializes the StateObject
-wrapped vm
property with a closure that calls the ViewModel
initializer. SwiftUI only then calls that closure one time, the first time the ContentView
appears in your view hierarchy. On subsequent updates, it reuses the already-created ViewModel
instead of calling the closure to create another.
But each time your program creates an instance of your new-style (Observation
-based) ContentView
, it initializes the State
-wrapped vm
property with a newly created ViewModel
. Then, on updates, SwiftUI discards the new ViewModel
in favor of the old one.
Upvotes: 3