Chapter 2. A More Flexible Generic Controller

In This Chapter

  • Understanding the limitations of the WebViewController implementation

  • Using delegation to encapsulate the model objects

  • Creating your very own protocol

  • Keeping file loading from stopping your application dead in its tracks

As you discovered at the close of the preceding chapter, after you add multiple objects of the same type to your app mix, the techniques you've been using all along to create a generic controller just stop working. In this chapter, I show you how to accomplish what you're after by using delegation instead.

It turns out that delegation, as you probably have noticed, is used often in the framework. In fact, as you'll see, it's also used in the next chapter when I show you the preferred way to deal with downloading data — the URL loading system. This chapter, then, not only shows you how to implement a more sophisticated version of your generic WebViewController, and shows you how to take advantage of delegation in your own applications, but also serves to provide the background you need to have in order to make it easier for you to use the asynchronous version of the URL loading system.

Seeing How the Old School Generic Controller Worked

To refresh your memory, you implemented your generic controller by sending a selector to the WebViewController that the WebViewController would have the Trip object perform in order to return the WebView Controller an NSURLRequest object based on the data that the user was interested in. It would get the NSURLRequest from a model object — Weather, for example. The WebViewController would then pass that on to its Web view, which would then load the data.

We are going to change that. You stay with the NSURLRequest object, but the object that supplies the NSURLRequest will be asked to do it in a much different way.

Stepping through the process

Here's how the sequence went with the Old School generic controller:

  1. The RootViewController sends a selector to the WebViewController.

    The RootViewController, which knows what the user wants to see displayed, sends a selector to the WebViewController in its tableView:didSelectRowAtIndexPath: method. (For the rest of this section I'll be highlighting the most important things you need to pay attention to in bold).

    if realtime) targetController =
       [WebViewController alloc] initWithTrip:trip
       tripSelector:@selector(returnWeather:)
       webControl:YES
       title:NSLocalizedString(@"Weather", @"Weather")];
    else [self displayOfflineAlert:
       [[menuList objectAtIndex:menuOffset]
                            objectForKey:kSelectKey]];
  2. The WebViewController stores the selector when it initializes itself.

    Check out the bolded section in Listing 2-1.

    Example 2-1. WebViewController Initializer

    - (id) initWithTrip:(Trip*)aTrip tripSelector:(SEL)
       aTripSelector webControl:(BOOL) ifWebControl
       title:(NSString*) aTitle{
      if (self = [super initWithNibName:@"WebViewController"
                                                bundle:nil]) {
        self.title = aTitle;
        tripSelector = aTripSelector;
        trip = aTrip;
        webControl = ifWebControl;
        [trip retain];
      }
      return self;
    }
  3. The WebViewController sends a message to Trip to perform theselector (method).

    Again, the bolded section in Listing 2-2 shows what that looks like.

    Example 2-2. Sending the performSelectorOnMainThread::: Message

    - (void)viewDidLoad {
    
      [super viewDidLoad];
      if (webControl) {
        UIBarButtonItem *backButton = [[UIBarButtonItem alloc]
         initWithTitle:@"Back" style:UIBarButtonItemStylePlain
                       target:self action:@selector(goBack:)];
        self.navigationItem.rightBarButtonItem = backButton;
        [backButton release];
      }
      [trip performSelectorOnMainThread:tripSelector
                            withObject:self waitUntilDone:NO];
    }
  4. Trip does what it's told and performs the selector method.

    Listing 2-3 gives the details.

    Example 2-3. Trip's returnWeather

    - (void) returnWeather:(id) theViewController {
      [theViewController
                      loadWebView:[weather weatherRealtime]];
      }
  5. The selector method sends a message to the Weather object, which returns the NSURLRequest.

    Listing 2-4 has that part.

    Example 2-4. Weather's weatherRealTime Method

    - (NSURLRequest*) weatherRealtime {
    
      NSURL *url = [NSURL URLWithString:@"http://www.
       weather.com/outlook/travel/businesstraveler/local/
       UKXX0085?lswe=CarServicing,%20UNITED%20KINGDOM&lwsa=Weathe
       rLocalUndeclared&from=searchbox_typeahead"];
      if (url == NULL) NSLog( @"Data not found %@", url);
    
      NSURLRequest* theNSURLRequest = [NSURLRequest
       requestWithURL:url];
      return theNSURLRequest;
    }

    This code simply creates an NSURLRequest that the Web view needs to load the data.

  6. The WebViewController uses that NSURLRequest in its message to the webView to load the necessary data.

    Here's that message — bolded in Listing 2-5.

    Example 2-5. Loading the Web View Data

    - (void)loadWebView:(NSURLRequest*) theNSURLRequest {
    
      [webView loadRequest:theNSURLRequest];
    }

    The loadRequest message is sent to the webView, and the Weather Web site is displayed.

The problem being . . .

As I said at the end of Chapter 1 of this minibook, this implementation of the WebViewController is 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 wasn't an issue 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 does Trip know which Sight object to send a message to?

You may remember from Chapter 1 that, although I'd like to include that information as a selector argument, the performSelectorOnMain Thread::: method can't return a value and can only take either a single argument of type id or no arguments at all. Unfortunately, as you saw in Step 3 in the preceding section, you're already using your single argument allowance to send a reference to the WebViewController that is used to send it the loadWebView: message. (The bolded code in the following block is the smoking gun here.)

[trip performSelectorOnMainThread:tripSelector
   withObject:self waitUntilDone:NO];

-(void) returnWeather:(id) theViewController {

[theViewController loadWebView:[weather weatherRealtime]];

}

Although it isn't very PC to say this, there are other ways to skin a cat.

One way is to have the SightListController set some kind of index in Trip to let it know which Sight object in the sights array the user has requested information about. Although this would work, you're starting down a slippery slope of tighter coupling between the Trip, SightListController, and the sights array. We would, after all, like all of our objects to be as ignorant as possible of how other objects work. That makes changing things much easier.

A better alternative is to use a pattern that the framework uses when faced with a similar problem.

After all, the challenge you face here is one that is faced by the framework on a regular basis. For example, when you start up an application, the framework wants to give you the opportunity to do some application-specific initialization. But because its code was probably written before your application was a gleam in your eye, it has no idea that your application even exists.

The solution to that is to have you create an application delegate that implements the ApplicationDidFinishLoading method. At start up, the UIApplication object's delegate instance variable is assigned a reference to your app delegate object — if you have implemented it. (I show you later how the application object knows that fact.) The UIApplication object then sends your app delegate the ApplicationDidFinishLoading message.

You can use the same technique here. I show you how to make the Weather, CarInformation, CarServicing, and all of the Sight objects I've come up with so far delegates of the WebViewController and then change just a few things so that instead of sending the performSelectorOnMain Thread::: message to the Trip object, it sends a similar request to its delegate instead.

I start with more about delegation.

Understanding Delegation

Delegation is a pattern (I explain patterns and their importance in more detail in iPhone Application Development For Dummies) used extensively in the UIKit and AppKit frameworks to customize the behavior of an object without subclassing. Instead, one object (a framework object) delegates the task of implementing one of its methods to another object.

To implement a delegated method, you put the code for your application-specific behavior in a separate (delegate) object. When a request is made of the delegating object that it wants to give you the opportunity to respond to, the delegate's method that implements the application-specific behavior is invoked.

The methods a class delegates are defined in a protocol. Protocols can be formal or informal. In this case I'm going to have you use a formal one; I explain why in a second.

Naming conventions

To help you recognize a delegate method, Apple has implemented some naming conventions that it uses in the framework

  • The name should start with the name of the class that's sending the message, but you should omit the prefix (UI for example) and make the first letter lowercase. For example, as shown below with the table view and application delegate method, respectively (in bold):

    - (BOOL)tableView:(NSTableView *)tableView
                                 shouldSelectRow:(int)row;
    - (BOOL)application:(NSApplication *)sender
                          openFile:(NSString *)filename;

    You follow the class name with a colon if there is an argument, as shown in the preceding code.

  • If there is only one argument — say, the sender — you add a description of the method to the class name. For example

    - (BOOL)applicationOpenUntitledFile:
                                (NSApplication *)sender;

    Based on that, you'll name your new delegate method:

    - (void) webViewController:
          (WebViewController *) controller
          nsurlRequestProcessMethod:(SEL)theProcessMethod;

    As you can see, you'll still be using a selector (you can't get rid of them sometimes) but in a different way.

Using Protocols

The Objective-C language provides a way to formally declare a list of methods (including declared properties) as a protocol. Formal protocols are supported by the language and the runtime system. For example, the compiler can check for types based on protocols, and objects can report whether they conform to a protocol. That's why I have you use a formal protocol — you can never be too safe when it comes to writing code.

Declaring a protocol

You declare formal protocols with the @protocol directive. To have your WebViewController require that its delegates implement a WebView Controller:nsurlRequestProcessMethod: method, you would code the following:

@protocol WebViewControllerDelegate <NSObject>

- (void) webViewController:
         (WebViewController *) controller
         nsurlRequestProcessMethod:(SEL)theProcessMethod;

@end

Methods can be optional or required. If you don't mark a method as optional, it is assumed to be required; but you can make that designation specific via the use of the @required keyword.

In the WebViewControllerDelegate protocol I just declared, I have a required method — WebViewController:nsurlRequestProcessMethod:

The more formal representation is

@protocol ProtocolName

method declarations

@end

Generally, protocol declarations are in the file of the class that defines it. In this case, you will add the WebViewControllerDelegate protocol declaration to the WebViewController.h file.

Adding delegation to WebViewController

To implement delegation in WebViewController, start by adding the code in bold to WebViewController.h. You'll also have to add the code in bold that has been commented out and then delete the code in bold that has been formatted with a strikethrough in Listing 2-6.

This business about deleting the bolded stuff with strikethrough and adding the unstruck bolded stuff will be the general operating procedure for the rest of this chapter (and minibook), so I won't bore you by repeating these instructions over and over and over.

Example 2-6. The new WebViewController.h File

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

@interface WebViewController : UIViewController
   <UIWebViewDelegate> {
// SEL                    tripSelector;
    id                    delegate;
    Trip                 *trip;
    IBOutlet UIWebView   *webView;
    BOOL                  webControl;
  }
  - (void)loadWebView:(NSURLRequest*) theNSURLRequest;
 //- (id) initWithTrip:(Trip*)aTrip tripSelector:(SEL)
     aTripSelector webControl:(BOOL) ifWebControl
     title:(NSString*) aTitle;
  - (id) initWithTrip:(Trip*)aTrip  delegate:(id) theDelegate
     webControl:(BOOL) ifWebControl title:(NSString*) aTitle;

  @end

  @protocol WebViewControllerDelegate <NSObject>
  - (void) webViewController:(WebViewController*)
                                                controller
           nsurlRequestProcessMethod:(SEL)theProcessMethod;

  @end

You did several things here:

  1. Added the instance variable delegate.

    This is the object that will implement the behavior you specified in the protocol. These are generally declared to be a generic object (id), since the point here is that you won't know what class is implementing the delegate behavior.

  2. Declared the WebViewControllerDelegate protocol.

  3. Deleted the instance variables and method declaration you had previously used to implement the generic WebViewController.

Changing the way WebViewController is instantiated and initialized

With the changes you have made to WebViewController, you'll have to make some changes in the RootViewController code that allocates and initializes it.

More specifically, make the following changes to the RootViewController's tableView:didSelectRowAtIndexPath: method, as shown in Listing 2-7.

Example 2-7. Changes to tableView:didSelectRowAtIndexPath:

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")];
       if  (realtime) targetController =
         [[WebViewController alloc] initWithTrip:trip
          delegate:[ trip returnWeatherDelegate]
          webControl:YES title:
          NSLocalizedString(@"Weather", @"Weather")];
       else [self displayOfflineAlert:
            [[menuList objectAtIndex:menuOffset]
                               objectForKey:kSelectKey]];
       break;
     case 4:
//     targetController = [[WebViewController alloc]
         initWithTrip:trip tripSelector:@selector
         (returnCarServicingInformation:)
//       webControl:NO title: NSLocalizedString
                   (@"Car Servicing", @"Car Servicing")];
       targetController =
        [[WebViewController alloc] initWithTrip:trip
          delegate: [trip returnCarServicingDelegate]
          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")];
targetController = [[WebViewController alloc]
            initWithTrip:trip
            delegate: [trip returnCarInformationDelegate]
            webControl:NO title:NSLocalizedString
                       (@"The Car", @"Car Information")];
    break;
}

The change you made here was simply to initialize WebViewController with the right delegate, depending on what the user selected in the table view. You changed it from using a selector as an argument to using its delegate instead.

The only involvement now by Trip is to pass back the delegate for each of the selections.

Of course, you'll also need to change the initialization method in the WebViewController. Make the changes shown in Listing 2-8 to WebViewController.m.

Example 2-8. Implementing the New Initialization Method

//- (id) initWithTrip:(Trip*)aTrip tripSelector:(SEL)
   aTripSelector webControl:(BOOL) ifWebControl
   title:(NSString*) aTitle {
- (id) initWithTrip:(Trip*)aTrip delegate:(id) theDelegate
   webControl:(BOOL) ifWebControl title:(NSString*) aTitle{

  if (self = [super initWithNibName:@"WebViewController"
   bundle:nil]) {
    self.title = aTitle;
//    tripSelector = aTripSelector;
    delegate = theDelegate;
    trip = aTrip;
    webControl = ifWebControl;
    [trip retain];
  }
  return self;
}

All you've done here is eliminate the tripSelector assignment and replace it with the delegate assignment instead.

You also need to make some changes in viewDidLoad, as shown in Listing 2-9. You'll be sending the delegate a message instead of performing the selector. Make these changes to WebViewController.m.

Example 2-9. Updating viewDidLoad

- (void)viewDidLoad {

  [super viewDidLoad];
  if (webControl) {
    UIBarButtonItem *backButton = [[UIBarButtonItem alloc]
                                   initWithTitle:@"Back" styl
    e:UIBarButtonItemStylePlain
                                   target:self action:@
    selector(goBack:)];
     self.navigationItem.rightBarButtonItem = backButton;
     [backButton release];
  }
  // [trip performSelectorOnMainThread:tripSelector
   withObject:self waitUntilDone:NO];
  [delegate webViewController:self
       nsurlRequestProcessMethod:@selector(loadWebView:)];
}

Again, this change is very straightforward. You've changed the method so that it now simply sends a message to its delegate to construct the NSURLRequest. You'll see in a second that this will essentially set off a chain of events — similar in end result, but very different in execution — to the chain of events I described at the beginning of this chapter.

One further note here.

Because the delegate method WebViewController:nsurlRequestProc essMethod: is required by the WebViewControllerDelegate protocol, you really don't have to be concerned if it's implemented. If it were optional though, as many of the framework delegate methods are, you would need to know whether the delegate implemented the method.

To do that, you can use the respondsToSelector: message:

if ([delegate respondsToSelector: @selector
       (webViewController:nsurlRequestProcessMethod:)]) {
  ... do something
}

respondsToSelector: is an NSObject method that tells you whether a method has been implemented. As I said, because this particular method is required, I don't have to determine that. However, I wanted to show you how much information is available at runtime in Objective-C and how to implement delegation for the optional methods of formal protocols and for all methods of informal protocols.

Adopting a protocol

Adopting a protocol is similar in some ways to declaring a superclass. In both cases, you're adding methods to your class. When you use a superclass, you're adding inherited methods; when you use a protocol, you're adding methods declared in the protocol list. A class adopts a formal protocol by listing the protocol within angle brackets after the superclass name.

@interface ClassName : ItsSuperclass < protocol list >

A class can adopt more than one protocol, and if so, names in the protocol list are separated by commas.

@interface Translator : NSObject < English, Italian >

Just as with any other class, you can add instance variables, properties, and even nonprotocol methods to a class that adopts a protocol.

In this case, the model classes will be adopting the WebViewControllerDelegate protocol.

Add the code in Listing 2-10 to the Sight.h. I show you how this works with the Sight class because it was the cause of the problems in the first place. Later, I have you apply it to the rest of the classes that will need to use it — CarInformation, CarServicing, and Weather.

Example 2-10. Modifying Sight.h

#import <MapKit/MapKit.h>
  #import "WebViewController.h"
  @class Trip;
  @class ServerManager;
  @interface Sight : NSObject <WebViewControllerDelegate> {

    NSString               *sightName;
    CLLocationCoordinate2D  coordinate;
    NSString               *title;
    NSString               *subtitle;
    NSString               *resource;
    NSString               *resourceType;
    NSString               *image;
    NSString               *imageType;
    Trip                   *trip;
    ServerManager          *sightServerManager;
    NSMutableData          *receivedData;
    SEL                     processMethod;
    UIViewController       *viewController;
  }
@property (nonatomic, retain) NSString *sightName;
@property (nonatomic, retain) NSString* image;
@property (nonatomic, retain) NSString* imageType;;
@property (nonatomic) CLLocationCoordinate2D coordinate;
- (NSString*) title;
- (NSString*) subtitle;
- (id) initWithTrip: (Trip*) the Trip
                      sightData:(NSDictionary*) sightData;

@end

As you can see, besides adopting the WebViewControllerDelegate protocol, there are a few new instance variables you need to add — I explain them in the next chapter.

Now, add the code in Listing 2-11 to Sight.m to implement the protocol method.

Example 2-11. Implementing the Protocol Method

- (void) webViewController:
                          (WebViewController *) controller
         nsurlRequestProcessMethod:(SEL)theProcessMethod {

  viewController = controller;
  processMethod = theProcessMethod;
  RoadTripAppDelegate *appDelegate = (RoadTripAppDelegate *)
   [[UIApplication sharedApplication] delegate];
  BOOL realtime = !appDelegate.useStoredData;
  if (realtime) {
    NSString* urlString = [[NSString alloc] initWithFormat:
   @"%@%@.%@", @"http://nealgoldstein.com/", resource,
   resourceType];
    [self downloadData: urlString];
  }
  else {
    [self get Data];
  }
}

Before I explain what is going on here, I want to discuss a little more about protocols.

When you adopt a protocol, you must implement all the required methods the protocol declares; otherwise, the compiler issues a warning. As you can see, the Sight class does define all the required methods declared in the WebViewControllerDelegate protocol.

Note

Even though Sight implements a protocol that is declared in the WebViewController object, it does not automatically have access to the instance variables of the WebViewController object. This means that any of those instance variables that need to be accessed by delegates must be properties. This is also why the naming convention — the one that starts with a class name and passes a reference to the delegator — makes sense. This gives the delegate a consistent way to access those instance variables (and any methods it needs as well).

As you noticed, though, the file access looks very different from what you're used to. That's because I have a second problem to deal with: the issue of file loading. That's what I explain starting in the next section

Asynchronous File Loading

Previously downloading a file from a server on the Internet and making it available to the WebViewController and subsequently the WebView was not very complex. Listing 2-12 shows how you did it in CarServicing.

Example 2-12. CarServicing File Access

- (NSURLRequest*) returnCarServicingInformation {

  NSURL *carServicingData = nil;
  RoadTripAppDelegate *appDelegate = (RoadTripAppDelegate *)
    [[UIApplication sharedApplication] delegate];
  BOOL realtime = !appDelegate.useStoredData;
  if (realtime) {
    carServicingData = [NSURL URLWithString:@"http://
   nealgoldstein.com/CarServicing.html"];
    [self saveCarServicingData:@"CarServicing"
   withDataURL:carServicingData];
    if (!carServicingData) NSLog(@"data not there for
   CarServicing");

  }
  else {
    carServicingData = [self getCarServicingData:@"CarServic
    ing"];
  }

  NSURLRequest* theNSURLRequest =
         [NSURLRequest requestWithURL:carServicingData];
  return theNSURLRequest;
}

- (void) saveCarServicingData:(NSString*) fileName
   withDataURL:(NSURL*) url {
NSData *dataLoaded =
                     [NSData dataWithContentsOfURL:url];
  if (dataLoaded == NULL)
                       NSLog( @"Data not found %@", url);
  NSArray *paths = NSSearchPathForDirectoriesInDomains
   (NSDocumentDirectory, NSUserDomainMask, YES);
  NSString *documentsDirectory = [paths objectAtIndex:0];
  NSString *filePath = [documentsDirectory stringByAppendingP
   athComponent:fileName];
  [dataLoaded writeToFile:filePath atomically:YES];
}

-(NSURL*) getCarServicingData:(NSString*) fileName {

  NSArray *paths = NSSearchPathForDirectoriesInDomains
   (NSDocumentDirectory, NSUserDomainMask, YES);
  NSString *documentsDirectory = [paths objectAtIndex:0];
  NSString *filePath = [documentsDirectory stringByAppendingP
   athComponent:fileName];
  NSURL* theNSURL= [NSURL fileURLWithPath:filePath];
  if (theNSURL == NULL) NSLog (@"Data not there");
  return theNSURL;
}

I explain how this code works in Book V. Basically, you check to see whether or not you want to use real time data. If you do, you download the file

NSData *dataLoaded = [NSData dataWithContentsOfURL:url];

and then save it. You then construct the NSURLRequest, which points to the downloaded data, and then return (via Trip) the NSURLRequest to the WebViewController, which in turn passes it to its webView.

To construct the NSURLRequest, you create an NSURL — an object that includes the utilities necessary for accessing the file. This is essentially the file's location. Then you create an NSURLRequest from the NSURL. The NSURLRequest is what the WebViewController needs to send to the Web view in the loadRequest: message, which tells it to load the data associated with that NSURLRequest. Remember, at this point the file has already been downloaded and you are passing the necessary information to the WebViewController, and subsequently the Web view to access it.

The NSURLRequest class encapsulates a URL and any protocol-specific properties, in a protocol-independent manner. The NSURLRequest class is also part of the URL loading system — a set of classes and protocols that provide the underlying capability for an application to access the data specified by a URL. (No more on that for now — you get lots more on the URL loading system in the next chapter.)

At the heart of the download is the dataWithContentsOfURL: message that does the download of the file from the supplied URL. It's also the heart of a potential real problem.

The problem with the dataWithContentsOfURL: message is that while it's executing, everything else in your applications stops. It sends out the load request to the server, waits until it gets the data back, and then and only then does it return control to the main execution thread. That means for large files, or for large numbers of files, the user is blocked from doing anything until all downloads are done.

What you need to do in almost every application with any quantity of downloaded data, is the ability to download files asynchronously — or to allow other things to go on while waiting for the data.

In the next chapter, I show you how to do just that, as well as explain the URL loading system — the preferred way for your iPhone application to access data.

What I won't be able to show you, however, is what you can do to allow the user to continue using the application while files download. (Although one strategy I often use is to start any required downloads before the user needs the data — at application startup time for example). I leave that for you to explore on your own.

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

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