I am working on a SwiftUI macOS application. My goal is to observe changes in my model using a ViewModel to update the UI accordingly. However, my MenuView does not observe changes when I add a new data to SwiftData.
Expected Results:
I expect that when I add a new project data, the MenuView updates automatically to reflect the new project in the items array.
Actual Results:
Instead, the MenuView only reflects changes on initialization and does not update when a new project is added to SwiftData.
What I’ve Tried:
I have tried initializing the viewModel in the init method of MenuView. The initialization works perfectly, but it seems that the MenuView does not observe changes after initialization.
I have searched for solutions on Stack Overflow and reviewed the SwiftUI documentation, but none of the solutions I found have worked for my specific case.
Code:
Here is the minimum amount of code necessary to reproduce the problem:
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")
}
}
}
How can I observe changes to the model in the ViewModel to update the items array in MenuView when a new project is added?