Search code examples
swiftmacosswiftuiappkit

Opening SwiftUI Settings from AppKit


On macOS 14, the way to open the settings has changed. The code I have been using so far is:

if #available(macOS 13, *) {
    NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
} else {
    NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
}

On macOS 14, this code throws the error message Please use SettingsLink for opening the Settings scene.

Actual Question: How can I open the app settings programmatically from AppKit in macOS 14 (Sonoma)


Solution

  • This is the new method:

    @main
    struct MyApp: App {
    
    var body: some Scene {
        WindowGroup {
            RootScreen()
        }
    
        Settings {
            SettingsScreen()
        }
    
        MenuBarExtra("My App", systemImage: "mic.fill") {
            SettingsLink {
               Text("Settings")
            }
        }
        .keyboardShortcut(".", modifiers: .command)
    }
    

    Alternative if you have NSMenu

    https://github.com/orchetect/SettingsAccess

    import SettingsAccess
    
    final class AppDelegateMac: NSObject, NSApplicationDelegate {
    
        private func setupStatusBar() {
            let menu = NSMenu()
            menu.addItem(NSMenuItem(title: "Settings", action: #selector(openSettings), keyEquivalent: ""))
    
            statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))
            statusBarItem!.menu = menu
            statusBarItem!.button?.image = NSImage(systemSymbolName: "mic.fill", accessibilityDescription: nil)
        }
    
        @objc private func openSettings() {
            if #available(macOS 14, *) {
                appState?.openSettingsSignal.send()
            }
        }
    }
    
    ---
    
    import SettingsAccess
    
    @main
    struct MyApp: App {
    
        var body: some Scene {
            WindowGroup {
                RootScreen()
                    .openSettingsAccess()
            }
        }
    }
    
    ---
    
    struct RootScreen: View {
    
        private let appState: AppState
        @Environment(\.openSettings) var openSettings
    
        var body: some View {
        }
        .onReceive(appState.openSettingsSignal) {
            try? openSettings()
        }
    }
    
    ---
    
    struct AppState {
        let openSettingsSignal = PassthroughSubject<Void, Never>()
    }