iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides) (81 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)
2.8Mb size Format: txt, pdf, ePub
Collecting XML data

This code, as it stands, will make the connection to the web service and retrieve the last 20 posts. However, there is one problem: you don’t see those posts anywhere. You need to implement delegate methods for
NSURLConnection
to collect the XML data returned from this request.

 

Figure 25.5  NSURLConnection flow chart

 

The delegate of an
NSURLConnection
is responsible for overseeing the connection and for collecting the data returned from the request. (This data is typically an XML or JSON document; for this web service, it is XML.) However, the data returned usually comes back in pieces, and it is the delegate’s job to collect the pieces and put them together.

 

In
ListViewController.m
, implement
connection:didReceiveData:
to put all of the data received by the connection into the instance variable
xmlData
.

 
// This method will be called several times as the data arrives
- (void)connection:(NSURLConnection *)conn didReceiveData:(NSData *)data
{
    // Add the incoming chunk of data to the container we are keeping
    // The data always comes in the correct order
    [xmlData appendData:data];
}
 

When a connection has finished retrieving all of the data from a web service, it sends the message
connectionDidFinishLoading:
to its delegate. In this method, you are guaranteed to have the complete response from the web service request and can start working with that data. For now, implement
connectionDidFinishLoading:
in
ListViewController.m
to print out the string representation of that data to the console to make sure good stuff is coming back.

 
- (void)connectionDidFinishLoading:(NSURLConnection *)conn
{
    // We are just checking to make sure we are getting the XML
    NSString *xmlCheck = [[NSString alloc] initWithData:xmlData
                                                encoding:NSUTF8StringEncoding];
    NSLog(@"xmlCheck = %@", xmlCheck);
}
 

There is a possibility that a connection will fail. If an instance of
NSURLConnection
cannot make a connection to a web service, it sends its delegate the message
connection:didFailWithError:
. Note that this message gets sent for a
connection
failure, like having no Internet connectivity or if the server doesn’t exist. For other types of errors, such as data sent to a web service in the wrong format, the error information is returned in
connection:didReceiveData:
.

 

In
ListViewController.m
, implement
connection:didFailWithError:
to inform your application of a connection failure.

 
- (void)connection:(NSURLConnection *)conn
  didFailWithError:(NSError *)error
{
    // Release the connection object, we're done with it
    connection = nil;
    // Release the xmlData object, we're done with it
    xmlData = nil;
    // Grab the description of the error object passed to us
    NSString *errorString = [NSString stringWithFormat:@"Fetch failed: %@",
                             [error 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];
}
 

Try building and running your application. You should see the XML results in the console shortly after you launch the application. If you put your device in Airplane Mode (or if it is not connected to a network), you should see a friendly error message when you try to fetch again. (For now, you will have to restart the application from
Xcode
in order to refetch the data after you’ve received the error.)

 

The XML that comes back from the server looks something like this:

 


  
    forums.bignerdranch.com
    Books written by Big Nerd Ranch
        ...
    
      Big Nerd Ranch General Discussions :: Big Nerd Ranch!
      http://forums.bignerdranch.com/viewtopic.php?f=4&t=532
      [email protected] (bignerd)
      Big Nerd Ranch General Discussions
      http://forums.bignerdranch.com/posting.php?mode=reply
      Mon, 27 Dec 2010 11:27:01 GMT
    

    ...
  


(If you aren’t seeing anything like this in your console, verify that you typed the URL correctly.)

 

Let’s break down the XML the server returned. The top-level element in this document is an
rss
element. It contains a
channel
element. That
channel
element has some metadata that describes it (a title and a description). Then, there is a series of
item
elements. Each
item
has a title, link, author, etc. and represents a single post on the forum.

 

In a moment, you will create two new classes,
RSSChannel
and
RSSItem
, to represent the
channel
and
item
elements. The
ListViewController
will have an instance variable for the
RSSChannel
. The
RSSChannel
will hold an array of
RSSItem
s. Each
RSSItem
will be displayed as a row in the table view. Both
RSSChannel
and
RSSItem
will retain some of their metadata as instance variables, as shown in
Figure 25.6
.

 

Figure 25.6  Model object graph

 
Parsing XML with NSXMLParser

To parse the XML, you will use the class
NSXMLParser
. An
NSXMLParser
instance takes a chunk of XML data and reads it line by line. As it finds interesting information, it sends messages to its delegate, like,

I found a new element tag,

or

I found a string inside of an element.

The delegate object is responsible for interpreting what these messages mean in the context of the application.

 

In
ListViewController.m
, delete the code you wrote in
connectionDidFinishLoading:
to log the XML. Replace it with code to kick off the parsing and set the parser’s delegate to point at the instance of
ListViewController
.

 
- (void)connectionDidFinishLoading:(NSURLConnection *)conn
{
    
NSString *xmlCheck = [[NSString alloc] initWithData:xmlData
                                                
encoding:NSUTF8StringEncoding];
    
NSLog(@"xmlCheck = %@", xmlCheck);
    // Create the parser object with the data received from the web service
    NSXMLParser *parser = [[NSXMLParser alloc] initWithData:xmlData];
    // Give it a delegate - ignore the warning here for now
    [parser setDelegate:self];
    // Tell it to start parsing - the document will be parsed and
    // the delegate of NSXMLParser will get all of its delegate messages
    // sent to it before this line finishes execution - it is blocking
    [parser parse];
    // Get rid of the XML data as we no longer need it
    xmlData = nil;
    // Get rid of the connection, no longer need it
    connection = nil;
    // Reload the table.. for now, the table will be empty.
    [[self tableView] reloadData];
}
 

The delegate of the parser,
ListViewController
, will receive a message when the parser finds a new element, another message when it finds a string within an element, and another when an element is closed.

 

For example, if a parser saw this XML:

 
Big Nerd Ranch.

it would send its delegate three consecutive messages:

I found a new element:

title

,

then,

I found a string:

Big Nerd Ranch

,

and finally,

I found the end of an element:

title

.

These messages are found in the
NSXMLParserDelegate
protocol:

 
// The "I found a new element" message
  - (void)parser:(NSXMLParser *)parser            // Parser that is sending message
 didStartElement:(NSString *)elementName          // Name of the element found
    namespaceURI:(NSString *)namespaceURI
   qualifiedName:(NSString *)qualifiedName
      attributes:(NSDictionary *)attributeDict;
// The "I found a string" message
  - (void)parser:(NSXMLParser *)parser            // Parser that is sending message
 foundCharacters:(NSString *)string;              // Contents of element (string)
// The "I found the end of an element" message
- (void)parser:(NSXMLParser *)parser              // Parser that is sending message
 didEndElement:(NSString *)elementName            // Name of the element found
  namespaceURI:(NSString *)namespaceURI
 qualifiedName:(NSString *)qName;

The
namespaceURI
,
qualifiedName
, and
attributes
arguments are for more complex XML, and we’ll return to them at the end of the chapter.

 
Constructing the tree of model objects

It is up to the
ListViewController
to make sense of that series of messages, and it does this by constructing an object tree that represents the XML feed. In this case, after the XML is parsed, there will be an instance of
RSSChannel
that contains a number of
RSSItem
instances. Here are the steps to constructing the tree:

 
  • When the parser reports it found the start of the
    channel
    element, create an instance of
    RSSChannel
    .
 
  • When the parser finds a
    title
    or
    description
    element and it is currently inside a
    channel
    element, set the appropriate property of the
    RSSChannel
    instance.
 
  • When the parser finds an
    item
    element, create an instance of
    RSSItem
    and add it to the
    items
    array of the
    RSSChannel
    .
 
  • When the parser finds a
    title
    or
    link
    element and it is currently inside a
    item
    element, set the appropriate property of the
    RSSItem
    instance.
 

This list doesn’t seem too daunting. However, there is one issue that makes it difficult: the parser doesn’t remember anything about what it has parsed. A parser may report,

I found a title element.

Its next report is

Now I’ve found the string inside an element.

At this point, if you asked the parser which element that string was inside, it couldn’t tell you. It only knows about the string it just found. This leaves the burden of tracking state on the parser’s delegate, and maintaining the state for an entire tree of objects in a single object is cumbersome.

 

Instead, you will spread out the logic for handling messages from the parser among the classes involved. If the last found element is a
channel
, then that instance of
RSSChannel
will be responsible for handling what the parser spits out next. The same goes for
RSSItem
; it will be responsible for grabbing its own
title
and
link
strings.

 


But the parser can only have one delegate,

you say. And you’re right; it can only have one delegate
at a time
. We can change the delegate of an
NSXMLParser
whenever we please, and the parser will keep chugging through the XML and sending messages to its current delegate. The flow of the parser and the related objects is shown in
Figure 25.7
.

 

Figure 25.7  Flow diagram of XML being parsed into a tree, creating the tree

 

When the parser finds the end of an element, it tells its delegate. If the delegate is the object that represents that element, that object returns control to the previous delegate (
Figure 25.8
).

 

Figure 25.8  Flow diagram of XML being parsed into a tree, back up the tree

 

Now that we have a plan, let’s get to work. Create a new
NSObject
subclass named
RSSChannel
. A channel object needs to hold some metadata, an array of
RSSItem
instances, and a pointer back to the previous parser delegate. In
RSSChannel.h
, add these properties:

 
@interface RSSChannel : NSObject
@property (nonatomic, weak) id parentParserDelegate;
@property (nonatomic, strong) NSString *title;
@property (nonatomic, strong) NSString *infoString;
@property (nonatomic, readonly, strong) NSMutableArray *items;
@end
 

In
RSSChannel.m
, synthesize the properties and override
init
.

 
@implementation RSSChannel
@synthesize items, title, infoString, parentParserDelegate;
- (id)init
{
    self = [super init];
    if (self) {
        // Create the container for the RSSItems this channel has;
        // we'll create the RSSItem class shortly.
        items = [[NSMutableArray alloc] init];
    }
    return self;
}
@end
 

Back in
ListViewController.h
, add an instance variable to hold an
RSSChannel
object and have the class conform to the
NSXMLParserDelegate
protocol.

 
// a forward declaration; we'll import the header in the .m
@class RSSChannel;
@interface ListViewController : UITableViewController

{
    NSURLConnection *connection;
    NSMutableData *xmlData;
    RSSChannel *channel;
 

In
ListViewController.m
, implement an
NSXMLParserDelegate
method to catch the start of a
channel
element. Also, at the top of the file, import the header for
RSSChannel
.

 
#import "RSSChannel.h"
@implementation ListViewController
- (void)parser:(NSXMLParser *)parser
    didStartElement:(NSString *)elementName
       namespaceURI:(NSString *)namespaceURI
      qualifiedName:(NSString *)qualifiedName
         attributes:(NSDictionary *)attributeDict
{
    NSLog(@"%@ found a %@ element", self, elementName);
    if ([elementName isEqual:@"channel"]) {
        // If the parser saw a channel, create new instance, store in our ivar
        channel = [[RSSChannel alloc] init];
        // Give the channel object a pointer back to ourselves for later
        [channel setParentParserDelegate:self];
        // Set the parser's delegate to the channel object
        // There will be a warning here, ignore it warning for now
        [parser setDelegate:channel];
    }
}
 

Build and run the application. You should see a log message that the channel was found. If you don’t see this message, double-check that the URL you typed in
fetchEntries
is correct.

 

Now that the channel is sometimes the parser’s delegate, it needs to implement
NSXMLParserDelegate
methods to handle the XML. The
RSSChannel
instance will catch the metadata it cares about along with any
item
elements.

 

The channel is interested in the
title
and
description
metadata elements, and you will store those strings that the parser finds in the appropriate instance variables. When the start of one of these elements is found, an
NSMutableString
instance will be created. When the parser finds a string, that string will be concatenated to the mutable string.

 

In
RSSChannel.h
, declare that the class conforms to
NSXMLParserDelegate
and add an instance variable for the mutable string.

 
@interface RSSChannel : NSObject

{
    NSMutableString *currentString;
}
 

In
RSSChannel.m
, implement one of the
NSXMLParserDelegate
methods to catch the metadata.

 
- (void)parser:(NSXMLParser *)parser
    didStartElement:(NSString *)elementName
       namespaceURI:(NSString *)namespaceURI
      qualifiedName:(NSString *)qualifiedName
         attributes:(NSDictionary *)attributeDict
{
    NSLog(@"\t%@ found a %@ element", self, elementName);
    if ([elementName isEqual:@"title"]) {
        currentString = [[NSMutableString alloc] init];
        [self setTitle:currentString];
    }
    else if ([elementName isEqual:@"description"]) {
        currentString = [[NSMutableString alloc] init];
        [self setInfoString:currentString];
    }
}

Note that
currentString
points at the same object as the appropriate instance variable – either
title
or
infoString
(
Figure 25.9
).

 

Figure 25.9  Two variables pointing at the same object

 

This means that when you append characters to the
currentString
, you are also appending them to the
title
or to the
infoString
.

 

In
RSSChannel.m
, implement the
parser:foundCharacters:
method.

 
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)str
{
    [currentString appendString:str];
}
 

When the parser finds the end of the
channel
element, the channel object will return control of the parser to the
ListViewController
. Implement this method in
RSSChannel.m
.

 
- (void)parser:(NSXMLParser *)parser
 didEndElement:(NSString *)elementName
  namespaceURI:(NSString *)namespaceURI
 qualifiedName:(NSString *)qName
{
    // If we were in an element that we were collecting the string for,
    // this appropriately releases our hold on it and the permanent ivar keeps
    // ownership of it. If we weren't parsing such an element, currentString
    // is nil already.
    currentString = nil;
    // If the element that ended was the channel, give up control to
    // who gave us control in the first place
    if ([elementName isEqual:@"channel"])
        [parser setDelegate:parentParserDelegate];
}

Other books

Brett McCarthy by Maria Padian
Free Spirits by Julia Watts
Bit of a Blur by Alex James
Ghost Hero by S. J. Rozan
El susurro de la caracola by Màxim Huerta
Night Sins by Lisa Renee Jones
Jennifer Estep Bundle by Jennifer Estep
The Scent of Apples by Jacquie McRae