Working with Sheets in Cocoa10/05/2001
One of the features we saw with the advent of Mac OS X was a new way to handle dialog boxes in an application. This new way is called "sheets," which is a special kind of window that is actually attached to another window, and the result is a cleaner, less cluttered workspace. It turns out that this behavior isn't limited solely to open, save, and alert windows. Indeed, we can make any window we want into a sheet. We see this behavior in many applications where a simple preferences window is implemented as a sheet attached to the main window of an application, or in an authentication window that appears as a sheet, and so on.
In today's column we will explore two different ways to implement sheets in Cocoa, with particular emphasis on converting our record deletion alert panel and our preferences window into sheets. The first is easily done with a prebuilt function from the AppKit, while the second involves using a more flexible and more fundamental method. All of the work we do today will be based on the AddressBook application that we were left with at the end of the last column, which can be downloaded here.
Before we can implement methods that make sheets, we must have a means to send messages to our two windows within the
Controller instance -- we need some outlets connected to our windows. Specifically, we want to add two outlets to our class
mainWindow. Additionally, we want to reconnect the "Preferences" menu item and the Close button in
prefsWindow to actions in
Controller, rather than to actions of
prefsWindow, as we had done previously.
In Interface Builder, add these two outlets and connect them to their respective windows. Also add the actions
closePrefsSheet:, and connect those to their appropriate controls. Note that before you can make connections between the preferences, the Close button, and action in
Controller, you must disconnect the old connections. If you attempt to make a new connection without doing so, Interface Builder will show you the old connection, which you can then disconnect. Save these changes and return to Project Builder. In Project Builder, add to the file
Controller.h the two instance variable declarations:
IBOutlet id mainWindow; IBOutlet id prefsWindow;
And the two action method declarations:
- (IBAction)openPrefsSheet:(id)sender; - (IBAction)closePrefsSheet:(id)sender;
We'll add the action method definitions a bit later, and with that we're set to make our sheets.
Several columns ago, we made an alert panel appear when the user attempted to delete a record, asking for confirmation of that delete command. We did this by using the AppKit function
NSRunAlertPanel. Today we will replace this function with another AppKit function that will allow us to create an alert sheet, rather than an alert panel. This function is called
NSBeginAlertSheet, and it takes several arguments.
The arguments that can be used with
NSBeginAlertSheet are listed below; there are 10 in all.
The first four arguments, as well as the last argument are no different than the arguments in
NSRunAlertPanel and serve the same purpose. The first argument is the title we wish to give the alert sheet. Arguments 2, 3, and 4 are the titles for each of the three possible buttons. As was true for
NSRunAlertPanel, if you don't want a button to be used, simply pass "nil" as the argument value for that button's title. The last argument, 10, is the message text to be displayed in the alert sheet. Nothing too complicated or different here.
The middle five arguments are new to us. The fifth argument is the window we wish to attach the alert sheet to. Remember, sheets belong to windows, and this is where we indicate which window it should go with. In our case, we want the alert sheet to be attached to the main application window, so we will pass
mainWindow for argument 5.
The next few arguments have to do with the structure of our new
deleteRecord implementation and how it differs from our old one. Unlike
NSRunAlertPanel, the return value of
NSBeginAlertSheet is void. This means we can't use the same mechanism as
NSRunAlertPanel to determine which button was pressed. Rather, pushing any of the buttons in the sheet causes a method to be called which then handles execution of select code based on which button was pressed. This is where arguments 6, 7, and 8 come into play. Argument 6 indicates which object contains this method. We will simply pass "self" as this argument.
Arguments 7 and 8 tell the function which method to call in
modalDelegate when a button is pressed. The data type of these two arguments is a new type we haven't encountered here before,
SEL stands for selector. Selectors are Objective-C's way of referring to method names outside of the context of a message. For example, whenever we use a method name in code (except for the declaration and definition syntax), it always occurs as part of a message:
As we continue to work with our AddressBook application, this time adding sheets, what are your comments?
Also in Programming With Cocoa:
Selectors allow us to refer to method names as an argument to a method or function, independent of a receiver object (although, in the case of
NSBeginAlertSheet we are in a sense indicating the receiver object in the
modalDelegate argument-self). However, we can't just pass the name of the method because the compiler will think we are passing a variable name. Rather, we have to use a special construct
@selector( methodName ), which will convert a method name into a selector. This tells the compiler that whatever is in the parentheses is a method name, and should be treated as such. The role of selectors in Objective-C is quite a bit larger than this discussion may let on, so for a more in-depth discussion of selectors, I refer you to Object-Oriented Programming and the Objective-C Language.
So we put the method we want executed when the sheet ends in either argument 7 or argument 8. But which one? The difference between arguments 7 and 8 is the timing of the method call. If we put our selector in argument 7, that method will be executed right when a button is pressed in the sheet. If we put the selector in argument 8, the method is invoked after the sheet has closed. You could, if you wanted, have two different methods invoked here. If you don't use one of these arguments, simply pass "nil" as the value.