Search code examples
swiftuideep-linkingswiftui-windowgroupmenubarextra

Need to stop my WindowGroup spawning new instances on MacOS


I'm creating a MacOS SwiftUI application that uses a WindowGroup and a MenuBarExtra and the MenuBarExtra has a menu option to launch my application. It utilises deep linking to allow a URL to load the WindowGroup and my ContentView appears. However, if I launch the app multiple times from my menu bar item I get multiple WindowGroups appearing.

I'd like to stop launching more than one view and simply launch the view if it's closed or bring the existing view to the front if it's already open.

Does anyone have any tips please?

I'd prefer not to use AppKit, so whatever solution you suggest it would be great if it was created in SwiftUI only.

This is what I have tried so far:

//
//  MyApp.swift
//  Power Schedule
//
//  Created by Paul Randall on 10/07/2023.
//
// Importing necessary frameworks
// MyApp.swift

import SwiftUI

// MyApp.swift

class AppState: ObservableObject {
    @Published var isMainWindowActive: Bool = false
}

@main
struct Power_ScheduleApp: App {
    @State private var menuBarExtraShown = true
    @State var isMainWindowOpen = false

    var body: some Scene {
        
        MenuBarExtra(isInserted: $menuBarExtraShown) {
            AppMenu()
        } label: {
            Label("", image: "psl")

        }
        .menuBarExtraStyle(.menu)

        WindowGroup {
            ContentView()
                .frame(width: 450, height: 948) // Set the desired width and height
                .fixedSize()
                .onAppear {
                    print("Main window appeared")
                    self.isMainWindowOpen = true
                }
                .onDisappear {
                    print("Main window disappeared")
                    self.isMainWindowOpen = false
                }
        }
        .handlesExternalEvents(matching: ["main"])
        .windowResizability(.contentSize)

        .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()
            }
        }
    }
}

struct AppMenu: View {
    @Environment(\.openURL) var openUrl
    @Environment(\.dismiss) var dismiss

    func launchAppMenu() {
        // Launch the window from a URL
        guard let url = URL(string: "launchps://main") else {return}
        openUrl(url)
    }
    func importMenu() {
        dismiss()
    }
    func quitAppMenu() {
        // func 3 code
    }

    var body: some View {
        Button(action: launchAppMenu, label: { Text("Launch My App") }).keyboardShortcut("n")
        Button(action: importMenu, label: { Text("Import Configs") }).keyboardShortcut("i")

        Divider()

        Button(action: quitAppMenu, label: { Text("Quit My App") }).keyboardShortcut("q")
    }
}

Solution

  • For macOS 13+

    Use Window instead of WindowGroup

    Window

    A scene that presents its content in a single, unique window.

    In your code:

    
    import SwiftUI
    
    // MyApp.swift
    
    @main
    struct Power_ScheduleApp: App {
        @State private var menuBarExtraShown = true
    
        var body: some Scene {
            
            MenuBarExtra(isInserted: $menuBarExtraShown) {
                AppMenu()
            } label: {
                Label("", image: "psl")
    
            }
            .menuBarExtraStyle(.menu)
    
           Window("Power Schedule", id: "main") { // <- open only one unique window
                ContentView()
                    .frame(width: 450, height: 948) // Set the desired width and height
                    .fixedSize()
            }
            .handlesExternalEvents(matching: ["main"])
            .windowResizability(.contentSize)
        }
    }