How to limit the number of windows in a WindowGroup?
Asked Answered
Y

5

10

I am building a single window application and want to use the new SwiftUI App Lifecycle.

import SwiftUI

@main
struct SingleWindowApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

The default implementation of WindowGroup allows multiple instances of the window (i.e. if you hit ⌘N). I wasn’t able to find a modifier that changes that behaviour.

How would I limit the number of windows within a WindowGroup to just 1?

Yell answered 23/11, 2020 at 11:34 Comment(0)
E
9

This should do it:

import SwiftUI

@main
struct SingleWindowApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .commands {
            CommandGroup(replacing: .newItem, addition: { })
        }
    }
}
Eugenaeugene answered 23/11, 2020 at 11:52 Comment(3)
Side note: an end user can still add new root view instances using Show Tab Bar command from View menu, and clicking the "+" button in the right side of the tab bar that appears. See this to find a way to disable tabs as well: #65460957Dabble
This disables the New command, but if the Single window is closed, the user then can't open a new one, and if the app wasn't configured to quit when the single window closed, it's just hanging out with no UI visible. Not sure the best way to do it in SwiftUI lifecycle, but "typical" UE would be that new was disabled if a window was open, or if none existed, would open one.During
This approach got my app rejected in the App Store. "Specifically, we found that when the user closes the main application window there is no menu item to re-open it."Mercedes
F
7

In Xcode 14 it's as simple as creating a Scene with only a Window with id main.

This opens the window and does not include a File -> New menu item.

var body: some Scene {
  Window("My Single Window", id: "main") {
    ContentView()
  }
}

Relevant documentation here: https://developer.apple.com/documentation/swiftui/window#Use-a-window-as-the-main-scene

Flowerer answered 21/7, 2023 at 20:57 Comment(0)
S
3

As i was facing the same problem, but on iPad where the command modifier has no effect, I found this: There's an "Application Scene Manifest" UIApplicationSceneManifest property in your Info.plist that is a dictionary and as a child "Enable Multiple Windows" UIApplicationSupportsMultipleScenes which is set to YES by default. Settings this option to NO gives the desired effect :)

Info.plist for disabling multiple window support

Sweepstake answered 22/5, 2022 at 19:4 Comment(0)
Q
2

Each time you open a WindowGroup object, you can use NSViewControllerRepresentable to get the NSWindow instance of the view.

Then consider defining an external object to store the collection of NSWindows. The next time you open the WindowGroup object, if the NSWindow collection is full, find a corresponding NSWindow to display. for example

if windowList.isfull {
let window = windowList.getWindow()
window.makeKey()
window.orderFront(nil)
} else {
  NSWorkspace.shared.open(url)
}

On how to obtain an instance of NSWindow from WindowGroup, you can consider an open source implementation: https://github.com/happycodelucky/SwiftUIWindowBinder

Quincentenary answered 29/9, 2022 at 2:20 Comment(0)
T
1

Here's how you can use normal SwiftUI code to disable "File -> New Window" when the main window is open, and enable "File -> New Window" when the main window is closed.

This code probably has some edge cases that could be polished, but it does work.

import SwiftUI

@main
struct MyApp: App {
    @State var isMainWindowOpen = false
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onAppear {
                    print("Main window appeared")
                    self.isMainWindowOpen = true
                }
                .onDisappear {
                    print("Main window disappeared")
                    self.isMainWindowOpen = false
                }

        }.commands {
            if isMainWindowOpen {
                CommandGroup(replacing: .newItem) {
                    Button("New Window", action: {})
                        .disabled(true)
                        // This is the same keyboard shortcut as the default New Window option.
                        // We're just doing this so that our disabled dummy option shows
                        // the same shortcut visually.
                        .keyboardShortcut(KeyboardShortcut("n", modifiers: [.command]))
                }
            } else {
                // By doing nothing here, we let the default 
                // "File -> New Window" item display and handle input.
                EmptyCommands()
            }
        }
    }
}
Traylor answered 14/4, 2023 at 17:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.