Friday, October 30, 2009

Section 8.2.  Property List Files










8.2. Property List Files


Let's investigate the property list file format by writing a .plist file of our own. Both Cocoa and Core Foundation provide methods for converting their property list values directly to the .plist format, so writing the file will be a simple matter of extending our model to provide a property list type; our views, to include a user action to trigger the writing of the file; and our controller, to link the two.



8.2.1. Writing a Property List



Model

Our first task is to add to Linear's data model the ability to express itself in property list types. We'll do this by adding categories to the Regression and DataPoint classes. Categories are an Objective-C mechanism for adding methods to a classany class, even those supplied by Cocoawithout disturbing the core implementation of the class. (Realistically, there's no reason we shouldn't add our new methods directly to Regression and DataPoint. The original code is under our control, and nobody is relying on the old version.)


In Xcode, with the Linear project open, press command-N to bring up the New File Assistant. The variety of starter content Xcode offers for new files is extensive but doesn't cover category files. Select Cocoa Objective-C class, and type Regression-PropertyList as the base file name. Make sure that the box is checked for generating a header file.


Xcode now presents you with a skeleton class declaration for an NSObject subclass called Regression_PropertyList. Edit the declaration to look like this:


   #import "Regression.h"

@interface Regression (PropertyList)

- (NSDictionary *) asPropertyList;

@end


We declare an interface for an additional category, named PropertyList, on an existing class (Regression) containing the single method -asPropertyList.


To save yourself the trouble of retyping it, copy the line


- (NSDictionary *) asPropertyList;


Press command-option-up arrow to switch your view to the .m file. Once again, the template we're given has to be edited a bit, but it's close. Regression-PropertyList.m should look like this:


    #import "Regression-PropertyList.h"
#import "DataPoint-PropertyList.h"

@implementation Regression (PropertyList)

- (NSDictionary *) asPropertyList
{
// Make an array to hold the property-list version of
// the data points.
NSMutableArray * pointArray = [NSMutableArray array];
NSEnumerator * iter = [dataPoints objectEnumerator];
DataPoint * curr;

// For each data point, add its property-list version
// to the pointArray.
while (curr = [iter nextObject]) {
[pointArray addObject: [curr asPropertyList]];
}

// Return a dictionary with the points
// and the three statistics.
return [NSDictionary dictionaryWithObjectsAndKeys:
pointArray,
@"points",
[NSNumber numberWithDouble: [self slope]],
@"slope",
[NSNumber numberWithDouble: [self intercept]],
@"intercept",
[NSNumber numberWithDouble: [self correlation]],
@"correlation",
nil];
}

@end


In the first part of the asPropertyList method, we build up an NSMutableArray containing the property list version of each data point. Then we build an NSDictionary with four keyspoints, slope, intercept, and correlationto identify the point list and the respective statistics. Note the use of NSNumber as the class embodying the simple number type for use in property lists.


The DataPoint-PropertyList.h header file uses the same principles:


    #import "DataPoint.h"

@interface DataPoint (PropertyList)
- (NSDictionary *) asPropertyList;

@end


The same is true for the DataPoint-PropertyList.m implementation file:


    #import "DataPoint-PropertyList.h"

@implementation DataPoint (PropertyList)

- (NSDictionary *) asPropertyList
{
return [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithDouble: x], @"abscissa",
[NSNumber numberWithDouble: y], @"ordinate",
nil];

}

@end



Because all the objects created by the asPropertyList methods come from class convenience methods, not from methods with new, alloc, or copy in their names, we know that they are autoreleased, and we need not worry about releasing them ourselves.






View

Now that we have a way to get our model into property list form, we need away to make use of it. The easiest way is to add a menu command that saves the active document as a property list. (The right way would be to add .plist as a supported document type for the Linear application and to add support for it to the dataRepresentationOfType: and loadDataRepresentation:ofType: methods in MyDocument, but for instructional purposes, we'll do it the easy way.) Open the MainMenu.nib Interface Builder file (Figure 8.1). The quickest way to do this is to click on the Project (top) icon in the Groups & Files list, then double-click MainMenu.nib as it appears in the detail list to the right.



Figure 8.1. The MainMenu.nib file as it first opens. In a document-based application, the main menu Nib contains only the menu bar, represented by the small window at the top and the icon at the right of the Nib window. The other two icons are placeholders for objects outside the Nib.







MainMenu.nib contains only the application menu bar, which appears in a small window containing the menu titles as you'd see them in the application's menu bar (Figure 8.1).Clicking a title drops down a representation of that menu, identical except as hidden items are visible in the Interface Builder display (Figure 8.2).



Figure 8.2. The File menu in MainMenu.nib expands when you click its title in the menu window. All items, including hidden ones, are visible in the menu as shown. Menu titles and key equivalents can be edited in place, and you can drag additional items into the menu from the Menus palette.







We want to add a Save as PList . . . item to the File menu. (We include an ellipsis after the label because we'll be presenting a save-file dialog that will allow the user to cancel.) Click the title of the File menu so it drops down, as shown in Figure 8.2. In the Interface Builder object palette at the upper right of your screen, select the first page, to show the available menus and menu items. Drag the object labeled Item from the palette to the opened File menu, and drop it just below the Save As . . . item. (If you drop it somewhere else, it doesn't matter, but if it bothers you, you can drag it from where you dropped it to where you wanted it to go.)



Detail Searches


The className-categoryName convention for naming category files can exploit the incremental search feature of the Project window's detail view. For an example, fill the detail view with all the files of the project by clicking the Project (top) icon in the Groups & Files list at the left. (If the detail view isn't visible, click the Editor icon in the Project window's toolbar.) Now type Regression in the search field in the toolbar. The detail view instantly narrows down to the four files that define the Regression classRegression.m and .h and Regression-PropertyList.m and .h. If, instead, you type Property, the list narrows to Regression-PropertyList.m and .h and DataPoint-PropertyList.m and .h, the category files having to do with property lists. With the handful of classes in our application, this doesn't seem like much of a trick, but detail searches work as quickly on projects with hundreds of source files in dozens of group folders.


For another example, select the Project Symbols group toward the bottom of the Groups & Files list. The detail view fills with every method, class, and category defined in the project. Typing Property in the search field reduces the detail list to the definitions and implementations of the two PropertyList categories and the two asPropertyList methods.




With the item in place, double-click the title (Item) and type Save as PList . . . as the new title. Press Return to end the edit.


Now we have a menu item. How do we make it do something? We know that Cocoa objects that send user commands keep two pieces of information: the action (what is to be done) and the target (what is to do it).


The action is the name of an Objective-C method taking one parameter: an identifier ending with a colon. We haven't written the action method yet, but we can make up a name for it: saveAsPList:.


What about the target? To what Cocoa object will we assign the task of responding to saveAsPList:? I don't know, says a stubborn part of our subconscious, anything that wants to, I guess.


This turns out not to be a stupid answer in Cocoa. Cocoa keeps a continually updated responder chain, a series of potential responders to user actions. The chain begins at the First Responder, which may be the selected view in the front window, and then proceeds to the front window, the window's document, and finally to the application itself. You have probably noticed that the second icon in an Interface Builder Nib window represents the First Responder. A user interface element can designate the First Responder, whatever it may be at the time, as the target of its action, and Cocoa will shop the action up the responder chain until it finds an object that can handle it.



The responder chain is a little more complicated than that. For more information, consult Apple's documentation for NSApplication, NSResponder, and the related Programming Topic articles.




If we tried control-dragging an action link from our new menu item to the First Responder icon, we'd quickly be balked. Interface Builder would present an Inspector for the link, asking us to designate the method selector for our desired action. The list it presents does not include saveAsPList:, because we just made that up. Before we make the link, we have to tell Interface Builder that saveAsPList: is a possible action.


Interface Builder allows you to do this by editing the First Responder as if it were a custom class. Click the Class tab in the MainMenu.nib window. Scroll the class list all the way to the left, and select NSObject in the first column. You will find First Responder listed among NSObject's subclasses. Click First Responder (see Figure 8.3). You now want an Inspector on the First Responder "class." Select Show Inspector from the Tools menu. The Inspector window shows a tab for Actions, under which are listed all the known actions that NSResponder subclasses respond to. To add our new action, click the Add button, and type saveAsPList: in the new line that results. Press the Return key to make your edit final.



Figure 8.3. Finding the First Responder "class" in the Nib's class listing. Select NSObject in the leftmost column of the class browser, and click First Responder in the second column.








Be sure to include the colon at the end of the selector saveAsPList:. Objective-C considers both saveAsPList and saveAsPList: to be legal, but completely different, method names. Forgetting the colon in action methods is one of the commonest errors Cocoa programmers make.




Now we can hook up our menu item. Click the Instances tab in the MainMenu.nib window to make the First Responder icon visible, and arrange the Nib window and the menu bar window so that you can see both the new item and the First Responder icon. When everything is in place, hold down the control key and drag from the new menu item to the First Responder icon. When the icon highlights, release the mouse button (Figure 8.4).



Figure 8.4. Linking a menu item to the First Responder. Control-drag from the new menu item to the First Responder icon. When the icon highlights, release the mouse button. The line between the two shows that the link has been made. The Inspector for the menu item appears, offering action methods for the link.

[View full size image]




If it had been hidden before, the Inspector window now appears, showing a list of all the methods known for the responder chain. Our saveAsPList: method is in the list, because we added it; Interface Builder guesses that saveAsPList: is the right action for a menu item named Save as PList . . . and highlights that action, saving us the trouble of hunting for it. Click the Connect button at the bottom of the Inspector window, and the link is made.




Controller

When we pulled the name of the saveAsPList: method out of the air, we had no particular implementer in mind for it, but now we have to think about it.


  • The implementer has to be on the responder chain; otherwise, it will never be offered saveAsPList: when it is shopped around.

  • The implementer has to be associated with one, and only one, Regression set.

  • The implementer should, preferably, be an existing class.


Fortunately, we have one such class: MyDocument. As a document class, it is on the responder chain. We've already made it the controller class associated with our Regression model. And, it exists.


We open MyDocument.m and add this line to the beginning:


   #import "Regression-PropertyList.h"


We also add the following lines somewhere in the implementation section:


   - (IBAction) saveAsPList: (id) sender
{
// The response to the Save As PList... command.
NSSavePanel * savePanel = [NSSavePanel savePanel];
// Take the shared save-file panel
// and set it to save only plists.
[savePanel setAllowedFileTypes:
[NSArray arrayWithObject: @"plist"]];

// Make a nice default name to present to the user.
NSString * defaultName;
defaultName = [[self displayName]
stringByAppendingPathExtension: @"plist"];
// Present the save panel and designate a method
// for receiving the result.
[savePanel beginSheetForDirectory: NSHomeDirectory()
file: defaultName
modalForWindow: [self windowForSheet]
modalDelegate: self
didEndSelector:
@selector(savePanelDidEnd:returnCode:contextInfo:)
contextInfo: NULL];
}

- (void) savePanelDidEnd: (NSSavePanel *) sheet
returnCode: (int) returnCode
contextInfo: (void *) contextInfo
{
// The response to the Save-as-plist save panel.
if (returnCode == NSOKButton) {
// The user OK'ed the save.
// Get the property-list representation...
NSDictionary * pList = [model asPropertyList];
// ... and write it out.
[pList writeToFile: [sheet filename]
atomically: YES];
}
}



Objective-C methods do not have to be declared in advance to be legal or usable by other objects.




With all this hooked up and compiled, we run Linear and fill the data table with the old familiar not-quite y = 2x:


1.0   2.05
2.01 4
3 5.987
4 8.1
5 10.0


We click the Compute button to update the statistics (2.00196, 0.0175146, and 0.999871, as before) and pull down the File menu to select our new Save As PList . . . command. A save-file sheet appears, most likely offering to save a file named Untitled in your home directory. You can change this if you like, but be sure that you put the file somewhere you can find it, because we will be inspecting its contents.





8.2.2. Examining Property Lists



As Text

When the save is done, we can quit Linear. Now go to the Finder and find the file you just saved. Drag its icon onto the Xcode icon in your dock. Xcode presents you with a text editor on the contents of the file, which looks something like this:


    <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>correlation</key>
<real>0.99987099999999995</real>
<key>intercept</key>
<real>0.017514600000000002</real>
<key>points</key>
<array>
<dict>
<key>abscissa</key>
<real>1</real>
<key>ordinate</key>
<real>2.0499999999999998</real>
</dict>
<dict>
<key>abscissa</key>
<real>2.0099999999999998</real>
<key>ordinate</key>
<real>4</real>
</dict>
<dict>
<key>abscissa</key>
<real>3</real>
<key>ordinate</key>
<real>5.9870000000000001</real>
</dict>
<dict>
<key>abscissa</key>
<real>4</real>
<key>ordinate</key>
<real>8.0999999999999996</real>
</dict>
<dict>
<key>abscissa</key>
<real>5</real>
<key>ordinate</key>
<real>10</real>
</dict>
</array>
<key>slope</key>
<real>2.00196</real>
</dict>
</plist>


You likely will be relieved to see that the property list file format is XML and that Cocoa's built-in writer for .plist files indents them fairly nicely. The top-level element is <plist>, which must contain one property list elementin this case, <dict>, for our Regression dictionary. A<dict> element's contents alternate between <key> string elements and property list value elements. One of the keys in the Regression dictionary, points, has an <array> value. An <array> may contain zero or more property list elements, of any type, though in this case, they are all <dict>s from our DataPoint objects.


The nice thing about XML is that it is standard: Correct XML will be accepted by any consumer of a document type definition, regardless of the source. A .plist file generated by Cocoa will be treated the same as one generated by a text editor.


The difficult thing about XML is that it must be correct. If you forget to close an element or miss the strict alternation of <key> and property list elements in <dict> lists, you will get nothing out of Apple's parser. There are three ways to cope with this restriction.


First, you can always start your own .plist files by editing a known good .plist file. It's difficult to omit the processing instruction or the <plist> skeleton if they are already in the file.


Second, you can use the macro or glossary facilities of your text editor to create a document skeleton and wrap your entries in proper tags. Bare Bones Software's BBEdit comes with a .plist glossary for just this purpose. Xcode 2.2, surprisingly, does not include property list macros. We'll be fixing that later in this chapter.


Third, you can use the Property List Editor application, found among the Xcode tools at /Developer/Applications/Utilities (Figure 8.5). The simplest use of Property List Editor is as a check on your text-edited file. Simply attempt to open your file with Property List Editor. If the file doesn't open, something's wrong. If the error isn't obvious, find a way to cut about half the list to the clipboard, leaving what, you hope, would still be a legal property list. Try opening the file with Property List Editor again. If the file opens, the error is in the part on the clipboard; if not, it's in the part still in the file. In either case, paste the missing lines back into the file, and cut half the elements out of the half of the file in which you isolated the problem in the previous pass. Repeat this process, reducing your search by halves, until you arrive at a stretch of XML small enough to proofread.



Figure 8.5. The Property List Editor application, found in the Utilities folder of the Developer Applications, presents the contents of property list files in a visual hierarchy that you may find easier to work with than the raw XML of the file itself. Trying to open a file with PLE is a good way to check whether the file contains a valid property list.

[View full size image]





One of the commonest errors is forgetting that the text portions of the property list XML are parsed character data, which means that < and & must be represented by &lt; and &amp;.






Property List Editor

Of course, you could simply use Property List Editor (PLE) to create your .plist files in the first place. Select New in the File menu (or press command-N) to open a window on a new, empty list. Add a root element to the list by clicking the New Root button. A line labeled Root appears under PropertyList with Dictionary listed under Class. Dictionary appears as a pop-up menu; clicking the mouse in the class cell shows the full range of choices of property list types.


The button at the upper left of the window is the New button. Its meaning changes depending on what is selected in the list.


  • When the property list is empty, the button is New Root. It puts one element in the file.

  • When the selected line is an element inside a containerand is closed, if it is a container itselfthe button is New Sibling. It adds one element to the same container as the selected element.

  • When the selected line is an open container, the button is New Child. It adds one element to the selected container.


Because it is a container, a dictionary, the root element line can be opened. It is closed, so the rules say that the New operation should be New Sibling, but there can't be more than one root element, so the New Sibling button is disabled. Open the root element, and click New Child three times. This creates three new key/value pairs, with the keys in the left column, the types of the values (String) in the middle, and the values themselves (empty) in the right. Name these key/value pairs Ingredients, Material, and Method; make the first two dictionaries and the third an array.


Open the Ingredients dictionary, and click New Child. Make the child's key Eggs and its type Dictionary. This dictionary, in turn, should have the key/string pairs unit count and quantity 3. Take care to change the type of the quantity value to Number before setting it. Add more siblings to Eggsor children to Ingredients as shown in Table 8.2.


Table 8.2. The Remaining Ingredients in an Omelet

Key

Type of Value

Value

Mushrooms

Count

2

Salt

Pinch

1

Butter

Ounce

2



The Material dictionary should be simple key/string pairs, as shown in Table 8.3. The Method array should contain strings, as shown in Table 8.4.


Table 8.3. Materials for Making an Omelet

Key

String

Bowl

Small

Fork

Table fork or small whisk

Crêpe Pan

10" nonstick

Spatula

Silicone, high-heat

Egg Slicer

Optional, for slicing mushrooms



Table 8.4. Making an Omelet

Instructions

Heat pan to medium (so butter foams but doesn't burn)

Warm eggs to room temperature

Slice mushrooms

Sauté until limp in 1/4 of the butter and set aside

Break eggs into bowl, add salt

Whisk eggs until color begins to change

Coat pan with 1/2 the butter

Pour eggs into pan and tilt to spread uniformly

When edges set, use spatula to separate from pan, then tilt liquid into gaps

Leave undisturbed for 30 seconds

Loosen from pan, and flip (using spatula to help) 1/3 over

Top with mushrooms

Slide onto plate, flipping remaining 1/3 over

Spread remaining butter on top



If you followed along with this exercise (see Figure 8.6), you've probably been persuaded that the Property List Editor has its advantages and disadvantages. On the plus side, it always generates correct property lists, and no matter how complex your property list structure becomes, PLE makes it easy to navigate. On the minus side, PLE was never meant for creating large, repetitious lists. By the third ingredient, you were probably wishing you could duplicate the quantity/numberunit/string dictionary pattern. Instead, you were forced to use the keyboard, the New button, and the Class pop-up. That PLE keeps dictionary keys in alphabetical order at all times is handy when you're browsing a property list, but it's a misfeature when you change a key and the line you are working on moves elsewhere on the screen.



Figure 8.6. Construction of the Omelet Property List

[View full size image]
















No comments:

Post a Comment