SwiftUI List在Mac端用ObservableObject选值触发控制台警告求助
SwiftUI List绑定ObservableObject选中状态时Mac端警告的解决方案
问题描述
开发带选值功能的SwiftUI List,将selection绑定到标注@ObservableObject的ViewModel对象时,功能正常,但仅在Mac设备运行时控制台会输出警告:
Publishing changes from within view updates is not allowed, this will cause undefined behavior
iOS端运行相同代码无此问题,预览模式下也不会触发警告。因需兼容iOS16,无法迁移至@Observable宏。
复现代码
import SwiftUI import CoreData final class SelectionModel: NSObject, ObservableObject { @Published public var selectedItems = Set<Item.ID>() } struct ContentView: View { @Environment(\.managedObjectContext) private var viewContext @FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)], animation: .default) private var items: FetchedResults<Item> @ObservedObject var viewModel: SelectionModel = SelectionModel() //@State var selectedItems = Set<Item.ID>() // 使用此方式无警告 var body: some View { NavigationStack { List(selection: $viewModel.selectedItems) { ForEach(items) { item in Text(item.timestamp!, formatter: itemFormatter) } } .toolbar { ToolbarItem { Button(action: addItem) { Label("Add Item", systemImage: "plus") } } } } } private func addItem() { withAnimation { let newItem = Item(context: viewContext) newItem.timestamp = Date() do { try viewContext.save() } catch { let nsError = error as NSError fatalError("Unresolved error \(nsError), \(nsError.userInfo)") } } } } private let itemFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .short formatter.timeStyle = .medium return formatter }() #Preview { ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) }
问题是否需要重视?
必须重视。虽然当前功能正常,但警告中的"未定义行为"意味着后续系统版本更新、业务逻辑复杂化时,可能出现UI错乱、崩溃等不可预知的问题。Mac端SwiftUI的List选中逻辑与iOS存在底层差异,这种警告的潜在风险比iOS端更高。
ObservableObject前提下的解决方法
方案1:延迟状态更新到视图周期外
给ViewModel的selectedItems添加自定义setter,通过DispatchQueue.main.async延迟更新,避开视图更新阶段的发布操作:
final class SelectionModel: NSObject, ObservableObject { private var _selectedItems = Set<Item.ID>() @Published public var selectedItems: Set<Item.ID> { get { _selectedItems } set { // 延迟到主队列下一个周期执行,避免在视图更新中发布变更 DispatchQueue.main.async { self._selectedItems = newValue } } } }
方案2:用@State中转并双向同步
在视图内使用@State变量作为List的selection绑定源,再通过onChange实现与ViewModel的双向同步:
struct ContentView: View { @Environment(\.managedObjectContext) private var viewContext @FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)], animation: .default) private var items: FetchedResults<Item> @ObservedObject var viewModel: SelectionModel = SelectionModel() @State private var localSelectedItems = Set<Item.ID>() var body: some View { NavigationStack { List(selection: $localSelectedItems) { ForEach(items) { item in Text(item.timestamp!, formatter: itemFormatter) } } // 本地状态变更同步到ViewModel .onChange(of: localSelectedItems) { newValue in viewModel.selectedItems = newValue } // ViewModel状态变更同步到本地 .onChange(of: viewModel.selectedItems) { newValue in localSelectedItems = newValue } .toolbar { ToolbarItem { Button(action: addItem) { Label("Add Item", systemImage: "plus") } } } } } private func addItem() { withAnimation { let newItem = Item(context: viewContext) newItem.timestamp = Date() do { try viewContext.save() } catch { let nsError = error as NSError fatalError("Unresolved error \(nsError), \(nsError.userInfo)") } } } }
方案3:手动控制ObservableObject的发布时机
不使用@Published,手动调用objectWillChange.send(),并确保发布操作在视图更新周期外执行:
final class SelectionModel: NSObject, ObservableObject { public var selectedItems = Set<Item.ID>() { didSet { DispatchQueue.main.async { self.objectWillChange.send() } } } }
内容的提问来源于stack exchange,提问作者Z S
相关产品推荐
相关产品推荐

