skip to Main Content

I am working on a new SwiftUI-only menu bar application and stumbled into the following problem; Whenever I define either a Window or WindowGroup in SwiftUI, at least one window is always opened on app launch. Conditional rendering (like if x { Window() }) isn’t supported either. The app should have an onboarding Window that is only shown depending on a user defaults setting. And there should be another window that can be manually opened via the menu bar item.

This is my SwiftUI App’s class:

import SwiftUI

@main
struct ExampleApp: App {
    @Environment(.openWindow) var openWindow
    @AppStorage("showIntroduction") private var showIntroduction = true

    init() {
        if showIntroduction {
            print("Show introduction")
            openWindow(id: "introduction")
        }
    }

    var body: some Scene {
        // How to hide this window by default?
        Window("Intro", id: "introduction") {
            WelcomeView()
        }
            .windowStyle(.hiddenTitleBar)
        Settings {
            SettingsView()
        }
        MenuBarExtra {
            MenuBarView()
        } label: {
            Text("Test")
        }
    }
}

Views have the .hidden() modifier – but this doesn’t support Windows or WindowGroups. When my view is hidden but wrapped in a Window or WindowGroup an empty window is rendered instead.

Is there any way to achieve this with plain SwiftUI? Or is it necessary to programmatically create and open an NSWindow if it shouldn’t be open by default?

2

Answers


  1. Option 1: Declare your app as menu bar utility

    put this in your info.plist or info tab in project settings:

    Application is agent (UIElement) YES
    

    Option 2: Switch activation policy programmatically

    Normal app, opens window at launch, has menu, dock item:

    NSApp.setActivationPolicy(.regular)
    

    Agent, no window at launch, can be a menu bar app if a status item is set:

    NSApp.setActivationPolicy(.accessory)
    

    You could set up a @NSApplicationDelegateAdaptor and switch it in applicationWillFinishLaunching(_:).

    Like this:

    App.swift

    import SwiftUI
    
    @main
    struct ExampleApp: App {
        
        @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
       
        var body: some Scene {
            WindowGroup {
                ContentView()
            }
            Settings {
                SettingsView()
            }
        }
    }
    

    AppDelegate.swift

    import AppKit
    
    class AppDelegate: NSObject, NSApplicationDelegate {
        func applicationWillFinishLaunching(_ notification: Notification) {
            let isAgent = UserDefaults.standard.bool(forKey: "activationIsAgent")
            print("isAgent: (isAgent)")
            if isAgent {
                NSApp.setActivationPolicy(.accessory)
            } else {
                NSApp.setActivationPolicy(.regular)
            }
        }
    }
    

    Then you’d still need a status bar item and put e.g. a toggling menu item for that UserDefaults key.

    @AppStorage("activationIsAgent") private var activationIsAgent: Bool = false
    
    Login or Signup to reply.
  2. I did not find a way to do this in pure SwiftUI.

    However, here is a light weight approach with NSWindow:

    import SwiftUI
    
    var welcomeWindow: NSWindow?
    
    @main
    struct ExampleApp: App {
        @AppStorage("showIntroduction") private var showIntroduction = true
        
        init() {
            if showIntroduction {
                print("Show introduction")
                
                welcomeWindow = NSWindow()
                welcomeWindow?.contentView = NSHostingView(rootView: WelcomeView())
                welcomeWindow?.identifier = NSUserInterfaceItemIdentifier(rawValue: "introduction")
                welcomeWindow?.styleMask = [.closable, .titled]
                welcomeWindow?.isReleasedWhenClosed = true
                welcomeWindow?.center()
                welcomeWindow?.becomeFirstResponder()
                welcomeWindow?.orderFrontRegardless()
            }
        }
        
        var body: some Scene {        
            Settings {
                SettingsView()
                    .navigationTitle("Settings")
            }
            MenuBarExtra {
                MenuBarView()
            } label: {
                Text("Test")
            }
        }
    }
    

    As you can see, there’s no dedicated Window Scene.

    If you don’t like the titled, closable design, you can close the window with this function in your WelcomeView, e.g. on a Button:

    Button("Done") {
        NSApp.windows.filter { $0.identifier?.rawValue == "introduction" }.first?.close()
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search