Learning Cocoa with Objective-C/Single-Window Applications/Event Handling

From WikiContent

Jump to: navigation, search
Learning Cocoa with Objective-C

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.

Figure 8-1. A Cocoa application receiving events

A Cocoa application receiving events

This chapter focuses on events—both user- and program-generated—and how you intercept, handle, and coordinate them in Cocoa.

Contents

Events

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:

Keyboard events
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.
Mouse events
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.
Tracking-rectangle events
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.
Periodic events
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.

The Event Cycle

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.

Figure 8-2. The event queue

The event queue

Responders

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.

Figure 8-3. The core application framework

The core application framework

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:

Event messages
Messages that correspond directly to an input event, such as a mouse click or a key press.
Action messages
Messages describing a higher-level command to be performed, such as cut: or paste:.
Responder chain
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.

First Responder

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.

Event Routing

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.

Dot View Application

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 ~/LearningCocoafolder. Then open the MainMenu.nibfile in Interface Builder.

Create the DotView Class

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:

  1. Create a subclass of NSView called DotView.
  2. Open the Show Info window (Tools → Show Info).
  3. 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.

    Figure 8-4. Adding the colorWell and slider outlets to DotView

    Adding the colorWell and slider outlets to DotView

  4. Add an outlet named slider to DotView, and set its type to NSSlider.
  5. Add an action named setRadius: to DotView.
  6. Add an action named setColor: to DotView.
  7. Click on the DotView subclass in the Classes tab, and generate its source files (Classes → Create Files for Dot View).
  8. Save the project (File → Save, or [[Image:Learning Cocoa with Objective-C_I_4_tt261.png|]]-S).

Create the Interface

To create our interface, perform the following steps:

  1. Change the title of the main window to "Dot View". To do this:
    1. Click on the main window, and the title of the Show Info window should change to "NSWindow Info".
    2. Select Attributes from the pull-down menu.
    3. In the Window Title field, change "Window" to "Dot View".
  2. Drag a CustomView view from the Containers Palette onto the Dot View window.
  3. Assign the DotView class to the CustomView.
    1. Click on the CustomView view.
    2. In the Info window, scroll up and select DotView from the Class list; the name of the view will change from CustomView to DotView.
  4. Drag a Slider from the Other Palette onto the Dot View window, and place it as shown in Figure 8-5.
  5. Drag a Color Well from the Other Palette onto the Dot View window, and place it as shown in Figure 8-5.

Figure 8-5. The Dot View user interface

The Dot View user interface

Connect the Controls to DotView

Create the following connections using Interface Builder.

  1. 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.
  2. Control-drag a connection from the color well to the DotView. Make the target/action connection to the setColor: method.
  3. Control-drag a connection from the DotView to the slider. Make the outlet connection to the slider outlet.
  4. Control-drag a connection from the DotView to the color well. Make the outlet connection to the colorWell outlet.

Save ([[Image:Learning Cocoa with Objective-C_I_4_tt263.png|]]-S) the nib file, and return to Project Builder, where we will finish the application.

Define the DotView Header

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.

Example 8-1. DotView.h

/* DotView */

#import <Cocoa/Cocoa.h>

@interface DotView : NSView
{
    IBOutlet NSColorWell * colorWell;
    IBOutlet NSSlider * slider;

    NSPoint center;                                                        // 1
    NSColor * color;                                                       // 2
    float radius;                                                          // 3
}
- (IBAction)setColor:(id)sender;
- (IBAction)setRadius:(id)sender;

@end

This code defines the following functionality:

  1. Defines an NSPoint structure that we'll use to store the center of the dot that will be drawn
  2. Defines the color of the dot
  3. Defines the radius for the dot

Define the DotView Class

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.mfile, 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.mfile should look is shown in Example 8-11.

Example 8-2. Skeleton code for DotView.m

#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

Implement the initWithFrame: method

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.

Example 8-3. The designated initializer method

- (id)initWithFrame:(NSRect)frame
{
    self = [super initWithFrame:frame];                                    // 1
    center.x = 50.0;                                                       // 2
    center.y = 50.0;                                                       // 3
    radius = 20.0;                                                         // 4
    color = [[NSColor redColor] retain];                                   // 5
    return self;                                                           // 6
}

This code performs the following tasks:

  1. Starts the initialization process by calling the designated initializer of the NSView class.
  2. Sets the x-coordinate of the center of the dot to 50.0.
  3. Sets the y-coordinate of the center of the dot to 50.0.
  4. Sets the radius of the dot to 20.0.
  5. 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.
  6. Returns self, the newly initialized view object.

Implement the awakeFromNib: method

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.

Example 8-4. Finish setting up the view

- (void)awakeFromNib
{
    [colorWell setColor:color];                                            // 1
    [slider setFloatValue:radius];                                         // 2
}

This code performs the following tasks:

  1. 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.)
  2. 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.

Implement the dealloc method

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.

Example 8-5. DotView deallocation method

- (void)dealloc
{
    [color release];                                                       // 1
    [super dealloc];                                                       // 2
}

This code performs the following tasks:

  1. Releases the NSColor object that we've retained in the object instance
  2. Calls the dealloc method of the parent NSView class so that any cleanup performed by the super class can be performed

Implement the drawRect: method

The drawRect: method is where the view draws itself to the screen. Add the drawRect: method as shown in Example 8-6.

Example 8-6. Drawing the interface

- (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:

  1. Declares an NSRect structure that defines the rectangle into which our dot will be drawn.
  2. Sets the current drawing color to white (whiteColor) and draws the background of the view.
  3. 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.
  4. 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.
  5. Sets the current color of DotView to the active drawing color, based on the color we defined in line 5 of Example 8-3.
  6. Creates an oval Bezier path inside the dotRect rectangle and fills it with the default color.

Implement the isOpaque method

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.

Example 8-7. Telling Cocoa that our view is opaque

- (BOOL)isOpaque
{
    return YES;
}

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.

Implement the mouseDown: method

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.

Example 8-8. Handling mouse-down events

- (void)mouseDown:(NSEvent *)event
{
    NSPoint eventLocation = [event locationInWindow];                      // 1
    center = [self convertPoint:eventLocation fromView:nil];               // 2
    [self setNeedsDisplay:YES];                                            // 3
}

This code performs the following tasks:

  1. 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.
  2. 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.
  3. 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.

Implement the setColor: action method

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.

Example 8-9. Changing the color with which we draw

- (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:

  1. Sets the newColor variable to the color obtained from the sender of the event.
  2. Retains the newColor object.
  3. 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.
  4. Sets the color variable to the newColor object.
  5. 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.

Implement the setRadius: action method

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.

Example 8-10. Changing the size of our dot

- (IBAction)setRadius:(id)sender
{
    radius = [sender floatValue];                                          // 1
    [self setNeedsDisplay:YES];                                            // 2
}

This code performs the following tasks:

  1. Sets the radius variable to a float value (floatValue) obtained from the slider.
  2. 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.

Example 8-11. The complete DotView.m file

#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 [[Image:Learning Cocoa with Objective-C_I_4_tt264.png|]]-S), and then build and run the Dot View application (Build → Build and Run, or [[Image:Learning Cocoa with Objective-C_I_4_tt265.png|]]-R). You should see something that looks like Figure 8-6.

Figure 8-6. Drawing dots with the Dot View application

Drawing dots with the Dot View application

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.

Event Delegation

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.

Figure 8-7. Delegation in action

Delegation in action

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.

Table 8-1. NSRunAlertPanel parameters

Parameter Description
title
The title of the sheet, displayed near the top of the sheet in a bold font.
Msg
An optional message string that appears near the bottom of the sheet. The string can contain printf-style arguments such as %@, %s, and %i.
defaultButton
The label for the sheet's default button, typically "OK".
alternateButton
The label for the sheet's alternate button, typically "Cancel".
otherButton
The label for a third button. If you pass nil, only two buttons will appear on the sheet.

Create a Delegate

Open the Dot View project in Project Builder, if you don't already have it open, and open the MainMenu.nibfile 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:

  1. Create a new subclass of NSObject called MyDelegate.
  2. Instantiate MyDelegate (Classes → Instantiate MyDelegate).
  3. 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.

Figure 8-8. Control-dragging from Window to MyDelegate

Control-dragging from Window to MyDelegate

  1. Make the connection to the delegate outlet of the window by clicking on the Connect button in the Info window.
  2. Save the nib file ([[Image:Learning Cocoa with Objective-C_I_4_tt275.png|]] -S).
  3. Create the files for the MyDelegate class, and add them to the project.
    1. Click on the Classes tab in the MainMenu.nibwindow.
    2. Select MyDelegate.
    3. Select Classes → Create Files for MyDelegate from the menu bar.

Return to Project Builder, and edit MyDelegate.mto match the code in Example 8-12.

Example 8-12. MyDelegate.m

#import "MyDelegate.h"

@implementation MyDelegate

- (BOOL)windowShouldClose:(NSWindow *)sender                              // 1
{
    int answer = NSRunAlertPanel(@"Close", @"Are you certain?",           // 2
                                 @"Close", @"Cancel", nil);
    switch (answer) {                                                     // 3
        case NSAlertDefaultReturn:
            return YES;
        default:
            return NO;
    }
}

@end

The code we added performs the following tasks:

  1. 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.
  2. Calls the NSRunAlertPanel function, which will open an alert dialog box that asks the user's permission to close the window.
  3. 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 ([[Image:Learning Cocoa with Objective-C_I_4_tt276.png|]]-S), and build and run the application ([[Image:Learning Cocoa with Objective-C_I_4_tt277.png|]]-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.

Figure 8-9. Dot View asking permission to close

Dot View asking permission to close

Delegation Using Sheets

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.

Table 8-2. NSBeginAlertSheet parameters

Parameter Description
title
The title of the sheet, displayed near the top of the sheet in a bold font.
defaultButton
The label for the sheet's default button, typically "OK".
alternateButton
The label for the sheet's alternate button, typically "Cancel".
otherButton
The label for a third button. If you pass nil, only two buttons will appear on the sheet.
docWindow
A reference to the window to which the sheet will be attached.
modalDelegate
A reference to the object that will respond when the user dismisses the sheet.
didEndSelector
A selector for a method implemented by the modalDelegate. The method will be invoked when the modal session is ended, but before the sheet is dismissed.
didDismissSelector
A selector for a method implemented by the modalDelegate. The method will be called after the sheet is dismissed. It is useful for any extra cleanup that might be necessary. Pass NULL if you don't need this functionality.
contextInfo
Additional data to pass to the modalDelegate as a parameter of the didEnd and didDismiss methods.
msg
An optional message string that appears near the bottom of the sheet. The string can contain printf-style arguments such as %@, %s, and %i.


Edit the MyDelegate.mcode, replacing the windowShouldClose: method and adding the sheetClosed: method, as shown in Example 8-13.

Example 8-13. Changing MyDelegate.m to use sheets

#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:

  1. Creates a string that will be displayed in the sheet.
  2. 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.
  3. 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.
  4. Returns NO so that the application's run loop can continue. The window will remain open, but a sheet will be attached to it.
  5. 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.

Warning

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 ([[Image:Learning Cocoa with Objective-C_I_4_tt290.png|]]-S), and then build and run the application ([[Image:Learning Cocoa with Objective-C_I_4_tt291.png|]]-R). Now when you try to close the window ([[Image:Learning Cocoa with Objective-C_I_4_tt292.png|]]-W), you should see something like Figure 8-10.

Figure 8-10. Aqua sheets in action

Aqua sheets in action

Notifications

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.

Figure 8-11. Notification

Notification

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:

  1. 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.
  2. 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.
  3. The notification center then sends a message to each registered observer, invoking the method specified by each observer and passing the notification.

Figure 8-12. Notifications

Notifications

How Notifications Work

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.

Notifications in Action

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.

Example 8-14. Adding a notification handler to Dot View

                     - (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:

  1. Obtains the default notification center for the application. All applications have a default notification center available to them.
  2. 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.
  3. Logs a message whenever the sheetDidBegin: method is called.

Save the project ([[Image:Learning Cocoa with Objective-C_I_4_tt297.png|]]-S), and then build and run the application ([[Image:Learning Cocoa with Objective-C_I_4_tt298.png|]]-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.

Memory-Management Considerations

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.

Example 8-15. Removing an object from the Notification Center

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [super dealloc];
}

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.

Exercises

  1. 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.
  2. 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).
  3. 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.
  4. Implement the NSApplication delegate method of applicationShouldTerminate: to display a confirmation dialog box when the user tries to quit the application.
  5. Change the initial color of the dot from red to some other color.
  6. 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?
Personal tools