SwiftUI中带Published结构体数组的ObservableObject为何不通知视图更新?
核心问题
我有一个关联Firebase的ViewModel,里面用@Published修饰了一个结构体数组,但从数据库获取更新后,依赖这个Published变量的视图没有收到更新通知。
代码
ViewModel
@MainActor class RestaurantsViewModel: ObservableObject { @Published var restaurants: [Restaurant] private var db = Firestore.firestore() init() { self.restaurants = [] db.collection("Restaurants").addSnapshotListener(includeMetadataChanges: true) { [self] (querySnapshot, error) in guard let documents = querySnapshot?.documents else { print("No documents") return } let newRestaurants = documents.map { queryDocumentSnapshot -> Restaurant in /* 解析文档数据的逻辑 */ } self.restaurants = newRestaurants } } }
Restaurant结构体
struct Restaurant: Identifiable { let id: String let name: String let location: CLLocation let votes: Int init(id: String, name: String, location: CLLocation, votes: Int) { self.id = id self.name = name self.location = location self.votes = votes } }
预期会更新的视图
struct RestaurantsView: View { // 已确认此处ViewModel实例与接收数据库更新的实例为同一个(通过打印内存地址验证) @EnvironmentObject var restarauntViewModel: RestaurantsViewModel var body: some View { VStack(content: { Text("") VStack(content: { ScrollView { VStack(content: { ForEach(restarauntViewModel.restaurants, id: \.id) { restaurant in RestaurantView(restaurant: restaurant) } }) } }) }) } }
struct RestaurantView: View { @State var restaurant: Restaurant var body: some View { HStack(content: { NameView(name: restaurant.name) VoteView(voteCount: restaurant.votes) }) .onTapGesture { withAnimation { /* 点击逻辑 */ } } } }
疑问
有没有什么我漏掉的明显原因,导致数据库更新时RestaurantsView里的ForEach不更新?
以下是几个可能的原因及对应的修复方式:
1. 主线程更新缺失
Firestore的快照回调默认在后台线程执行,即便ViewModel标注了@MainActor,闭包本身仍会在后台运行。直接在闭包内修改@Published变量,会导致更新通知无法正确触发SwiftUI视图刷新。
修复方法:将更新restaurants的代码切换到主线程:
db.collection("Restaurants").addSnapshotListener(includeMetadataChanges: true) { [self] (querySnapshot, error) in guard let documents = querySnapshot?.documents else { print("No documents") return } let newRestaurants = documents.map { queryDocumentSnapshot -> Restaurant in /* 解析逻辑 */ } // 切换至主线程更新 DispatchQueue.main.async { self.restaurants = newRestaurants } }
或者用Task绑定主线程执行:
Task { @MainActor in self.restaurants = newRestaurants }
2. RestaurantView的@State误用
RestaurantView中用@State修饰restaurant是错误的——@State用于存储视图内部的可变状态,而非接收外部传入的数据源。外部传入的restaurant更新时,@State会保留初始值,无法响应外部变化。
修复方法:将@State改为普通的let(若无需在视图内修改)或@Binding(若需修改restaurant):
struct RestaurantView: View { let restaurant: Restaurant var body: some View { HStack(content: { NameView(name: restaurant.name) VoteView(voteCount: restaurant.votes) }) .onTapGesture { withAnimation { /* 点击逻辑 */ } } } }
3. Restaurant结构体的Equatable缺失(可选验证)
SwiftUI的ForEach依赖元素的可识别性和相等性判断是否需要更新视图。虽然已实现Identifiable,但如果Restaurant属性更新后,SwiftUI无法正确判断数组元素变化,也可能导致不刷新。
修复方法:让Restaurant遵循Equatable协议:
struct Restaurant: Identifiable, Equatable { let id: String let name: String let location: CLLocation let votes: Int init(id: String, name: String, location: CLLocation, votes: Int) { self.id = id self.name = name self.location = location self.votes = votes } // 手动实现Equatable,因CLLocation默认不遵循该协议 static func == (lhs: Restaurant, rhs: Restaurant) -> Bool { lhs.id == rhs.id && lhs.name == rhs.name && lhs.location.coordinate.latitude == rhs.location.coordinate.latitude && lhs.location.coordinate.longitude == rhs.location.coordinate.longitude && lhs.votes == rhs.votes } }
这样ForEach能更精准地检测元素变化,触发对应RestaurantView的更新。
4. 拼写错误检查
注意到RestaurantsView中的@EnvironmentObject变量名为restarauntViewModel(少了一个字母u,正确应为restaurantViewModel),虽已验证内存地址一致,但仍建议统一拼写,避免潜在引用错误。
内容的提问来源于stack exchange,提问作者Brinkster

