SwiftUI中@Observable的ViewModel设置枚举时初始化两次问题求助
SwiftUI中@Observable初始化两次问题解析
问题现象
在SwiftUI开发中,使用新Observation框架的@Observable修饰ViewModel类时,设置枚举变量loginStatus会导致ViewModel.init()与AuthManager.init()被调用两次;但改用旧的ObservableObject搭配@Published实现时,初始化仅执行一次,符合预期。
问题复现代码
ContentView部分
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") } } }
ViewModel部分
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) { loginStatus = isLoggedIn ? .loggedIn : .loggedOut } }
AuthManager相关部分
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 } } }
正常运行代码
ContentView部分
struct ContentView: View { @StateObject 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") } } }
ViewModel部分
class ViewModel: ObservableObject { private let auth = AuthManager() enum LoginStatus { case unknown, loggedIn, loggedOut } @Published var loginStatus = .unknown init() { print(#function) auth.delegate = self } func login() { auth.login() } } extension ViewModel: AuthManagerDelegate { func authStateDidChange(isLoggedIn: Bool) { loginStatus = isLoggedIn ? .loggedIn : .loggedOut } }
AuthManager相关部分
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 } } }
原因分析
核心差异在于状态容器的特性与使用方式不匹配:
@StateObject的特性:
对于ObservableObject类型的ViewModel,必须使用@StateObject持有。@StateObject的核心机制是仅在视图首次初始化时执行一次实例创建,后续视图因状态变化重新渲染时,会复用已有的实例,不会触发新的初始化,因此只会打印一次初始化日志。@State的误用:
你在使用@Observable时错误地用@State持有ViewModel。@State原本是为值类型设计的状态容器,当用它持有引用类型时:- 每次视图结构体因状态变化(如
loginStatus更新)重新创建时,@State属性的初始化表达式(ViewModel())都会被执行一次,导致ViewModel.init()和依赖的AuthManager.init()被调用两次; - 虽然SwiftUI会保留原实例不会替换,但初始化表达式本身的执行会触发两次日志打印。
- 每次视图结构体因状态变化(如
解决方法
对于@Observable修饰的ViewModel,有两种正确的持有方式:
- 使用
@StateObject(兼容iOS 14+,适配引用类型的生命周期):
struct ContentView: View { @StateObject private var vm = ViewModel() // ... body 代码不变 ... }
- 使用
@State并在视图init中初始化:
通过显式初始化@State的底层容器,避免视图重新创建时重复执行初始化表达式:
struct ContentView: View { @State private var vm: ViewModel init() { _vm = State(initialValue: ViewModel()) } // ... body 代码不变 ... }
内容的提问来源于stack exchange,提问作者Ryan Kanno
相关产品推荐
相关产品推荐

