Auxiliary window control in Mac SwiftUI & SwiftData app

I've got a Mac Document App using SwiftUI and SwiftData.

All is working well with the models editing, etc.

There's a feature I need to implement, and can't seem to make it work.

From the main window of the app, I need to be able to launch an auxilliary window containing a view-only representation of the model being edited. The required workflow is something like this:

  1. Open a document (SwiftData)
  2. Select a sub-model of the document
  3. Launch the aux window to display the view of the model data (must be in a separate window, because it will be on a different physical display)
  4. Continue making edits to the sub-model, as they are reflected in the other window

So, below is the closest I've been able to come, and it's still not working at all. What happens with this code:

Click on the "Present" button, the encounter-presentation Window opens, but never loads the data model or the view. It's just an empty window.

This is the spot in the main view where the auxiliary window will be launched:

@State
var presenting: Presentation? = nil

var presentingThisEncounter: Bool {
    presenting?.encounter.id == encounter.id
}

@Environment(\.openWindow) var openWindow

...


if presentingThisEncounter {
    Button(action: { presenting = nil }) {
        Label("Stop", systemImage: "stop.fill")
            .padding(.horizontal, 4)
    }
    .preference(key: PresentationPreferenceKey.self, value: presenting)
} else {
    Button(action: {
        presenting = Presentation(encounter: encounter, display: activeDisplay)
        openWindow(id: "encounter-presentation")
    }) {
        Label("Present", systemImage: "play.fill")
            .padding(.horizontal, 4)
    }
    .preference(key: PresentationPreferenceKey.self, value: nil)
}

Presentation is declared as:

class Presentation: Observable, Equatable {

Here's the contents of the App, where the DocumentGroup & model is instantiated, and the aux window is managed:

@State
var presentation: Presentation?

var body: some Scene {
    DocumentGroup(editing: .encounterList, migrationPlan: EncounterListMigrationPlan.self) {
        ContentView()
            .onPreferenceChange(PresentationPreferenceKey.self) { self.presentation = $0 }
    }
    
    
    Window("Presentation", id: "encounter-presentation") {
        VStack {
            if let presentation = presentation {
                PresentingView(presentation: presentation)
            }
        }
    }
}

And the definition of PresentationPreferenceKey:

struct PresentationPreferenceKey: PreferenceKey {
    static var defaultValue: Presentation?

    static func reduce(value: inout Presentation?, nextValue: () -> Presentation?) {
        value = nextValue()
    }
}
Answered by Frameworks Engineer in 805847022

Hello @karim. Consider using a WindowGroup scene instead of a Window scene, specifically the one that lets you define a particular type of data to be displayed.

Check out the example under the Present data in a window section of https://developer.apple.com/documentation/swiftui/windowgroup#Present-data-in-a-window

Once you've defined a WindowGroup for a specific type, you can open it using openWindow from the environment, passing in a value.

The types represented by WindowGroups should be small, as they may be stored so the windows may be restored when your app is quit and relaunched.

An ID type of an Identifiable type is a good choice and typically used instead of the model type, as seen in the example. Then in your WindowGroup's view you'd query for the rest of the model using that identifier.

The WindowGroup gives you a Binding, so you can change the value that the window represents at runtime. (Imagine you have buttons to navigate to displaying another model, but within the same window.)

Accepted Answer

Hello @karim. Consider using a WindowGroup scene instead of a Window scene, specifically the one that lets you define a particular type of data to be displayed.

Check out the example under the Present data in a window section of https://developer.apple.com/documentation/swiftui/windowgroup#Present-data-in-a-window

Once you've defined a WindowGroup for a specific type, you can open it using openWindow from the environment, passing in a value.

The types represented by WindowGroups should be small, as they may be stored so the windows may be restored when your app is quit and relaunched.

An ID type of an Identifiable type is a good choice and typically used instead of the model type, as seen in the example. Then in your WindowGroup's view you'd query for the rest of the model using that identifier.

The WindowGroup gives you a Binding, so you can change the value that the window represents at runtime. (Imagine you have buttons to navigate to displaying another model, but within the same window.)

I've been diverted on another project for a bit, so I'm coming back to this after a bit: This answer put me on the right track, I think, but there's something I'm still missing:

If I have a DocumentGroup that drives the main document interface, and I want to use WindowGroup to display auxiliary information about a particular model, those two root-level objects need to share the same SwiftData ModelContext, no?

But DocumentGroup auto-injects the ModelContext through the (editing: .encounterList, migrationPlan: EncounterListMigrationPlan.self) initializer.

How can I then share that context with the WindowGroup? I can get the binding to the id just fine, but I can't figure out how to load the model from that point.

Auxiliary window control in Mac SwiftUI & SwiftData app
 
 
Q