You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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
        }
    }
}

原因分析

核心差异在于状态容器的特性与使用方式不匹配:

  1. @StateObject的特性:
    对于ObservableObject类型的ViewModel,必须使用@StateObject持有。@StateObject的核心机制是仅在视图首次初始化时执行一次实例创建,后续视图因状态变化重新渲染时,会复用已有的实例,不会触发新的初始化,因此只会打印一次初始化日志。

  2. @State的误用:
    你在使用@Observable时错误地用@State持有ViewModel。@State原本是为值类型设计的状态容器,当用它持有引用类型时:

    • 每次视图结构体因状态变化(如loginStatus更新)重新创建时,@State属性的初始化表达式(ViewModel())都会被执行一次,导致ViewModel.init()和依赖的AuthManager.init()被调用两次;
    • 虽然SwiftUI会保留原实例不会替换,但初始化表达式本身的执行会触发两次日志打印。

解决方法

对于@Observable修饰的ViewModel,有两种正确的持有方式:

  1. 使用@StateObject(兼容iOS 14+,适配引用类型的生命周期):
struct ContentView: View {
    @StateObject private var vm = ViewModel()

    // ... body 代码不变 ...
}
  1. 使用@State并在视图init中初始化:
    通过显式初始化@State的底层容器,避免视图重新创建时重复执行初始化表达式:
struct ContentView: View {
    @State private var vm: ViewModel

    init() {
        _vm = State(initialValue: ViewModel())
    }

    // ... body 代码不变 ...
}

内容的提问来源于stack exchange,提问作者Ryan Kanno

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 08:35:09