Post

Detect app launch at login on macOS

WARNING: This post is no longer relevant for macOS 13 “Ventura” and later versions, since Apple has made changes to how login items are registered. Be careful.

The Problem

While working on KiWings, I wanted to find a way to know if my sandboxed app was launched by the OS at login or was launched manually by the user. There are 2 possible scenarios:

  1. If an app is launched at login, it should start minimized to the tray area, so as to let the user focus on other things. This is the default behavior of my app so far.
  2. If the app is launched manually, its better to grab the user’s attention by showing the launch popup window from the tray/notification area that basically conveys, “Hey user, I’m over here”.

The solution

I used the LaunchAtLogin package to get things going on launching the app automatically at login and it works nicely with my sandboxed app. However, it doesn’t support scenario-2 yet, and there is an enhancement requested for this scenario as well, but, no neat plug-and-play solution seems to be in sight so far.

Browsing the comments, I came across Tim Schroeder’s solution for the above problem, which basically boils down to the following:

Something

Firstly, we need to keep the helper app running and listen for TerminateHelper notification. Once helper receives this notification, it will exit. Note that the helper app is launched only when our application is configured to launch at login:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
final class AppDelegate: NSObject, NSApplicationDelegate {
	let nc = DistributedNotificationCenter.default()
	
	@objc func terminateApp() {
		NSLog("LaunchAtLogin helper terminateApp called")
		nc.removeObserver(self)
		NSApp.terminate(nil)
	}
	
	func applicationDidFinishLaunching(_ notification: Notification) {
		let bundleIdentifier = Bundle.main.bundleIdentifier!
		let mainBundleIdentifier = bundleIdentifier.replacingOccurrences(of: #"-LaunchAtLoginHelper$"#, with: "", options: .regularExpression)

		// Ensures the app is not already running.
		guard NSRunningApplication.runningApplications(withBundleIdentifier: mainBundleIdentifier).isEmpty else {
			NSApp.terminate(nil)
			return
		}

		let pathComponents = (Bundle.main.bundlePath as NSString).pathComponents
		let mainPath = NSString.path(withComponents: Array(pathComponents[0...(pathComponents.count - 5)]))
		NSWorkspace.shared.launchApplication(mainPath)
		nc.addObserver(self, selector: #selector(terminateApp), name: NSNotification.Name("TerminateHelper"), object: nil)
	}
}

Then, all thats left is to check if the helper is running from our main application. Once this check is done, we send a notification to the helper app that it can now terminate safely.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// This variable will never be changed once initialized
public static let wasLaunchedOnLogin: Bool = {
        let bundleIdentifier = Bundle.main.bundleIdentifier!
        let mainBundleIdentifier = "\(bundleIdentifier)-LaunchAtLoginHelper"
        // If helper is not running
        if NSRunningApplication.runningApplications(withBundleIdentifier: mainBundleIdentifier).isEmpty {
            return false
        } else {
            // Helper is running, ask it to terminate
            let nc = DistributedNotificationCenter.default()
            nc.post(name: NSNotification.Name("TerminateHelper"), object: nil)
            return true
        }
}()

I like this solution, because it requires no modification for developers using the LaunchAtLogin package. However, not every user of the package wants this functionality, and thus it may be undesirable for them to leave the helper app running.

This post is licensed under CC BY 4.0 by the author.