SwiftUI异步获取数据时ObservedObject失效,StateObject正常?
问题场景
我有两个SwiftUI视图:根应用视图MyApp和使用ObservedObject视图模型的SecondView。从根视图创建带Firebase对象ID的SecondViewModel并传递给SecondView,希望SecondView出现时向Firebase请求数据,期间显示加载界面。
使用ObservedObject时,SecondView在获取数据后从未重渲染;换成StateObject则解决了问题,但不清楚原因。我认为根视图不会重渲染并重建ViewModel,而且登录视图登录后强制RootView重渲染,但SecondView不应被多次渲染。
相关代码
MyApp代码
@main struct MyApp: App { @ObservedObject private var userService = UserService() init() { FirebaseApp.configure() } var body: some Scene { WindowGroup { NavigationView { Group { if (userService.currentUser != nil) { SecondView(secondViewModel: SecondViewModel(somethingId: "someIdGoesHere")) } else { LoginView() } } .environmentObject(userService) } } } }
SecondViewModel代码
class SecondViewModel: ObservableObject { @Published private(set) var thing: Thing? = nil private let thingService = ThingService() private let thingId: String init(thingId: String) { self.thingId = thingId } func load() async { self.thing = await thingService.GetThing(thingId: thingId) // Async call to firebase } }
SecondView代码
struct SecondView: View { // Task runs to completion but 'Loading...' never goes away. @ObservedObject var secondViewModel: SecondViewModel var body: some View { VStack(spacing: 5) { if let thingText = secondViewModel.thing { Text(thingText) } else { Text("Loading...") } } .onAppear { Task { await secondViewModel.load() } } } }
核心原因
问题出在ViewModel的生命周期管理和视图body重计算的时机:
MyApp的body会重复生成新的ViewModel实例
当登录成功后,userService.currentUser更新(触发@Published通知),MyApp的body会重新计算。此时代码里每次都会创建一个全新的SecondViewModel实例传给SecondView。旧的ViewModel虽然完成了数据加载,但已经被丢弃,新的ViewModel还没执行load方法,所以thing始终为nil,显示"Loading..."。@ObservedObject和@StateObject的本质差异
@ObservedObject:视图不持有对象的所有权,完全依赖外部传入的实例。如果外部传入新实例,视图会立即绑定新对象,旧对象的状态变化不会再触发视图更新。@StateObject:视图拥有对象的所有权,SwiftUI会确保在视图的整个生命周期内只创建一次该对象,无论视图body重算多少次,实例始终保持不变。当你把SecondView里的@ObservedObject改成@StateObject时,ViewModel实例稳定,数据加载后@Published的thing变化就能正确触发视图重渲染。
修复方案
方案一:让SecondView自己持有ViewModel(推荐)
把SecondViewModel的初始化移到SecondView内部,用@StateObject持有:
struct SecondView: View { // 视图自己持有ViewModel,生命周期和视图绑定 @StateObject private var secondViewModel = SecondViewModel(thingId: "someIdGoesHere") var body: some View { VStack(spacing: 5) { if let thingText = secondViewModel.thing { Text(thingText) } else { Text("Loading...") } } .onAppear { Task { await secondViewModel.load() } } } }
此时MyApp里只需直接调用SecondView()即可。
方案二:在根视图保持ViewModel实例稳定
如果必须在外部传入ViewModel,在MyApp里用@StateObject持有SecondViewModel,确保每次传递的是同一个实例:
@main struct MyApp: App { @ObservedObject private var userService = UserService() // 用@StateObject确保ViewModel实例稳定 @StateObject private var secondViewModel = SecondViewModel(thingId: "someIdGoesHere") init() { FirebaseApp.configure() } var body: some Scene { WindowGroup { NavigationView { Group { if (userService.currentUser != nil) { // 传递同一个实例 SecondView(secondViewModel: secondViewModel) } else { LoginView() } } .environmentObject(userService) } } } }
内容的提问来源于stack exchange,提问作者Swiss

