Search code examples
swiftmacoscocoamenubarautostart

How enable autostart for a macOS menu bar app?


I am building an macOS app for the menu bar and it should automatically start with system start. I started with implementing the autostart functionality for a standard window based macOS app following this tutorial this tutorial. I have

  • added a new target inside the main project (the helper app)
  • changed skip install to yes for the helper app
  • set the helper app to be a background only app
  • added a new copy file build phase to the main application to copy the helper application into the bundle
  • linked the ServiceManagement.framework
  • Implemented the functionality in the app delegates, that the helper app gets launched with system start. After it has launched, it launches the main app (see the tutorial link for more info or the source code down below)

That worked fine, the app launched automatically :) So I started changing the project, that the main application becomes a menu bar app. However than, the app wouldn't auto launch anymore :/ Does someone have a solution for that?

Heres the code of the app delegate of the main app:

import Cocoa
import ServiceManagement

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)


func applicationDidFinishLaunching(_ aNotification: Notification) {

    statusItem.button?.title = "Test"
    statusItem.button?.target = self
    statusItem.button?.action = #selector(showWindow)


    // auto start
    let launcherAppId = "com.####.####Helper"
    let runningApps = NSWorkspace.shared.runningApplications
    let isRunning = !runningApps.filter { $0.bundleIdentifier == launcherAppId }.isEmpty

    SMLoginItemSetEnabled(launcherAppId as CFString, true)

    if isRunning {
        DistributedNotificationCenter.default().post(name: .killLauncher, object: Bundle.main.bundleIdentifier!)
    }
}

func applicationWillTerminate(_ aNotification: Notification) {
    // Insert code here to tear down your application
}

@objc func showWindow() {
    let storyboard = NSStoryboard(name: "Main", bundle: nil)
    guard let vc = storyboard.instantiateController(withIdentifier: "ViewController") as? ViewController else {
        fatalError("Unable to find main view controller")
    }

    guard let button = statusItem.button else {
        fatalError("Unable to find status item button")
    }

    let popover = NSPopover()
    popover.contentViewController = vc
    popover.behavior = .transient
    popover.show(relativeTo: button.bounds, of: button, preferredEdge: .maxY)

}


}

extension Notification.Name {
   static let killLauncher = Notification.Name("killLauncher")
}

And this is the app delegate of the helper app:

import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {



func applicationDidFinishLaunching(_ aNotification: Notification) {
    let mainAppIdentifier = "com.####.####"
    let runningApps = NSWorkspace.shared.runningApplications
    let isRunning = !runningApps.filter { $0.bundleIdentifier == mainAppIdentifier }.isEmpty

    if !isRunning {
        DistributedNotificationCenter.default().addObserver(self, selector: #selector(self.terminate), name: .killLauncher, object: mainAppIdentifier)

        let path = Bundle.main.bundlePath as NSString
        var components = path.pathComponents
        components.removeLast()
        components.removeLast()
        components.removeLast()
        components.append("MacOS")
        components.append("####") //main app name

        let newPath = NSString.path(withComponents: components)

        NSWorkspace.shared.launchApplication(newPath)
    }
    else {
        self.terminate()
    }
}

func applicationWillTerminate(_ aNotification: Notification) {
    // Insert code here to tear down your application
}

@objc func terminate() {
    NSApp.terminate(nil)
}


}

extension Notification.Name {
  static let killLauncher = Notification.Name("killLauncher")
}

Thank you very much for your help :)


Solution

  • My code looks pretty much the same, except how I compose the path in the helper app:

    var pathComponents = (Bundle.main.bundlePath as NSString).pathComponents
    pathComponents.removeLast()
    pathComponents.removeLast()
    pathComponents.removeLast()
    pathComponents.removeLast()
    let newPath = NSString.path(withComponents: pathComponents)
    NSWorkspace.shared.launchApplication(newPath)
    

    Also, if I remember correctly, I had to make sure the Main.storyboard file still had the "Application Scene" with an Application object and an empty main menu.

    Application Scene