Alright, I've managed to make a test app that works. Here is the unabridged process:
Make a new Cocoa App (your main app)
- In your target's summary tab, check off "Enable Entitlements", "Code Sign Application", and "Enable Entitlements"
(These gray steps are just to reproduce exactly what I did)
- Delete the window and menu bar from your XIB.
- Remove references to the window in the header and implementation file.
- In your -info.plist file, add LSUIElement and set it to YES.
- Add to the top of your implementation file:
Code:
#import <ServiceManagement/ServiceManagement.h>
- In Target->Build Phases->Link Binary with Frameworks, add ServiceManagement.framework
Here is all the code for the implementation file... all the sample app is is a menu bar item with the name "SimpleStatusItem". Clicking on it presents a menu with two options: toggle the autolaunch, and quit it. It also lets you know whether it's set to autolaunch or not.
Code:
- (void)dealloc
{
[super dealloc];
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength] retain];
statusItem.title = @"SimpleStatusItem";
statusItem.highlightMode = YES;
NSMenu* statusMenu = [[NSMenu alloc] init];
statusMenu.delegate = self;
statusItem.menu = statusMenu;
NSMenuItem* launchItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:@"Autolaunch" action:@selector(toggle:) keyEquivalent:@""];
[statusMenu addItem:launchItem];
[launchItem release];
NSMenuItem* quitItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:@"Quit" action:@selector(terminate:) keyEquivalent:@""];
[statusMenu addItem:quitItem];
[quitItem release];
[statusMenu release];
}
- (void)menuNeedsUpdate:(NSMenu*)menu
{
NSString* bundleID = @"com.taylormarks.loginhelper";
NSArray* jobDicts = nil;
jobDicts = (NSArray*)SMCopyAllJobDictionaries(kSMDomainUserLaunchd);
[[menu itemWithTitle:@"Autolaunch"] setState:NSOffState];
if ((jobDicts != nil) && [jobDicts count] > 0)
{
for (NSDictionary* job in jobDicts)
{
if ([bundleID isEqualToString:[job objectForKey:@"Label"]])
{
if ([[job objectForKey:@"OnDemand"] boolValue])
{
[[menu itemWithTitle:@"Autolaunch"] setState:NSOnState];
}
break;
}
}
CFRelease((CFDictionaryRef)jobDicts);
jobDicts = nil;
}
}
- (void)toggle:(NSMenuItem*)item
{
NSString *ref = @"com.taylormarks.loginhelper";
if (item.state == NSOffState)
{
if (!SMLoginItemSetEnabled((CFStringRef)ref, true))
{
NSLog(@"SMLoginItemSetEnabled failed to enable.");
}
}
else
{
if (!SMLoginItemSetEnabled((CFStringRef)ref, false))
{
NSLog(@"SMLoginItemSetEnabled failed to disable.");
}
}
}
- (void)applicationWillTerminate:(NSNotification*)notification
{
statusItem.menu.delegate = nil;
[statusItem.menu removeAllItems];
statusItem.menu = nil;
[statusItem release];
}
Make a new Cocoa App (the autolaunch helper app)
(If this looks familiar, it's because it starts out the exact same process that was just done for the main app.)
- In your target's summary tab, check off "Enable Entitlements", "Code Sign Application", and "Enable Entitlements"
(You'll almost definitely want to do these with the autolaunch helper app, even if you didn't want them with the main app)
- Delete the window and menu bar from your XIB.
- Remove references to the window in the header and implementation file.
- In your -info.plist file, add LSUIElement and set it to YES.
- This is the code for the implementation of the helper:
Code:
- (void)dealloc
{
[super dealloc];
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSString* appPath = [[[[[[NSBundle mainBundle] bundlePath] // .app/Contents/Library/LoginItems/.app
stringByDeletingLastPathComponent] // .app/Contents/Library/LoginItems
stringByDeletingLastPathComponent] // .app/Contents/Library
stringByDeletingLastPathComponent] // .app/Contents
stringByDeletingLastPathComponent];// .app
[[NSWorkspace sharedWorkspace] launchApplication:appPath];
[NSApp terminate:nil];
}
@end
- Archive the helper app
- Distribute->Export Developer ID-signed Application (save it in your main app's project directory)
Back to the Main App
- Target -> Build Phases -> Add Build Phase -> Add Copy Files
- Destination: Wrapper, Subpath "Contents/Library/LoginItems".
- Add the app bundle
- Add Build Phase -> Add Run Script:
Code:
#!/bin/sh
PATH=${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/Contents/Library/LoginItems/loginhelper.app/Contents/embedded.provisionprofile
if cat PATH
then rm PATH
fi
- Set Strip Debug Symbols During Copy in Main App's Target Build Settings to NO for both Debug and Release
- Distribute with Export Developer ID-signed Application (Save in Applications)
I verified the functionality of this test app with this process:
Testing...
- Launch App, Verify it shouldn't autolaunch, terminate app, login/out, shouldn't be running
- Launch App, set it to autolaunch, verify, terminate app, launch, verify, terminate, login/out, should be running
- Turn off autolaunch, verify, terminate app, launch, verify, login/out, shouldn't be running
- Launch app, set it to autolaunch, login/out, should be running*
* It took several minutes before this login finished and the app appeared in my menu bar. I don't know what the hold up was, but I almost gave up and set about trying to find out what was wrong before it suddenly popped up... not particularly desirable.
Edit:
Apple evidently didn't like my code signature for my helper app. I fixed it by doing this in terminal:
Code:
codesign -f -v -s "3rd Party Mac Developer Application:" loginhelper.app
Assuming you only have a single 3rd Party Mac Developer Application certificate, that should work without modification. loginhelper.app should be replaced with whatever the name of your helper app is. I was in the directory where the helper app's archive had been saved when I did it.
Maybe in the future I can make a script for the helper app that automatically does this when it's archived...