iOS has a simple event-based XML parser built in, which makes it fairly easy to do less involved parsing operations without having to load up a third-party framework. This tutorial will show you how to build a simple iPhone application that will download an XML feed from Twitter containing a user’s tweets, and then display them with a pretty UI. (You could easily adapt this to parse other XML documents, such as RSS feeds.)
Getting Started
First, create a new View-based application. Give it a memorable name like “TwitterXML.”
Now that you have a clean slate to work off, let’s rename some classes. I find Xcode’s default naming scheme a bit silly, with the way it prepends the project name to each file. I think the Application Delegate should be called, simply, AppDelegate.m instead of the needlessly long TwitterXMLAppDelegate.m. However, you can’t just rename the file to whatever your preference is, as that would break things.
You can rename a class project-wide, file and all, by using the Refactoring tool. You can call it up by right-clicking on the class name in the implementation file and choosing “Refactoring” from the resulting menu.
This renaming business is entirely optional as far as this tutorial goes, but it’s worth know how to do. Imagine if you made a typo in a class name and didn’t realize it until after you had already referenced it in a few places. It’s nice to have an automated fix.
Setting Up the Header File
Most of our code is going to go in the View controller, named TweetViewController in my case. Switch to the corresponding .h file and we can start setting up properties and whatnot.
First, we need to implement the NSXMLParserDelegate protocol so our class can respond to NSXMLParser delegate methods. This is easily done by adding NSXMLParserDelegate to the @interface line, like so:
@interface TweetViewController : UIViewController <NSXMLParserDelegate> {
Now we need to declare some variables and other objects in the interface block. We need a string to hold the name of the Twitter user whose profile we will be accessing, a mutable array to hold the statuses we’ve pulled from the parser and a few that are used to hold data temporarily during the parsing process. Also, we need a few IBOutlets so we can update the View once we finish reading the XML data.
@interface TweetViewController : UIViewController <NSXMLParserDelegate> { NSString *twitterUser; NSMutableArray *statuses; NSString *currentElement; NSMutableDictionary *currentElementData; NSMutableString *currentElementString; IBOutlet UIImageView *backgroundImage; IBOutlet UILabel *tweetLabel; IBOutlet UIImageView *avatar; }
Of course, we need to make these objects into properties. This means adding a few property declarations after the interface block ends.
@property (nonatomic, retain) NSString *twitterUser; @property (nonatomic, retain) NSMutableArray *statuses; @property (nonatomic, retain) NSString *currentElement; @property (nonatomic, retain) NSMutableDictionary *currentElementData; @property (nonatomic, retain) NSMutableString *currentElementString; @property (nonatomic, retain) UIImageView *backgroundImage; @property (nonatomic, retain) UILabel *tweetLabel; @property (nonatomic, retain) UIImageView *avatar;
And then you need to synthesize them in the .m file by adding the following line right after the @implementation line:
@synthesize twitterUser, statuses, currentElement, currentElementData, currentElementString, backgroundImage, tweetLabel, avatar;
Now that that’s out of the way, we can get to the interesting part.
Setting Up the Parser
The first method in the View controller is <em>viewDidLoad</em>, which fires as soon as the View as loaded. (Subtle, isn’t it?) We will be putting our initialization stuff in there. Basically, we just need to ready our properties, set the Twitter username and start the parser.
- (void)viewDidLoad { [super viewDidLoad]; statuses = [[NSMutableArray alloc] init]; currentElement = [[NSString alloc] init]; currentElementData = [[NSMutableDictionary alloc] init]; currentElementString = [[NSMutableString alloc] init]; twitterUser = [NSString stringWithString:@"collis"]; [self parseXMLForUser:twitterUser]; }
After the first arrays and dictionaries are initialized, the twitterUser string is set to the username of the Twitter account we want the app to pull the latest statuses from. I’m using Collis, one of the co-founders of Envato, as an example. You could put any user you want there, so long as they have a cool-looking background on their profile!
The last line calls the parseXMLForUser: method and passes the twitterUser string along with it. We will work on that part next.
The parseXMLForUser: method is responsible for setting up the parser, as well as building the Twitter API URL.
- (void)parseXMLForUser:(NSString *)user { //Build the Twitter API URL by combining the user with the rest of the URL NSString *urlString = [NSString stringWithFormat:@"http://twitter.com/statuses/user_timeline/%@.xml?count=3", user]; NSURL *url = [NSURL URLWithString:urlString]; //Create an instance of NSXMLParser and download the XML data from the URL NSXMLParser *parser = [[NSXMLParser alloc] initWithContentsOfURL:url]; //Set this class as its own delegate so we can process NSXMLParser callbacks [parser setDelegate:self]; //Disable namespace support and other things we don't really need [parser setShouldProcessNamespaces:NO]; [parser setShouldReportNamespacePrefixes:NO]; [parser setShouldResolveExternalEntities:NO]; [parser parse]; //Go go gadget XML parser... [parser release]; }
The first part of the method should look familiar to anyone who has worked with C or Java before. It takes our user argument (which contains the text of twitterUser) and splices it into the URL string, just before the .xml part. Cocoa expects URLs to be of the NSURL object type, we create a new one of those and pass it urlString.
After that is done, we create a new instance of the NSXMLParser class and nickname it “parser.” We also pass it the new URL object, which it will use to download the contents it finds there at runtime. Next we set the parser’s delegate to self, or the current class. The next three lines turn off some features we don’t really need. Finally, we kick the parser into action and leave a [parser release] command to clean up after it’s done.
That was simple, wasn’t it? Sadly, that was only the beginning. In order for the parser to, well, parse we still need to implement the delegate methods for NSXMLParser. And we need to make a spiffy UI.
Building the Parser
NSXMLParser is what is called an event-based parser. This means it loops around, searching a document for anything that looks like an XML tag. When it finds one, it raises an event. Basically it says “I found an opening tag named ‘something'” and leaves you to deal with it. The parser does the same thing with ending tags and the text between them. We have to implement delegate methods to handle these events and save the data they find.
Let’s start with a couple of simple ones.
- (void)parserDidStartDocument:(NSXMLParser *)parser { NSLog(@"The XML document is now being parsed."); } - (void)parser:(NSXMLParser *)parser parseErrorOccurred:(NSError *)parseError { NSLog(@"Parse error: %d", [parseError code]); }
The first method fires when NSXMLParser starts to parse the document. For this application, there isn’t really anything that we need to do at that point. Putting an NSLog there is great for debugging, though. (If your app is crashing, it’s helpful to know whether it’s getting to that step or not.) I’m sure you can guess what the parseErrorOccurred method does. (It logs an error code if the XML document is malformed or if, for some other reason, the parser could not process it.)
Moving on, we have a method that is called when the parser finds an opening XML tag.
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict { //Store the name of the element currently being parsed. currentElement = [elementName copy]; //Create an empty mutable string to hold the contents of elements currentElementString = [NSMutableString stringWithString:@""]; //Empty the dictionary if we're parsing a new status element if ([elementName isEqualToString:@"status"]) { [currentElementData removeAllObjects]; } }
This one is a bit more complicated. When it’s called, its arguments are populated with information that the parser found out about the element currently being parsed. It’s name is what we care about, primarily. Using the properties we created earlier, the method keeps track of the element currently being parsed (we need to know its name in other methods) and whatever is inside the element (between the opening and closing tag). The conditional statement at the end empties our dictionary every time the parser moves on to a new <status> element, as we will have already copied its contents to the statuses array.
The next delegate method takes any characters found inside an XML element and stores it in the currentElementString property for later.
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string { //Take the string inside an element (e.g. <tag>string</tag>) and save it in a property [currentElementString appendString:string]; }
And finally, the penultimate method. This one contains the real meat of the parser. It is called whenever NSXMLParser comes across a closing XML tag. And so, it serves as a good place to put most of the data-saving logic.
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName { //If we've hit the </status> tag, store the data in the statuses array if ([elementName isEqualToString:@"status"]) { [statuses addObject:[currentElementData copy]]; } //Trim any extra spaces and newline characters from around currentElementString NSString *string = [currentElementString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; //Store the status data in the currentElementData dictionary if ([currentElement isEqualToString:@"created_at"]) { [currentElementData setObject:string forKey:@"created_at"]; } else if ([currentElement isEqualToString:@"text"]) { [currentElementData setObject:string forKey:@"text"]; } else if ([currentElement isEqualToString:@"retweeted"]) { [currentElementData setObject:string forKey:@"retweeted"]; } else if ([currentElement isEqualToString:@"id"]) { [currentElementData setObject:string forKey:@"id"]; } else if ([currentElement isEqualToString:@"profile_image_url"]) { [currentElementData setObject:string forKey:@"profile_image_url"]; } else if ([currentElement isEqualToString:@"profile_background_image_url"]) { [currentElementData setObject:string forKey:@"profile_background_image_url"]; } else if ([currentElement isEqualToString:@"profile_link_color"]) { [currentElementData setObject:string forKey:@"profile_link_color"]; } }
The first code chunk saves the contents of the currentElementData dictionary to the statuses array if, and only if, the ending tag being processed currently is </status>. If you remember from before, currentElementData will be emptied the next time the didStartElement method is called. Otherwise, the block will be skipped and the application will handle the tasks it needs to run for child elements of <status>.
After stripping out extraneous spaces and newline characters from either side of currentElementString, so we don’t end up with weird output, we have a rather long if/else if block. This checks whether the element being parsed is one we want to save (e.g. “text” or “profile_image_url”) and if it is, it adds it to the element data dictionary.
The code may seem a bit strange at first, but it should make more sense after you become more familiar with it.
And now, for the last delegate method. This one fires when the document has finished parsing. This is the place to launch any operations we want to be started after we have our data. As you can see below, logging the statuses array to the console and then calling a method to display that data is what we will be doing here.
- (void)parserDidEndDocument:(NSXMLParser *)parser { //Document has been parsed. It's time to fire some new methods off! NSLog(@"%@", statuses); [self updateView]; }
The View
After all that code, let’s work on the interface. Double-click the TweetViewController.xib file (or whatever your View XIB is called) in the Xcode sidebar to open it in Interface Builder. Now that your screen is sufficiently cluttered with windows, you want to drag a UIImageView from the Library window into your View canvas. Make sure that it is sized to fit the whole available area.
Of course, the Image View won’t be much use to us unless we link it with the controller. Right-click on the File’s Owner icon and drag the little rubberband/wire thing from the backgroundImage Outlet over to the UIImageView and drop it. The File’s Owner overlay window should update to show the Image View as being connected to backgroundImage.
I think this app would be better if it used a horizontal orientation, don’t you? Click the little arrow icon in the upper right corner of the View canvas. Interface Builder should automagically resize the Image View inside it to still fill the View. Save the XIB file out and switch back to Xcode. Now we have to configure the application to use a landscape orientation instead of the default portrait one.
Inside your controller class there should be a method called shouldAutorotateToInterfaceOrientation. It’s commented out by default. Uncomment it and change the interfaceOrientation to UIInterfaceOrientationLandscapeLeft. It should look like this:
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { return (interfaceOrientation == UIInterfaceOrientationLandscapeLeft); }
Back in Interface Builder, drop in a new UIImageView. We won’t be hooking this one up to an IBOutlet. Instead, we will set the image to the word bubble image I made (it’s in the project file), resize the view to be the same point-width as the image (375×208) and position it neatly over the background image view. I also lowered the opacity a bit, just because I liked the effect.
Now we need a way to display the contents of the latest tweet. So drag a UILabel onto the View canvas and resize it to fit nicely over the word bubble graphic. Turn the “# Lines” setting up to five or so, set the font size to something that looks legible and change the “Line Breaks” option to “Word Wrap. Then wire it up to the tweetLabel IBOutlet, like you did with the UIImageView.
Ready to tie everything together? Switch back to Xcode and add one last method to the controller class.
- (void)updateView { //Select the latest tweet NSDictionary *latestTweet = [statuses objectAtIndex:0]; //Set the tweet label [tweetLabel setText:[latestTweet objectForKey:@"text"]]; //Set the background image after downloading it. NSString *urlString = [latestTweet objectForKey:@"profile_background_image_url"]; NSURL *url = [NSURL URLWithString:urlString]; NSData *data = [NSData dataWithContentsOfURL:url]; UIImage *background = [[UIImage alloc] initWithData:data]; [backgroundImage setImage:background]; backgroundImage.contentMode = UIViewContentModeScaleAspectFill; }
While it looks nearly as intimidating as the NSXMLParser didEndElement delegate method, it’s actually quite a bit simpler. The first line gets the newest tweet from the statuses array, the =[tweetLabel setText:…]= line updates the UILabel with the text of the message, and the last part changes the background image behind the word bubble to be the same as the Twitter user’s profile background.
That last part needs the most explanation. Before we can display the image (which is what the setImage line does) we have to download it first. Taking the urlString, which of course is a string containing the web address where the image can be found, we convert it to a NSURL object, which is named url. We create a new NSData object and use it’s dataWithContentsOfURL method to download the image. (Cocoa requires that URLs used with it’s objects be of the NSURL class.) Next we initialize a UIImage object with the NSData object and set it as the image in the UIImageView named backgroundImage. Oh, and we set the content mode to UIViewContentModeScaleAspectFill so it’s not squished funny.
Now if you build and run the app, you should get something like this:
Before we free up our allocated memory and finish the app up, let’s add one more thing: an avatar field! Switch back to Interface Builder and add a new UIImageView. Resize it to 52×52 or so and wire it up to the “avatar” IBOutlet. Re-using the code from the background image bit, we can quickly modify it for the avatar.
//Set the avatar image after downloading it. NSString *avatarUrlString = [latestTweet objectForKey:@"profile_image_url"]; NSURL *avatarUrl = [NSURL URLWithString:avatarUrlString]; NSData *avatarData = [NSData dataWithContentsOfURL:avatarUrl]; UIImage *avatarImage = [[UIImage alloc] initWithData:avatarData]; [avatar setImage:avatarImage]; avatar.contentMode = UIViewContentModeScaleAspectFill;
That goes in the updateView method, after everything else.
Now, before we can say the application is finished, there is one thing that needs to be done. Any memory we specifically allocated should be released. It’s not a huge deal in a single-view app, as it will be forcefully freed up on exit, but it’s a good habit to get into. (In more complicated applications, you can expect to see frequent crashes if you don’t release objects when you’re done with them.) It’s easy to do. For every object we explicitely alloc or retain, we have to release somewhere. The dealloc method is called when the application quits in this case, so we put most of our release statements there.
You can learn more about iOS memory management in this screencast.
- (void)dealloc { [twitterUser release]; [statuses release]; [currentElement release]; [currentElementData release]; [currentElementString release]; [backgroundImage release]; [tweetLabel release]; [avatar release]; [super dealloc]; }
And we’re done!
Additional Challenge
Want to add to this sample application? Try making the following change: Use an NSTimer to cycle through the items in the statuses array and update the View accordingly. Most of the groundwork has been laid for you already.