A user may have several RaiseMan documents open when he decides that it is too hard to read them with a purple background. The user opens the preferences panel, changes the background color, but then is disappointed to find that the color of the existing windows doesn't change. When the user sends you an e-mail about this problem, you reply, “The defaults are read only when the document window is created. Just save the document, close it, and open it again.” In response, the user sends you a mean e-mail. It would be better to update all of the existing windows. But how many are there? Will you have to keep a list of all open documents?
The task is actually much easier than that. Every running application has an instance of NSNotificationCenter
. The NSNotificationCenter
functions much like a bulletin board. Objects register as interested in certain notifications (“Please write me if anyone finds a lost dog”); we call the registered object an observer. Other objects can then post notifications to the center (“I have found a lost dog”). That notification is subsequently forwarded to all objects that are registered as interested. We call the object that posted the notification a poster.
Lots of standard Cocoa classes post notifications: Windows send notifications that they have changed size. When the selection of a table view changes, the table view sends a notification. The notifications sent by standard Cocoa objects are listed in the online documentation.
In our example, you will register all of your MyDocument
objects as observers. Your preference controller will post a notification when the user chooses a new color. When sent the notification, the MyDocument
objects will change the background color.
Before the MyDocument
object is deallocated, you must remove it from the notification center's list of observers. Typically, this is done in the dealloc
method.
When programmers first hear about the notification center, they sometimes think it is a form of interprocess communications. They think, “I will create an observer in one application and post notifications from an object in another.” This scheme doesn't work, however: A notification center allows objects in an application to send notifications to other objects in that same application. Notifications do not travel between applications.
Notification objects are very simple. A notification is like an envelope into which the poster will place information for the observers. It has two important instance variables: name
and object
. Nearly always, object
is a pointer to the object that posted the notification. (It is analogous to a return address.)
Thus, the notification also has two interesting methods:
- (NSString *)name - (id)object
The NSNotificationCenter
is the brains of the operation. It allows you to do three things:
Register observer objects.
Post notifications.
Unregister observers.
Here are some commonly used methods implemented by NSNotificationCenter
:
Returns the notification center.
- (void)addObserver:(id)anObserver selector:(SEL)aSelector name:(NSString *)notificationName object:(id)anObject
Registers anObserver
to receive notifications with the name notificationName
and containing anObject
(Figure 11.1). When a notification of the name notificationName
containing the object anObject
is posted, anObserver
is sent an aSelector
message with this notification as the argument:
If notificationName
is nil
, the notification center sends the observer all notifications with an object matching anObject
.
If anObject
is nil
, the notification center sends the observer all notifications with the name notificationName
.
The observer is not retained by the notification center. Notice that the method takes a selector.
Posts a notification to the notification center (Figure 11.2).
- (void)postNotificationName:(NSString *)aName object:(id)anObject
Creates and posts a notification.
- (void)removeObserver:(id)observer
Posting a notification is the easiest step, so you will start there. When your PreferenceController
object receives a changeBackgroundColor:
message, it will post a notification with the new color. Name the notification @"BNRColorChanged"
. (Experienced programmers put a prefix on the notification so that it doesn't conflict with other notifications that may be flying around the application.) Make your changeBackgroundColor:
method in PreferenceController.m
look like this:
- (IBAction)changeBackgroundColor:(id)sender { NSColor *color = [sender color]; NSData *colorAsData; colorAsData = [NSKeyedArchiver archivedDataWithRootObject:color]; [[NSUserDefaults standardUserDefaults] setObject:colorAsData forKey:BNRTableBgColorKey]; NSNotificationCenter *nc; nc = [NSNotificationCenter defaultCenter]; NSLog(@"Sending notification BNRColorChanged"); [nc postNotificationName:@"BNRColorChanged" object:self]; }
To register as an observer, you must supply several things: the object that is the observer, the names of the notifications in which it is interested, and the message that you want sent when an interesting notification arrives. You can also specify that you are interested only in notifications with a certain object attached to them. (Remember that this is often the object that posted the notification. Thus, when you specify that you want resize notifications with a certain window attached, you are saying that you are interested only in the resizing of that particular window.)
Edit your MyDocument
class's init
method as follows:
- (id)init { if (self = [super init]) { employees = [[NSMutableArray alloc] init]; NSNotificationCenter *nc; nc = [NSNotificationCenter defaultCenter]; [nc addObserver:self selector:@selector(handleColorChange:) name:@"BNRColorChanged" object:nil]; NSLog(@"Registered with notification center"); } return self; }
Remember to remove your document from the notification center if it is closed. Edit the dealloc
method in MyDocument.m
as follows:
- (void)dealloc { [self setEmployees:nil]; NSNotificationCenter *nc; nc = [NSNotificationCenter defaultCenter]; [nc removeObserver:self]; NSLog(@"Unregistered with notification center: %@", [self fileName]); [super dealloc]; }
When the notification arrives, the method handleColorChange:
is called. Add this method to your MyDocument.m
file:
- (void)handleColorChange:(NSNotification *)note { NSLog(@"Received notification: %@", note); PreferenceController *sender = [note object]; NSColor *newColor = [sender tableBgColor]; // Due to bug in 10.3.0, this change will not be visible // on some systems unless the table view has at least one row. [tableView setBackgroundColor:newColor]; [tableView setNeedsDisplay:YES]; }
Build and run the application. Open several windows and change the preferred background color. Note that all of them receive the notification and change color immediately.
If an object has made itself the delegate of some standard Cocoa object, it is probably interested in receiving notifications from that object as well. For example, if you have implemented a delegate to handle the windowShouldClose:
delegate method for a window, that same object is likely to be interested in the NSWindowDidResizeNotification
from that same window.
If a standard Cocoa object has a delegate and posts notifications, the delegate is automatically registered as an observer for the methods it implements. If you are implementing such a delegate, how would you know what to call the method?
The naming convention is simple: Start with the name of the notification. Remove the “NS” from the beginning and make the first letter lowercase. Remove the “Notification” from the end. Add a colon. For example, to be notified that the window has posted an NSWindowDidResizeNotification
, the delegate would implement the following method:
- (void)windowDidResize:(NSNotification *)aNotification
This method will be called automatically after the window resizes. You can also find this method listed in the documentation and header files for the class NSWindow
.
If you wanted to include more than just the poster with the notification, you would use the user info dictionary. Every notification has a variable called userInfo
that can be attached to an NSDictionary
filled with other information that you want to pass to the observers:
NSDictionary *d = [NSDictionary dictionaryWithObject:@"867-5309" forKey:@"phoneNumber"]; NSNotification *n = [NSNotification notificationWithName:@"BNRFoo" object:self userInfo:d]; [[NSNotificationCenter defaultCenter] postNotification:n];
On the other end, the receiver of the notification can use the data in the userInfo
dictionary:
- (void)myMethod:(NSNotification *)note { NSString *thePhoneNumber; thePhoneNumber = [[note userInfo] objectForKey:@"phoneNumber"]; }
Just as you defined global variables to hold onto constant strings in Chapter 10 so that the compiler would catch your misspellings, most programmers define global variables for the names of their notifications and the keys in their user info dictionary.
Make your application beep when it gives up its active status. NSApplication
posts an NSApplicationDidResignActiveNotification
notification. Your AppController
is a delegate of NSApplication
. NSBeep()
will cause a system beep.