Make Swift Cocoa app launch on startup on OS X 10.11
Asked Answered
K

2

23

I need to write a function that adds my application to Startup items on OS X 10.11. That's what I found at the moment:

func applicationIsInStartUpItems() -> Bool {
    return (itemReferencesInLoginItems().existingReference != nil)
}

func itemReferencesInLoginItems() -> (existingReference: LSSharedFileListItemRef?, lastReference: LSSharedFileListItemRef?) {

    if let appUrl : NSURL = NSURL.fileURLWithPath(NSBundle.mainBundle().bundlePath) {
        let loginItemsRef = LSSharedFileListCreate(nil, kLSSharedFileListSessionLoginItems.takeRetainedValue(), nil).takeRetainedValue() as LSSharedFileListRef?
        if loginItemsRef != nil {
            let loginItems: NSArray = LSSharedFileListCopySnapshot(loginItemsRef, nil).takeRetainedValue() as NSArray
            if(loginItems.count > 0) {
                let lastItemRef: LSSharedFileListItemRef = loginItems.lastObject as! LSSharedFileListItemRef
                for var i = 0; i < loginItems.count; ++i {
                    let currentItemRef: LSSharedFileListItemRef = loginItems.objectAtIndex(i) as! LSSharedFileListItemRef
                    if let itemURL = LSSharedFileListItemCopyResolvedURL(currentItemRef, 0, nil) {
                        if (itemURL.takeRetainedValue() as NSURL).isEqual(appUrl) {
                            return (currentItemRef, lastItemRef)
                        }
                    }
                }
                return (nil, lastItemRef)
            } else {
                let addatstart: LSSharedFileListItemRef = kLSSharedFileListItemBeforeFirst.takeRetainedValue()
                return(nil,addatstart)
            }
        }
    }
    return (nil, nil)
}

func toggleLaunchAtStartup() {
    let itemReferences = itemReferencesInLoginItems()
    let shouldBeToggled = (itemReferences.existingReference == nil)
    if let loginItemsRef = LSSharedFileListCreate( nil, kLSSharedFileListSessionLoginItems.takeRetainedValue(), nil).takeRetainedValue() as LSSharedFileListRef? {
        if shouldBeToggled {
            if let appUrl : CFURLRef = NSURL.fileURLWithPath(NSBundle.mainBundle().bundlePath) {
                LSSharedFileListInsertItemURL(loginItemsRef, itemReferences.lastReference, nil, nil, appUrl, nil, nil)
            }
        } else {
            if let itemRef = itemReferences.existingReference {
                LSSharedFileListItemRemove(loginItemsRef,itemRef);
            }
        }
    }
}

But LSSharedFileListCreate, LSSharedFileListInsertItemURL, LSSharedFileListItemRemove, kLSSharedFileListItemBeforeFirst, LSSharedFileListItemCopyResolvedURL, LSSharedFileListCopySnapshot, kLSSharedFileListSessionLoginItems were deprecated in OS X 10.11. How to make this work on latest version of Mac OS? How to change or rewrite this code?

Kareykari answered 11/2, 2016 at 12:23 Comment(1)
You can use this library github.com/sindresorhus/LaunchAtLoginDictator
M
16

In Swift 3.0 it looks like this:

In your main application AppDelegate:

func applicationDidFinishLaunching(_ aNotification: Notification) {
    // Check if the launcher app is started
    var startedAtLogin = false
    for app in NSWorkspace.shared().runningApplications {
        if app.bundleIdentifier == NCConstants.launcherApplicationIdentifier {
            startedAtLogin = true
        }
    }

    // If the app's started, post to the notification center to kill the launcher app
    if startedAtLogin {
        DistributedNotificationCenter.default().postNotificationName(NCConstants.KILLME, object: Bundle.main.bundleIdentifier, userInfo: nil, options: DistributedNotificationCenter.Options.deliverImmediately)
    }
}

In the Launcher application AppDelegate:

func applicationDidFinishLaunching(_ aNotification: Notification) {

    let mainAppIdentifier = "<main-app-bundle-id>"
    let running = NSWorkspace.shared().runningApplications
    var alreadyRunning = false

    // loop through running apps - check if the Main application is running
    for app in running {
        if app.bundleIdentifier == mainAppIdentifier {
            alreadyRunning = true
            break
        }
    }

    if !alreadyRunning {
        // Register for the notification killme
        DistributedNotificationCenter.default().addObserver(self, selector: #selector(self.terminate), name: NCConstants.KILLME, object: mainAppIdentifier)

        // Get the path of the current app and navigate through them to find the Main Application
        let path = Bundle.main.bundlePath as NSString
        var components = path.pathComponents
        components.removeLast(3)
        components.append("MacOS")
        components.append("<your-app-name>")

        let newPath = NSString.path(withComponents: components)

        // Launch the Main application
        NSWorkspace.shared().launchApplication(newPath)
    }
    else {
        // Main application is already running
        self.terminate()
    }

}

func terminate() {
    print("Terminate application")
    NSApp.terminate(nil)
}

Eventually, in the main application I added a user interface with a toggle button. The user can choose to launch the app at login or not. The choice is stored into the UserDefaults. In the View Controller:

@IBAction func toggleLaunchAtLogin(_ sender: Any) {
    if toggleOpenAppLogin.selectedSegment == 0 {
        if !SMLoginItemSetEnabled(NCConstants.launcherApplicationIdentifier as CFString, true) {
            print("The login item was not successfull")
            toggleOpenAppLogin.setSelected(true, forSegment: 1)
        }
        else {
            UserDefaults.standard.set("true", forKey: "appLoginStart")
        }
    }
    else {
        if !SMLoginItemSetEnabled(NCConstants.launcherApplicationIdentifier as CFString, false) {
            print("The login item was not successfull")
            toggleOpenAppLogin.setSelected(true, forSegment: 0)
        }
        else {
            UserDefaults.standard.set("false", forKey: "appLoginStart")
        }
    }

}

I hope this can help somebody.

Mcguire answered 19/5, 2017 at 12:13 Comment(5)
Nice and really helping answer, even after so much time!Kareykari
Where is NCConstants.KILLME defined?Luba
In the launcher application AND the main application. You can define it like this : class NCConstants { // Notify constant static let KILLME = Notification.Name("killme")}Mcguire
Is <your-app-name> the product Name or the Project Name?Sheedy
What is Launcher application?Dictator
K
16

You have to use the Service Management framework now. You create a helper application that you add to your application bundle and its job is to run code to launch your main application. Some resources for you:

Kitty answered 12/2, 2016 at 7:25 Comment(4)
The answer is not Swift related, but gives general directionScrivner
Your tutorial is fantastic. I have tried it now and everything works as described except by a little tiny fact that it does not work. I have this error on console Could not resolve CFBundleIdentifier specified by service followed by the identifier of my help file that I have triple checked is typed correctly. That is expected because in my experience not a single API from Apple works as expected when you first try.Roubaix
@SpaceDog Have you tried pulling the source from GitHub and trying that project?Kitty
Yes, I have created a question to explain the problem... please check that out.... you will have to create another tutorial for that case... 😃Roubaix
M
16

In Swift 3.0 it looks like this:

In your main application AppDelegate:

func applicationDidFinishLaunching(_ aNotification: Notification) {
    // Check if the launcher app is started
    var startedAtLogin = false
    for app in NSWorkspace.shared().runningApplications {
        if app.bundleIdentifier == NCConstants.launcherApplicationIdentifier {
            startedAtLogin = true
        }
    }

    // If the app's started, post to the notification center to kill the launcher app
    if startedAtLogin {
        DistributedNotificationCenter.default().postNotificationName(NCConstants.KILLME, object: Bundle.main.bundleIdentifier, userInfo: nil, options: DistributedNotificationCenter.Options.deliverImmediately)
    }
}

In the Launcher application AppDelegate:

func applicationDidFinishLaunching(_ aNotification: Notification) {

    let mainAppIdentifier = "<main-app-bundle-id>"
    let running = NSWorkspace.shared().runningApplications
    var alreadyRunning = false

    // loop through running apps - check if the Main application is running
    for app in running {
        if app.bundleIdentifier == mainAppIdentifier {
            alreadyRunning = true
            break
        }
    }

    if !alreadyRunning {
        // Register for the notification killme
        DistributedNotificationCenter.default().addObserver(self, selector: #selector(self.terminate), name: NCConstants.KILLME, object: mainAppIdentifier)

        // Get the path of the current app and navigate through them to find the Main Application
        let path = Bundle.main.bundlePath as NSString
        var components = path.pathComponents
        components.removeLast(3)
        components.append("MacOS")
        components.append("<your-app-name>")

        let newPath = NSString.path(withComponents: components)

        // Launch the Main application
        NSWorkspace.shared().launchApplication(newPath)
    }
    else {
        // Main application is already running
        self.terminate()
    }

}

func terminate() {
    print("Terminate application")
    NSApp.terminate(nil)
}

Eventually, in the main application I added a user interface with a toggle button. The user can choose to launch the app at login or not. The choice is stored into the UserDefaults. In the View Controller:

@IBAction func toggleLaunchAtLogin(_ sender: Any) {
    if toggleOpenAppLogin.selectedSegment == 0 {
        if !SMLoginItemSetEnabled(NCConstants.launcherApplicationIdentifier as CFString, true) {
            print("The login item was not successfull")
            toggleOpenAppLogin.setSelected(true, forSegment: 1)
        }
        else {
            UserDefaults.standard.set("true", forKey: "appLoginStart")
        }
    }
    else {
        if !SMLoginItemSetEnabled(NCConstants.launcherApplicationIdentifier as CFString, false) {
            print("The login item was not successfull")
            toggleOpenAppLogin.setSelected(true, forSegment: 0)
        }
        else {
            UserDefaults.standard.set("false", forKey: "appLoginStart")
        }
    }

}

I hope this can help somebody.

Mcguire answered 19/5, 2017 at 12:13 Comment(5)
Nice and really helping answer, even after so much time!Kareykari
Where is NCConstants.KILLME defined?Luba
In the launcher application AND the main application. You can define it like this : class NCConstants { // Notify constant static let KILLME = Notification.Name("killme")}Mcguire
Is <your-app-name> the product Name or the Project Name?Sheedy
What is Launcher application?Dictator

© 2022 - 2024 — McMap. All rights reserved.