'Creating a macos windowless menu bar application with SwiftUI

I am looking for a solution to create a macos windowless menu bar application with SwiftUI.

I have implement the function ralated to the menu bar, the problem is removing the main window and remove the application from the dock.

I have tried to set Application is agent (UIElement) to YES in Info.plist but it only hide the application from the dock while the window is still there.

And I have tried to modify @main but it doesn't work too.

Is there any way to achieve that? Thank you so much!

My code:

  • App.swift
import SwiftUI

@main
struct DiskHealthApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var delegate
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

class AppDelegate: NSObject, NSApplicationDelegate {
    var statusItem: NSStatusItem?
    var popOver = NSPopover()
    func applicationDidFinishLaunching(_ notification: Notification) {
        let menuView = ContentView()
        
        popOver.behavior = .transient
        popOver.animates = true
        
        popOver.contentViewController = NSViewController()
        popOver.contentViewController?.view = NSHostingView(rootView: menuView)
        
        statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
        
        if let menuButton = statusItem?.button {
            menuButton.image = NSImage(systemSymbolName: "externaldrive", accessibilityDescription: nil)
            menuButton.action = #selector(menuButtonToggle)
        }
    }
    
    @objc func menuButtonToggle() {
        if let menuButton = statusItem?.button {
            self.popOver.show(relativeTo: menuButton.bounds, of: menuButton, preferredEdge: NSRectEdge.minY)
        }
    }
}
  • ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}


Solution 1:[1]

Setting just the Application is agent (UIElement) to YES will not be sufficient. You will also have to change your AppDelegate by adding the following,

  1. A NSPopover
  2. Add a NSStatusItem

into your AppDelegate for things to work

How to make an NSPopover?

  1. Go to your app delegate. ( If you don't have an AppDelegate. Create an AppDelegate class and delegate it into the starting point of your app which will be annotated with @main. Add your AppDelegate as follows )
@main
struct SomeApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
}

  1. After doing so, you can begin making your Menubar app, by changing your Appdelegate to represent the following
class AppDelegate: NSObject, NSApplicationDelegate {

    // popover
    var popover: NSPopover!
    
    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Create the SwiftUI view (i.e. the content).
        let contentView = ContentView()

        // Create the popover and sets ContentView as the rootView
        let popover = NSPopover()
        popover.contentSize = NSSize(width: 400, height: 500)
        popover.behavior = .transient
        popover.contentViewController = NSHostingController(rootView: contentView)
        self.popover = popover
        
        // Create the status bar item
        self.statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))
        
        if let button = self.statusBarItem.button {
            button.image = NSImage(named: "Icon")
            button.action = #selector(togglePopover(_:))
        }
    }
    
    // Toggles popover
    @objc func togglePopover(_ sender: AnyObject?) {
        if let button = self.statusBarItem.button {
            if self.popover.isShown {
                self.popover.performClose(sender)
            } else {
                self.popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
            }
        }
    }
    
}

  1. After doing so you should/can set Application is agent(UIElement) to YES

Final Step

This section will be broken down into 2 sections, namely 4.1 and 4.2

  • 4.1 is for those who used AppDelegate life cycle to initialize their project
  • 4.2 is for those who created a project using SwiftUI life cycle.
4.1 - AppDelegate Life cycle

Go to your Main.storyboard and to delete the Window Controller scene If you have the Main.storyboard. This should get rid of the NSWindow that pops up.

enter image description here (image credit)

4.2 - SwiftUI Life cycle

Here, since you don't have a Storyboard file to delete scenes, at this point your app will launch with NSWindow and NSPopover. To remove the NSWindow that opens, go to your app starting point which is annotated as @main, and make the following changes to the code

@main
struct SomeApp: App {
    // Linking a created AppDelegate
    @NSApplicationDelegateAdaptor(AppDelegate.self) var delegate
    var body: some Scene {
        // IMPORTANT
        Settings {
            AnyView()
        }
    }
}

For more info, refer this article

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1