iPhone XML Parsing

My current project is an iPhone App to be used for Boy Scout record tracking. The initial release will just contain the requirements for all ranks and merit badges. After that release, I will work on adding the capability to check off completion of each requirement. That will entail creating some custom UITableViewCells and using Core Data. Something a little bit advanced for me right now. My first goal was to get up to speed on Objective-C and the basics of UITableView. My guide for this journey has been Bill Dudney and Chris Adamson’s book iPhone SDK Development (The Pragmatic Programmers). I have been reading the book and trying out code for the better part of a week before I decided it was time to jump into my app. The basic layout will be a series of UITableViews in a Navigation based app, similar to the iPhone Mail app. First screen is just a list of Rank and Merit Badges. Selecting one of these takes you down another level showing a list of all the BSA ranks or merit badges, depending on the user’s choice. Stepping down one more level displays the requirements for their selection. Requirements were pulled from the BSA website and processed into individual XML files using Ruby and Hpricot.

After basic screen layout was completed using code samples from the iPhone SDK Development book, I was ready to start parsing the XML files to populate the screens. The ranks.xml file is just a listing of all the ranks with name and image attribute.

1
2
3
4
5
6
7
8
9
10
    <ranks>
      <rank name="Scout" img="scout.png"></rank>
      <rank name="Tenderfoot" img="tenderfoot.png"></rank>
      <rank name="Second Class" img="secondclass.png"></rank>
      <rank name="First Class" img="firstclass.png"></rank>
      <rank name="Star" img="star.png"></rank>
      <rank name="Life" img="life.png"></rank>
      <rank name="Eagle" img="eagle.png"></rank>
      <rank name="Palms" img="palms.png"></rank>
    </ranks>

Mine is simple, just an XML element, rank, which has two attributes, name and img. The first thing to do before parsing your XML, make sure your XML is valid. I spent several hours trying to determine why parsing my file was failing. I assumed I had coded something wrong. After trying several things, I finally took a close look and noticed something was missing. Here is the original:

1
2
3
4
5
6
7
8
9
10
    <ranks>
      <rank name="Scout" img="scout.png"</rank>
      <rank name="Tenderfoot" img="tenderfoot.png"</rank>
      <rank name="Second Class" img="secondclass.png"</rank>
      <rank name="First Class" img="firstclass.png"</rank>
      <rank name="Star" img="star.png"</rank>
      <rank name="Life" img="life.png"</rank>
      <rank name="Eagle" img="eagle.png"</rank>
      <rank name="Palms" img="palms.png"</rank>
    </ranks>

Do you see what is wrong? It is missing the closing '>' after the ‘img’ attribute. Use an XML validator like The W3C Markup Validation Service to catch mistakes like these. Starting out with a valid XML file will make parsing a lot easier.

The predominant use of NSXMLParser is for parsing internet based XML sources like Twitter. The first step is to open your XML source.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
     tweetsData = [[NSMutableData alloc] init];
      NSURL *url = [NSURL URLWithString:
          @"http://twitter.com/statuses/public_timeline.xml"];
      NSURLRequest *request = [[NSURLRequest alloc] initWithURL: url];
      NSURLConnection *connection = [[NSURLConnection alloc]
                 initWithRequest:request
                 delegate:self];
    .
  .
  .
    - (void)connection:(NSURLConnection *)connection
         didReceiveData:(NSData *)data {
      [tweetsData appendData: data];
    }
    .
  .
  .
    - (void) connectionDidFinishLoading: (NSURLConnection*) connection {
      [activityIndicator stopAnimating];
      [self startParsingTweets];
    }
    - (void) startParsingTweets {
      NSXMLParser *tweetParser = [[NSXMLParser alloc] initWithData:tweetsData];
      tweetParser.delegate = self;
      [tweetParser parse];
      [tweetParser release];
    }

This sample code from Dudley and Adamson’s book shows that you open a NSURLConnection which starts downloading the data. didReceiveData receives updates as they come in and appends to tweetsData. Only after connectionDidFinishLoading is called does the parsing start. You create a NSXMLParser and set the delegate to self.

Elements hold most of the information in an XML document. When NSXMLParser traverses an element in an XML document, it sends at least three separate message to its delegate, in the following order:

1
2
 parser:didStartElement:namespaceURI:qualifiedName:attributes:
  parser:foundCharacters: parser:didEndElement:namespaceURI:qualifiedName:

The parser could send parser:foundCharacters multiple times depending on XML data size.

To handle these messages, your code must implement these methods to parse your specific XML format:

1
2
3
4
5
6
 - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName
      namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qualifiedName
      attributes:(NSDictionary *)attributeDict;
  - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string;
  - (void)parser:(NSXMLParser *)parser  didEndElement:(NSString *)elementName
      namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName;

My parser is slightly different in that my XML file is bundled locally with the app. I don’t have to do the NSURLConnection code, so the NSXMLParser instantiation is slightly different.

1
2
3
4
5
6
7
 NSXMLParser *ranksParser = [[NSXMLParser alloc]
      initWithData:[NSData dataWithContentsOfFile:[[[NSBundle mainBundle]
      resourcePath] stringByAppendingPathComponent:@"ranks.xml"]]];
  XMLParser *parser = [[XMLParser alloc] init];
  [ranksParser setDelegate:parser];
  [ranksParser parse];
  [ranksParser release];

Here is my delegate code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName
          namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qualifiedName
          attributes:(NSDictionary *)attributeDict {
      if([elementName isEqualToString:@"ranks"]) {
          // Initialize the array.
          appDelegate.ranks = [[NSMutableArray alloc] init];
      }
      else if([elementName isEqualToString:@"mbs"]) {
          //Initialize the array.
          appDelegate.mbs = [[NSMutableArray alloc] init];
      }
      else if([elementName isEqualToString:@"rank"]) {
          NSDictionary *item = [[NSDictionary alloc] initWithDictionary:attributeDict];
          [appDelegate.ranks addObject:item];
      }
      else if([elementName isEqualToString:@"mb"]) {
          NSDictionary *item = [[NSDictionary alloc] initWithDictionary:attributeDict];
          [appDelegate.mbs addObject:item];
      }
  }

I didn’t have to implement foundCharacters or didEndElement because my XML files do not contain any element value data, just element attributes which is easily processed in didStartElement. Here is the result: