Home / iPhone Tips and Tutorials

Command Dispatch Pattern Example

This section provides an example of the Command Dispatch pattern by calling an authenticated service from YouTube. In this type of communication, a number of failure modes need to be considered.

  • The user may not provide valid credentials.
  • The device may not be on a functional network.
  • YouTube may not respond in a timely manner or may fail for some reason.

The application needs to handle each of these conditions in an elegant and reliable manner. This example surveys the major code components and discusses some of the implementation details.

The app in the included project is a simple demonstration app. It is not intended for anything other than demonstrating this pattern.

Prerequisites

The things you need to have to successfully see this app operate are as follows:

  • A YouTube account
  • At least one video uploaded to your YouTube account (it doesn't need to be public, just uploaded to the account)
  • The project zip file from the Wrox companion website

This project was developed using XCode 4.1 and iOS 4.3. The application was developed using the YouTube API as it stood in October 2011. It is under Google's control however and is subject to change.

Major Objects

After you download the project and load it up in XCode, you see the following classes.

Commands

In the commands group you'll fi nd the following.

BaseCommand

The BaseCommand object is the superclass for all the command objects. It provides many methods needed by every command class. These methods include:

  • Methods to send completion, error, and login needed notifi cations
  • A method to help issuing objects listen for completion notifi cations
  • Methods used to support the actual NSURLRequests

BaseCommand extends NSOperation so all the logic of the command is in the main method of each subclass of this object.

GetFeed

The main method of this command, shown in Listing-1, calls YouTube and loads the list of videos uploaded by the currently logged in user. YouTube determines the identity of the logged in user by a token passed in an HTTP header on the request. Without that header, YouTube returns an HTTP status code of 0 instead of a more standard 4xx HTTP error.

LISTING-1: CommandDispathDemo/service-interface/GetFeed.h

- (void)main {
    NSLog(@"Starting getFeed operation");
    // Check to see if the user is logged in
    if ([self isUserLoggedIn]) { // only do this if the user is logged in

	// Build the request
	NSString *urlStr =
	    @"https://gdata.youtube.com/feeds/api/users/default/uploads";
	NSLog(@"urlStr=%@",urlStr);
	NSMutableURLRequest *request =
	    [ self createRequestObject:[NSURL URLWithString:urlStr]];

	// Sign the request with the user's auth token
	[self signRequest:request];

	// Send the request
	NSHTTPURLResponse *response=nil;
	NSError *error=nil;
	NSData *myData = [self sendSynchronousRequest:request
				     response_p:&response
						error:&error];

	// Check to see if the request was successful
	if ([super wasCallSuccessful:response error:error]) {
	    [self buildDictionaryAndSendCompletionNotif: myData];
	}
    }
}

In this code listing, many of the methods that called on self are implemented in the BaseCommand superclass. The GetFeed command is prototypical of the Command Dispatch pattern. The main method checks to make sure the user is logged in because there's no reason to call the server if you know this call will fail. If the user is logged in, then the code builds the request, adds the authentication header to it, then sends a synchronous request. The final part of the code calls a superclass method to determine if the call succeeded. This method uses both the NSError object and the HTTP status code from the NSHTTPURLResponse object to determine success. If the call failed, then either an error notification or login needed notification is broadcast.

LoginCommand

This method sends the request to YouTube to authenticate the user. This command is somewhat more involved because it doesn't use several of the helper methods found in the BaseCommand object. It does not use these methods because it should not generate a Needs Authentication failure message if the login fails. It reports only a status of good completion or failed completion.

The login listener handles the errors that come from failed login attempts. For more information on the protocol that YouTube requires, reference http://code.google.com/apis/youtube/2.0/ developers_guide_protocol_understanding_video_feeds.html.

Exception Listeners

In the listeners group you'll find the view controllers that are presented when an error occurs or when the user needs to log in. Both the NetworkErrorViewController and the LoginViewController extend the InterstitialViewController, which provides several common helper methods. Both view controllers are presented as modal view controllers.

  • NetworkErrorViewController: Provides the user with the choice to retry or abort the failed operations. If the user selects retry, then the failed commands are placed back on the operation queue.
  • LoginViewController: Solicits a username and password from the user. It stays at the top of the view stack until the user successfully logs in.
  • InterstitialViewController: As a parent of the other exception listeners, it provides support functionality such as the code to collect multiple error notifications and re-dispatch them upon error resolution.

The key code in the listeners is found in the viewDidDisappear: method (see Listing-2), which is called when the view has completely disappeared. If the commands are queued before the view has completely disappeared, there is a chance that another error may trigger a repeated presentation of the view, thereby causing a fatal error in the application. iOS 5 has a better capacity to handle this case because users can specify a block of code to execute when the view disappears. The code does not need to determine the cause of the disappearance before handling the triggering commands.

LISTING-2: CommandDispatchDemo/NetworkErrorViewController.m

- (void) viewDidDisappear:(BOOL)animated {
    if (retryFlag) {
	// re-enqueue all of the failed commands
	[self performSelectorAndClear:@selector(enqueueOperation)];
    } else {
	// just send a failure notification for all failed commands
	[self
	    performSelectorAndClear:
			@selector(sendCompletionFailureNotification)];
    }
    self.displayed = NO;
}

The application delegate registers itself as the listener for both network error and login-needed notifications (see Listing-3). It collects exception notifications and manages the presentation of the correct view controller when an error occurs.

The code demonstrates the notification handler for the login-needed notification. Because it deals with the user interface, its contents must be executed on the main thread using GCD.

LISTING-3: CommandDispatchDemo/CommandDispatchDemoAppDelegate.m

/**
 * Handles login needed notifications generated by commands
 **/
- (void) loginNeeded:(NSNotification *)notif {
    // make sure it all occurs on the main thread
    dispatch_async(dispatch_get_main_queue(), ^{
	// make sure only one thread adds a command at a time
	@synchronized(loginViewController) {
	    [loginViewController addTriggeringCommand:
	    			[notif object];
	    if (!loginViewController.displayed) {
		// if the view is not displayed then display it.
		[[self topOfModalStack:self.window.rootViewController]
				presentModalViewController:loginViewController
				animated:YES];
	    }
	    loginViewController.displayed = YES;
	}
    }); // End of GC Dispatch block
}

View Controllers

There is one primary view controller in this simple app. The RootViewController (see the following code) extends UITableViewController. When this controller loads it creates and enqueues a command to load the user's list of videos (aka the YouTube feed). It patiently waits for the completion of that command by yielding the flow of control back to the main run loop. It is blissfully unaware that it will always fail on the first call because the user is not logged in.

The requestVideoFeed method found in CommandDispatchDemo/RootViewController.m starts the process to load the video list like so:

- (void)requestVideoFeed {
    // create the command
    GetFeed *op = [[GetFeed alloc] init];

    // add the current authentication token to the command
    CommandDispatchDemoAppDelegate *delegate =
	(CommandDispatchDemoAppDelegate *)[[UIApplication
				sharedApplication] delegate ];
    op.token = delegate.token;

    // register to hear the completion of the command
    [op listenForMyCompletion:self selector:@selector(gotFeed:)];

    // put it on the queue for execution
    [op enqueueOperation];
    [op release];
}

Notice that the code does not need to check if the user is logged in; the command does that when it executes.

The gotFeed: method, shown in the following code, handles the eventual return of data from YouTube. Midway through the example the requestVideoFeed: method registers the gotFeed: method as the target method for the completion notifi cation. This method loads the data for the table view if the call succeeds. Otherwise it shows a UIAlertView.

- (void) gotFeed:(NSNotification *)notif {
    NSLog(@"User info = %@", notif.userInfo);
    BaseCommand *op = notif.object;
    if (op.status == kSuccess) {
	self.feed = op.results;

	// if entry is a single item, change it to an array,
	// the XML reader cannot distinguish single entries
	// from arrays with only one element
	id entries = [[feed objectForKey:@"feed"] objectForKey:@"entry"];
	if ([entries isKindOfClass:[NSDictionary class]]) {
	    NSArray *entryArray = [NSArray arrayWithObject:entries];
	    [[feed objectForKey:@"feed"] setObject:entryArray forKey:@"entry"];
	}
	dispatch_async(dispatch_get_main_queue(), ^{
	    [self.tableView reloadData];
	});
    } else {
	dispatch_async(dispatch_get_main_queue(), ^{
	    UIAlertView *alert = [[UIAlertView alloc]
		initWithTitle:@"No Videos"
		     message:@"The login to YouTube failed"
		    delegate:self
	    cancelButtonTitle:@"Retry"
	    otherButtonTitles:nil];
	    [alert show];
	    [alert release];
	});
    }
}

The class YouTubeVideoCell is a UITableViewCell subclass that asynchronously loads the thumbnail of a video. It uses the LoadImageCommand object to accomplish this.

/**
 * Start the process of loading the image via the command queue
 **/
- (void) startImageLoad {
    LoadImageCommand *cmd = [[LoadImageCommand alloc] init];
    cmd.imageUrl = imageUrl;
    // set the name to something unique
    cmd.completionNotificationName = imageUrl;
    [cmd listenForMyCompletion:self selector:@selector(didReceiveImage:)];
    [cmd enqueueOperation];
    [cmd release];
}

The issuing class changes the completion notification name. It does this so that it, and only it, receives a notification for this particular image. Otherwise it would need to examine the returned notification to see if it were the command that it originally issued.

The beauty of the Command Dispatch pattern is that all the messy exception handling logic and login presentation logic is divorced from the primary view controllers in the app. When a view controller generates a command, it is blissfully ignorant of any exception handling or authentication that occurs to actually complete the request. It simply issues a request, waits for a response, and processes the response. It does not care that it may have taken five retries and a user registration for the request to be completed successfully. In addition, the service request code does not need to know where the request originated or where the results are going; it can simply focus on executing the call and broadcasting the results.

Further benefits are seen when a developer can write initial happy-path code and see demonstrable results, and then in the future add the exception listeners with zero impact to the happy-path code. In addition, if designed properly, all the network service calls can leverage the same base command class resulting in abbreviated command classes.

In a universal app, you could alter the views presented by the exception listeners so that an error presentation on an iPhone is sized suitable for that platform, and error presentation on an iPad is better suited for the larger platform.

This pattern provides a way to rapidly show results, provide excellent separation of concerns between business logic and exception handling, reduce duplicate code, and provide for a better user experience.

[Previous] [Contents]