macOS SwiftUI: Instantiate new non-document window in document app

Viewed 620

Is there a pure-SwiftUI way to open a new window? I have a sorta-working document-based app whose app looks like this:

@main struct MyApp: App
{
    var
    body: some Scene {
        DocumentGroup(newDocument: ProjectDocument.init) { inGroup in
            ProjectWindowContentView(document: inGroup.document)
                .frame(minWidth: 301.0, minHeight: 100.0)
                .onReceive(self.addTerrainGeneratorLayerCommand) { _ in
                    inGroup.document.addTerrainGeneratorLayer()
                }
        }
        .commands {
            ...
        }
    }
    ...
}

Now I want to add a menu command to instantiate a little self-contained utility tool in its own window. So far any discussion I see online actually involves making a new NSWindow with an NSHostingView as its content view. This seems not very SwiftUI-ish, and given their recent additions of DocumentGroup and WindowGroup seems like a pretty big oversight.

I tried putting a WindowGroup in the app’s scene, but it only shows my window if I comment out the DocumentGroup.

2 Answers

We need a WindowGroup for a window, so the first/simple step is just to add it, like

(Demo created with Xcode 12.5 / macOS 11.3, based on Document App template)

struct PlayDocumentXApp: App {
    var body: some Scene {
        DocumentGroup(newDocument: PlayDocumentXDocument()) { file in
            ContentView(document: file.$document)
        }
        WindowGroup("Tools") {
            Text("This is tool window")
                .frame(width: 200, height: 400)
        }
    }
}

and we have it available via File > New menu

demo

If we want to create/show it programmatically from other place, say via commands, we can use URL-based approach, like

demo2

struct PlayDocumentXApp: App {
    var body: some Scene {
        DocumentGroup(newDocument: PlayDocumentXDocument()) { file in
            ContentView(document: file.$document)
        }
        .commands {
            CommandMenu("Test") {
                Button("Show Tools") {
                    NSWorkspace.shared.open(URL(string: "myTools://my")!)
                }
            }
        }
        WindowGroup("Tools") {
            Text("This is tool window")
                .frame(width: 200, height: 400)
                .handlesExternalEvents(preferring: Set(arrayLiteral: "myTools://my"), allowing: Set(arrayLiteral: "myTools://my"))
        }
    }
}

demo3

From the previous excellent answer, I came to this snippet.

  • It shows how to add the command in the Window menu
  • The syntax is a bit shorter ( handleExternalEvents(matching:) )
WindowGroup("Tools", id: "tools") {
    Text("This is tool window")
}
.commands {
    CommandGroup(after: CommandGroupPlacement.windowList) {
        Button(action: {
            NSWorkspace.shared.open(URL(string: "myTools://my")!)
        }, label: {
            Text("Tool Window")
        })
    }
}
.handlesExternalEvents(matching: ["myTools://my"])

Related