iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides) (92 page)

Read iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides) Online

Authors: Aaron Hillegass,Joe Conway

Tags: #COM051370, #Big Nerd Ranch Guides, #iPhone / iPad Programming

BOOK: iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides)
11.56Mb size Format: txt, pdf, ePub
Handling the response

A
BNRConnection
is the delegate for the
NSURLConnection
it manages, so it needs to implement the delegate methods for
NSURLConnection
that retrieve the data and report success or failure. First, implement the data collection method in
BNRConnection.m
.

 
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    [container appendData:data];
}
 

Figure 28.9  BNRConnection flow

 

The
BNRConnection
will hold on to all of the data that returns from the web service. When that web service completes successfully, it must first parse that data into the
xmlRootObject
and then call the
completionBlock
(
Figure 28.9
). Finally, it needs to take itself out of the array of active connections so that it can be destroyed. Implement the following method in
BNRConnection.m
.

 
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    // If there is a "root object"
    if ([self xmlRootObject]) {
        // Create a parser with the incoming data and let the root object parse
        // its contents
        NSXMLParser *parser = [[NSXMLParser alloc] initWithData:container];
        [parser setDelegate:[self xmlRootObject]];
        [parser parse];
    }
    // Then, pass the root object to the completion block - remember,
    // this is the block that the controller supplied.
    if ([self completionBlock])
        [self completionBlock]([self xmlRootObject], nil);
    // Now, destroy this connection
    [sharedConnectionList removeObject:self];
}
 

Remember that
connectionDidFinishLoading:
is called when the connection finishes successfully; so, the root object is delivered to the
completionBlock
, and no
NSError
is passed. If there is a problem with the connection, the opposite needs to occur: the completion block is called without the root object, and an error object is passed instead. In
BNRConnection.m
, implement the method that handles the connection failing.

 
- (void)connection:(NSURLConnection *)connection
            didFailWithError:(NSError *)error
{
    // Pass the error from the connection to the completionBlock
    if ([self completionBlock])
        [self completionBlock](nil, error);
    // Destroy this connection
    [sharedConnectionList removeObject:self];
}
 

Build and run the application. After a moment, you will see the
ListViewController
’s table view populate. If there is a problem, you will see an alert view.

 

You be wondering why
BNRConnection
’s
completionBlock
takes an argument of type
id
instead of say,
RSSChannel
. The answer is simple:
BNRConnection
doesn’t know anything about the type of its root object except that it receives
NSXMLParserDelegate
messages. Nor should it. This makes the
BNRConnection
usable in any situation that requires fetching XML data with
NSURLConnection
.

 

Now, at the end of the day, the changes we’ve made to
Nerdfeed
so far give us the exact same behavior as we had before. You might be wondering,

Why fix what ain’t broke?

Well, in the rest of this chapter and the next two, you will be adding features to
Nerdfeed
. Using a store object is going to make implementing those features a lot easier. (Also, the
BNRConnection
class is pretty handy – you can use that in any application. In fact, that very same class is in a number of applications that are currently on the App Store.)

 
Another request

Now that we have
BNRConnection
and a store object, adding new requests for the store to handle is really straight-forward. Let’s have the
BNRFeedStore
fetch the top songs from
iTunes
. First, we need to make a minor change to the
RSSChannel
and
RSSItem
classes because Apple’s RSS feed uses a different element name (
entry
) to identify items in their RSS feed. In
RSSChannel.m
, modify
parser:didStartElement:namespaceURI:qualifiedName:attributes:
to create an
RSSItem
when it encounters the
entry
element.

 
- (void)parser:(NSXMLParser *)parser
    didStartElement:(NSString *)elementName
       namespaceURI:(NSString *)namespaceURI
      qualifiedName:(NSString *)qualifiedName
         attributes:(NSDictionary *)attributeDict
{
    if ([elementName isEqual:@"title"]) {
        currentString = [[NSMutableString alloc] init];
        [self setTitle:currentString];
    }
    else if ([elementName isEqual:@"description"]) {
        currentString = [[NSMutableString alloc] init];
        [self setInfoString:currentString];
    } else if ([elementName isEqual:@"item"]
            || [elementName isEqual:@"entry"]
) {
        RSSItem *entry = [[RSSItem alloc] init];
        [entry setParentParserDelegate:self];
        [parser setDelegate:entry];
        [items addObject:entry];
    }
}
 

Then in
RSSItem.m
, have the
RSSItem
return control to the channel when it ends an
entry
element.

 
- (void)parser:(NSXMLParser *)parser
 didEndElement:(NSString *)elementName
  namespaceURI:(NSString *)namespaceURI
 qualifiedName:(NSString *)qName
{
    currentString = nil;
    if ([elementName isEqual:@"item"]
    || [elementName isEqual:@"entry"]
) {
        [parser setDelegate:parentParserDelegate];
    }
}
 

Now we will add another request that a controller can ask the
BNRFeedStore
to carry out. In
BNRFeedStore.h
, declare a new method.

 
@interface BNRFeedStore : NSObject
+ (BNRFeedStore *)sharedStore;
- (void)fetchTopSongs:(int)count
       withCompletion:(void (^)(RSSChannel *obj, NSError *err))block;
- (void)fetchRSSFeedWithCompletion:(void (^)(RSSChannel *obj, NSError *err))block;
@end
 

The implementation of this method will be really easy now that we have the format of the store determined and the
BNRConnection
class ready to go. The two differences between
fetchTopSongs:withCompletion:
and
fetchRSSFeedWithCompletion:
are that
fetchTopSongs:withCompletion:
will take an argument supplied by the controller to determine how many songs to fetch and will access a different web service. Implement this method in
BNRFeedStore.m
.

 
- (void)fetchTopSongs:(int)count
       withCompletion:(void (^)(RSSChannel *obj, NSError *err))block
{
    // Prepare a request URL, including the argument from the controller
    NSString *requestString = [NSString stringWithFormat:
        @"http://itunes.apple.com/us/rss/topsongs/limit=%d/xml", count];
    NSURL *url = [NSURL URLWithString:requestString];
    // Set up the connection as normal
    NSURLRequest *req = [NSURLRequest requestWithURL:url];
    RSSChannel *channel = [[RSSChannel alloc] init];
    BNRConnection *connection = [[BNRConnection alloc] initWithRequest:req];
    [connection setCompletionBlock:block];
    [connection setXmlRootObject:channel];
    [connection start];
}
 

Now we have a store object that returns two different RSS feeds, and the controller doesn’t have to know any of the details about how they are fetched. Let’s give the user a way to switch between the two feeds. In
ListViewController.h
, declare a new
enum
for the feed types and an instance variable to hold on to the current feed type.

 
typedef enum {
    ListViewControllerRSSTypeBNR,
    ListViewControllerRSSTypeApple
} ListViewControllerRSSType;
@interface ListViewController : UITableViewController
{
    RSSChannel *channel;
    ListViewControllerRSSType rssType;
}
 

In
ListViewController.m
, add a
UISegmentedControl
to the
navigationItem
that will change the
rssType
.

 
- (id)initWithStyle:(UITableViewStyle)style
{
    self = [super initWithStyle:style];
    if (self) {
        UIBarButtonItem *bbi =
            [[UIBarButtonItem alloc] initWithTitle:@"Info"
                                             style:UIBarButtonItemStyleBordered
                                            target:self
                                            action:@selector(showInfo:)];
        [[self navigationItem] setRightBarButtonItem:bbi];
        UISegmentedControl *rssTypeControl =
                            [[UISegmentedControl alloc] initWithItems:
                                [NSArray arrayWithObjects:@"BNR", @"Apple", nil]];
        [rssTypeControl setSelectedSegmentIndex:0];
        [rssTypeControl setSegmentedControlStyle:UISegmentedControlStyleBar];
        [rssTypeControl addTarget:self
                           action:@selector(changeType:)
                 forControlEvents:UIControlEventValueChanged];
        [[self navigationItem] setTitleView:rssTypeControl];
        [self fetchEntries];
    }
    return self;
}
 

Then, in
ListViewController.m
, implement
changeType:
, which will be sent to the
ListViewController
when the segmented control changes.

 
- (void)changeType:(id)sender
{
    rssType = [sender selectedSegmentIndex];
    [self fetchEntries];
}
 

Finally, modify
fetchEntries
in
ListViewController.m
to make the appropriate request to the store depending on the current
rssType
. To do this, move the completion block into a local variable, and then pass it to the right store request method. (The code that needs to be executed when either request finishes is the same.) This method will now look like this:

 
- (void)fetchEntries
{
    void (^completionBlock)(RSSChannel *obj, NSError *err) =
    ^(RSSChannel *obj, NSError *err) {
        // When the request completes, this block will be called.
        if (!err) {
            // If everything went ok, grab the channel object and
            // reload the table.
            channel = obj;
            [[self tableView] reloadData];
        } else {
            // If things went bad, show an alert view
            NSString *errorString = [NSString stringWithFormat:@"Fetch failed: %@",
                                     [err localizedDescription]];
            // Create and show an alert view with this error displayed
            UIAlertView *av = [[UIAlertView alloc] initWithTitle:@"Error"
                                                         message:errorString
                                                        delegate:nil
                                               cancelButtonTitle:@"OK"
                                               otherButtonTitles:nil];
            [av show];
        }
    };
    // Initiate the request...
    if (rssType == ListViewControllerRSSTypeBNR)
        [[BNRFeedStore sharedStore] fetchRSSFeedWithCompletion:completionBlock];
    else if (rssType == ListViewControllerRSSTypeApple)
        [[BNRFeedStore sharedStore] fetchTopSongs:10
                                    withCompletion:completionBlock];
}
 

Build and run the application. When the application launches, the BNR forums feed will load as the default. Tap the
Apple
item in the segmented control, and the top ten songs on
iTunes
will appear. Simple stuff, huh? (Tapping on a row while looking at Apple’s RSS feed won’t show anything in the detail view controller, but you will fix this in the next section.)

 

Other books

The Strivers' Row Spy by Jason Overstreet
Affinity by Sarah Waters
The Woman in Black by Martyn Waites
No Strings Attached by Hilary Storm
The Pirate's Desire by Jennette Green