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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 04:27:02