Parsing XML Files

Reading XML files is one of the common tasks we perform in our application, In this tutorial let’s see how we can use NSXMLParser to parse XML in our iPhone app.
Introduction
NSXMLParser is a forward only reader or an event driven parser. What it means is, an event is raised whenever the parser comes across a start of an element, value, CDATA and so on. The delegate of NSXMLParser can then implement these events to capture XML data. Some of the events are raised multiple times like the start of an element, value of an element and so on. Since NSXMLParser is known as an event driven parser, we can only read data at the present node and cannot go back. The iPhone only supports NSXMLParser and not NSXMLDocument, which loads the whole XML tree in memory.

Books Application
To understand how to use an instance of NSXMLParser, let’s create a simple navigation based application where we will list the title of the book in the table view and upon selecting a title, display the detail information in a detail view. Click here to see the sample XML file, used in this application.

Create a new application in XCode by selecting Navigation-Based Application, I have named my app XML. Since the NSXMLParser is a forward only parser or an event driven parser, we need to store the data locally, which can be used later. To store this data, we will create a class which replicates the elements and attributes in the XML file. An instance of this class represents one single Book element in the XML file. I have named this class “Book” and its source code is listed below

//Book.h
#import <UIKit/UIKit.h>

@interface Book : NSObject {

NSInteger bookID;
NSString *title; //Same name as the Entity Name.
NSString *author; //Same name as the Entity Name.
NSString *summary; //Same name as the Entity Name.

}

@property (nonatomic, readwrite) NSInteger bookID;
@property (nonatomic, retain) NSString *title;
@property (nonatomic, retain) NSString *author;
@property (nonatomic, retain) NSString *summary;

@end

//Book.m
#import "Book.h"

@implementation Book

@synthesize title, author, summary, bookID;

- (void) dealloc {

[summary release];
[author release];
[title release];
[super dealloc];
}

@end

Notice that the name of the property is the same as the element name in the XML file. Since the XML file has n number of Book elements, we need an array to hold all the books we read, so we declare an array in the application delegate and this is how the source code changes

//XMLAppDelegate.h
#import <UIKit/UIKit.h>

@interface XMLAppDelegate : NSObject <UIApplicationDelegate> {

UIWindow *window;
UINavigationController *navigationController;

NSMutableArray *books;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet UINavigationController *navigationController;

@property (nonatomic, retain) NSMutableArray *books;

@end

The Delegate
To keep the source code clean, we will also declare a delegate, which will be used by the instance of NSXMLParser and this how its source code looks like

//XMLParser.h
#import <UIKit/UIKit.h>

@class XMLAppDelegate, Book;

@interface XMLParser : NSObject {

NSMutableString *currentElementValue;

XMLAppDelegate *appDelegate;
Book *aBook;
}

- (XMLParser *) initXMLParser;

@end

Let’s look at how the variables will be used. currentElementValue holds the current element value, appDelegate so we can access the array which holds the list of books and finally a reference to the Book class itself. Notice that we do not keep track of the current element name being processed, because the event will tell us that. Finally, we have a constructor called initXMLParser and let’s see what it does

//XMLParser.m
- (XMLParser *) initXMLParser {

[super init];

appDelegate = (XMLAppDelegate *)[[UIApplication sharedApplication] delegate];

return self;
}

Very simple, gets a reference to the application delegate and returns itself.

Parsing the XML File
Now that we have everything set up, let’s look at the code to read the XML file

//XMLAppDelegate.m
- (void)applicationDidFinishLaunching:(UIApplication *)application {

NSURL *url = [[NSURL alloc] initWithString:@"http://sites.google.com/site/iphonesdktutorials/xml/Books.xml"];
NSXMLParser *xmlParser = [[NSXMLParser alloc] initWithContentsOfURL:url];

//Initialize the delegate.
XMLParser *parser = [[XMLParser alloc] initXMLParser];

//Set delegate
[xmlParser setDelegate:parser];

//Start parsing the XML file.
BOOL success = [xmlParser parse];

if(success)
NSLog(@"No Errors");
else
NSLog(@"Error Error Error!!!");

// Configure and show the window
[window addSubview:[navigationController view]];
[window makeKeyAndVisible];
}

The code is very simple, we create an instance of NSURL, create an instance of NSXMLParser, initialize the delegate, assign the delegate and start parsing by passing the parse message. It returns YES, if the parsing is successful, NO if there is an error or if the operation is aborted.

Parsing the start of an element

The delegate of the parser does not have to implement all the methods that it raises, so we can pick and choose which events we care about. If we do not want to handle the event when the parser starts reading the document, we can choose to ignore it by not implementing it. We will only implement three methods which is called when the parser encounters the start of an element, end of an element or value of an element.

Let’s look at parser:didStartElement:namespaceURI:qualifiedName:attributes method which is called when the parser encounters the start of an element.

//XMLParser.m
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qualifiedName
attributes:(NSDictionary *)attributeDict {

if([elementName isEqualToString:@"Books"]) {
//Initialize the array.
appDelegate.books = [[NSMutableArray alloc] init];
}
else if([elementName isEqualToString:@"Book"]) {

//Initialize the book.
aBook = [[Book alloc] init];

//Extract the attribute here.
aBook.bookID = [[attributeDict objectForKey:@"id"] integerValue];

NSLog(@"Reading id value :%i", aBook.bookID);
}

NSLog(@"Processing Element: %@", elementName);
}

From the above code we first initialize the array when it encounters the “Books” element, which can also be done in parserDidStartDocument method. If the element is “Book” then we initialize the local book object and read the attribute of the present XML book element from the attribute dictionary object.

Parsing an element’s value
Now that we have a local book object representing the current book element in the XML tree, the next thing to do is to populate the local object with the XML data. The parser now moves to the title element and the same method is called again, but this time we do not do anything. Parser then moves to the element value and it sends parser:foundCharacters event to the delegate, let’s see how the code look like

//XMLParser.m
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {

if(!currentElementValue)
currentElementValue = [[NSMutableString alloc] initWithString:string];
else
[currentElementValue appendString:string];

NSLog(@"Processing Value: %@", currentElementValue);

}

The code is very easy to read, if the mutable string is nil then we initialize it with the string parameter. If the currentElementValue is not nil then we simply append the data to the existing string value.

Parsing the end of an element
The parser now moves to the end of the element and hence parser:didEndElement:namespaceURI:qualifiedName is sent to the delegate. This is where we set the currentElementValue to the correct property of the local book object and set the currentElementValue to nil. This is how the code looks like

//XMLParser.m
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {

if([elementName isEqualToString:@"Books"])
return;

//There is nothing to do if we encounter the Books element here.
//If we encounter the Book element howevere, we want to add the book object to the array
// and release the object.
if([elementName isEqualToString:@"Book"]) {
[appDelegate.books addObject:aBook];

[aBook release];
aBook = nil;
}
else
[aBook setValue:currentElementValue forKey:elementName];

[currentElementValue release];
currentElementValue = nil;
}

If the element it encounters is “Books” then there is nothing to do as we are almost done reading the file. If the element name is “Book” then we add the book object to the array and set the local book object to nil and release its memory, so it can be used again. If the end element is not “Books” or “Book” then it must be one of the sub element of “book” and we set the currentElementValue to the current book property using setValue:forKey. We can do this, because the properties declared in the book is the same as the XML element names.

The cycle starts again by initializing the book object and reading the attribute, reading the children elements and setting its value to the local object and finally adding the object to the array. The parser calls the three functions again and again as long as it does not encounters eof.

Complete listing of XMLParser.m file

//XMLParser.m
#import "XMLParser.h"
#import "XMLAppDelegate.h"
#import "Book.h"

@implementation XMLParser

- (XMLParser *) initXMLParser {

[super init];

appDelegate = (XMLAppDelegate *)[[UIApplication sharedApplication] delegate];

return self;
}

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qualifiedName
attributes:(NSDictionary *)attributeDict {

if([elementName isEqualToString:@"Books"]) {
//Initialize the array.
appDelegate.books = [[NSMutableArray alloc] init];
}
else if([elementName isEqualToString:@"Book"]) {

//Initialize the book.
aBook = [[Book alloc] init];

//Extract the attribute here.
aBook.bookID = [[attributeDict objectForKey:@"id"] integerValue];

NSLog(@"Reading id value :%i", aBook.bookID);
}

NSLog(@"Processing Element: %@", elementName);
}

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {

if(!currentElementValue)
currentElementValue = [[NSMutableString alloc] initWithString:string];
else
[currentElementValue appendString:string];

NSLog(@"Processing Value: %@", currentElementValue);

}

- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {

if([elementName isEqualToString:@"Books"])
return;

//There is nothing to do if we encounter the Books element here.
//If we encounter the Book element howevere, we want to add the book object to the array
// and release the object.
if([elementName isEqualToString:@"Book"]) {
[appDelegate.books addObject:aBook];

[aBook release];
aBook = nil;
}
else
[aBook setValue:currentElementValue forKey:elementName];

[currentElementValue release];
currentElementValue = nil;
}

- (void) dealloc {

[aBook release];
[currentElementValue release];
[super dealloc];
}

@end

This is how the data looks like in the table view and the detail view controller.

The data is then shown in a UITableView with a detail view, the complete code is not shown here but you can download the source code and to get a better understanding UITableView, you can follow my suggested readings below.

Summary
Reading XML files is very easy and it can be done with only three methods as seen above. I hope this tutorial has got you started in reading XML files.

Happy Programming,
iPhone SDK Articles


Attachments

Suggested Readings