How to make a Mac app launch at login with SMAppService

Updated · Dmytro Virych

Guide · 3 min read

On macOS 13 and later, call SMAppService.mainApp.register() to open your app at login and unregister() to stop, and read SMAppService.mainApp.status to show the real state in your settings.

A menu bar app, a clipboard manager or a timer is only useful if it is already running when the person needs it. "Open at login" is the setting that makes that happen, and since macOS 13 Apple has given it a small, dedicated API: SMAppService.

Which API should you use?

Use SMAppService from the ServiceManagement framework. Apple's documentation says that in macOS 13 and later you use it "to register and control LoginItems, LaunchAgents, and LaunchDaemons" for your app, and that its register() and unregister() methods "provide a replacement for" SMLoginItemSetEnabled.

That older function is deprecated as of macOS 13, and Apple's note on it is short: "Please use SMAppService instead." It also needed a separate helper app inside your bundle, in Contents/Library/LoginItems, whose only job was to launch the main app. SMAppService.mainApp needs no helper: Apple describes it as the service that "corresponds to the main application as a login item".

If your app still supports macOS 12 or earlier, you need the old path for those systems. If its deployment target is macOS 13 or later, you only need the new API.

The code

import ServiceManagement

func setOpensAtLogin(_ enabled: Bool) throws {
    if enabled {
        try SMAppService.mainApp.register()
    } else {
        try SMAppService.mainApp.unregister()
    }
}

That is the whole mechanism. Per Apple, after register() "the application launches on subsequent logins". It does not relaunch the app now, because it is already running. After unregister() the app "continues running, but becomes unregistered to prevent future launches at login".

Why read the status instead of saving a Boolean?

The person can switch your app off in the Login Items section of System Settings without ever opening your app. A Boolean you saved in UserDefaults would then show "on" when the system says "off". So treat SMAppService.mainApp.status as the only source of truth, and read it whenever you show the setting. It is one of four values:

StatusApple's description
.enabledThe service has been successfully registered and is eligible to run.
.requiresApprovalThe service has been successfully registered, but the user needs to take action in System Preferences.
.notRegisteredThe service hasn't registered with the Service Management framework, or the service attempted to reregister after it was already registered.
.notFoundAn error occurred and the framework couldn't find this service.

A SwiftUI toggle that follows that rule:

import ServiceManagement
import SwiftUI

struct OpenAtLoginToggle: View {
    @State private var status = SMAppService.mainApp.status

    var body: some View {
        Toggle("Open at login", isOn: Binding(
            get: { status == .enabled },
            set: { wanted in
                do {
                    if wanted {
                        try SMAppService.mainApp.register()
                    } else {
                        try SMAppService.mainApp.unregister()
                    }
                } catch {
                    print("Open at login:", error)
                }
                status = SMAppService.mainApp.status
            }
        ))
        .onAppear { status = SMAppService.mainApp.status }

        if status == .requiresApproval {
            Button("Allow in System Settings…") {
                SMAppService.openSystemSettingsLoginItems()
            }
        }
    }
}

The status is read again after every change and every time the view appears, so the toggle shows what the system will actually do.

What errors can register() and unregister() throw?

Apple documents two you should expect in normal use:

  • register() on a service that is already registered returns kSMErrorAlreadyRegistered. In Swift that arrives as a thrown error.
  • unregister() on a service that is not registered returns kSMErrorJobNotFound.

Neither is a real failure for a toggle, which is why the example above logs the error and then trusts the status. register() can also fail with kSMErrorLaunchDeniedByUser when the person has not approved the item. The .requiresApproval branch sends them to the right pane of System Settings with openSystemSettingsLoginItems().

Should it be on by default?

For the Mac App Store, no. Guideline 2.4.5 (iii) says apps "may not auto-launch or have other code run automatically at startup or login without consent". An app you sell from your own site is not reviewed against that guideline, but the same rule is a good default. A setting the person turned on themselves is one they won't resent, and a first-launch prompt with the toggle in it asks for consent and makes the feature easy to find.

#swift#login-items

Sources