Programmatically open a new window in SwiftUI on macOS

In macOS 13 we finally have a way to programmatically present a window in SwiftUI. We can call the new openWindow action from the environment and pass it a scene id or a value.

In this post we are going to see how we could open a note from a list of notes in a new window like, for example, in the Notes app on the Mac.

First, we'll add a context menu to each item in the list, so that the user can right-click and choose to open a note in a new window. Inside the button action we'll call the openWindow action from the environment and pass it the note id.

struct ContentView: View {
    ...
    
    @Environment(\.openWindow) var openWindow
    
    var body: some View {
        NavigationSplitView {
            List(
                dataStore.notes, selection: $selectedNote
            ) { note in
                NavigationLink(note.name, value: note.id)
                    .contextMenu {
                        Button("Open Note in New Window") {
                            openWindow(value: note.id)
                        }
                    }
            }
        } detail: {
            NoteView(noteId: selectedNote)
        }
    }
}

Notice that we are passing the note id, rather than the note value. Our detail view will get a binding to the note from the shared data store based on that id. This way it will allow users to edit the same note, rather than its copy.

Users can now right-click on a note in the list and see that they can open it in a new window for editing. Clicking on the button, however, won't open a window just yet.

Screenshot of a list of notes in the sidebar and a context menu open for one of the notes

To actually present a new window, we'll need to add another WindowGroup scene to the app's body property. The new scene will indicate that it expects to receive a value of type Note.ID. We'll also make sure, that the main scene and the detail scene get passed the shared data store as an environment object.

@main
struct OpenWindowApp: App {
    @StateObject private var dataStore = DataStore()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(dataStore)
        }
        WindowGroup("Note", for: Note.ID.self) { $noteId in
            NoteView(noteId: noteId)
                .environmentObject(dataStore)
        }
    }
}

With this setup our users can now click on the action in the context menu and open a note in a new window.

Screenshot of the main app window and a new window on top of it that has the text of a note

If the user has already opened a new window for the same note, SwiftUI will reuse the existing window and bring it to the front instead.

The value that we pass to openWindow action should conform to Hashable and Codable. Hashable conformance lets SwiftUI match the presented value to the right scene. Codable conformance allows SwiftUI to implement state restoration for the open windows on our behalf. When the user reopens the app, all the windows that were open before closing the app will be restored.

You can get the sample project for this post from our GitHub repository. You can play around with it and test opening multiple windows, opening a new window for the note that already has a separate window, and check state restoration of the windows. I didn't implement data persistence in the sample, so if you test state restoration, windows will be restored but the note text won't be there.

If you have a document-based app, you can also look into the new APIs to programmatically open documents such as newDocument and openDocument actions in the environment.