Search

Notifications, part 2 – Handling and Spying

Mark Dalrymple

8 min read

Jul 15, 2012

Notifications, part 2 – Handling and Spying

Last time you saw how to register for notifications. Now time to handle them!

With the classic notification registration API, you specify an object to be sent a message, and the selector to use. That message can take at most one argument, which is a pointer to an NSNotification object.

So, given this registration:

NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver: self
        selector: @selector(networkByeBye:)
        name: kNetworkWentByeByeNotification
        object: self.networkMonitor];

The resulting handler method would look like this:

- (void) networkByeBye: (NSNotification *) notification {
    // handle the notification
} // networkByeBye

When you register for notifications using a block, you supply a queue, and a block to schedule on that queue when the notification is posted. Because the code is in the block there’s no need to implement a method, or even have an object involved. Of course, if a lot of work is being done in the handler, you’ll want to factor it out into its own method or function.

For completeness, here’s an example from last time. The notification block takes an NSNotification object:

_token = [center addObserverForName: kNetworkWentByeByeNotification
                 object: nil
                 queue: [NSOperationQueue mainQueue]
                 usingBlock: ^(NSNotification *notification) {
                     NSLog (@"Network went down: %@", notification);
                 }];

Recall that the token is an opaque object that you can use to unregister the notification later.

The Notification Object

So, about that notification object. That’s how the code posting the notification “Hey! The network just went down” communicates with the code handling the notification. “Oh dear, I need to redraw my view.”

There’s three pieces of information baked into the notification object:

The notification name is an NSString that’s used to broadcast the notification, in the code blocks above, the name would be kNetworkWentByeByeNotification. Sometimes this might be all the information you need to convey. “This thing happened.”

You can also use the notification name to disambiguate multiple notifications that call the same method. There might be a large body of common code that runs for each notification (perhaps for network-connected and also for network-disconnected), but then a little bit of extra code for just one of the cases (update a timestamp label when the network goes away):

- (void) networkChanged: (NSNotification *) notification {
    // do stuff
    if ([notification.name isEqualToString: kNetworkWentByeByeNotification]) {
        NSLog (@"Network went away");
        // update timestamp label
    }
} // networkChanged

The object associated with the notification is the second piece of information held by the NSNotification object. This is provided by the code that posts the notification:

// NetworkMonitor
[center postNotificationName: kNetworkWentByeByeNotification
        object: self
        userInfo: userInfo];

Say you had a view class that broadcast when it was tapped. “Yo! I was tapped!”. And you put two of them on screen. One implementation approach would be to have two different methods (or blocks) that handle the notifications. (This is actually the approach I’d take). You could also have a single notification handler that discriminated on the object:

- (void) tapcasterTapped: (NSNotification *) notification {
    if (notification.object == self.shoesizeTapcaster) {
        // Upload the shoe size.
    } else if (notification.object == self.bloodtypeTapcaster) {
        // Take a blood sample from the user.
    }
} // tapcasterTapped

The third piece of information is the userInfo, a dictionary containing arbitrary key/value pairs that the notification poster decided would be of interest to anyone receiving the notification. There are no predefined conventions about what gets passed in the userInfo, so you can do whatever you want. For the network monitor, it could include such information like the exact network interface that went down, the time it went down, the IP address and port it was using, the time since the last connection, and the Bonjour name used to advertise a service.

It’s good form to provide symbolic constants for the keys:

extern NSString *const kNetworkInterfaceKey;
extern NSString *const kNetworkDownTimestampKey;
extern NSString *const kNetworkAddressKey;
extern NSString *const kNetworkPortKey;
extern NSString *const kNetworkUptimeSecondsKey;
extern NSString *const kNetworkBonjourKey;

That way you eliminate “typos” as a cause for programmer pain and suffering.

Then when posting the notification, cook up a user info dictionary:

NSDictionary *userInfo =
    [NSDictionary dictionaryWithObjectsAndKeys:
                      self.interface,         kNetworkInterfaceKey,
                      [NSDate date],          kNetworkDownTimestampKey,
                      self.connectionAddress, kNetworkAddressKey,
                      self.connectionPort,    kNetworkPortKey,
                      [NSNumber numberWithInt: timeDelta], kNetworkUptimeSecondsKey,
                      self.bonjourName,       kNetworkBonjourKey,
                      nil];
// Post notification.
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center postNotificationName: kNetworkWentByeByeNotification
        object: self
        userInfo: userInfo];

(Objective-C literals will make this look soooo much nicer)

And then you can pick out the information you want when handling the notification:

NSString *interface = [notification.userInfo objectForKey: kNetworkInterfaceKey];
NSLog (@"interface is %@", interface);

Time out for Design

Like absolutely everything else involved in programming, you can apply rules and conventions to stuff that doesn’t necessarily need it.

When I declare notifications and keys, I typically declare them together, with the keys that go along with a notification underneath the notification:

extern NSString *const kNetworkWentByeByeNotification;
extern NSString *const     kNetworkInterfaceKey;
extern NSString *const     kNetworkDownTimestampKey;
extern NSString *const     kNetworkAddressKey;
extern NSString *const     kNetworkPortKey;
extern NSString *const     kNetworkUptimeSecondsKey;
extern NSString *const     kNetworkBonjourKey;

Suffixing the constant with “Notification” and “Key” show how they relate to each other.

For the actual string values, there’s (at least) two camps. And I waffle between the two.

One idea is to have keys be very readable:

NSString *const kNetworkWentByeByeNotification = @"network went bye bye";
NSString *const     kNetworkInterfaceKey = @"network interface";
NSString *const     kNetworkDownTimestampKey = @"network down timestamp";
NSString *const     kNetworkAddressKey = @"network address";
NSString *const     kNetworkPortKey = @"network port";
NSString *const     kNetworkUptimeSecondsKey = @"network uptime seconds";
NSString *const     kNetworkBonjourKey = @"bonjour key";

That way the dictionary is very easy to visually parse when printed out:

{
    "bonjour key" = Nerdinalia;
    "network address" = "192.168.254.13";
    "network down timestamp" = "2012-07-16 19:52:19 +0000";
    "network interface" = en1;
    "network port" = 666;
    "network uptime seconds" = 10;
}

The other is to have the names match the constant you’d type in to access the dictionary:

NSString *const kNetworkWentByeByeNotification = @"kNetworkWentByeByeNotification";
NSString *const     kNetworkInterfaceKey = @"kNetworkInterfaceKey";
NSString *const     kNetworkDownTimestampKey = @"kNetworkDownTimestampKey";
NSString *const     kNetworkAddressKey = @"kNetworkAddressKey";
NSString *const     kNetworkPortKey = @"kNetworkPortKey";
NSString *const     kNetworkUptimeSecondsKey = @"kNetworkUptimeSecondsKey";
NSString *const     kNetworkBonjourKey = @"kNetworkBonjourKey";

The advantage of this method is for programmers that aren’t very familiar with your API. They can figure out which dictionary keys to access to get at the data they want. This is the same userInfo dict, but with the other nomenclature:

{
    kNetworkBonjourKey = Nerdinalia;
    kNetworkAddressKey = "192.168.254.13";
    kNetworkDownTimestampKey = "2012-07-16 19:52:49 +0000";
    kNetworkInterfaceKey = en1;
    kNetworkPortKey = 666;
    kNetworkUptimeSecondsKey = 10;
}

It’s obvious that if you want the network interface, you would use kNetworkInterfaceKey to get at it.

Spying

It’s pretty easy to spy on notifications flying around your program. The notification key names are usually self-documenting (especially those coming from cocoa), as are the contents of the userInfo.

Recently I had a need to see notifications flying around my app. Specifically, I was wondering why I wasn’t getting NSMetadataQuery notifications for my iCloud document container. I thought I had everything set up, and checked my cloud document marklar with NSFileManager and there were files there, but nothing came back with my metadata query. I know there are some metadata query notifications happening, so I wanted to look at them.

It’s pretty easy to spy on all the notifications that are happening. Just register a notification handler with a nil object and a nil name. Passing nil for those tell the notification center to treat them like wild cards. Here’s a way to spy on everything going through your app’s notification center:

NSNotificationCenter *center;
center = [NSNotificationCenter defaultCenter];
token = [center addObserverForName: nil
                object: nil
                queue: nil
                usingBlock: ^(NSNotification *notification) {
                    QuietLog (@"NOTIFICATION %@ -> %@",
                              notification.name, notification.userInfo);
                }];

The block API is really easy for stand-alone handlers like this. If you intend on removing this handler you’ll want to hang on to the returned token and pass it to removeObserver:.

This is pretty cool. Here’s some of the stuff it prints on launch:

NOTIFICATION NSWillBecomeMultiThreadedNotification -> (null)
NOTIFICATION _UIWindowDidCreateWindowContextNotification -> {
    "_UIWindowContextIDKey" = "-464166441";
}
...
NOTIFICATION UIWindowDidBecomeVisibleNotification -> (null)
NOTIFICATION UIDeviceOrientationDidChangeNotification -> {
    UIDeviceOrientationRotateAnimatedUserInfoKey = 1;
}
NOTIFICATION UIWindowDidBecomeKeyNotification -> (null)
NOTIFICATION UIApplicationDidFinishLaunchingNotification -> (null)
NOTIFICATION UIApplicationDidBecomeActiveNotification -> (null)
NOTIFICATION _UIApplicationDidEndIgnoringInteractionEventsNotification -> (null)
...
NOTIFICATION UINavigationControllerDidShowViewControllerNotification -> {
    UINavigationControllerLastVisibleViewController =
        "<GRMainMenuViewController: 0x255320>";
    UINavigationControllerNextVisibleViewController =
        "<GRClassChooserViewController: 0x2e0d80>";

This ended up being useful to me because I saw no metadata notifications. Then I noticed I never called -startQuery. I make dumb errors too.

Distributed Notifications

We usually use the default notification center. You’re welcome to create your own notification centers if you wish (therefore NSNotificationCenter is not a singleton class). On the desktop, you can access NSDistributedNotificationCenter, which is a easy form of interprocess communication. One process can post a distributed notification, and other processes can register handlers to receive those notifications. To see everything that comes across distributed notifications, you just add an observer with nil name and object like with the default notification center:

center = [NSDistributedNotificationCenter defaultCenter];
token = [center addObserverForName: nil
                object: nil
                queue: nil
                usingBlock: ^(NSNotification *notification) {
                    QuietLog (@"DISTRIBUTED %@ -> %@",
                              notification.name, notification.userInfo);
                }];

You can see distributed notifications when tracking menus twiddling stuff in the system preferences:

DISTRIBUTED com.apple.HIToolbox.beginMenuTrackingNotification -> {
    ToolboxMessageEventData = 145;
}
DISTRIBUTED com.apple.HIToolbox.endMenuTrackingNotification -> (null)
DISTRIBUTED AppleShowScrollBarsSettingChanged -> (null)
DISTRIBUTED com.apple.HIToolbox.beginMenuTrackingNotification -> {
    ToolboxMessageEventData = 25921;
}
DISTRIBUTED com.apple.HIToolbox.endMenuTrackingNotification -> (null)
DISTRIBUTED AppleSideBarDefaultIconSizeChanged -> (null)

Workspace Notifications

Finally, there’s NSWorkspace, which has its own notification center. NSWorkspace will tip you off to cool events like application launches and exits, applications juggling in and out, and the machine going to sleep. Registering a notification is the same as before, just using a different notification center:

center = [[NSWorkspace sharedWorkspace] notificationCenter];
token = [center addObserverForName: nil
                object: nil
                queue: nil
                usingBlock: ^(NSNotification *notification) {
                    QuietLog (@"WORKSPACE %@ -> %@",
                              notification.name, notification.userInfo);
                }];

And then spy on some of the workspace notifications:

WORKSPACE NSWorkspaceDidActivateApplicationNotification -> {
    NSWorkspaceApplicationKey =
        "<NSRunningApplication: 0x7ff56aa01230 (com.literatureandlatte.scrivener2 - 25588)>";
}
WORKSPACE NSWorkspaceDidDeactivateApplicationNotification -> {
    NSWorkspaceApplicationKey =
        "<NSRunningApplication: 0x7ff568d09970 (com.apple.Terminal - 145)>";
}
WORKSPACE NSWorkspaceWillSleepNotification -> (null)
WORKSPACE NSWorkspaceDidWakeNotification -> (null)
WORKSPACE NSWorkspaceDidTerminateApplicationNotification -> {
    NSApplicationName = "backupd-helper";
    NSApplicationPath =
        "/System/Library/CoreServices/backupd.bundle/Contents/Resources/backupd-helper";
    NSApplicationProcessIdentifier = 25947;
    NSApplicationProcessSerialNumberHigh = 0;
    NSApplicationProcessSerialNumberLow = 3703688;
    NSWorkspaceApplicationKey =
        "<NSRunningApplication: 0x7ff568e12da0 ((null) - 25947)>";
    NSWorkspaceExitStatusKey = 0;
}

You can see I juggled from Scrivener (where I’m writing this posting) to the Terminal (where I’m working on the code and running it), the system going to sleep and waking up, and then the backup helper exiting.

If you’re having trouble seeing workspace notifications, you might need to do the hack I outlined earlier. You can get the code to spy on notifications from a command-line tool at this gist, and you’re welcome to paste it into your own applications to spy on the notifications flying around.

(Tune in Thursday for the final part, gotchas.)

Mark Dalrymple

Author Big Nerd Ranch

MarkD is a long-time Unix and Mac developer, having worked at AOL, Google, and several start-ups over the years.  He’s the author of Advanced Mac OS X Programming: The Big Nerd Ranch Guide, over 100 blog posts for Big Nerd Ranch, and an occasional speaker at conferences. Believing in the power of community, he’s a co-founder of CocoaHeads, an international Mac and iPhone meetup, and runs the Pittsburgh PA chapter. In his spare time, he plays orchestral and swing band music.

Speak with a Nerd

Schedule a call today! Our team of Nerds are ready to help

Let's Talk

Related Posts

We are ready to discuss your needs.

Not applicable? Click here to schedule a call.

Stay in Touch WITH Big Nerd Ranch News