Question

How can I set a menu bar app to automatically startup on login? I want this to be default. Can i do this simply by adding a bool in the info.plist?

Was it helpful?

Solution

(This solution is for non-sandboxed apps only. The LSSharedFile functions used in this solution are available only to non-sandboxed apps.)

You use the Session Login Items shared file list. This is the list that is displayed in System Preferences when you check your login items under your profile settings.

The typical scenario in an app is to provide a checkbox in the app's preferences, that allows the user to choose wether they want to start the app or not on login. If you intend to distribute through the app store DO NOT set the app to launch on login by default. You will get rejected :)

So, in this scenario, we will make a property, say in the App Delegate, called "launchOnLogin" and we will bind the value of the check box to this property.

The getter method will check if our app's bundle id is in the shared list and return true or false.

The setter method ads or removes the item from the shared list.

Here goes:

AppDelegate.h

@property (atomic, assign) BOOL launchOnLogin;

AppDelegate.m

- (BOOL)launchOnLogin 
{
    LSSharedFileListRef loginItemsListRef = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);
    CFArrayRef snapshotRef = LSSharedFileListCopySnapshot(loginItemsListRef, NULL);
    NSArray* loginItems = [NSMakeCollectable(snapshotRef) autorelease];
    NSURL *bundleURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
    for (id item in loginItems) {
        LSSharedFileListItemRef itemRef = (LSSharedFileListItemRef)item;
        CFURLRef itemURLRef;
        if (LSSharedFileListItemResolve(itemRef, 0, &itemURLRef, NULL) == noErr) {
            NSURL *itemURL = (NSURL *)[NSMakeCollectable(itemURLRef) autorelease];
            if ([itemURL isEqual:bundleURL]) {
                return YES;
            }
        }
    }
    return NO;
}

- (void)setLaunchOnLogin:(BOOL)launchOnLogin
{    
    NSURL *bundleURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
    LSSharedFileListRef loginItemsListRef = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);

    if (launchOnLogin) {
        NSDictionary *properties;
        properties = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:@"com.apple.loginitem.HideOnLaunch"];
        LSSharedFileListItemRef itemRef = LSSharedFileListInsertItemURL(loginItemsListRef, kLSSharedFileListItemLast, NULL, NULL, (CFURLRef)bundleURL, (CFDictionaryRef)properties,NULL);
        if (itemRef) {
            CFRelease(itemRef);
        }
    } else {
        LSSharedFileListRef loginItemsListRef = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);
        CFArrayRef snapshotRef = LSSharedFileListCopySnapshot(loginItemsListRef, NULL);
        NSArray* loginItems = [NSMakeCollectable(snapshotRef) autorelease];

        for (id item in loginItems) {
            LSSharedFileListItemRef itemRef = (LSSharedFileListItemRef)item;
            CFURLRef itemURLRef;            
            if (LSSharedFileListItemResolve(itemRef, 0, &itemURLRef, NULL) == noErr) {
                NSURL *itemURL = (NSURL *)[NSMakeCollectable(itemURLRef) autorelease];
                if ([itemURL isEqual:bundleURL]) {
                    LSSharedFileListItemRemove(loginItemsListRef, itemRef);
                }
            }
        }
    }
}

That's pretty much it. Now if you do the binding and everything correctly, you'll see how your app appears and dissappears from the System Preferences list in realtime.

The part that actually makes the app launch on login

// Get the path of the app
NSURL *bundleURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];

// Get the list you want to add the path to
LSSharedFileListRef loginItemsListRef = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);

// Set the app to be hidden on launch
NSDictionary *properties = @{@"com.apple.loginitem.HideOnLaunch": @YES};

// Add the item to the list
LSSharedFileListItemRef itemRef = LSSharedFileListInsertItemURL(loginItemsListRef, kLSSharedFileListItemLast, NULL, NULL, (CFURLRef)bundleURL, (CFDictionaryRef)properties,NULL);

Please note that the code above is retain/release but it is pretty trivial to convert it to ARC if you need it.

Hope this helps.

OTHER TIPS

For anyone looking for an updated Swift compatible version of this code, I've made a gist here: https://gist.github.com/plapier/f8e1dde1b1624dfbb3e4

Just call toggleLaunchAtStartup() from within your app. (Most likely inside a checkbox IBAction).

And the actual code for reference:

import Foundation

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

func itemReferencesInLoginItems() -> (existingReference: LSSharedFileListItemRef?, lastReference: LSSharedFileListItemRef?) {
    if let appURL : NSURL = NSURL.fileURLWithPath(NSBundle.mainBundle().bundlePath) {
        if let loginItemsRef = LSSharedFileListCreate(nil, kLSSharedFileListSessionLoginItems.takeRetainedValue(), nil).takeRetainedValue() as LSSharedFileListRef? {

            let loginItems: NSArray = LSSharedFileListCopySnapshot(loginItemsRef, nil).takeRetainedValue() as NSArray
            let lastItemRef: LSSharedFileListItemRef = loginItems.lastObject as! LSSharedFileListItemRef

            for (index, loginItem) in enumerate(loginItems) {
                let currentItemRef: LSSharedFileListItemRef = loginItems.objectAtIndex(index) as! LSSharedFileListItemRef
                if let itemURL = LSSharedFileListItemCopyResolvedURL(currentItemRef, 0, nil) {
                    if (itemURL.takeRetainedValue() as NSURL).isEqual(appURL) {
                        return (currentItemRef, lastItemRef)
                    }
                }
            }

            return (nil, lastItemRef)
        }
    }

    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) {
                println("Add login item")
                LSSharedFileListInsertItemURL(loginItemsRef, itemReferences.lastReference, nil, nil, appUrl, nil, nil)
            }
        } else {
            if let itemRef = itemReferences.existingReference {
                println("Remove login item")
                LSSharedFileListItemRemove(loginItemsRef,itemRef);
            }
        }
    }
}

Used with Xcode 6.3.2, Swift 1.2

You can run the apple script given below from the application to add it to the Login Item.

on run args_list
    tell application "System Events" to make login item at end with properties {path:item 1 of args_list, hidden:false}
end run

Save the above script as AddToLogin.scpt and add this to project.

Add the code given below to the application startup

NSString *appPath =[[NSBundle mainBundle] bundlePath] ;
NSString *scriptPath = [[NSBundle mainBundle] pathForResource:@"AddToLogin" ofType:@"scpt"];

NSArray *argArray;
if(scriptPath)
{
    argArray = [NSArray arrayWithObjects:scriptPath, appPath, nil];
    NSTask * task = [[NSTask alloc] init];
    [task setLaunchPath:@"/usr/bin/osascript"];
    [task setArguments:argArray];
    [task launch];
    [task waitUntilExit];
}

The link given below explains the osascript way to do the same

http://hints.macworld.com/article.php?story=20111226075701552

Because this answer is a number of years old, there's a nice Swift Package that handles this

https://github.com/sindresorhus/LaunchAtLogin

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top