Graphical interfaces are driven by user events— mouse clicks and keystrokes. Most of an application’s time is spent waiting for the user to tell the application what to do next. However, a running application can also receive events not originating from the user interface, such as packets arriving over a network interface or timers firing periodically. In Cocoa, both types of events result in a message sent to an object in your application, as depicted in Figure 8-1.
This chapter focuses on events—both user- and program-generated—and how you intercept, handle, and coordinate them in Cocoa.
Events in Cocoa are represented by instances of the
NSEvent
class. An event object can be queried to
discover its window, the location of the event within the window, and
the time the event occurred (relative to system startup). You can
also find out which, if any, modified keys (such as Command, Shift,
Option, and Control) were pressed. An event also contains the type of
event it represents. There are many event types, falling primarily
into the following categories:
Generated
when a key is pressed or released or when a
modified key changes. Of these event types, key-down events are the
most useful. When you handle a key-down event, you can determine the
character or characters associated with the event by calling the
characters
method on the event object.
Generated by changes in the state of the mouse button (down and up) and during mouse dragging. Optionally, mouse events can also be generated when the mouse moves without any button depressed.
Generated by the window server when the mouse enters or exits a programmatically set area (a tracking rectangle) in a window. This lets an application change either the cursor or the content that the mouse is currently over. For example, when you move the mouse over the window control buttons, you generate events triggered by the mouse entering and exiting a rectangular region around the control. This lets the control highlight itself. Also, an application usually changes the cursor to an I-beam when the mouse is moved over editable text.
Generated by timers to notify an application that a certain time interval has elapsed. An application can request that periodic events be placed in its event queue at a certain event frequency. This is useful for applications that want to perform some task at regular intervals, such as updating the frames of an animation or checking email every few minutes.
Every
application has a central object named
NSApp
, which is an instance of the
NSApplication
class. At the core of its responsibilities is the management of the
run loop
. A run loop monitors
the various sources of events and decides which object is responsible
for handling each event. It then sends a message, passing to the
object an NSEvent
object instance to describe the
particulars of the event. The event message passes from
NSApp
to the appropriate window, to a view
(commonly a control) within the window, and eventually to the target
object.
This is how a button “knows” that
it has been clicked. The application forwards mouse click events to
it. The button object can then either process the event directly or,
more commonly, pass it on to a custom object that you define through
the target/action pattern or delegation. When the handling objects
are finished responding to the message, control unwinds and returns
to NSApp
, where the run loop processes the next
event.
This cycle, also known as the event cycle, usually starts at launch time when the application sends a stream of Quartz commands to the window server for it to draw the application interface. The application then begins its main run loop and begins accepting input from the user. When users click or drag the mouse or type on the keyboard, the window server detects and processes these actions, passing them to the application as events.
Events sent to the application by the window server are placed on a
queue in the order they are received. On each cycle of the run loop,
NSApp
processes the topmost event in the queue, as
shown in Figure 8-2. When NSApp
finishes processing the event, it gets the next event from the queue
and repeats the process again and again until the application
terminates.
Recall
that when we introduced the core program framework of
NSWindow
, NSView
, and
NSApplication
in Chapter 6, we
mentioned that each of these classes inherits functionality from the
NSResponder
class, as shown in Figure 8-3.
The NSResponder
class defines the default
message-handling behavior of all event-handling objects in an
application. The responder model is built around the following three
concepts:
Messages that correspond directly to an input event, such as a mouse click or a key press.
Messages
describing a higher-level command to be performed, such as
cut:
or paste:
.
A series of responder objects to which an event or action message is applied. When a given responder object doesn’t handle a message, that message is passed to the next object in the chain.
Responder chains allow responder objects to delegate responsibility
to other objects in the system. The series in a responder chain is
determined by the interrelationships between the
application’s view, window, and its
NSApp
object. For a view, the next responder is
usually its superview. The next responder of a
window’s content view is the window itself. From
there, the event is passed to the NSApp
object.
For action messages, the responder chain is longer. Messages are
first passed up the responder chain to the window. Then, if the
previous sequence occurred in the key window, the same path is
followed for the main window. After that, the message is passed to
the NSApp
object.
Each window in an application keeps track of the object in its view hierarchy with first responder status, which is the view that first responds to keyboard events for that window. For example, in TextEdit, the new document window is the first responder, as it is the first to receive events from the keyboard. By default, a window is its own first responder, but any view within the window can become the first responder when the user clicks it with the mouse. If the view cannot handle the event, the event is passed on to the next object in the responder chain.
In a desktop environment where multiple windows can be open at any given time, a user selects a window with the mouse to make it active. When this happens, that window becomes key, and the window’s first responder becomes the target of any events generated by the user. If a different window is selected, it becomes key, and its first responder becomes current. If no object has been selected, or if the window has no controls, the window is its own first responder.
Using Interface Builder, or programmatically, you can configure an
initialFirstResponder
so that when a window
appears, the first logical control capable of using keystrokes is
brought into focus as the first responder. Recall that in Chapter
5’s Currency Converter application, we set the first
text field to be the initial first responder.
Views and controls can reject first responder status. For example, a
view displaying a static image probably shouldn’t
accept first responder status. A responder can indicate that it
doesn’t want to accept first responder status by
implementing the
acceptFirstResponder
method and returning
NO
.
An
event is
routed based on its type. The NSApp
object sends
most event messages to the window in which the user action occurred.
A mouse event is then forwarded to the view in the
window’s view hierarchy within which the mouse was
clicked. Key events are routed to the first responder. If the view
can respond to the event—that is, if it can accept first
responder status and define an NSResponder
method
corresponding to the event message—it handles the event. If the
view cannot handle an event, the event is forwarded to the next
responder in the responder chain.
The NSWindow
class handles some events itself, such as window-moved,
window-resized, and window-exposed events, and
doesn’t forward them to a view.
NSApp
also processes a few events, such as application-activate and
application-deactivate events.
To
illustrate
event handling, we’ll build an application using a
custom NSView
subclass that responds to a mouse
click by drawing a colored dot. Working through this example will let
you see how custom event handling works, while reinforcing the work
we did with NSBezierPath
in the last chapter. In
addition, we’ll use the Slider and Color Well
controls for the first time.
In Project Builder, create a new Cocoa Application project (File → New Project → Application → Cocoa Application) named “Dot View”, and save it in your ~/LearningCocoa folder. Then open the MainMenu.nib file in Interface Builder.
Creating the
functionality of this application will utilize many of the same
skills presented in previous chapters. We will start shortening the
descriptions of how to perform tasks that we’ve
performed before so that we can provide more details on new topics as
they are introduced. If you can’t remember how to do
something, you should use the procedures presented in previous
chapters to help you out. Create the DotView
class
using the following steps:
Create a subclass of NSView
called
DotView
.
Open the Show Info window (Tools → Show Info).
Add an outlet named colorWell
to
DotView
using the Info window, and set its type to
NSColorWell
using the drop-down box in the Type
column of the Outlet display, as shown in Figure 8-4.
Add an outlet named slider
to
DotView
, and set its type to
NSSlider
.
Add an action named setRadius:
to
DotView
.
Add an action named setColor:
to
DotView
.
Click on the DotView
subclass in the Classes tab,
and generate its source files (Classes → Create
Files for Dot View).
Save the project (File → Save, or
-S).
To create our interface, perform the following steps:
Change the title of the main window to “Dot View”. To do this:
Click on the main window, and the title of the Show Info window should change to “NSWindow Info”.
Select Attributes from the pull-down menu.
In the Window Title field, change “Window” to “Dot View”.
Drag a CustomView view from the Containers Palette onto the Dot View window.
Assign the DotView
class to the CustomView.
Click on the CustomView view.
In the Info window, scroll up and select DotView from the Class list; the name of the view will change from CustomView to DotView.
Drag a Slider from the Other Palette onto the Dot View window, and place it as shown in Figure 8-5.
Drag a Color Well from the Other Palette onto the Dot View window, and place it as shown in Figure 8-5.
Create the following connections using Interface Builder.
Control-drag a connection from the slider to the
DotView
. Make the target/action connection to the
setRadius:
method in the Connections pane of the
Info window.
Control-drag a connection from the color well to the
DotView
. Make the target/action connection to the
setColor:
method.
Control-drag a connection from the DotView
to the
slider. Make the outlet connection to the slider
outlet.
Control-drag a connection from the DotView
to the
color well. Make the outlet connection to the
colorWell
outlet.
Save (
-S) the nib file, and return to Project Builder, where we will finish the application.
Our next step is to finish defining the DotView.h header file. Edit the source to match that of Example 8-1. The code that you need to add to the Interface Builder code is shown in boldface.
This code defines the following functionality:
Defines an NSPoint
structure that
we’ll use to store the center of the dot that will
be drawn
Defines the color of the dot
Defines the radius for the dot
Now that we have defined the header,
it’s time to add the code for the implementation of
the DotView
class. The code for this class is too
long to fit nicely on one page, so we’re going to
approach this in two steps. First, Example 8-2 shows
the skeleton of our class, with all the methods to implement shown in
boldface. As you can see, you will be asked to insert code from later
examples as we build the Dot View application. The sections that
follow will provide the necessary code, along with explanations of
what that code will do.
First, enter the boldface text from Example 8-2 into your DotView.m file, and insert the appropriate code from Example 8-3 through Example 8-10 as you work through the following sections. A complete version of how your DotView.m file should look is shown in Example 8-11.
#import "DotView.h" @implementation DotView - (id)initWithFrame:(NSRect)frame { // Insert code from Example 8-3 } - (void)awakeFromNib { // Insert code from Example 8-4 } - (void)dealloc { // Insert code from Example 8-5 } - (void)drawRect:(NSRect)rect { // Insert code from Example 8-6 } - (BOOL)isOpaque { // Insert code from Example 8-7 } - (void)mouseDown:(NSEvent *)event { // Insert code from Example 8-8 } - (IBAction)setColor:(id)sender{
// Insert code from Example 8-9}
- (IBAction)setRadius:(id)sender{
// Insert code from Example 8-10}
@end
The
initWithFrame:
method is the designated initializer for NSView
and its subclasses (see Section 3.6.2.1 in Chapter 3 for a refresher of what this means). Add the
initWithFrame:
method, as shown in Example 8-3.
This code performs the following tasks:
Starts the initialization process by calling the designated
initializer of the NSView
class.
Sets the x-coordinate of the center of the dot
to 50.0
.
Sets the y-coordinate of the center of the dot
to 50.0.
Sets the radius of the dot to 20.0
.
Sets the initial color in which the dot will be drawn to red. This line also retains the color so that it will be able for use later. Refer back to Section 4.3 in Chapter 4 if necessary.
Returns self
, the newly initialized view object.
As introduced in Chapter 6, the
awakeFromNib:
method is called when the interface
has been fully unpacked from the nib file and all connections have
been made. At this point, we want to set the initial state of the
slider and color well controls. Add the
awakeFromNib:
method as shown in Example 8-4.
This code performs the following tasks:
Sets the color of the color well to the color set up in the initializer. In this case, the default color will be set to red. (For reference, see line 5 in Example 8-3.)
Sets the initial value of the slider to the radius we defined in the initializer (line 4 from Example 8-3).
When this method completes, the application will be displayed to the user.
The dealloc
method is called when the view is
disposed of, giving it a chance to clean up its memory usage. Add the
dealloc
method as shown in Example 8-5.
This code performs the following tasks:
Releases the NSColor
object that
we’ve retained in the object instance
Calls the dealloc
method of the parent
NSView
class so that any cleanup performed by the
super
class can be performed
The drawRect:
method is where the view draws itself to
the screen. Add the drawRect:
method as shown in
Example 8-6.
- (void)drawRect:(NSRect)rect { NSRect dotRect; // 1 // Draw the background [[NSColor whiteColor] set]; // 2 NSRectFill([self bounds]); // Set the location of the dot dotRect.origin.x = center.x - radius; // 3 dotRect.origin.y = center.y - radius; // Define the size the dot dotRect.size.width = 2 * radius; // 4 dotRect.size.height = 2 * radius; // Set the default color [color set]; // 5 // Draw the dot [[NSBezierPath bezierPathWithOvalInRect:dotRect] fill]; // 6 }
The code performs the following tasks:
Declares an NSRect
structure that defines the
rectangle into which our dot will be drawn.
Sets the current drawing color to white
(whiteColor
) and draws the background of the view.
Determines the origin of the rectangle into which our dot will be
drawn. Because we have to determine the rectangle of the dot by
specifying its origin rather than its center, and we want the center
of the dot to be the location of our center
NSPoint
, we have to offset the origin
appropriately. This code finds a point offset towards the origin of
the view’s coordinate system that will place the
dot’s center exactly where the user clicked.
Defines the size of the rectangle into which our dot will be drawn.
Since the code in step 3 determined the lower-left corner of the
rectangle, we simply need to size the rectangle to be the diameter
(2 * radius
) of the dot.
Sets the current color of DotView
to the active
drawing color, based on the color we defined in line 5 of Example 8-3.
Creates an oval Bezier path inside the dotRect
rectangle and fills it with the default color.
The Quartz graphics
engine is designed to draw multiple layers of content quickly with
various levels of transparency. However, no matter how much
performance the engineers at Apple manage to squeeze out of the code,
the Quartz engine can operate faster if it knows that a view
doesn’t need to be composited with its background.
The isOpaque
method of NSView
lets this optimization be performed. Add the
isOpaque
method as shown in Example 8-7.
If we return NO
to this method, Quartz will
composite anything drawn by our view with the contents of views
behind it. Since we return YES
, Quartz
doesn’t need to perform this operation and can save
a bit of time.
Overriding NSResponder
methods in a view is the
best way to handle events for the view. One such method is
mouseDown:
, which is invoked when the user
presses the mouse button. All of the NSResponder
event-handling methods receive an NSEvent
object
instance as an argument. This event contains the mouse location where
the click occurred in the coordinate system of the window.
Add an implementation of this method to match, as shown in Example 8-8.
This code performs the following tasks:
Gets the location of the mouse click from the event. The location of a mouse click is expressed in terms of the coordinate system of the window in which the click occurred.
Converts the location of the event from the window coordinate system
to the coordinate system of the view. When called with a
nil
view parameter, it translates the point from
the window coordinate system to which the view belongs. If you call
this method with a view object, the coordinates will be converted
into the coordinate system of that view. In this case, we need the
coordinates converted from the coordinate system of the window.
Sets a flag indicating that the view needs to be redrawn. The redraw
will be done automatically by the NSApp
object
after the event is handled and the run loop has exited our code.
This method is called by the color well
whenever the user changes the color of the dot. This method assumes
that the sender is a control capable of returning a color. Edit the
setColor:
method as shown in Example 8-9.
- (IBAction)setColor:(id)sender { NSColor * newColor = [sender color]; // 1 [newColor retain]; // 2 [color release]; // 3 color = newColor; // 4 [self setNeedsDisplay:YES]; // 5 }
This code performs the following tasks:
Sets the newColor
variable to the color obtained
from the sender
of the event.
Retains the newColor
object.
Releases the old color
object. Remember, as
presented in Chapter 4, that we release the old
object after retaining the new one so that there will be no problems
if the two objects are actually the same.
Sets the color
variable to the
newColor
object.
Sets a flag indicating that the view needs to be redrawn to display the new color defined by the user. The redraw will be done automatically by the AppKit after the event is handled.
This method is called
by the slider whenever the user moves the slider left or right to
change the size of the dot. This method assumes that the send is a
control capable of returning a floating-point number. Edit the
setRadius:
method as shown in Example 8-10.
This code performs the following tasks:
Sets the radius
variable to a float value
(floatValue
) obtained from the slider.
Sets a flag indicating that the view needs to be redrawn to display the newly resized dot, based on the movement of the slider. The redraw will be done automatically by the AppKit after the event is handled.
When you’ve completed entering all of the code from Example 8-2 through Example 8-10, your DotView.m file should look like the code shown in Example 8-11.
#import "DotView.h" @implementation DotView - (id)initWithFrame:(NSRect)frame { self = [super initWithFrame:frame]; center.x = 50.0; center.y = 50.0; radius = 20.0; color = [[NSColor redColor] retain]; return self; } - (void)awakeFromNib { [colorWell setColor:color]; [slider setFloatValue:radius]; } - (void)dealloc { [color release]; [super dealloc]; } - (void)drawRect:(NSRect)rect { NSRect dotRect; [[NSColor whiteColor] set]; NSRectFill([self bounds]); dotRect.origin.x = center.x - radius; dotRect.origin.y = center.y - radius; dotRect.size.width = 2 * radius; dotRect.size.height = 2 * radius; [color set]; [[NSBezierPath bezierPathWithOvalInRect:dotRect] fill]; } - (BOOL)isOpaque { return YES; } - (void)mouseDown:(NSEvent *)event { NSPoint eventLocation = [event locationInWindow]; center = [self convertPoint:eventLocation fromView:nil]; [self setNeedsDisplay:YES]; } - (IBAction)setColor:(id)sender { NSColor * newColor = [sender color]; [newColor retain]; [color release]; color = newColor; [self setNeedsDisplay:YES]; } - (IBAction)setRadius:(id)sender { radius = [sender floatValue]; [self setNeedsDisplay:YES]; } @end
Once your DotView.m file is complete, save the project (File → Save, or
-S), and then build and run the Dot View application (Build → Build and Run, or
-R). You should see something that looks like Figure 8-6.
Perform the following actions on the application to see if the code that we added works:
Click anywhere in the view, and you’ll see the dot
move to the point that you clicked. Each time you click the mouse, a
mouseDown
event is sent to the view, resulting in
the mouseDown:
method being called.
Move the slider left and right, and watch the size of the dot get smaller or bigger, respectively. Notice that the slider issues events to the application as it moves, allowing you to see the results of the action dynamically.
Click on the color well, and pick a new color for the dot from the palette that appears. Just like the slider, the color well responds dynamically.
Note that when you resize the application, the view doesn’t autosize as we’d probably like. Exercise 6 at the end of this chapter (Section 8.5) adds this functionality.
In an object-oriented application, an object often must know what’s going on with other objects in the system. One of the patterns used extensively in Cocoa is delegation. Think of delegation as a means by which an object’s behavior can be modified without needing to create a custom subclass.
A delegate is a helper object that receives messages from another object when specific events occur. An object sends requests to its delegate, allowing the delegate to influence its behavior and aid in decision-making.
For an object to delegate responsibility, it must declare a
delegate
outlet,
along with a set of delegate messages that will be sent to it when
“interesting” things happen. To
become a delegate, an object must implement one or more of the
delegate methods. There are several types of delegation messages,
depending on the expected role of the delegate:
Some messages are purely informational, occurring after an event has happened. These allow a delegate to coordinate its actions with the delegating object.
Some messages are sent before an action will occur, allowing the delegate to veto or permit the action.
Other delegation messages assign a specific task to a delegate, such as filling a browser with cells.
As an example, take a child who is told by a friend to act silly. Depending on the circumstances, he may or may not do as his friend suggests. If his parents are around, the child might ask his parents (or at least glance at one of the parents to see if they are looking) if he should act silly before doing so. In this case, the child is delegating the decision of whether to act silly in front of his parents. The parent then has the opportunity to approve or deny the request to act silly. Figure 8-7 shows this relationship, albeit abstractly.
You can set a custom object as the delegate of a Cocoa framework
object by making a connection in Interface Builder, or you can set it
programmatically by using the
setDelegate:
method. Your custom classes can also
define their own delegate variables and delegation protocols for
client objects. Just remember that delegates are not retained by the
objects that delegate messages to them.
To show delegation in action, we will
modify our Dot View application to respond to a request to close the
application’s window. We will create and add a
delegate that, when a window sends a
windowShouldClose
message to it, will create an
alert box asking the user if it is okay to close. To create the alert
box, Cocoa provides the NSRunAlertPanel
function.
This function has the following signature:
int NSRunAlertPanel (NSString * title, NSString * message, NSString * defaultButtonLabel, NSString * alternateButtonLabel, NSString * otherButtonLabel, ...) // printf style args for message
Table 8-1 provides a brief summary of the parameters for this function.
Open the Dot View project in Project Builder, if you don’t already have it open, and open the MainMenu.nib file in Interface Builder. Perform the following steps to create a delegate class and to assign an instance of it as a delegate to the window:
Create a new subclass of NSObject
called
MyDelegate
.
Instantiate MyDelegate
(Classes →
Instantiate MyDelegate).
In the Instances pane of the MainMenu.nib
window, Control-drag a connection from the Window icon to the
MyDelegate
object icon, as shown in Figure 8-8.
Make the connection to the delegate outlet of the window by clicking on the Connect button in the Info window.
Save the nib file (
-S).
Create the files for the MyDelegate
class, and add
them to the project.
Click on the Classes tab in the MainMenu.nib window.
Select MyDelegate.
Select Classes → Create Files for MyDelegate from the menu bar.
Return to Project Builder, and edit MyDelegate.m to match the code in Example 8-12.
The code we added performs the following tasks:
Implements the
windowShouldClose:
method. When a window has a delegate
that implements this method, it asks the delegate if it should close
before doing so.
Calls the NSRunAlertPanel
function, which will
open an alert dialog box that asks the user’s
permission to close the window.
Returns YES
or NO
depending on
the result from the alert dialog box. If the alert dialog box returns
a value that matches the constant
NSAlertDefaultReturn
, then the window will be
closed.
Now save the project (
-S), and build and run the application (
-R). When you
try to close the application window, you will see something that
looks like Figure 8-9. Notice what happens when you
hit the Cancel button on the alert box. Notice that the alert only
comes up when you close the window. If you quit the application, the
alert panel will not appear. A different delegate method, the
applicationShouldTerminate:
method of the
NSApplication
class, is needed to enable this
functionality. Exercise 4 at the end of the chapter (Section 8.5) will do so.
Another example of delegation appears in the implementation of Aqua’s sheets—a new type of dialog box that is attached to a document window’s titlebar. Sheets slide out from the window title, making their relationship to a document clear. Sheets are modal only for the window to which they are attached, so you can proceed to other tasks in an application before dismissing them.
Adding support for sheets is more complicated than using a standard
dialog box, because the function that displays an alert
sheet—NSBeginAlertSheet
— is
asynchronous. In other words, it does not wait for the user to
dismiss the sheet before returning control to the caller. Instead, it
returns control immediately after presenting the sheet. To discover
the result of the user’s interaction with the sheet,
you must pass a reference to a delegate object, along with a method
selector to invoke as a callback when the sheet is dismissed. When
the sheet finished, the callback method will be invoked and passed a
result code, indicating which button the user pressed.
The
NSBeginAlertSheet
function has the following signature:
void NSBeginAlertSheet(NSString * title, NSString * defaultButtonLabel, NSString * alternateButtonLabel, NSString * otherButtonLabel, NSWindow * docWindow, id modalDelegate, SEL didEndSelector, SEL didDismissSelector, void * contextInfo, NSString * message, ...) // printf args for message
This looks a bit daunting at first, but it’s really
not as difficult to use as it might look. Table 8-2 provides a brief parameter summary for
NSBeginAlertSheet
.
Edit the MyDelegate.m code, replacing the
windowShouldClose:
method and adding the
sheetClosed:
method, as shown in Example 8-13.
#import "MyDelegate.h" @implementation MyDelegate - (BOOL)windowShouldClose:(NSWindow *)sender { NSString * msg = @"Should this window close?"; // 1 SEL sel = @selector(sheetClosed:returnCode:contextInfo:); // 2 NSBeginAlertSheet(@"Close", // title // 3 @"OK", // default label @"Cancel", // alternate button label nil, // other button label sender, // document window self, // modal delegate sel, // selector to method NULL, // dismiss selector sender, // context info msg, // message nil); // params for msg string return NO; // 4 } - (void)sheetClosed:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo { if (returnCode == NSAlertDefaultReturn) { // 5 [(NSWindow *)contextInfo close]; } } @end
This code performs the following tasks:
Creates a string that will be displayed in the sheet.
Obtains a selector to the method that calls the sheet back when
finished. In this case, we want the
sheetClosed:returnCode:contextInfo:
method (which
we define in step 5) called.
Calls the NSBeginAlertSheet
function with a whole
set of arguments describing what the sheet should display and what
object and methods it should call when it is done.
Returns NO
so that the
application’s run loop can continue. The window will
remain open, but a sheet will be attached to it.
Checks the value returned using the equality (==
)
operator from the sheet — which checks to see if two values are
equal to each other — and closes the window or not, depending
on its value.
Too often, newcomers to C (and many not-so-newcomers) make the error
of using the
assignment operator
(=
) when they mean to use the
equality
(==
) operator. Using the assignment operator in a
check like this will usually result in an expression that is legal,
but will not work as expected. Such errors can be subtle and hard to
catch.
Save the project (
-S), and then build and run the application (
-R). Now when you try to close the window (
-W), you should see something like Figure 8-10.
Another way to communicate events between objects in Cocoa is via a notification. A notification is a message broadcast to all objects in an application that are interested in the event that the notification represents.
Notifications can also pass along relevant data about the event. Notifications differ from delegation in that notification happens after the object has performed the action instead of before. The object receiving a notification doesn’t get a chance to say whether or not an action will be taken. Also, an object can have many notification observers, but only one delegate.
Using our child/friend/parent example again, once the child has acted silly, there might be a set of friends who will want to know about it. Through notification, our child can tell his friends that he acted silly, as shown in Figure 8-11.
It would be impractical for everyone who wanted to know that the child had acted silly to register her interest directly with the child. The child would have to implement the functionality needed to keep track of all the interested friends and send notifications to them in turn. Luckily, Cocoa has provided a set of classes to help us with this. Here’s the way the notification process, shown in Figure 8-12, works in Cocoa:
Objects interested in an event that
happens elsewhere in an application—say, the addition of a
record to a database—register themselves with a notification
center (an instance of the
NSNotificationCenter
class) as observers of that event. During
the registration process, the observer specifies that a method should
be invoked by the notification center when the event occurs.
The object that adds the record to the database (or some such event)
posts a notification (an instance of the
NSNotification
class) to the notification center.
The notification object contains a tag that identifies the
notification, the ID of the object posting the notification, and,
optionally, a dictionary of supplemental data.
The notification center then sends a message to each registered observer, invoking the method specified by each observer and passing the notification.
A class that posts notifications defines the names of those
notifications in its header file as static
NSString
objects. For example, the
NSWindow
class defines a set of 16 notifications in its header file that allow
other objects to monitor changes in the window’s
status:
APPKIT_EXTERN NSString * NSWindowDidBecomeKeyNotification; APPKIT_EXTERN NSString * NSWindowDidBecomeMainNotification; APPKIT_EXTERN NSString * NSWindowDidChangeScreenNotification; APPKIT_EXTERN NSString * NSWindowDidDeminiaturizeNotification; APPKIT_EXTERN NSString * NSWindowDidExposeNotification; APPKIT_EXTERN NSString * NSWindowDidMiniaturizeNotification; APPKIT_EXTERN NSString * NSWindowDidMoveNotification; APPKIT_EXTERN NSString * NSWindowDidResignKeyNotification; APPKIT_EXTERN NSString * NSWindowDidResignMainNotification; APPKIT_EXTERN NSString * NSWindowDidResizeNotification; APPKIT_EXTERN NSString * NSWindowDidUpdateNotification; APPKIT_EXTERN NSString * NSWindowWillCloseNotification; APPKIT_EXTERN NSString * NSWindowWillMiniaturizeNotification; APPKIT_EXTERN NSString * NSWindowWillMoveNotification; APPKIT_EXTERN NSString * NSWindowWillBeginSheetNotification; APPKIT_EXTERN NSString * NSWindowDidEndSheetNotification;
An object that wants to receive one of these notifications must use the notification name when registering with the notification center.
To show how to work with notifications, we will add some
functionality to our Dot View application delegate to listen to
notifications that NSWindow
generates when sheets
are used. Edit the MyDelegate.m file, adding the
code shown in Example 8-14. The order of methods in a
source-code file doesn’t matter; however, you
usually see init
methods towards the top of a
class implementation.
- (id)init { NSNotificationCenter * center = [NSNotificationCenter defaultCenter]; // 1 [center addObserver:self // 2 selector:@selector(sheetDidBegin:) name:NSWindowWillBeginSheetNotification object:nil]; return self; } - (void)sheetDidBegin:(NSNotification *)notification { NSLog(@"Notification: %@", [notification name]); // 3 }
The code that we added performs the following functionality:
Obtains the default notification center for the application. All applications have a default notification center available to them.
Adds the MyDelegate
instance object as an observer
requesting that the sheetDidBegin:
method be
called whenever an
NSWindowWillBeginSheetNotification
is received by
the notification center. Passing nil
as the object
parameter indicates that our object is interested in notifications
from any window. If we want to limit the notifications to just a
particular window, we can pass a reference to that window here.
Logs a message whenever the sheetDidBegin:
method
is called.
Save the project (
-S), and then build and run the application (
-R). When you run Dot View and try to close the window, the sheet will appear with the following message in the console pane of Project Builder:
2002-04-01 12:07:47.837 Dot View[800] Notification: NSWindowWillBeginSheetNotification
Obviously, you wouldn’t display notifications like this to users of your applications. Rather, you would use them inside your application to coordinate various activities.
Notification centers do not retain
observer objects, so you should be careful to remove any observers
before they are deallocated. This is to prevent the notification
center from sending a message to an object that no longer exists.
MyDelegate
registers itself as an observer in its
init
method, so the object should remove itself in
its dealloc
method. Add the
dealloc
method implementation to
MyDelegate.m, as shown in
Example 8-15.
If an object registers another object with the notification center without releasing it after removing it from the notification center, the application will leak memory. Therefore, any object that registers itself, or another object, with the notification center should remove itself or the registered object from the notification center before it is deallocated.
Allow the dot in the Dot View application to follow a mouse drag so
that the user can interactively place the dot. Hint: use the
mouseDragged:
method defined in the
NSResponder
class.
Set the initial position of the dot in the Dot View application to the center of the view instead of the coordinates (50.0,50.0).
Optimize the setRadius:
method so that
setNeedsDisplay
is only called if the original
value and the new value for radius
variable are
not the same.
Implement the NSApplication
delegate method of
applicationShouldTerminate:
to display a
confirmation dialog box when the user tries to quit the application.
Change the initial color of the dot from red to some other color.
Change the Dot View Application so the size of the view changes if the user resizes (or maximizes) the window. What happens to the slider and color well when the window is resized?