Monday, 10 September 2012

A Simple RSS reader for the iPhone

Build a Simple RSS reader for the iPhone 

Let’s get started

  1. Open Xcode and choose the “File” menu, in which you’ll click the “New Project…” item.
  2. Click “Application” under “iPhone OS” in the list at left.
  3. On the right, choose “Navigation-Based Application”. Then click the “Choose…” button. You’ll be prompted to pick a name and location. Type in the name “TAB RSS reader”.
  4. Save it wherever you wish.

The Xcode project window will appear, with the standard 3 panes – I recommend pulling the horizontal divider on the right side all the way to the top, since you’ll need that editor area and all the real estate you can give it.

Do you see a “Build and Go” button in the toolbar? Click it, or go to the “Build” menu, and click “Build and Go (Run)” there. It should open the Simulator application and launch a simple iPhone app that displays a blank navigation bar and blank table. Whee! Your first iPhone app. Now let’s sculpt it into something.


The project template that Apple provides has a lot of things already set up to get us started. On the list at the left of the project window, find “MainWindow.xib”, and double-click it. This is the basic framing of your application’s UI. Be careful not to mess around here too much. You just need to do one thing: you should see a “Navigation Controller” window with a basic interface mocked up – double-click on the navigation bar (which has no title in it), and type “The Apple Blog”. Press return. Save and quit Interface Builder.
Click once on “RootViewController.h” in the list, and see the code on the right. Make it look like this:


@interface RootViewController : UITableViewController {
 IBOutlet UITableView * newsTable;
 UIActivityIndicatorView * activityIndicator;
 CGSize cellSize;
 NSXMLParser * rssParser;
 NSMutableArray * stories;

 // a temporary item; added to the "stories" array one at a time, and cleared for the next one
                    NSMutableDictionary * item;

 // it parses through the document, from top to bottom...
 // we collect and cache each sub-element value, and then save each item to our array.
 // we use these to track each current item, until it's ready to be added to the "stories" array
                 NSString * currentElement;
     NSMutableString * currentTitle, * currentDate, 
     * currentSummary, * currentLink;
     }
     @end


That’s the declaration file, where we’re telling the compiler what to expect when it runs through the controller logic. Here’s where the real work happens… Open “RootViewController.m“.
You’ll see that there’s more of the basic code to make that table view display – this controller is the table’s “delegate” – the table looks here to find out what it’s supposed to see/display/do in various situations, and sends calls for methods when the user performs various actions.
Change the value of 

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section to return [stories count];
In our declarations, we told it we would have an array (NSMutableArray – a modifiable collection of objects), which we called “stories”. The [brackets] around that bit signify that it’s a message – we’re asking the stories array what its current count is – that is, how many items it has. Our RSS reader will grab as many items as it can (one for each story in the RSS feed), and place them in that array, so this method will tell the table This is how many rows we need: one for each item in the array, or for each item in the feed. Before, it was set to 0, so you’re giving it more information on our array.
Next up, modify the method below the one you just changed, like so:


- (UITableViewCell *)tableView:(UITableView *)tableView 

cellForRowAtIndexPath:(NSIndexPath *)indexPath {
 static NSString *MyIdentifier = @"MyIdentifier";
 UITableViewCell *cell = [tableView 
dequeueReusableCellWithIdentifier:MyIdentifier];

 if (cell == nil) {
 cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:MyIdentifier] autorelease];
 }

 // Set up the cell
 int storyIndex = [indexPath indexAtPosition: [indexPath 
length] - 1];
 [cell setText:[[stories objectAtIndex: storyIndex] 
objectForKey: @"title"]];

 return cell;
}

As you can see, we used the “setText:” method to tell the cell what the contents will be. Each row in the table is basically a cell, and its properties are set in this method.
There are 4 methods highlighted in green about 3/4 of the way down – you can delete those if you wish, since we won’t be using them. They have to do with adding/deleting items.
If you were to run the program again now, it still wouldn’t do anything: we haven’t added the ability to download the feed and use it yet, so let’s do that now.
Edit the “viewDidAppear:” method to look like this:

- (void)viewDidAppear:(BOOL)animated {
 [super viewDidAppear:animated];

 if ([stories count] == 0) {
 NSString * path = @"http://feeds.feedburner.com/TheAppleBlog";
 [self parseXMLFileAtURL:path];
 }
 cellSize = CGSizeMake([newsTable bounds].size.width, 60);
}

This is where we tell the parser which feed to download. It calls a method, which you’ll want to paste in now:

- (void)parseXMLFileAtURL:(NSString *)URL {
 stories = [[NSMutableArray alloc] init];

 //you must then convert the path to a proper NSURL or it won't work

 NSURL *xmlURL = [NSURL URLWithString:URL];
 // here, for some reason you have to use NSClassFromString when trying to alloc NSXMLParser, otherwise you will get an object not found error
 // this may be necessary only for the toolchain

 rssParser = [[NSXMLParser alloc] initWithContentsOfURL:xmlURL];

 // Set self as the delegate of the parser so that it will receive the parser delegate methods callbacks.

 [rssParser setDelegate:self];

 // Depending on the XML document you're parsing, you may want to enable these features of NSXMLParser.

   [rssParser setShouldProcessNamespaces:NO];
        [rssParser setShouldReportNamespacePrefixes:NO];
 [rssParser setShouldResolveExternalEntities:NO];
 [rssParser parse];
}

This is a method we’ve added that creates the empty array for stories, creates a parser, and starts downloading the feed. As the parser works, this controller we’re working in will receive the various delegate methods, which you can paste in now:

        - (void)parserDidStartDocument:(NSXMLParser *)parser {
 NSLog(@"found file and started parsing");
        }

- (void)parser:(NSXMLParser *)parser parseErrorOccurred:
(NSError *)parseError {
NSString * errorString = [NSString stringWithFormat:
@"Unable to download story feed from web site (Error code %i )", [parseError code]];
 NSLog(@"error parsing XML: %@", errorString);
 UIAlertView * errorAlert = [[UIAlertView alloc] 
initWithTitle:@"Error loading content" 
message:errorString delegate:self cancelButtonTitle:@"OK" 
otherButtonTitles:nil];
 [errorAlert show];
}

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:
(NSString *)qName attributes:(NSDictionary *)attributeDict{
 //NSLog(@"found this element: %@", elementName);
 currentElement = [elementName copy];
 if ([elementName isEqualToString:@"item"]) {
  // clear out our story item caches...
  item = [[NSMutableDictionary alloc] init];
  currentTitle = [[NSMutableString alloc] init];
  currentDate = [[NSMutableString alloc] init];
  currentSummary = [[NSMutableString alloc] init];
  currentLink = [[NSMutableString alloc] init];
 }
}

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

 //NSLog(@"ended element: %@", elementName);
 if ([elementName isEqualToString:@"item"]) {
// save values to an item, then store that item into the array...
 [item setObject:currentTitle forKey:@"title"];
 [item setObject:currentLink forKey:@"link"];
 [item setObject:currentSummary forKey:@"summary"];
 [item setObject:currentDate forKey:@"date"];

 [stories addObject:[item copy]];
 NSLog(@"adding story: %@", currentTitle);
 }
}

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string{
 //NSLog(@"found characters: %@", string);
 // save the characters for the current item...
 if ([currentElement isEqualToString:@"title"]) {
  [currentTitle appendString:string];
 } 
        else if ([currentElement isEqualToString:@"link"]) {
  [currentLink appendString:string];
 } 
        else if ([currentElement isEqualToString:@"description"])        {
  [currentSummary appendString:string];
 }
        else if ([currentElement isEqualToString:@"pubDate"]) {
  [currentDate appendString:string];
 }
      }

- (void)parserDidEndDocument:(NSXMLParser *)parser {

 [activityIndicator stopAnimating];
 [activityIndicator removeFromSuperview];

 NSLog(@"all done!");
 NSLog(@"stories array has %d items", [stories count]);
        [newsTable reloadData];
}

Unfortunately, the NSXMLParser is the only simple XML-parsing tool available on iPhone (some of my favorites from the Mac are missing). So, this means we have to crunch through the file in order from top to bottom. We have a series of strings that we assign values to, and then collect them into story items, which are saved one by one. Once it hits the closing “item” tag, it saves that story, clears out the fields, and starts on the next item until we reach the end of the file. Not my favorite approach, but it works.

Finishing up

We need to shut off any potential memory leaks (it’s a good habit to get into, when you don’t have garbage collection – who needs that anyway?). Drop in this change:

- (void)dealloc {
 [currentElement release];
 [rssParser release];
 [stories release];
 [item release];
 [currentTitle release];
 [currentDate release];
 [currentSummary release];
 [currentLink release];

 [super dealloc];
}

Next open up “RootViewController.xib“, and hold down the “control” key on your keyboard, while dragging from the “RootViewController” cube icon over to the table view, and release. You should see a list of three items appear, so click on the “newsTable” item. Save and quit Interface Builder.


Build and Go

If you click “Build and Go”, you’ll see the results we have so far. If you were to run this on an actual iPhone and not in simulator, the results would be different slightly: the hardware is slower, and if you’re on EDGE, the RSS feed will take a very long time to download. But, hey, it works! One thing that doesn’t work yet: when you tap on an item in the table, nothing happens. This is default behavior, but let’s make the stories open in Safari – that’s an easy thing to do. Just change this method:


- (void)tableView:(UITableView *)tableView 
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
 // Navigation logic

int storyIndex = [indexPath indexAtPosition: [indexPath length] - 1];

NSString * storyLink = [[stories objectAtIndex: storyIndex] 
objectForKey: @"link"];

// clean up the link - get rid of spaces, returns, and tabs...
storyLink = [storyLink stringByReplacingOccurrencesOfString:
@" " withString:@""];
storyLink = [storyLink stringByReplacingOccurrencesOfString:
@"n" withString:@""];
storyLink = [storyLink stringByReplacingOccurrencesOfString:
@" " withString:@""];

NSLog(@"link: %@", storyLink);
// open in Safari
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:
storyLink]];
}

Now, click “Build and Go” again, to see that it works.






No comments:

Post a Comment