Chapter 1. Show Me the Data

In This Chapter

  • 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).

Seeing the Sights

You will be implementing the sequence in the RoadTrip application you see documented in Figures 1-1 and 1-2.

Tinkers to Evers to Chance.

Figure 1-1. Tinkers to Evers to Chance.

The sights on the Map.

Figure 1-2. The sights on the Map.

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:

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

  2. Make a few changes in the RootViewController to load a SightListController.

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

  4. 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.

Starting with the Table View

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:

  1. Supply the number of sections you want.

    You do that with the help of the numberOfSectionsInTableView: method of the UITableViewDataSource protocol.

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

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

  4. 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.

Adding the controller

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:

  1. In the RoadTrip project window, select the Classes folder, and then choose File

    Adding the controller

    Selecting the Classes folder first tells Xcode to place the new file in the Classes folder.

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

  3. Enter SightListController.m in the File Name field and then click Finish.

Tip

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

  1. Choose File

    Adding the controller
  2. 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.

  3. Enter Sight in the File Name field and then click Finish.

I keep my Sight.m and .h classes in the Model Classes folder.

Tip

At this point, you're all set up to start adding code.

Setting up the controller

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:

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

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

Example 1-2. Setting Up the Header File

#import <UIKit/UIKit.h>
@class Trip;
@class Sight;

@interface SightListController : UITableViewController {

  Trip         *trip;
}

- (id) initWithTrip:(Trip*) theTrip;
- (UIImage *) createThumbNail:(Sight*) aSight;

@end

Creating the SightListController in the RootViewController

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.

Seeing the Sights

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.

Sight.plist Is a 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.

The Sight property list.

Figure 1-3. The Sight property list.

When you know how to work with property lists, it's actually easy, but like most things, getting there is half the fun.

Working with property lists

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.

  • Primitives: The term primitives is not a reflection on how civilized these property objects are, but it is a word used to describe the simplest kind of object. They are what they are.

  • Containers: Containers can hold primitives as well as other containers.

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.

Adding a plist to Your Project

Book II covers this plists in general, but here I'll have you build a specific Sight plist:

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

    Adding a plist to Your Project

    The New File dialog appears.

  2. Choose Resource under the Mac OS X heading in the left pane, and then select Property List, as shown in Figure 1-4.

    Creating the plist.

    Figure 1-4. Creating the plist.

  3. Click the Next button.

    You see a new dialog asking for some more information.

  4. 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.

    A new plistfile.

    Figure 1-5. A new plistfile.

  5. In the Type pop-up menu, change the type from Dictionary to Array, as I have in Figure 1-6.

    Change the root to Array.

    Figure 1-6. Change the root to Array.

  6. Click the icon (the one with the three parallel lines) at the end of the entry, as shown in Figure 1-7.

    Add an entry.

    Figure 1-7. Add an entry.

    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.

    A new entry.

    Figure 1-8. A new entry.

  7. Select Dictionary from the Type pop-up menu, as I have in Figure 1-9.

    Select Dictionary.

    Figure 1-9. Select Dictionary.

    Your new entry is made into a dictionary, as shown in Figure 1-10.

    A new Dictionary.

    Figure 1-10. A new Dictionary.

  8. 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.

    Note

    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.

    An (new) entry in the dictionary.

    Figure 1-11. An (new) entry in the dictionary.

  9. 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.

  10. 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:

    Key

    Value

    resource

    GoldenGateBridge

    resource type

    html

    image

    GGBPhoto

    image type

    jpg

    subtitle

    Don't miss it

    latitude

    37.818774

    longitude

    −122.478415

    The sight name here is Golden Gate Bridge.

    Figure 1-12. The sight name here is Golden Gate Bridge.

  11. 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.

    Hide the dictionary entries.

    Figure 1-13. Hide the dictionary entries.

  12. 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.

    Adding a new dictionary.

    Figure 1-14. Adding a new 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

  13. Repeat Steps 11 and 12 to create another dictionary with the following key value pairs.

    Key

    Value

    sight name

    Coit Tower

    title

    Coit Tower

    resource

    CoitTower

    resource type

    html

    image

    CoitTower

    image type

    jpg

    subtitle

    Great San Francisco views

    latitude

    37.802369

    longitude

    −122.405819

Warning

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.

Tip

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.

Adding images

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

    Adding images
Adding the .jpg files.

Figure 1-15. Adding the .jpg files.

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.

Initializing the Sight Objects with plist Data

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:

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

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

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

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

Categories and extensions

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.

Loading the Sights

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.

Tiptoeing through an array

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:

  1. Take each entry (for) in the array (in sightsList) and copy it into the variable that you've declared (NSMutableDictionary* sightData).

  2. Use it as an argument in the initWithTrip: message initWithTrip:sightData:.

  3. 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 ( Type aVariable in expression ) { statements }

or

Type aVariable;
for ( aVariable in expression ) {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.

Using 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.

Understanding a dictionary's keys and values

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.

The Sight object initializes itself with the plist dictionary

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 Sights (and later Hotels) 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.

Displaying the Sights in the SightListController Table View

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.

Note

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.

Drawing the Thumbs

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:

  1. Make sure there is an image to create a thumbnail from:

    if (aSight.image) {
  2. 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];
  3. Create a rectangle that corresponds to the size you want the image to be:

    CGRect rect = CGRectMake(0.0, 0.0, 36, 36);
  4. Use the UIGraphicsBeginImageContext function to create a new image-based graphics context:

    UIGraphicsBeginImageContext(rect.size);
  5. 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();
  6. When you're done creating images, use the UIGraphicsEndImageContext function to close the graphic context:

    UIGraphicsEndImageContext();
  7. Return the new scaled image:

    return scaledImage;

Compile and Run RoadTrip

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.

Houston, We Have a Problem . . .

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

  1. The RootViewController, which knows what the user wants to see displayed, sends a selector to the WebViewController in its tableView:didSelectRowAtIndexPath: method.

  2. The WebViewController stores the selector when it initializes itself.

  3. The WebViewController view sends a message to Trip perform the selector.

  4. Trip performs the selector method, which sends a message to the Weather object, which returns the NSURLRequest.

  5. WebViewController uses that NSURLRequest in its message to the Web view to load the necessary data.

The problem being . . .

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.218.105