Creating and using property lists
Discovering how dictionaries work
Updating dictionaries and property lists
Having a property list object array) write itself to a file
What you accomplished in Book V is all fine and dandy. You've essentially created a "wire frame" for the user experience and filled in a few of the pieces.
Although those fillings are valuable, the real meat of the RoadTrip application, besides the maps, is in the Sights and Hotels sections. But those, as opposed to car information and car servicing, are going to require some significant data. In this minibook, I show you how to deal with all that data using property lists, the URL Loading System, and Core Data.
In this chapter, you work on the Sights selection on the main screen. I show you how to create a Sights menu that uses a local image and then downloads the data about the sight from a Web server when the user selects that particular sight.
I put the data on the Web server because, if you were to offer this as a commercial app, you would want the data on the Internet so you can update it with the latest information — summer versus winter hours, for example, or letting users know that tours now start on the half hour rather than the quarter hour. You'd also want it on the Internet so you can easily add new things to play with, based on user feedback. (You can also easily add an e-mail or Twitter feature to this app, for example, although I don't have room in this book to go into that.)
In Book V, you created a generic WebViewController
. In this chapter, you work towards creating a generic TableViewController
that's driven by the data in a property list (also known as a plist).
You will be implementing the sequence in the RoadTrip application you see documented in Figures 1-1 and 1-2.
Pretty impressive, right? I love the way the Golden Gate Bridge gets highlighted in Figure 1-2.
To get this all to work properly, you need to do the following:
Create a table view to display the names of the sights as well as an image of the site, and then create the Web view to display the text that describes the sight.
Make a few changes in the RootViewController
to load a SightListController
.
Create a Sight
object that "knows" what sight it is, has an image or picture of itself, holds some text describing it, and knows its location (which will also enable it to act as an annotation — you do want to be able to find it on a map don't you?). To do that, you have to create a property list (an iPhone data structure that I explain in detail) and then programmatically load it and use the data to create the Sight
objects.
Make a few changes in Trip
to create the Sight
objects and load a SightListController
(as well as implement a new annotations mechanism).
In this chapter, I show you how to do all this, although I do it in a slightly different order that will be easier for you to follow.
You start by creating part of the SightListController
, followed by the plist. After that, you code the Sight class and then finish coding the SightListController
.
The way you select an application function (refer to Figure 1-1) is by selecting a row in a table view, your good friend from Book V. Although I don't go into too much detail here, I want to review how table views work.
As I think about creating yet another table view (more than two of anything qualifies for me as yet another and time to start thinking of a generic version), take a look at what the table view needs.
As I mention in Book V, Chapter 2, to get table views to work for you, you need to do the following:
Supply the number of sections you want.
You do that with the help of the numberOfSectionsInTableView:
method of the UITableViewDataSource
protocol.
Supply the number of rows you want in each section and specify what you want to call your section headers.
The tableView:numberOfRowsInSection:
method and the tableView:titleForHeaderInSection:
method, respectively, take care of that for you. (Both are part of the UITableViewDataSource
protocol.)
Supply the text (or graphic) for each row.
You return that from the implementation of the tableView:cellForRowAtIndexPath:
method of the UITableViewDataSource
protocol. This message is sent for each visible row in the table view, and you return a Table View cell to display the text or graphic. This is how you get the list of sights and those nice photos you see in Figure 1-1 — more on how it actually works later.
Respond to a user selection of the row.
You use the tableView:didSelectRowAtIndexPath:
method of the UITableViewDelegate
, protocol to take care of this task. In this method, you create a view controller and a new view. For example, when the user selects Golden Gate Bridge in Figure 1-1, the tableView:didSelectRowAtIndexPath:
method is called, a WebViewController
controller is created, and the text description of the Golden Gate Bridge is displayed in a Web view — just as the car servicing information was displayed in Book V. In this case, the "brains" behind the data will be the Sight
object, just as the CarServicing
object was the brains behind the car servicing information.
A UITableView
object must have a data source and a delegate. The data source supplies the content for the table view, and the delegate manages the appearance and behavior of the table view. The data source adopts the UITableViewDataSource
protocol, and the delegate adopts the UITableViewDelegate
protocol — no surprises there. Of the preceding methods, only the tableView:didSelectRowAtIndexPath:
is part of the UITableViewDelegate
, the others are included in the UITableViewDataSource
protocol.
If you've been following along with me, note that I'll be extending what you did in Chapter 6 of Book V. You can find the application up to that point on my Web site http://nealgoldstein.com
under Book VI Start Here.
Okay, check out how easy it is to come up with the view controller and nib files:
In the RoadTrip project window, select the Classes folder, and then choose File
Selecting the Classes folder first tells Xcode to place the new file in the Classes folder.
In the left column of the dialog, select Cocoa Touch Classes under the iPhone OS heading, select the UIViewController subclass template in the top-right pane and then click Next.
Be sure the UITableViewController subclass is selected and the With XIB for User Interface is not selected. I show you how to create the view controller without using a nib file.
You see a new dialog asking for some more information.
Enter SightListController.m in the File Name field and then click Finish.
To make things easier to find, I keep my SightListController.m
and .h
classes in the Classes folder.
Now do it all over again for the Sight
model class (with the classes folder still selected).
Choose File
In the leftmost column of the dialog, first select Cocoa Touch Classes under the iPhone OS heading, then select the Objective-C class template (not the UIViewController subclass template) in the topmost pane, make sure the Subclass drop-down menu has NSObject
selected and then click Next.
You see a new dialog asking for some more information.
Enter Sight
in the File Name field and then click Finish.
I keep my Sight.m
and .h
classes in the Model Classes folder.
At this point, you're all set up to start adding code.
As I mentioned when you where creating the controller file, you're going to be creating the table view without using a nib file. Although the end-result of what you are doing here is pretty sophisticated, the actual nuts-and-bolts part is really quite easy. You get it all done by using an initialization method, as follows:
Add the code in bold in Listing 1-1 to SightListController.m
.
You won't be using all the includes yet, but I like to get them out of the way. As you can see, all you're doing is invoking a superclass's method initWithStyle:UITableViewStyleGrouped
. This method creates the table view and also sets the delegate. Frankly, unless you need to do something special, rolling out the initWithStyle:UITableViewStyleGrouped
: method makes creating table views a lot less cumbersome.
Add the code in bold in Listing 1-2 to SightListController.h
.
The bolded stuff is simply the instance variables and the methods you'll be adding. I explain createThumbNail:
, which takes images and sizes them for the table view, later in this chapter.
Example 1-1. Initialization
#import "SightListController.h"#import "Trip.h"
#import "Sight.h"
#import "RoadTripAppDelegate.h"
#import "WebViewController.h"
@implementation SightListController- (id) initWithTrip:(Trip*) theTrip {
if (self = [super initWithStyle:UITableViewStyleGrouped]) {
trip = theTrip;
self.title = @"See the sights";
}
return self;
}
@end
To get your controllers in place, the first thing you have to do is make it possible to select the Sights entry in the Main view. That means uncommenting out the SightListController
selection in the tableView:didSelectRowAtIndexPath:
method over in RootViewController.m
— the stuff in bold in Listing 1-3.
Example 1-3. Uncommenting Out SightListController
Selection
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { RoadTripAppDelegate *appDelegate = (RoadTripAppDelegate *) [[UIApplication sharedApplication] delegate]; [appDelegate.lastView replaceObjectAtIndex:0 withObject: [NSNumber numberWithInteger:indexPath.section]]; [appDelegate.lastView replaceObjectAtIndex:1 withObject: [NSNumber numberWithInteger:indexPath.row]]; [tableView deselectRowAtIndexPath:indexPath animated:YES]; int menuOffset = [self menuOffsetForRowAtIndexPath:indexPath]; UIViewController *targetController = [[menuList objectAtIndex:menuOffset] objectForKey:kControllerKey]; if (targetController == nil) { BOOL realtime = !appDelegate.useStoredData; switch (menuOffset) { case 0: if (realtime) targetController = [[MapController alloc] initWithTrip:trip]; else [self displayOfflineAlert: [[menuList objectAtIndex:menuOffset] objectForKey:kSelectKey]]; break; case 1:targetController =
[[SightListController alloc] initWithTrip:trip];
break; case 2: // targetController = [[HotelController alloc] initWithTrip:trip]; break;
case 3: if (realtime) targetController = [[WebViewController alloc] initWithTrip:trip tripSelector:@ selector(returnWeather:) webControl:YES title:NSLocalizedString(@"Weather", @"Weather")]; else [self displayOfflineAlert: [[menuList objectAtIndex:menuOffset] objectForKey:kSelectKey]]; break; case 4: targetController = [[WebViewController alloc] initWithTrip:trip tripSelector:@selector(returnCarServicin gInformation:) webControl:NO title:NSLocalizedString (@"Car Servicing", @"Car Servicing")]; break; case 5: targetController = [[WebViewController alloc] initWithTrip:trip tripSelector:@selector(returnCarInformation:) webControl:NO title:NSLocalizedString (@"The Car", @"Car Information")]; break; } if (targetController) { [[menuList objectAtIndex:menuOffset] setObject:targetController forKey:kControllerKey]; } } if (targetController) { [[self navigationController] pushViewController:targetController animated:YES]; [targetController release]; } }
The following code allocates a SightListController
(a view controller) and then sends it the initWithTrip::
message. (I explain this at the very end of Book V, Chapter 2; you might want to review that if it has been a while since you looked at it.).
targetController = [[SightListController alloc] initWithTrip:trip];
You also need to import the SightListController
and add it to the RootViewController.m
file.
#import "SightListController.h"
That's all you need to do in the RootViewController
class.
If you were to build the project as it stands now, it would compile (with a few errors you can ignore), and you'd get a table view of Sights with nothing in your list. Now that you've gotten the foundation built, it's time to go to work.
There are a couple of things you need to do to see your (virtual) sights. You need to be able to display the sights (and their photos) in the SightListController
, and you need to be able to have sight model objects that will provide the data not only to the controller, but to the Web view as well. Take a look at the Sight
object that will do that.
Add the code in bold in Listing 1-4 to Sight.h
.
Example 1-4. Sight.h
#import <MapKit/MapKit.h>#import "WebViewController.h"
@class Trip;
@interface Sight : NSObject {NSString *sightName;
CLLocationCoordinate2D coordinate;
NSString *title;
NSString *subtitle;
NSString *resource;
NSString *resourceType;
NSString *image;
NSString *imageType;
Trip *trip;
}@property (nonatomic, retain) NSString *sightName;
@property (nonatomic, retain) NSString *image;
@property (nonatomic, retain) NSString *imageType;;
@property (nonatomic) CLLocationCoordinate2D coordinate;
- (id) initWithTrip:(Trip*) theTrip
sightData:(NSDictionary*) sightData;
@end
There are a few includes here you won't be needing until later, but I have you add them now just to get them taken care of and out of the way.
Now you have an object that can provide data for both the SightListController
and its Web view, and things are starting to fall into place. Well, almost, because you'll need to figure out where the data for each Sight
comes from. And that's where the property lists come in.
Up to now you've gotten the data displayed in the table view by hard coding it in your application, such as when you created the menuList
back in Book V, Chapter 2. Listing 1-5 shows how you did it then:
Example 1-5. viewDidLoad
- (void)viewDidLoad { [super viewDidLoad]; sectionsArray = [[NSArray alloc] initWithObjects: [[NSNumber alloc]initWithInt:4], [[NSNumber alloc]initWithInt:2], nil]; self.title = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleName"]; menuList = [[NSMutableArray alloc] init]; [menuList addObject:[NSMutableDictionary dictionaryWithObjectsAndKeys: NSLocalizedString(@"Map", @"Map Section"),kSelectKey, NSLocalizedString(@"Where you are", @"Map Explain"), kDescriptKey, nil, kControllerKey, nil]]; [menuList addObject:[NSMutableDictionary dictionaryWithObjectsAndKeys: NSLocalizedString(@"Sights", @"Sights Section"), kSelectKey, NSLocalizedString(@"Places to see", @"Places to see Explain"), kDescriptKey, nil, kControllerKey, nil]]; [menuList addObject:[NSMutableDictionary dictionaryWithObjectsAndKeys: NSLocalizedString(@"Hotels", @"Hotels Section"), kSelectKey, NSLocalizedString(@"Places to stay", @"Places to stay Explain"), kDescriptKey, nil, kControllerKey, nil]]; [menuList addObject:[NSMutableDictionary dictionaryWithObjectsAndKeys: NSLocalizedString(@"Weather", @"Weather Section"), kSelectKey, NSLocalizedString(@"Current conditions", @"Weather Explain"), kDescriptKey, nil, kControllerKey, nil]]; [menuList addObject:[NSMutableDictionary dictionaryWithObjectsAndKeys: NSLocalizedString(@"Servicing", @"Service Section"), kSelectKey, NSLocalizedString(@"Service records",
@"Service records Explain"), kDescriptKey, nil, kControllerKey, nil]]; [menuList addObject:[NSMutableDictionary dictionaryWithObjectsAndKeys: NSLocalizedString(@"The Car", @"Car Information Section"), kSelectKey, NSLocalizedString(@"About the car", @"About the car"), kDescriptKey, nil, kControllerKey, nil]];
Of course, doing it this way does create a problem. Although I could hard code the initial Sight
data in my program, doing so doesn't give me much flexibility. This makes it really hard to update and makes the code cumbersome. In addition, although you won't be doing it here as part of the run through of the RoadTrip app, it also makes it very difficult to allow the user to enter his or her own sights.
Either I have to build some kind of array into my program for the sights I want to provide the user (and "waste" the CPU cycles and memory to build it every time I run the program), or I can store all of this information in a file and, based on the state I'm in (California, Colorado, whatever) or some other user preference, display the data in that file.
When that kind of data is in a file, I don't have to rebuild my program every time I add or change a sight — all I have to do is change the file, which (as you'll see) is really pretty easy. I can even allow users to add their own sights. I talk about that part of the puzzle in more detail in Chapter 3 of this minibook, but for now I concentrate on where you get the initial data.
Fortunately, Cocoa supports an easy-to-use mechanism called a property list (plist) to manage this kind of data.
The next section covers property lists and you will create a data source for you app's data source requirements using a plist. In Book V, you created a generic Web controller; think of using a plist in a Table view controller as a creating a generic Table view controller. The Table view controller simply displays what is in the plist.
Property lists are used extensively by applications and other system software on Mac OS X and iPhone OS. For example, the Mac OS X Finder stores file and directory attributes in a property list, and the iPhone OS uses them for user defaults. You also get a Property List editor with Xcode, which makes property list files (or plists as they are referred to) easy to create and maintain in your own programs.
Figure 1-3 displays the property list I show you how to build, one that will contain the data necessary for each Sight
object in the RoadTrip app.
When you know how to work with property lists, it's actually easy, but like most things, getting there is half the fun.
Book II has an explanation of property lists, but it never hurts to go through it again in a new context.
Property lists are perfect for storing small amounts of data that consist primarily of strings and numbers. What adds to their appeal is the ability to easily read them into your program, use the data, and (although you won't be doing it in the RoadTrip application) modify the data and then write them back out again. That's because Cocoa provides a small set of objects that have that behavior built right in.
The technical term for these kinds of objects is serializable. A serializable object can convert itself into a stream of bytes so that it can be stored in a file and can then reconstitute itself into the object it once was when it is read back in — yes "beam me up, Scotty" does exist, at least on your computer.
These objects, called property list objects, that you have to work with are as follows:
NSData
and NSMutableData
NSDate
NSNumber
NSString
and NSMutableString
NSArray
and NSMutableArray
NSDictionary
and NSMutableDictionary
As shown in the plist in Figure 1-3, the root is an array and the data for each one of the Sights
is held in a dictionary. If you are a little hazy on arrays and dictionaries I will be explaining them in detail.
You'll notice a division in the preceding list. That is because there are two kinds of property list objects.
One thing that differentiates property list object containers (NSArray, NSDictionary
), besides their ability to hold other objects, is that they both have methods called writeToFile::
, which write the property list to a file, and a corresponding initWithContentsOfFile:
, which initializes the object with the contents of a file. So, if I create an array or dictionary and fill it chock full of objects of the property list type, all I have to do to save it to a file is tell it to go save itself — or create an array or dictionary and then tell it to initialize itself from a file.
You have already worked with arrays in Book V when you saved the current state of the object, and I'll expand on them as well as dictionaries in later sections. These containers can contain other containers as well as the primitive types. Thus, you might have an array of dictionaries (and you do), and each dictionary might contain other arrays and dictionaries, as well as the primitive types.
NSData
and NSMutableData
are wrappers (an object that is there mostly to turn something into an object) in which you can dump any kind of data and then have that data act as an object. You've used NSData
before when you downloaded the car servicing information in Book V as well as when you used the geocoder
. You haven't seen NSDate
yet, and I won't be using it in the book, but for your information, it is a Cocoa class for date and time handling.
Book II covers this plists in general, but here I'll have you build a specific Sight plist:
In the Groups & Files list (at the left in the Xcode project window), select Static Data (at the top of the list) and then choose File
The New File dialog appears.
Choose Resource under the Mac OS X heading in the left pane, and then select Property List, as shown in Figure 1-4.
Click the Next button.
You see a new dialog asking for some more information.
Enter the filename Sight.plist; then press Return (Enter) or click Finish.
You now see a new item called Sight.plist
under Static Data, in the Groups & Files list shown in Figure 1-5.
In the editor pane, you can see Xcode's Property List editor with the root entry selected. In this case, the Type has defaulted to Dictionary
; the other option is Array
, which is what you want, so get ready to change the root to Array
.
In the Type pop-up menu, change the type from Dictionary
to Array
, as I have in Figure 1-6.
Click the icon (the one with the three parallel lines) at the end of the entry, as shown in Figure 1-7.
A new entry appears, as you can see in Figure 1-8.
Item 0 is the file that holds your Sights. This is the first entry for an actual Sight itself.
Select Dictionary from the Type pop-up menu, as I have in Figure 1-9.
Your new entry is made into a dictionary, as shown in Figure 1-10.
Click the triangle next to Item 0 and make sure it's pointing down, as shown in Figure 1-10. Then select the 3 lines icon. (Make sure the triangle is pointing down; if not, you won't see the 3 lines but you will see a +.)
You see a new entry under the dictionary, like the one in Figure 1-11.
These disclosure triangles work the same way as those in the Finder and the Xcode editor. The Property List editor interprets what you want to add based on the triangle. So, if the items are revealed (that is, the triangle is pointing down), it assumes you want to add a subitem. If the subitems are not revealed (that is, the triangle is pointing sideways), it assumes you want to add an item at that level. In this case, with the arrow pointing down, you will be adding a new entry — a subitem — to the dictionary. If the triangle were pointing sideways, you would be entering a new entry under the root. The icon at the end of the row also helps. If it is three lines, as you see in Figure 1-10, you're going to be creating a new subitem of the entry in that row. A +, like in Figure 1-11, tells you that you're going to be creating a new item at the same level.
In the Key field, enter sight name
and then double-click (or tab to) the Value field and enter Golden Gate Bridge
, as shown in Figure 1-12.
Click the + icon at the end of the entry (row) you just added, and you will get a new entry. In the Key field, enter title
and in the Value filed enter Golden Gate Bridge
.
This one will be at the same level.
It can be any of the property list objects I talk about at the beginning of this chapter, but String
, which will already be selected, is the one you want here.
Repeat Step 10 for the following keys and values:
Value | |
---|---|
resource | GoldenGateBridge |
resource type | html |
image | GGBPhoto |
image type | jpg |
subtitle | Don't miss it |
latitude | 37.818774 |
longitude | −122.478415 |
Click the disclosure triangle to hide the Dictionary
entries, as shown in Figure 1-13.
You want to create a parallel dictionary at this level.
Click the + icon next to the dictionary and add another dictionary.
As you can see, the default is String
, as shown in Figure 1-14, so you will need to change it to Dictionary
.
Add the following entries below to this dictionary:
Key | Value |
---|---|
sight name | Alcatraz |
Title | Alcatraz |
resource | Alcatraz |
resource type | html |
image | Alcatraz |
image type | jpg |
subtitle | Don't miss the boat back |
latitude | 37.826664 |
longitude | −122.423021 |
Repeat Steps 11 and 12 to create another dictionary with the following key value pairs.
Make sure you spell the entries exactly as specified or else you won't be able to access them using the examples in this book.
When you're done, your plist should look like the one back in Figure 1-3.
If you want to be really lazy (although I don't suggest it this time), you could download the complete project from http://nealgoldstein.com
and simply copy the plist in the same way I explain how to copy the .png
files in the next section.
Just as you added the CarSpecs.html
file in Book V, Chapter 4, you do the same thing with the images you need for the Sights
objects.
The content for the Sight
objects are in the files (as you could tell from the plist) as GGBPhoto.jpg Alcatraz.jpg and CoitTower.jpg. (while png is the preferred format for iPhone icons, jpg or jpeg is used for photos.) To make the content available to the application, you need to include it in the application bundle itself, although you could have downloaded it the first time the application ran.
The files are included in the final code for this chapter, so first you have to download NNN from http://nealgoldstein.com
.
Now, you can add the files to your bundle one of two ways:
Open the Project window and drag the .jpg
files into the Project folder from the NNN Project folder.
(You should put it in the Static Data folder in your project.)
or
Choose Project
At this point, you have two of the necessary components for dealing with the sights in your application: You have the data for each Sight
in the plist you just created, and you have a Sight
object that has an instance variable defined to hold the data.
So how do you create each Sight
and get the data into it? That's another job for Trip
.
Creating a property list for your RoadTrip app data is a good start, but you also need to know how to create each of the Sight
objects you need and then figure out how to initialize each object with the data from the plist. You find out how to do that stuff in this section, and, as an added bonus, I also explain a bit more about arrays and dictionaries.
You can get the Sight
objects ball rolling with some additions to Trip.h
. Make the following addition to the Trip.h
files:
Add an NSMutable array to hold the list of sights.
NSMutableArray *sights;
you're not (yet) familiar with mutable arrays, don't worry, I explain them in great detail shortly. Technically, you could use a regular array here, but I'm preparing you for the time when you'll modify this app to allow the user to add and eliminate sights.
Make sights
a property in Trip.h
.
@property (nonatomic, retain) NSMutableArray * sights;
If you've read my other books or earlier chapters in this book, you know that I'm not a big fan of using properties to access model information. In this case, however, I feel that it's my responsibility to show you how you can use them. So, if you don't share my anal approach to that subject, this is how you'd do it. This technique will make your list of sights available to the view controller — the one you'll code in a minute, as in the one that displays the list of sights in a table view.
Make the following additions to Trip.m
. (This stuff is a bit more involved, unfortunately, so be prepared for a workout.)
Add the following code to Trip.m
.
By now you should know that you can put the # import
anywhere above the @implementation Trip
. You need to put the @synthesize
after the @implementation Trip
.
#import "Sight.h" ... @implementation Trip @synthesize sights;
The @ synthesize
tells the complier to generate the assessors for the sights property.
Add the following code above @implementation Trip
.
@interface Trip () - (void) loadSights; @end
Although I suggested using loadSights
in Book V, I'm actually going to implement it here. loadSights
is an internal method to Trip. It takes a property list and loads an array of Sight
objects. I do want this to be a private method — accessible only to the Trip
class. If you're coming from C++, you probably want this method to be private, but there's no private construct in Objective-C. What you need to do to hide them is make their declarations in the implementation file and create an Objective-C extension (kind of like a category). If you're unfamiliar with categories and extensions, I explain them in the next section.
One of the features of the dynamic runtime dispatch mechanism employed by Objective-C is that you can add methods to existing classes without sub-classing. The Objective-C term for these new methods is categories. A category allows you to add methods to an existing class — even to one to which you do not have the source. This powerful feature allows you to extend the functionality of existing classes.
This looks a lot like class interface declaration — except the category name is listed within parentheses after the class name, and there is no superclass (or colon for that matter). Categories (unlike protocols, which I address in the next chapter) do have access to all the instance variables and methods of a class. And I do mean all, even ones declared @private
, but you'll need to import the interface file for the class it extends. You can also add as many categories as you want.
You can add methods to a class by declaring them in an interface file under a category name and defining them in an implementation file under the same name. What you can't do is add more instance variables.
The methods the category adds become honestly and truly part of the class type; they aren't treated as "step methods." The methods you would add to Trip
using a category become part of the Trip
class and are inherited by all the class's subclasses, just like other methods. The category methods can do anything that methods defined in the class proper can do. At runtime, there's no difference.
You use a category like this to add more functionality to an existing class. For example, UITableView.h
contains an extension to NSIndexPath
to make it easier to represent a section and row, something you've taken advantage of every time you reference an indexPath.row
, for example:
@interface NSIndexPath (UITableView) + (NSIndexPath *)indexPathForRow:(NSUInteger)row inSection:(NSUInteger)section; @property(nonatomic,readonly) NSUInteger section; @property(nonatomic,readonly) NSUInteger row; @end
What you're doing here, however, is a bit more specialized. You're creating a class extension.
Class extensions are like anonymous (unnamed) categories, except that the methods they declare must be implemented in the main @implementation
block for the corresponding class.
Doing it this way allows you to have a publicly declared set of methods and to then have additional methods declared privately for use solely by the class.
Class extensions allow you to declare additional methods for a class in a location other than the class @interface
. This declaration is what you did when you added the following code above @implementation Trip
:
@interface Trip () - (void) loadSights; @end
Keep the following facts about this little snippet of code in mind:
As opposed to a category, no name is given in the parentheses in the second @interface
block;
The implementation of the loadSights
method appears within the main @implementation
block for the class.
The implementation of the loadSights
method must appear within the @implementation
for the class just like the methods/properties found in the public @interface
.
If the developer doesn't have the source code or doesn't include the .m
file, he or she will have no idea that the methods in an extension exist. Of course, this being Objective-C, the developer could always invoke the method if they knew about it.
But at least it makes the point — don't look behind the curtain.
To do some sight loading, add the code in bold in Listing 1-6 to the already existing initWithName:
method in Trip.m
. This addition invokes the method you declared in the previous section, which loads the list of sights from a property list and turns them into Sight
objects — this code is starting to get exciting.
Example 1-6. initWithName Now Invokes loadSights
- (id) initWithName:(NSString*) theTrip { if ((self = [super init])) { tripName = theTrip;
[tripName retain];
carInformation = [[CarInformation alloc]
initWithTrip:self];
car Servicing = [[CarServicing alloc] initWithTrip:self];
weather = [[Weather alloc] init];
[self loadSights];
}
return self;
}
Now you see how to actually use all that data you created in the plist.
Add the code in Listing 1-7 to Trip.m
.
Example 1-7. Using the plist
- (void) loadSights { NSString *sightsDataPath = [[NSBundle mainBundle] pathForResource:@"Sight" ofType:@"plist"]; NSArray * sightsList = [[NSArray alloc] initWithContentsOfFile:sightsDataPath]; sights = [[NSMutableArray alloc] initWithCapacity:[sightsList count]]; for (NSMutableDictionary* sightData in sightsList) { Sight* newSight = [[Sight alloc] initWithTrip:self sightData:sightData]; [sights addObject:newSight]; } }
This method starts by locating the Sights.plist
you just created in your application bundle:
NSString *sightsDataPath = [[NSBundle mainBundle] pathForResource:@"Sights" ofType:@"plist"];
"What bundle?" you say? Well, when you build your iPhone application, Xcode packages it as a bundle — one containing the following:
The application's executable code
Any resources that the app has to use (for instance, the application icon, other images, and localized content — in this case the plist, html files, and .png files)
The info.plist
, also known as the information property list, which defines key values for the application, such as bundle ID, version number, and display name
Next it loads that file into the sightsList
array:
NSArray * sightsList = [[NSArray alloc]initWithContentsOfFile :sightsDataPath];
initWithContentsOfFile:
, as I mention earlier, is a method in the array and dictionary classes:
Next you allocate an array, sights
(the array that you made a property earlier), to hold the Sight
objects:
sights = [[NSMutableArray alloc] initWithCapacity:[sightsList count]];
Then you take each entry in the sightsList
array and use it to create a Sight
object and load each into the sights
array (the one you just created — this is why you made it mutable):
for (NSMutableDictionary* sightData in sightsList) {
Sight* newSight = [[Sight alloc] initWithTrip self
sightData:sightData];
[sights addObject:newSight];
}
You allocate a new Sight
and initialize it with the dictionary in the plist you created for each sight and that was reconstituted when you loaded the plist into the sightsList
. After that, you add the new Sight
object to the sights
array. If you aren't familiar with the for
method, I explain it — as well as more about arrays — in the next section.
Two kinds of arrays are available to you in Cocoa. The first is an NSMutableArray
, which allows you to add objects to the area as needed — that is, the amount of memory allocated to the class is dynamically adjusted as you add more objects.
The second kind of array is an NSArray
, which allows you to store a fixed number of objects, which are specified when you initialize the array. Because in this case you need the dynamic aspect of an NSMutableArray
, I start my explanation there. As you remember, you used an array to save the last view in Book V, Chapter 3.
NSMutableArray
arrays (I just call them arrays from now on when what I have to say applies to both NSArray
and NSMutableArray
) are ordered collections that can contain any sort of object. The collection doesn't have to be made up of the same objects. So you can have a number of Budget
objects, for example, or Xyz
objects mixed in — all that's fine, as long as they're all objects.
As I've said, arrays can hold only objects. But sometimes you may, for example, want to put a placeholder in a mutable array and later replace it with the "real" object. You can use an NSNull
object for this placeholder role.
This is how you allocate and initialize the mutable array.
sights = [[NSMutableArray alloc] initWithCapacity:[sightsList count]];
When you create a mutable array, you have to estimate the maximum size, which helps optimization. This is just a formality, and whatever you put here does not limit the eventual size. Here I use [sightsList count]
— this is the number of dictionaries you created —, each of which will provide the data for a corresponding Sight
object.
After I create a mutable array, I can start to add objects to it.
[sights addObject:newSight];
When you add an object to an Objective-C array, the object isn't copied, but rather receives a retain
message before it's added to the array. When an array is deallocated, each element is sent a release
message.
Technically (computer science-wise) what makes a collection an array is that you access its elements using an index, and that index can be determined at runtime. You get an individual element from an array by sending the array the objectAtIndex:
message, which returns the array element you requested.
Depending on what you are doing with the array or how you're using it (arrays are very useful), objectAtIndex:
will be one of the main array methods that you use.
Another method you'll use (and actually did use already) is count
, which gives you the number of elements in the array — I showed you that when I explained about initializing the arrays, where it looked like this:
sights = [[NSMutableArray alloc] initWithCapacity:[sightsList count]];
Objective-C 2.0 provides a language feature that allows you to enumerate over the contents of a collection. This is called fast enumeration, and it became available in Mac OS X 10.5 (Leopard) with version 2.0 of Objective-C. As I've mentioned, this book is based on Mac OS 10.6 — and OS 3.0 on the iPhone. (If you need to program for OS X 10.4, you need to use an NSEnumerator
, which I don't cover in this book.) Enumeration uses the for in
feature, which is a variation on a for
loop.
What enumeration effectively does is sequentially march though an array, starting at the first element and returning each element for you to do "something with." The "something with" you will want to do in this case is use that element as an argument in the initWithTrip::
message.
For example, this code marches through the array and sends the initWithTrip::
message using each element in the array (an NSDictionary
).
for (NSMutableDictionary* sightData in sightsList) { Sight* newSight = [[Sight alloc] initWithTrip: self sightData: sightData]; [sights addObject:newSight]; }
Here's how this works:
Take each entry (for
) in the array (in sightsList
) and copy it into the variable that you've declared (NSMutableDictionary* sightData
).
Use it as an argument in the initWithTrip:
message initWithTrip:sightData:
.
Continue until you run out of entries in the array.
The identifier sightData
can be any name you choose. NSDictionary
is the type of the object in the array (or it can be id
, although I won't get into that here).
To be more formal, the construct you just used is called for in
, and it looks like
for ( TypeaVariable
inexpression
) {statements
}
or
TypeaVariable
; for (aVariable
inexpression
) {statements
}
where you fill in what is italicized. There is one catch, however: You aren't permitted to change any of the elements during the iteration, which means you can go through the array more than once without worry.
The following section takes a more detailed look at dictionaries.
Dictionaries are the citified versions of arrays. They both pretty much do the same things, but dictionaries add a new level of sophistication.
I love dictionaries, now. But I have to admit that when I started programming with Objective-C and Cocoa, trying to get my head around the idea of dictionaries was a real challenge — not because dictionaries are hard, because they really aren't. The problem was because of what you can do with them. Not only will you use them to hold property list objects, but you can also use them to hold application objects — just as you did with the array that holds Sight
objects.
So, in many ways dictionaries are like the arrays you used earlier — they are a container for other objects. Dictionaries are made up of pairs of keys and values. A key-value pair within a dictionary is called an entry. Both the key and the value must be objects, so each entry consists of one object that is the key (usually an NSString
) and a second object that is that key's value (which can be anything, but in a property list must be a property list object). Within a dictionary, the keys are unique.
You use a key to look up the corresponding value. This works like your real-world dictionary, where the word is the key, and its definition is the value. (Do you suppose that's why they're called dictionaries?)
So, for example, if you have an NSDictionary
that stores the name for each Sight
, you can ask that dictionary to cough up the name (value) for the site name (key).
self.sightName = [sightData valueForKey: @"sight name"];
You'll see this in action when you implement the Sight
methods in the next section.
Although you can use any kind of object as a key in an NSDictionary
, keys you plan on using in property list dictionaries have to be strings, and I'll stick to that here. You can also have any kind of object for a value, but again, if you're using them in a property list, they all have to be property list objects as well.
NSDictionary
has a couple of basic methods you will be using:
count
— The count
method gives you the number of entries in the dictionary.
objectForKey:
— The objectForKey:
method gives the value for a given key.
In addition, the methods writeToFile:atomically:
and initWithContentsOfFile:
cause a dictionary to write a representation of itself to a file and to read itself in from a file, respectively.
If an array or dictionary contains objects that are not property list objects, you can't save and then restore them using the built-in methods for doing so.
Just as with an array, a dictionary can be static (NSDictionary
) or mutable (NSMutableDictionary). NSMutableDictionary
adds a couple of additional basic methods — setObjectForKey:
and removeObjectForKey:
, which enable you to add and remove entries, respectively.
Now you can enter the code for the Sight
. Add the code in bold in Listing 1-8 to Sight.m
.
Example 1-8. Sight.m
#import "RoadTripAppDelegate.h"
#import "Sight.h"#import "Trip.h"
@implementation Sight@synthesize coordinate;
@synthesize sightName, image, imageType;
- (id) initWithTrip: (Trip*) theTrip sightData:
(NSDictionary*) sightData {
if ((self = [super init])) {
trip = theTrip;
[trip retain];
self.sightName =
[sightData valueForKey: @"sight name"];
title = [sightData valueForKey: @"title"];
[title retain];
subtitle = [sightData valueForKey: @"subtitle"];
[subtitle retain];
resource = [sightData valueForKey: @"resource"];
[resource retain];
resourceType =
[sightData valueForKey: @"resource type"];
[resourceType retain];
if ([sightData valueForKey: @"image"]) {
self.image = [sightData valueForKey: @"image"];
self.imageType =
[sightData valueForKey: @"image type"];
}
else {
image = nil;
imageType = nil;
}
coordinate.latitude =
[[sightData valueForKey: @"latitude"] doubleValue];
coordinate.longitude =
[[sightData valueForKey: @"longitude"] doubleValue];
}
return self;
}
@end
Although in general this is just another run-of-the-mill initialization method, I want to point out a few things.
First off, coordinate
is a property here. You do this because in Chapter 3 of this minibook you'll be making Sight
an annotation and, as I mention in Book V, Chapter 5, having a coordinate
property is one of the requirements for an annotation. I'll have you implement the rest of what you need to do in Chapter 3 when I show you how to display Sight
s (and later Hotel
s) as annotations on the map.
You use the valueForKey:
method I explain in the previous section to get the information from the dictionary and assign it to the Sight
instance variable.
At this point, you only have a few more things left to do before you'll be able to see the results of your efforts so far — the SightListController
with a list of sights and thumbnail pictures of each.
In this home stretch, the first thing you do is implement a few of the required methods in the SightListController
. Replace the numberOfSectionsInTableView:
and numberOfRowsInSection:
methods in SightListController.m
with the methods in Listing 1-9.
Example 1-9. Some Table View Methods
- (NSInteger)numberOfSectionsInTableView: (UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [trip.sights count]; }
That's all pretty straightforward except for the [trip.sights count]
business. trip.sights
is the array of Sight
objects you created in Trip
, and as I explain earlier, the count
method tells you how many entries there are in the array.
In order to display the list of sights, you'll be doing essentially what you did in Book V to create table views — fire up the tableView:cellForRowAtIndexPath:
method in order to do a little bit of formatting. Listing 1-10 shows you how that was done back then when you were dealing with the RootViewController
.
Example 1-10. Displaying a Cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableVie wCellStyleDefault reuseIdentifier:kCellIdentifier] autorelease]; cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; CGRect subViewFrame = cell.contentView.frame; subViewFrame.origin.x += kInset; subViewFrame.size.width = kInset+kSelectLabelWidth; UILabel *selectLabel = [[UILabel alloc] initWithFrame:subViewFrame]; selectLabel.textColor = [UIColor blackColor]; selectLabel.highlightedTextColor = [UIColor whiteColor]; selectLabel.font = [UIFont boldSystemFontOfSize:18]; selectLabel.backgroundColor = [UIColor clearColor]; [cell.contentView addSubview:selectLabel]; subViewFrame.origin.x += kInset+kSelectLabelWidth; subViewFrame.size.width = kDescriptLabelWidth; UILabel *descriptLabel = [[UILabel alloc] initWithFrame:subViewFrame]; descriptLabel.textColor = [UIColor grayColor]; descriptLabel.highlightedTextColor = [UIColor whiteColor]; descriptLabel.font = [UIFont systemFontOfSize:14]; descriptLabel.backgroundColor = [UIColor clearColor]; [cell.contentView addSubview:descriptLabel];
int menuOffset = [self menuOffsetForRowAtIndexPath:indexPath]; NSDictionary *cellText = [menuList objectAtIndex:menuOffset]; selectLabel.text = [cellText objectForKey:kSelectKey]; descriptLabel.text = [cellText objectForKey:kDescriptKey]; [selectLabel release]; [descriptLabel release]; } return cell; }
As you can see, the text for each cell comes from a dictionary that acts as the model for each row.
In the case of the SightListController
, instead of a dictionary, you'll be using a Sight
object. Add the code in Listing 1-11 to Sight.m
.
Example 1-11. tableView:cellForRowAtIndexPath:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITabl eViewCellStyleDefault reuseIdentifier:kCellIdentifier] autorelease]; } cell.textLabel.text = [((Sight*)[trip.sights objectAtIndex:indexPath.row]) sightName]; cell.imageView.image = [self createThumbNail:((Sight*) [trip.sights objectAtIndex:indexPath.row])]; return cell; }
This listing is a mostly ordinary tableView:cellForRowAtIndexPath:
method code. What's interesting is that you're getting the cell's text from the Sight
object in the trip.sights
array:
cell.textLabel.text = [((Sight*)[trip.sights objectAtIndex:indexPath.row]) sightName];
Similarly, you get the cell's image from the Sight
object, create a thumbnail from it (I explain that later), and add that to the cell:
cell.imageView.image = [self createThumbNail:((Sight*) [trip.sights objectAtIndex:indexPath.row])];
Even though you are using the standard default cell style in our table view . . .
cell = [[[UITableViewCell alloc] initWithStyle:UITableV iewCellStyleDefault reuseIdentifier:kCellIdentifier] autorelease];
you can easily add an image by simply assigning the image to the right cell subview. As an image expands to the right, it pushes the text in the same direction.
Because you're using the kCellIdentifier
constant (which makes changing your mind about values at some later point much easier), you also have to import the file that declares it.
#import "Constants.h"
Later, as you will see, you'll have the Sight
also return a NSURLRequest
, just as you did in the model objects in Book V. The idea here is to enable a Web view to display the content — in this case, a brief description of the sight.
The difference between the SightListController
and the RootViewController
implementation of tableView:cellForRowAtIndexPath
is that the SightListController
implementation relies on model objects created for a file (a file that you could modify at some later point) rather than the hard coded dictionary you created.
You only have one thing left to do in order to see the SightListController
table view populated with a nice list of sights and pretty thumbnail pictures, and that is implement the createThumbNail:
method to take the images and reduce them to a size you can display in the table view.
The code for transforming images into handy thumbnails is shown in Listing 1-12:
Example 1-12. Creating the Thumbnails
- (UIImage*) createThumbNail:(Sight*) aSight { if (aSight.image) { NSString *filePath = [[NSBundle mainBundle] pathForResource:aSight.image ofType:aSight.imageType]; UIImage* selectedImage = [[UIImage alloc] initWithContentsOfFile:filePath]; CGRect rect = CGRectMake(0.0, 0.0, 36, 36); UIGraphicsBeginImageContext(rect.size); [selectedImage drawInRect:rect]; UIImage* theScaledImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return theScaledImage; } else { return nil; } }
I'm not going to get too much into drawing in this book (I'm saving that for my next book, Developing iPhone Games For Dummies), but I do want to lay down the basic steps:
Make sure there is an image to create a thumbnail from:
if (aSight.image) {
If the image is there, then you need to grab it from the bundle:
NSString *filePath = [[NSBundle mainBundle]pathForResource:aSight.image ofType:aSight.imageType]; UIImage* selectedImage = [[UIImage alloc] initWithContentsOfFile:filePath];
Create a rectangle that corresponds to the size you want the image to be:
CGRect rect = CGRectMake(0.0, 0.0, 36, 36);
Use the UIGraphicsBeginImageContext
function to create a new image-based graphics context:
UIGraphicsBeginImageContext(rect.size);
After creating this context, you can draw your image contents into it:
[selectedImage drawInRect:rect];
and then use the UIGraphicsGetImageFromCurrentImageContext
function to generate an image based on what you drew:
UIImage* theScaledImage = UIGraphicsGetImageFromCurrentImageContext();
When you're done creating images, use the UIGraphicsEndImageContext
function to close the graphic context:
UIGraphicsEndImageContext();
Return the new scaled image:
return scaledImage;
Your code should now compile and run correctly. You'll have a couple of complier warnings because you haven't implemented the title
and subtitle
methods of Sight
, but you'll get to that soon. If you select Sights for the main table view, you'll see a list of sights, with an image. But if you select a row, you won't see anything because you haven't implemented its tableView:didSelectRowAtIndexPath:
method yet.
That's what you'll need to do next.
In Book V, you implemented the generic WebViewController
that can — and did — display any kind of content that the user selected in the main table view (via the RootViewController
), and no doubt you'd like to use it here as well.
Using Weather
as an example, you implemented it in the following way back in Book V. (I go into more detail in the next chapter when I explain the problem you are now facing in more detail).
The RootViewController
, which knows what the user wants to see displayed, sends a selector to the WebViewController
in its tableView:didSelectRowAtIndexPath:
method.
The WebViewController
stores the selector when it initializes itself.
The WebViewController
view sends a message to Trip
perform the selector.
Trip
performs the selector method, which sends a message to the Weather
object, which returns the NSURLRequest
.
WebViewController
uses that NSURLRequest
in its message to the Web view to load the necessary data.
The implementation of the generic WebViewController
was predicated on Trip
being able to identify the content object that owned the content that needed to be displayed. In the case of the content in Book V, this worked well because for any given choice of content (car information, car servicing information, or the weather) there was indeed only one possible object that was associated with a user choice. For example, if the user chose Car Information, there was only one CarInformation
object for Trip
to get the NSURLRequest
from. But in the case of Sight
, that isn't true. How, then, does Trip
know which Sight
object to send a message to?
What I'd like to do is include that information as a selector argument —, but the performSelectorOnMainThread:::
method does have its limitations — the selector you want performed can't return a value and can only take either a single argument of type id
or no arguments at all. Unfortunately, you are already using your single argument allowance to send a reference to the WebViewController
that is used to send that very same WebViewController
the loadWebView:
message.
Fortunately, as you'll see in the next chapter, there's a good solution: making the model objects WebViewController
delegates.
3.16.218.105