Sharing ModelContext between SwiftUI WindowGroup and NSObject

20 hours ago 4
ARTICLE AD BOX

I'm working on a SwiftUI MacOS application that currently has two windows, the primary window, and a command palette window. I'm still pretty new to SwiftUI having only picked it up a couple of months ago, but since I couldn't get the SwiftUI Window struct to work with the .plain style while still allowing me to focus the input on open I created the command palette window using AppKit as shown here:

import AppKit import Combine import FlusterData import SwiftData import SwiftUI @MainActor class CommandPaletteController: NSObject, ObservableObject { private var panel: CommandPalettePanel? private var appData: AppDataContainer { AppDataContainer.shared } func toggle( appState: AppState, onCommandSelected: @escaping (CommandPaletteItem) -> CommandPaletteSelectResponse ) { if let panel = panel, panel.isVisible { hide() } else { show(appState, onCommandSelected) } } func show( _ appState: AppState, _ onCommandSelected: @escaping (CommandPaletteItem) -> CommandPaletteSelectResponse ) { let rootView = CommandPaletteContainerView( close: { [weak self] in self?.hide() }, onCommandSelected: onCommandSelected, ) .ignoresSafeArea() .environmentObject(appState) .modelContainer(appData.sharedModelContainer) if let _panel = panel { _panel.contentView = NSHostingView(rootView: rootView) } else { panel = CommandPalettePanel(rootView: rootView) panel?.center() // Close on click-away NotificationCenter.default.addObserver( forName: NSWindow.didResignKeyNotification, object: panel, queue: .main ) { [weak panel] _ in panel?.orderOut(nil) } } panel?.makeKeyAndOrderFront(nil) // Bring the app to the foreground so the text field gets focus NSApp.activate(ignoringOtherApps: true) } func hide() { panel?.orderOut(nil) } }

My problem is that for the life of me, I cannot get the ModelContext to work as expected, leading to endless crashes; too many to even pinpoint here. I was able to apply band-aids to many of them by requiring that the modelContext fetch a new model instead of passing it around, which obviously is not great for performance, but now just updating the value in certain contexts causes a complete crash without any error message.

The latest example of that is this:

public func applyMdxParsingResults( results: MdxSerialization_MdxParsingResultBuffer, modelContext: ModelContext ) { self.markdown.preParsedBody = results.parsedContent if let frontMatter = results.frontMatter { self.frontMatter.applyRustFrontMatterResult(res: frontMatter) } var tags: [TagModel] = [] for idx in (0..<results.tagsCount) { if let tag = results.tags(at: idx) { if let existingResult = self.tags.first(where: { $0.value == tag.body }) { tags.append(existingResult) } else { tags.append(TagModel(value: tag.body)) } } } self.tags = tags // -- Citations -- var citations: [BibEntryModel] = [] let citationFetchDescriptor = FetchDescriptor<BibEntryModel>() let allCitations = try! modelContext.fetch(citationFetchDescriptor) <- Crashes here

This is what my main struct looks like. It wasn't until I modified the command palette from a view to an independent window that I ran into this issue, so I'm guessing the issue lies between this file and the CommandPaletteController file, but I'm lost as to how to resolve it.

import SwiftData import SwiftUI import UniformTypeIdentifiers @main struct Fluster_DesktopApp: App { @StateObject private var appState: AppState = AppState.shared @AppStorage(DesktopAppStorageKeys.colorScheme.rawValue) private var selectedTheme: AppTheme = .dark @AppStorage(DesktopAppStorageKeys.defaultNoteView.rawValue) private var defaultNoteView: DefaultNoteView = .markdown private var appData: AppDataContainer { AppDataContainer.shared } @StateObject private var paletteController = CommandPaletteController() var body: some Scene { WindowGroup("Fluster", id: DesktopWindowId.mainDesktopWindowGroup.rawValue) { ContentView() .toolbarBackground(.hidden, for: .automatic) .preferredColorScheme(selectedTheme.colorScheme) } .modelContainer(appData.sharedModelContainer) .environmentObject(appState) .environment(\.createDataHandler, appData.dataHandlerCreator()) .windowStyle(.automatic) .windowToolbarStyle(.unified) .commands { SidebarCommands() TextEditingCommands() ToolbarCommands() // Or define your own custom one: CommandMenu("Layout") { Button("Toggle Sidebar") { NSApp.sendAction(#selector(NSSplitViewController.toggleSidebar(_:)), to: nil, from: nil) } .keyboardShortcut("l", modifiers: [.command, .shift]) } CommandMenu("Tools") { Button("Command Palette") { paletteController.toggle( appState: appState, onCommandSelected: handleCommandPaletteSelect ) } .keyboardShortcut("p", modifiers: [.command, .shift]) } } } func toggleDarkMode() { let currentDarkMode = UserDefaults.standard.string(forKey: DesktopAppStorageKeys.colorScheme.rawValue) == AppTheme.dark.rawValue if currentDarkMode { UserDefaults.standard.set( AppTheme.light.rawValue, forKey: DesktopAppStorageKeys.colorScheme.rawValue) } else { UserDefaults.standard.set( AppTheme.dark.rawValue, forKey: DesktopAppStorageKeys.colorScheme.rawValue) } } func handleCommandPaletteSelect(_ command: CommandPaletteItem) -> CommandPaletteSelectResponse { if command.onAccept != nil { command.onAccept!() } if command.itemType == .children { return .appendToTree(command) } else { switch command.id { case .pushCommandPaletteView(let data): appState.commandPaletteNavigate(to: data) case .viewNoteById(let noteId): appState.setEditingNoteId(editingNoteId: noteId) appState.mainView = defaultNoteView.toMainKey() case .navigate(let mainKey): appState.mainView = mainKey case .toggleDarkMode: toggleDarkMode() case .createNewNote: MainNavigationEventEmitter.shared.emitChange(to: MainViewKey.noteEditingPage) case .showPanelRight: print("Show Panel Right") case .parentWithNoFunctionality: return .clearAndClose case .root: return .clearAndClose } return .clearAndClose } } }

Thank you in advance, any help would be greatly appreciated.

Read Entire Article