Developer Blog

Answers to Common Questions in Cocoa Development

I recently created my first Cocoa app, and it was a real pain. I wanted to add a bunch of common features I see in every app, and hunting down the right answers on stack overflow proved to be very difficult. The following is a list of answers to common problems I ran into.

Note that Apple’s new rules about app sandboxing makes some of these features unavailable, and I think that’s garbage. Also note that these are quick and dirty solutions, and in now way represent best practices. All of this was done in Xcode 5 with a target of 10.7.

Adding an Icon to the System Tray

Winning solution: Creating a Standalone StatusItem Menu

I’m not going to replicate the whole article, so you’ll have to go there to see how to rig up things in Interface Builder, but here are the relevant bits of code to insert:

1
2
3
4
5
6
7
// MyAppDelegate.h
@interface MyAppDelegate : NSObject <NSApplicationDelegate> {
    IBOutlet NSMenu *statusMenu;
    NSStatusItem *statusItem;
    NSImage *statusImage;
    NSImage *statusHighlightedImage;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// MyAppDelegate.m
- (void) applicationDidFinishLaunching: (NSNotification *) aNotification {
  // Create the NSStatusBar and set its length
  statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength: NSSquareStatusItemLength];

  // Allocates and loads the images into the application which will be used for our NSStatusItem
  NSBundle *appBundle = [NSBundle mainBundle];
  statusImage = [appBundle imageForResource:@"icon.grey"];
  statusHighlightedImage = [appBundle imageForResource:@"icon.highlighted"];

  // Sets the images in our NSStatusItem
  [statusItem setImage:statusImage];
  [statusItem setAlternateImage:statusHighlightedImage];

  // Tells the NSStatusItem what menu to load
  [statusItem setMenu:statusMenu];
  // Sets the tooptip for our item
  [statusItem setToolTip:@"My Custom Menu Item"];
  // Enables highlighting
  [statusItem setHighlightMode:YES];
}

Note that getting the sizing of the images right was obnoxious, and I couldn’t find any solid information about it in Apple branded documentation. I can at least point out that the retina image assets seem to work the way you would expect, so adding a icon.png and an icon@2x.png does the right thing. I had to mess with the sizes a bunch though, and found that a non-retina icon that is 21x21 pixels works. Continuing to mess with things… it seems like the retina version of the icon had to be 41x41 pixels (not exactly double… “it just works!”).

For more info, this is the best resource I found NSStatusItem – What Size Should Your Icon Be?.

Registering a Global Hotkey to Bring Your App Into Focus

From what I read, it seems like the way you do this in Cocoa is to use the Carbon api. Regardless, after enough digging, I finally found this: DDHotKey. It makes things incredibly easy:

1
2
3
4
5
6
7
8
9
10
11
12
13
// MyAppDelegate.m
#import "DDHotKeyCenter.h"

- (void) applicationDidFinishLaunching: (NSNotification *) aNotification {
  // register hotkey
  DDHotKeyCenter *c = [[DDHotKeyCenter alloc] init];
  if ([c registerHotKeyWithKeyCode:34 modifierFlags:(NSCommandKeyMask | NSShiftKeyMask) target:self action:@selector(hotkeyAction:withObject:) object:nil]) {
      NSLog(@"registered");
  }
  else {
      NSLog(@"not registered");
  }
}

This example will register command + shift + i as your hotkey. To ensure that your app pops up to the front, you can do:

1
2
3
4
5
- (void) hotkeyAction: (NSEvent *)hotKeyEvent withObject:(id)anObject {
  NSApplication *myApp = [NSApplication sharedApplication];
  [myApp activateIgnoringOtherApps:YES];
  [self.window orderFrontRegardless];
 }

Make App Hide When Losing Focus

1
2
3
4
// MyAppDelegate.m
- (void) applicationDidResignActive: (NSNotification *) notification {
    [[NSApplication sharedApplication] hide:self];
}

Prevent App From Showing In Dock/Alt+Tab

This one requires you to edit your MyApp-Info.plist file (usually under “Supporting Files” in the project). Add a new key for “Application is agent (UIElement)” and set it to YES.

Start App at Login

Winning solution: Stack Overflow – How do you make your app open at login (with a few changes for Xcode 5)

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// MyAppDelegate.m
+ (BOOL) willStartAtLogin: (NSURL *) itemURL {
  Boolean foundIt=false;
  LSSharedFileListRef loginItems = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);
  if (loginItems) {
    UInt32 seed = 0U;
    NSArray *currentLoginItems = (__bridge NSArray *)(LSSharedFileListCopySnapshot(loginItems, &seed));
    for (id itemObject in currentLoginItems) {
      LSSharedFileListItemRef item = (__bridge LSSharedFileListItemRef)itemObject;

      UInt32 resolutionFlags = kLSSharedFileListNoUserInteraction | kLSSharedFileListDoNotMountVolumes;
      CFURLRef URL = NULL;
            OSStatus err = LSSharedFileListItemResolve(item, resolutionFlags, &URL, /*outRef*/ NULL);
      if (err == noErr) {
        foundIt = CFEqual(URL, (__bridge CFTypeRef)(itemURL));
        CFRelease(URL);

        if (foundIt)
          break;
      }
    }
    CFRelease(loginItems);
  }
  return (BOOL)foundIt;
}

+ (void) setStartAtLogin: (NSURL *) itemURL enabled: (BOOL) enabled {
  OSStatus status;
  LSSharedFileListItemRef existingItem = NULL;

  LSSharedFileListRef loginItems = LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL);
  if (loginItems) {
    UInt32 seed = 0U;
    NSArray *currentLoginItems = (__bridge NSArray *)(LSSharedFileListCopySnapshot(loginItems, &seed));
    for (id itemObject in currentLoginItems) {
      LSSharedFileListItemRef item = (__bridge LSSharedFileListItemRef)itemObject;

      UInt32 resolutionFlags = kLSSharedFileListNoUserInteraction | kLSSharedFileListDoNotMountVolumes;
      CFURLRef URL = NULL;
            OSStatus err = LSSharedFileListItemResolve(item, resolutionFlags, &URL, /*outRef*/ NULL);
      if (err == noErr) {
        Boolean foundIt = CFEqual(URL, (__bridge CFTypeRef)(itemURL));
        CFRelease(URL);

        if (foundIt) {
          existingItem = item;
          break;
        }
      }
    }

    if (enabled && (existingItem == NULL)) {
      LSSharedFileListInsertItemURL(loginItems, kLSSharedFileListItemBeforeFirst,
        NULL, NULL, (__bridge CFURLRef)itemURL, NULL, NULL);

    } else if (!enabled && (existingItem != NULL))
    LSSharedFileListItemRemove(loginItems, existingItem);

    CFRelease(loginItems);
  }
}

If you’d like a checkbox that is bound to your app’s “start at login” state, you can create a property:

1
2
// MyAppDelegate.h
@property BOOL startAtLogin;

then use Interface Builder to bind a checkbox to this property, and implement the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// MyAppDelegate.m
- (NSURL *) appURL {
  return [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
}

- (BOOL) startAtLogin {
  return [ISPAppDelegate willStartAtLogin:[self appURL]];
}

- (void) setStartAtLogin: (BOOL) enabled {
  [self willChangeValueForKey:@"startAtLogin"];
  [ISPAppDelegate setStartAtLogin:[self appURL] enabled:enabled];
  [self didChangeValueForKey:@"startAtLogin"];
}

Conclusion

Now you have the basics of an app that can do whatever you’d like without having to figure out the boilerplate. If there’s some interest, I’d be willing to roll all of this up into a public github repo that people can check out and use as a starting place for utility apps. For those who are curious, the reason I had to figure all of this out was to build Insert Pic.

Comments