SwiftUI中通过ViewModel观测SwiftData模型变更并更新UI的方法
问题:SwiftUI macOS应用中ViewModel无法观测SwiftData模型变更
我正在开发一款SwiftUI macOS应用,目标是通过ViewModel观测SwiftData模型的变更,从而相应地更新UI。但我的MenuView在向SwiftData添加新数据后,无法观测到变更。
预期结果
添加新Project数据后,MenuView能自动更新,在items数组中反映新增的项目。
实际结果
MenuView仅在初始化时反映数据变化,向SwiftData添加新Project后不会更新。
已尝试方案
- 在MenuView的init方法中初始化ViewModel,初始化过程正常,但MenuView在初始化后无法观测到变更。
- 搜索Stack Overflow并查阅SwiftUI文档,未找到适配具体场景的方案。
可复现代码
import SwiftUI import SwiftData struct MenuView: View { @Environment(\.modelContext) private var modelContext @Environment(\.openWindow) var openWindow @StateObject var viewModel: MenuViewModel = MenuViewModel() var body: some View { VStack { ForEach(viewModel.items, id: \.id) { item in Menu(item.title) { EnvironmentsMenuView(environments: item.environments) { do { modelContext.delete(item.project) try modelContext.save() } catch { print("Failed to clear all projects data.") } } } } Divider() Button(action: addProject) { Text("Add Project") } Divider() Button("Delete Projects") { do { try modelContext.delete(model: ProjectStack.self) try modelContext.delete(model: Project.self) try modelContext.save() try viewModel.fetchItems() } catch { print("Failed to clear all projects data.") } } Button("Exit") { NSApplication.shared.terminate(nil) } } .onAppear { viewModel.setModelContext(modelContext) do { try viewModel.fetchItems() } catch { print(error) } } } func addProject() { do { let descriptor = FetchDescriptor<DAEnvironment>() let environments = try modelContext.fetch(descriptor) if environments.isEmpty { print("Environments is empty") } else { print("Environments is not empty") } Project.add(with: openWindow) } catch { print(error) } } } class MenuViewModel: ObservableObject { @Published var items: [MenuItem] = [] var modelContext: ModelContext? func setModelContext(_ modelContext: ModelContext) { self.modelContext = modelContext } func fetchItems() throws { guard let modelContext = self.modelContext else { throw MenuViewModelError.modelContextNotFound } do { let descriptor = FetchDescriptor<Project>() let projects = try modelContext.fetch(descriptor) for project in projects { var commandsByEnvironment: [DAEnvironment: Command] = Dictionary() for command in project.commands ?? [] { if let environment = command.environment { if commandsByEnvironment[environment] == nil { commandsByEnvironment[environment] = command } } } let menuEnvironmentItems: [MenuEnvironmentItem] = commandsByEnvironment.map { (environment, command) in return MenuEnvironmentItem( id: environment.slug ?? "", title: environment.name ?? "", command: command, projectId: project.id ) } let menuItem = MenuItem( title: project.title, environments: menuEnvironmentItems, project: project ) DispatchQueue.main.async { self.items.append(menuItem) } } } catch { print("Cannot fetch projects") } } }
解决方案
1. 使用SwiftData的@Query自动观测变更(推荐)
SwiftData的@Query属性包装器会自动监听模型数据的增删改,无需手动处理通知。直接在ViewModel中用它获取Project数据,再转换为MenuItem数组:
修改ViewModel:
import Combine import SwiftData class MenuViewModel: ObservableObject { @Published var items: [MenuItem] = [] // 用@Query自动获取并监听Project数据 @Query private var projects: [Project] private var cancellables = Set<AnyCancellable>() init(modelContext: ModelContext) { // 初始化Query时传入当前ModelContext self._projects = Query(modelContext: modelContext) // 初始转换数据 updateItems() // 监听projects数组变更,自动更新items self.$projects .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.updateItems() } .store(in: &cancellables) } private func updateItems() { var newItems: [MenuItem] = [] for project in projects { var commandsByEnvironment: [DAEnvironment: Command] = [:] for command in project.commands ?? [] { guard let environment = command.environment else { continue } commandsByEnvironment[environment] = command } let menuEnvironmentItems = commandsByEnvironment.map { (environment, command) in MenuEnvironmentItem( id: environment.slug ?? "", title: environment.name ?? "", command: command, projectId: project.id ) } let menuItem = MenuItem( title: project.title, environments: menuEnvironmentItems, project: project ) newItems.append(menuItem) } self.items = newItems } }
修改MenuView的ViewModel初始化逻辑:
struct MenuView: View { @Environment(\.modelContext) private var modelContext @Environment(\.openWindow) var openWindow // 初始化时传入当前ModelContext @StateObject var viewModel: MenuViewModel init() { let container = try! ModelContainer(for: Project.self, DAEnvironment.self, Command.self) _viewModel = StateObject(wrappedValue: MenuViewModel(modelContext: container.mainContext)) } // 移除onAppear中的fetchItems调用,因为@Query会自动处理数据加载 // ... 其余代码保持不变 }
2. 手动监听ModelContext变更通知
如果不想用@Query,可以通过监听ModelContext的didSave通知,在数据变更时重新拉取数据:
修改ViewModel:
import Combine import SwiftData class MenuViewModel: ObservableObject { @Published var items: [MenuItem] = [] var modelContext: ModelContext? { didSet { guard let modelContext = modelContext else { return } // 监听ModelContext的保存通知 NotificationCenter.default.addObserver( self, selector: #selector(refetchItems), name: .modelContextDidSave, object: modelContext ) } } @objc private func refetchItems() { DispatchQueue.main.async { do { try self.fetchItems() } catch { print(error) } } } func fetchItems() throws { guard let modelContext = self.modelContext else { throw MenuViewModelError.modelContextNotFound } let descriptor = FetchDescriptor<Project>() let projects = try modelContext.fetch(descriptor) // 先清空原有数据,避免重复添加 var newItems: [MenuItem] = [] for project in projects { // ... 原有转换逻辑不变 let menuItem = MenuItem(...) newItems.append(menuItem) } self.items = newItems } // 销毁时移除通知监听 deinit { NotificationCenter.default.removeObserver(self) } }
3. 确保添加Project时使用正确的ModelContext
检查Project.add(with:)方法,确保它使用的是当前MenuView中的modelContext,而非新创建的上下文:
extension Project { static func add(with openWindow: OpenWindowAction) { // 从环境中获取当前ModelContext guard let modelContext = openWindow.environment(\.modelContext) else { return } let newProject = Project(title: "New Project") modelContext.insert(newProject) do { try modelContext.save() } catch { print(error) } } }
内容的提问来源于stack exchange,提问作者Lucas Buchalla Sesti
相关产品推荐
相关产品推荐

