Learning Cocoa with Objective-C/Single-Window Applications/Models and Data Functionality

From WikiContent

< Learning Cocoa with Objective-C | Single-Window Applications
Revision as of 12:56, 7 March 2008 by Docbook2Wiki (Talk)
(diff) ←Older revision | Current revision (diff) | Newer revision→ (diff)
Jump to: navigation, search
Learning Cocoa with Objective-C

In the previous four chapters, we covered the front end of Cocoa applications, windows and views, and how you can use controllers behind them. Now we turn our attention to the back end—the model—and how the data functionality of an application works.

To take full advantage of Cocoa's data-handling mechanisms, we must first explain two concepts that we didn't cover when we first introduced Objective-C and the Foundation Kit (see Chapter 4): protocols and key-value coding. After covering these topics, we deal with how to connect a user interface to an underlying data model and how that model can be saved and opened.

Contents

Protocols

Many pieces of Cocoa functionality make use of an Objective-C language feature called a protocol. A protocol is simply a list of method declarations. A class is said to conform to a protocol when it provides implementations of the methods that a protocol defines.

To help explain the concept of a protocol, think of the similarities between a waiter at a restaurant and a vending machine.[1] Even though the waiter and the vending machine are nowhere close to being similar objects from an inheritance standpoint, they can both implement methods related to serving food and taking money. Roughly, we could describe a protocol implemented by these two objects as the following methods:

takeOrder
serveFood
takeMoney
returnChange
complainTo

Of course, a vending machine doesn't usually serve very tasty or nutritious food and doesn't respond very well, if at all, to complaints. Additionally, you usually have to give vending machines money before they will take your order. But let's not get caught up too much in the details of our analogy. At a very basic level, the vending machine and waiter aren't all that different from each other—at least from the point of view of the person getting food. And note that it is easy to take this protocol and find other food-service situations in which it applies, such as getting a donut from the local convenience store.

In object-oriented programming, protocols are useful in the following situations:

  • To declare methods that other classes are expected to implement. This lets programs define methods that they will call on objects but that other developers will actually implement, and this is crucial to loading bundles and plug-ins.
  • To declare the public interface to an object while concealing its class. This lets more than one implementation of an object "hide" behind a protocol and prevents users from using unadvertised methods.
  • To capture similarities among classes that are not hierarchically related. Classes that are unrelated in most respects might need to implement similar methods for use by similar outside components. Protocols help formalize these relationships while preserving encapsulation.

Objective-C defines two kinds of protocols: informal and formal. An informal protocol uses categories to list a group of methods but doesn't associate them with any particular class or implementation.

A formal protocol , on the other hand, binds the list methods to a type definition that allows typing of objects by protocol name. Additionally, when a class declares that it implements a formal protocol, all of the methods of the protocol must be implemented.

Key-Value Coding

Key-value coding is a kind of shorthand. It is defined by an informal protocol used for accessing instance variables (also known as properties) indirectly by using string names (known as keys ), rather than directly through the invocation of an accessor method[2] or as instance variables. The key-value coding informal protocol (more accurately, the NSKeyValueCoding protocol) is available for use by any object that inherits from NSObject. Several Cocoa components, as well as its scripting support, take advantage of key-value coding.

The two basic methods for manipulating objects using the key-value coding protocol are as follows:

valueForKey:
Returns the value for the property identified by the key. The default implementation searches for a public accessor method based on the key name given. For example, if the key name given is price, a method named price or getPrice will be invoked. If neither method is found, the implementation will look for an instance variable named price and access it directly.
takeValue:forKey:
Sets the value for the property identified by the key to the value given. For our example of price, the default implementation will search for a public accessor method named setPrice. If the method is not found, it will attempt to access the price instance variable directly.

A Key-Value Coding Example

To show key-value coding in action, we will create a simple example based on a FoodItem class. By now, you should be familiar with what you'll see in this example, except for how we use key-value coding. Perform the following steps:

  1. Create a new Foundation Tool in Project Builder (File → New Project → Tool → Foundation Tool) named "keyvaluecoding", and save it in your ~/LearningCocoafolder.
  2. Create a new Objective-C class named FoodItem (File → New File → Cocoa → Objective-C Class), as shown in Figure 9-1. Be sure to create both the .h and .m files.

    Figure 9-1. Creating the FoodItem class

    Creating the FoodItem class

  3. Edit the FoodItem.hfile as follows:
    #import <Foundation/Foundation.h>
    
    @interface FoodItem : NSObject {
        NSString * name;                                              // a
                                NSNumber * price;                                             // b
    }
    
    - (NSString *)name;                                               // c
                            - (void)setName:(NSString *)aName;                                // d
    
                            - (NSNumber *)price;                                              // e
                            - (void)setPrice:(NSNumber *)aPrice;                              // f
    
    @end
    

    This code adds the following things:

    1. The name instance variable of type NSString. This variable will store the name of the food item.
    2. The price instance variable of type NSNumber. This variable will store the price of the food item.
    3. Accessor method that returns the name of the food item.
    4. Accessor method that allows the name of the food item to be set.
    5. Accessor method that returns the price of the food item.
    6. Accessor method that allows the price of the foot item to be set.
  4. Edit the FoodItem.mfile as follows:
    #import "FoodItem.h"
    
    @implementation FoodItem
    
    - (id)init                                                         // a
                            {
                                [super init];
                                [self setName:@"New Item"];
                                [self setPrice:[NSNumber numberWithFloat:0.0]];
                                return self;
                            }
    
                            - (NSString *)name                                                 // b
                            {
                                return name;
                            }
    
                            - (void)setName:(NSString *)newName                                // c
                            {
                                [newName retain];
                                [name release]
                                name = newName;
                            }
    
                            - (NSNumber *)price                                                // d
                            {
                                return price;
                            }
    
                            - (void)setPrice:(NSNumber *)newPrice                              // e
                            {
                                [newPrice retain];
                                [price release];
                                price = newPrice;
                            }
    
    @end 
    

    The code we added performs the following tasks:

    1. Initializes the object with some default values.
    2. Implements the name accessor method.
    3. Implements the setName: accessor method. Notice that we retain the new object, release the old one, then set the name variable to the new object, in accordance with the rules we discussed in Chapter 4.
    4. Implements the price accessor method.
    5. Implements the setPrice: accessor method.
  5. Edit the main.mfile (located in the Sources folder in Project Builder's left pane) as follows:
    #import <Foundation/Foundation.h>
    #import "FoodItem.h"
    
    int main (int argc, const char * argv[]) {
        NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
        FoodItem * candyBar = [[FoodItem alloc] init];
    
                                [candyBar takeValue:@"Aero" forKey:@"name"];
                     // a
                                [candyBar takeValue:[NSNumber numberWithFloat:1.25] forKey:@"price"]; // b
    
                                NSLog(@"item name: %@", [candyBar valueForKey:@"name"]);              // c
                                NSLog(@"    price: %@", [candyBar valueForKey:@"price"]);             // d
                                [candyBar release];                                                   // e
        
        [pool release];
        return 0;
    }
    

    The code that we added performs the following tasks:

    1. Instructs the candyBar object to set the name instance variable to Aero.[3]
    2. Instructs the candyBar object to set the price instance variable to 1.25. We use the NSNumber class to wrap primitive types for use as objects in collections and in key-value coding.
    3. Instructs the candyBar object to return the object assigned to the name variable and prints it using the NSLog function.
    4. Instructs the candyBar object to return the object assigned to the price variable and prints it out using the NSLog function.
    5. Releases the candyBar object.
  6. Save the project ([[Image:Learning Cocoa with Objective-C_I_5_tt305.png|]]-S), and then build and run the application (Build → Build and Run, or [[Image:Learning Cocoa with Objective-C_I_5_tt306.png|]]-R).

The following output appears in Project Builder's console:

2002-04-10 15:31:57.584 Key Value Coding[1382] item name: Aero
2002-04-10 15:31:57.585 Key Value Coding[1382]     price: 1.25

Key Value Coding has exited with status 0.

Obviously, for a real program of this length, using key-value coding is overkill compared to setting and retrieving the instance variables directly through accessor methods. Where key-value coding comes into its own is for hooking up model objects—those that implement logic and/or store data—to the generic view objects that Cocoa provides.

Some of Cocoa's view components let you define an identifier attribute. When the identifier attribute is set to a property key name for a model object, the component can automatically get, display, and set the value of the property without having to know anything about its implementation. We're going to see how this works with table views in the next section.

We're now done with this example application. Close the project in Project Builder before moving on.

Table Views

Table views are objects that display data as rows and columns. In a table view, a row typically maps to one object in your data model, while a column maps to an attribute of the object for that row. Figure 9-2 shows a table view and its component parts.

Figure 9-2. Table view mapped to an object model

Table view mapped to an object model

In Model-Viewer-Controller (MVC) terms, a data source is a controller object that communicates with a model object (typically an array) and the view object. This relationship is shown in Figure 9-3.

Figure 9-3. A data source as a controller between a model and a view

A data source as a controller between a model and a view

To have their data displayed properly, model objects must implement a couple of methods from the NSTableDataSource informal protocol:

- (int)numberOfRowsInTableView:(NSTableView *)tableView;

- (id)tableView:(NSTableView *)tableView 
          objectValueForTableColumn:(NSTableColumn *)tableColumn row:(int)row;

The first method allows the table view to ask its data source how many rows of data are in the data model. The second method is used by the table view to retrieve an object value from the data model by row and column.

Table View Example

To show how table views and models go together, we'll build a simple application to keep track of food items in a form that might be used to generate a menu. In Project Builder, create a new Cocoa Application project (File → New Project → Application → Cocoa Application) named "Menu", and save it in your ~/LearningCocoafolder. Then open the MainMenu.nibfile (located in the Resources folder of Project Builder's left pane) in Interface Builder.

Create the Interface

To create the interface, perform the following steps:

  1. Title the main window "Menu" in the Window Title field of the Info window (Tools → Show Info, or Shift-[[Image:Learning Cocoa with Objective-C_I_5_tt311.png|]]-I).
  2. Drag a table view object (NSTableView) from the Cocoa-Data views palette, as shown in Figure 9-4.

    Figure 9-4. Adding a table view object to the interface

    Adding a table view object to the interface

  3. Resize the table view to fill the window.
  4. Change the Autosizing attributes so that the table view will always occupy the entire window, as shown in Figure 9-5.

Figure 9-5. Changing the size attributes of the table view

Changing the size attributes of the table view

Configure the Table Columns

The next step is to configure the columns by adjusting their width, giving them titles, and, most importantly, assigning identifiers to the columns so Cocoa's key-value coding can operate.

  1. Make the width of the columns equal. Select the leftmost column (you may have to double-click), hold the cursor over the right edge of the column so that it turns into a pair of horizontally opposed arrows, then drag the column edge so the column view is divided in half.
  2. Double-click on the header bar for the left column and type Item Name, then press Tab to move to the header bar for the right column.
  3. Type Price as the header for the right column, then press Tab to select the left column.
  4. Edit the Identifier field for the left column in the Attributes pane of the Info panel so it reads name, as shown in Figure 9-6.

    Figure 9-6. Editing the identifier attribute of a table column

    Editing the identifier attribute of a table column

    Tip

    Don't confuse the Column Title field, located at the top of the Attributes panel, with the Identifier field at the bottom of the window. These serve two entirely different purposes. The Column Title field is for the benefit of your users and should contain the title you've assigned to that column in steps 2 and 3. The Identifier is an internal programmatic name that refers to the name of the property that should be displayed in the column.

  5. Repeat for the right column, assigning it an Identifier of price.

Declare the Data Source Class

A data source can be any object in your application that supplies the table view with data.

  1. Create a subclass of NSObject, and name it MyDataSource.
  2. Instantiate the MyDataSource class (Classes → Instantiate MyDataSource).
  3. Draw a connection from the table view object to the MyDataSource object in the Instances window. Make sure that you have selected the table view, not its surrounding scroll view before you draw the connection. The table view will turn a darker shade of gray when selected.
  4. Select the dataSource outlet in the Connections pane of the Info window, as shown in Figure 9-7, and click the Connect button.

    Figure 9-7. Connecting the table view to MyDataSource

    Connecting the table view to MyDataSource

  5. Click on MyDataSource in the Classes tab of the MainMenu.nib window, and create the interface files (Classes → Create Files for MyDataSource).
  6. Save ([[Image:Learning Cocoa with Objective-C_I_5_tt316.png|]]-S) the nib, and return to Project Builder.

Create the Data Source

The back end of our table will consist of two classes: the MyDataSource class that we defined in Interface Builder and the same FoodItem class that we created earlier in this chapter.

  1. Add the files for the FoodItem class to the project. Using the Finder, locate the FoodItem.hand FoodItem.mfiles (in the ~/LearningCocoa/Key View Codingdirectory), and drag them into the Other Sources folder of the Groups & Files panel in Project Builder. When the sheet appears to confirm your copy, ensure that the "Copy items" checkbox is selected, and then click the Add button, as shown in Figure 9-8.

    Figure 9-8. Adding our FoodItem class files to the Menu Project

    Adding our FoodItem class files to the Menu Project

  2. Open the MyDataSource.h file, and edit it to match the following code:
    #import <Cocoa/Cocoa.h>
    
    @interface MyDataSource : NSObject
    {
        NSMutableArray * items;
    }
    
    @end
    

    The code we added in the header file simply declares a single array, named items, as an instance variable. We will hold the many items to be displayed in the user interface of our application in this array.

  3. Open the MyDataSource.mfile, and edit it to match the following code:
    #import "MyDataSource.h"
    #import "FoodItem.h"
    
    @implementation MyDataSource
    - (id)init
                            {
                                [super init];
    
                                // Some initial data for our interface
                                FoodItem * chimi = [[FoodItem alloc] init];                   // a
                                FoodItem * fajitas = [[FoodItem alloc] init];
    
                                [chimi setName:@"Chimichanga"];
                                [chimi setPrice:[NSNumber numberWithFloat:8.95]];
                                [fajitas setName:@"Steak Fajitas"];
                                [fajitas setPrice:[NSNumber numberWithFloat:10.95]];
                                items = [[NSMutableArray alloc] init];
                                [items addObject:chimi];
                                [items addObject:fajitas];
                                [chimi release];
                                [fajitas release];                                            // b
                                return self;
                            }
    
                            - (int)numberOfRowsInTableView:(NSTableView *)tableView         
                            {
                                return [items count];                                         // c
                            }
    
                            - (id)tableView:(NSTableView *)tableView               
                            objectValueForTableColumn:(NSTableColumn *)tableColumn
                                        row:(int)row
                            {
                                NSString * identifier = [tableColumn identifier];             // d
                                FoodItem * item = [items objectAtIndex:row];                  // e
                                return [item valueForKey:identifier];                         // f
                            }
    
    @end
    

    The code that we added performs the following tasks:

    1. Creates a couple of sample menu items and puts them into an NSMutableArray instance.
    2. Releases the food items, now that they are stored safely in the array.
    3. Returns the number of items in the food items array. This lets the table view know how many rows contain data.
    4. Gets the identifier of the column for which the table view wants data.
    5. Obtains the food item that is at the specified index in the array.
    6. Returns the value of the food item object that matches the property name of the identifier obtained from the table column.
  4. Save the project (File → Save, or [[Image:Learning Cocoa with Objective-C_I_5_tt320.png|]]-S), and then build and run the application (Build → Build and Run, or [[Image:Learning Cocoa with Objective-C_I_5_tt321.png|]]-R). You should see something like Figure 9-9.

Figure 9-9. The Menu application in action

The Menu application in action

Play with the application a little bit: resize the window; resize the individual table columns; reorder the table columns by dragging around the column headers. Quit the application ([[Image:Learning Cocoa with Objective-C_I_5_tt323.png|]]-Q) when you are done.

Allow Modification of Table Entries

When playing with our Menu application so far, you could select the items in the table, and you could even edit them. But when you try to complete editing an item name or a price by hitting Return or exiting the table cell, the edit doesn't take. To let the user edit the fields, a third method must be implemented to save changes back to the data source:

- (void)tableView:(NSTableView *)tableView 
   setObjectValue:(id)object 
   forTableColumn:(NSTableColumn *)tableColumn 
              row:(int)row;

Add the following method to our MyDataSource.m file after the tableView:objectValueForTableColumn:row: method:

                  - (void)tableView:(NSTableView *)tableView
                     setObjectValue:(id)object
                     forTableColumn:(NSTableColumn *)tableColumn
                                row:(int)row
                  {
                      NSString * identifer = [tableColumn identifier];                   // 1
                      FoodItem * item = [items objectAtIndex:row];                       // 2
                      [item takeValue:object forKey:identifer];                          // 3
                  }
               

The code that we added does the following things:

  1. Gets the identifier of the column for which the table view wants to set data.
  2. Obtains the food item that is at the specified index in the array.
  3. Sets the property of the food item that matches the identifier that we obtained in step 1.

Now save the project (File → Save, or [[Image:Learning Cocoa with Objective-C_I_5_tt326.png|]]-S). Before you can build and run the application to test the editing features, you first need to ensure that you have quit out of any running Menu application. Build and run the app again (Build → Build and Run, or [[Image:Learning Cocoa with Objective-C_I_5_tt327.png|]]-R) from within Project Builder. You should now be able to edit the fields in the table and have those changes made in the underlying data model.

Adding Entries to the Model

With an application like Menu, adding entries to the model can be useful. The following steps guide you through the process of adding this functionality to the application:

  1. We're going to add a button to the interface. To enable a new row to be added when this button is pushed, we'll need to add an action newButtonPressed: and an outlet table to MyDataSource. An easy way to do this is to add the declarations yourself in the code. In Project Builder, edit the MyDataSource.h file to match the following code. The code you need to add is shown in boldface.
    #import <Cocoa/Cocoa.h>
    
    @interface MyDataSource : NSObject
    {
        NSMutableArray * items;
        IBOutlet NSTableView * table;
    }
    - (IBAction)newButtonPressed:(id)sender;
    
    @end
    
  2. Save the header file (File → Save, or [[Image:Learning Cocoa with Objective-C_I_5_tt329.png|]]-S).
  3. In Interface Builder's Classes pane, select MyDataSource, and reload the source file (Classes → Read MyDataSource.h). This causes Interface Builder to reparse the header file and pick up the new outlet and action.
  4. Resize the table view to make room for a button.
  5. Drag a button from the Cocoa-Views panel onto the interface, and change its name to New Item.
  6. Select the table view, and reset its Autosizing attributes as shown in Figure 9-10.

    Figure 9-10. Adding a button to Menu

    Adding a button to Menu

  7. Control-drag a connection between the MyDataSource object in the Instances tab of the MainMenu.nib window and the table view. Connect it to the table outlet, as shown in Figure 9-11.

    Figure 9-11. Connecting the table view to the table outlet

    Connecting the table view to the table outlet

  8. Control-drag a connection between the New Item button and the MyDataSource object in the Instances tab. Connect it to the newActionPressed: button, as shown in Figure 9-12.

    Figure 9-12. Connecting the button to the data source

    Connecting the button to the data source

  9. Save the nib file (File → Save, or [[Image:Learning Cocoa with Objective-C_I_5_tt333.png|]]-S), and return to Project Builder.
  10. Edit the MyDataSource.m file, adding the newButtonPressed: method shown in the following code:
                            - (IBAction)newButtonPressed:(NSEvent *)event {
                                FoodItem * item = [[FoodItem alloc] init];                     // a
    
                                [items insertObject:item atIndex:0];                           // b
                                [item release];
                                [table reloadData];                                            // c
                                [table selectRow:0 byExtendingSelection:NO];                   // d
                            }
                         
    

    The code we added performs the following tasks:

    1. Creates a new item object.
    2. Inserts the new item object into our data model array.
    3. Instructs the table view to reload its data. This will cause the table view to call the numberOfRowsInTableView: method again and load all the rows from the model.
    4. Selects the row we just added into the table. This highlights the new row, so the user of the application can edit it.
  11. Save the project files (File → Save, or [[Image:Learning Cocoa with Objective-C_I_5_tt335.png|]]-S), and then build and run the application (Build → Build and Run, or [[Image:Learning Cocoa with Objective-C_I_5_tt336.png|]]-R). When you press the New Item button, a new row should be created, as shown in Figure 9-13.

Figure 9-13. Adding a new entry to our application

Adding a new entry to our application

To edit the new fields, simply click in either column, and enter a new food item and price.

Saving Data: Coding and Archiving

Virtually all applications need to make some of their objects persistent. In user-speak, this means that all applications need a way to save their data. For example, the Menu application doesn't save the state of the data model, so all changes are lost as soon as you quit the application. Cocoa applications typically use coding and archiving to store document contents and other critical application data to disk for later retrieval. Some applications may also use coding and archiving to send objects over a network to another application.

Coding, as implemented by the NSCoder class, takes a connected group of objects, such as the array of food items in our sample application (an object graph), and serializes that data. During serialization, the state, structure, relationships, and class memberships are captured. To be serialized, an object must conform to the NSCoding protocol (consisting of the encodeWithCoder: and initWithCoder: methods).

Archiving, as implemented by the NSArchiver class (which extends NSCoder), extends this behavior by storing the serialized data in a file.

Adding Coding and Archiving to the Menu Application

To show how to archive objects, we will modify our Menu application to save and load files that contain the list of food items. To do this, we need to hook up the File → Open and File → Save menu items, add the save and open sheet functionality, and make sure that the FoodItem class can be archived.

  1. In Project Builder, open FoodItem.h and modify the @interface declaration as follows. Adding <NSCoding> declares that the Song class conforms to the coding protocol.
    #import <Foundation/Foundation.h>
    
    @interface FoodItem : NSObject <NSCoding> {
        NSString * name;
        NSNumber * price;
    }
    
    .
    .
    .
    

    NSCoding appears in brackets to signify that the FoodItem interface implements the coding protocol. If you were to read this declaration aloud, it would sound like: "FoodItem extends from the NSObject class and implements the NSCoding protocol."

  2. Open the FoodItem.mfile, and add the NSCoding methods after the init method as follows:
                            - (id)initWithCoder:(NSCoder *)coder
                            {
                                [super init];
                                [self setName:[coder decodeObject]];                             // a
                                [self setPrice:[coder decodeObject]];                            // b
                                return self;
                            }
    
                            - (void)encodeWithCoder:(NSCoder *)coder
                            {
                                [coder encodeObject:[self name]];                                // c
                                [coder encodeObject:[self price]];                               // d
                            }
                         
    

    The code we added performs the following tasks:

    1. Decodes the next object from the coder's data stream and sets the name instance variable.
    2. Decodes the next object from the coder's data stream and sets the price instance variable.
    3. Encodes the name instance variable to the coder's data stream.
    4. Encodes the price instance variable to the coder's data stream.
  3. Open MyDataSource.h, and add the following two action methods:
    #import <Cocoa/Cocoa.h>
    
    @interface MyDataSource : NSObject
    {
        NSMutableArray * items;
        IBOutlet NSTableView * table;
    }
    
    - (IBAction)newButtonPressed:(id)sender;
    - (IBAction)save:(id)sender;
                            - (IBAction)open:(id)sender;
    
    @end
    
  4. Save the source files, and then open the MainMenu.nibfile in Interface Builder.
  5. Reparse the MyDataSource.h file in Interface Builder. To do this, click on the MyDataSource object in the Classes tab, and then select the Classes → Read File MyDataSource.h menu option.
  6. Click on the File menu of the MainMenu.nib - MainMenuwindow to reveal the menu options.
  7. Control-drag a connection from the File → Open... menu item to the MyDataSource instance in the Instances tab, as shown in Figure 9-14. Connect it to the open: target.

    Figure 9-14. Connecting the Open... menu item to MyDataSource

    Connecting the Open... menu item to MyDataSource

  8. Control-drag a connection from the File → Save menu item to the MyDataSource instance. Connect it to the save: target.
  9. Save the nib file (File → Save, or [[Image:Learning Cocoa with Objective-C_I_5_tt342.png|]]-S), and return to Project Builder.
  10. Add the save: and open: methods to MyDataSource.m, as well as two helper methods, as shown here:
    #import "MyDataSource.h"
    #import "FoodItem.h"
    
    @implementation MyDataSource
    
    
    .
    .
    .
    - (IBAction)save:(id)sender
                            {
                                NSSavePanel * savePanel = [NSSavePanel savePanel];                  // a
                                SEL sel = @selector(savePanelDidEnd:returnCode:contextInfo:);       // b
                                [savePanel beginSheetForDirectory:@"~/Documents"                    // c
                                                             file:@"menu.items"
                                                   modalForWindow:[table window]
                                                    modalDelegate:self
                                                   didEndSelector:sel
                                                      contextInfo:nil];
                            }
    
                            - (void)savePanelDidEnd:(NSSavePanel *)sheet
                                         returnCode:(int)returnCode
                                        contextInfo:(void *)context
                            {
                                if (returnCode == NSOKButton) {                                     // d
                                    [NSArchiver archiveRootObject:items toFile:[sheet filename]];
                                }
                            }
    
                            - (IBAction)open:(id)sender
                            {
                                NSOpenPanel * openPanel = [NSOpenPanel openPanel];                  // e
                                SEL sel = @selector(openPanelDidEnd:returnCode:contextInfo:);
                                [openPanel beginSheetForDirectory:@"~/Documents"
                                                             file:nil
                                                            types:nil
                                                   modalForWindow:[table window]
                                                    modalDelegate:self
                                                   didEndSelector:sel
                                                      contextInfo:nil];
                            }
    
                            - (void)openPanelDidEnd:(NSOpenPanel *)sheet
                                         returnCode:(int)returnCode
                                        contextInfo:(void *)contextInfo
                            {
                                if (returnCode == NSOKButton) {
                                    NSMutableArray * array;                                         // f
                                    array = [NSUnarchiver unarchiveObjectWithFile:[sheet filename]]; 
                                    [array retain];
                                    [items release];
                                    items = array;
                                    [table reloadData];
                                }
                            }
    
    @end
    

    The code that we added does the following things:

    1. Creates a new Save panel—Cocoa's standard user-interface widget for selecting where a file should be saved. The way we use the Save panel uses delegation in a manner similar to the sheet we added to the Dot View application (Chapter 8).
    2. Obtains the selector for the callback method that the Save panel should use when the user has selected the file to which data will be saved.
    3. Instructs the Save panel to display itself as a sheet attached to the current window. MyDataSource doesn't have a direct reference to the window to which the sheet should be attached, but since it does have a reference to the table, we can simply ask the table for the window object.
    4. Archives the items array to the given file if the callback method gets a status code indicating that the user selected the file to which to save.
    5. Creates a new Open panel—Cocoa's standard user-interface widget for selecting files to open. Open panels work very much like save panels.
    6. Unarchives an array object from the file selected by the user; this releases the old array assigned to the items variable and assigns a retained instance of the new items.
  11. Now save the project (File → Save, or [[Image:Learning Cocoa with Objective-C_I_5_tt344.png|]]-S), and then build and run the application (Build → Build and Run, or [[Image:Learning Cocoa with Objective-C_I_5_tt345.png|]]-R).

Add a few items to your list of food items, then save ([[Image:Learning Cocoa with Objective-C_I_5_tt346.png|]]-S), and you should see the save dialog sheet slide out from the titlebar of the application window, as shown in Figure 9-15.

Figure 9-15. Saving our menu list

Saving our menu list

Quit the application, restart it ([[Image:Learning Cocoa with Objective-C_I_5_tt348.png|]]-R), and then open ([[Image:Learning Cocoa with Objective-C_I_5_tt349.png|]]-O) the data file you just saved. All the changes you made should show up. Make sure to quit ([[Image:Learning Cocoa with Objective-C_I_5_tt350.png|]]-Q) the application when you are done.

Using Formatters

The next task for the Menu application is to add a formatter to the Price column so that the amounts of our food items are shown using a currency format.

  1. Open MainMenu.nib in Interface Builder.
  2. Drag a currency formatter (NSNumberFormatter) from the Cocoa-Views palette to the price column, as shown in Figure 9-16.

    Figure 9-16. Adding a number formatter to the Menu application

    Adding a number formatter to the Menu application

  3. In the number-formatter inspector, set up the format to use the currency settings shown in Figure 9-17.

    Figure 9-17. The number formatter inspector

    The number formatter inspector

  4. Save the nib file.

Return to Project Builder, and build and run the application (Build → Build and Run, or [[Image:Learning Cocoa with Objective-C_I_5_tt353.png|]] -R). The menu interface should look like Figure 9-18.

Figure 9-18. Menu with the prices nicely formatted

Menu with the prices nicely formatted

Sorting Tables

The last thing we will add to our Menu application is the ability for the contents of the table to be sorted when a table column header is clicked. To do this, we will rely upon the ability of Cocoa collections to be sorted using comparators. We will add comparison methods to the FoodItem class so that an instance object can say that it should be sorted either before or after another instance.

  1. In Interface Builder, set the data view as the delegate of the table view by Control-dragging a connection between the table view and the MyDataSource instance object and connecting it to the delegate outlet, as shown in Figure 9-19.

    Figure 9-19. Making the data source act as the table view's delegate

    Making the data source act as the table view's delegate

  2. Save the nib file (File → Save or [[Image:Learning Cocoa with Objective-C_I_5_tt356.png|]]-S), and return to Project Builder.

    Open the FoodItem.mfile, and add the following two methods to the file after the other methods:

                         - (NSComparisonResult)compareName:(FoodItem *) item                    // a
                         {
                             return [name compare:[item name]];     
                         }
    
                         - (NSComparisonResult)comparePrice:(FoodItem *) item                   // b
                         {
                             return [price compare:[item price]];              
                         }
                      
    

    These methods perform the following actions:

    1. This method returns a comparison result by using the compare: method of the NSString class to compare the name of the given object with the name of the current instance.
    2. This method returns a comparison result by using the compare: method of the NSNumber class to compare the price of the given object with the price of the current instance.
  3. Open the MyDataSource.m file, and add the following method:
                         - (void)tableView:(NSTableView *)tableView
                             didClickTableColumn:(NSTableColumn *)tableColumn
                         {
                             NSString * identifier = [tableColumn identifier];               // a
                             if ([identifier isEqualToString:@"name"]) {             
                                 [items sortUsingSelector:@selector(compareName:)];          // b
                             } else {                
                                 [items sortUsingSelector:@selector(comparePrice:)];         // c
                             }
                             [table reloadData];                                             // d
                         }
                      
    

    This method does the following things:

    1. Obtains the identifier from the column so that we know with which property to sort.
    2. Tells the items array to sort itself using the compareName: method of each item in the array.
    3. Tells the items array to sort itself using the comparePrice: method of each item in the array.
    4. Tells the table view that the underlying data has changed and that it needs to reload itself.

Build and run the application (Build → Build and Run, or [[Image:Learning Cocoa with Objective-C_I_5_tt359.png|]]-R). Add a few items to the Menu, and then sort by name, then price, and see the results.

Exercises

  1. Change the title of the left column from Item Name to Food Item.
  2. Add the code necessary to display a confirmation dialog box when the user tries to quit the application.
  3. Examine the code in the Menu application for memory management problems.

Notes

  1. Duncan waited tables for many years while in college and is thankful that nobody tried to tip him over in order to get more food or money out of him.
  2. Accessor methods, along with properties, were introduced in Chapter 3.
  3. Aero is a very tasty chocolate bar made in Europe by Nestlé. You can occasionally find them in the U.S. at specialty stores.
Personal tools