Ooops! That might have been you. I
source code from mdn blog:
…idget Press and Macoscope
Iphone Tutorial: Creating a RSS Feed Reader
Iphone Tutorial: Creating a RSS Feed With Podcast Part 1
Creating an RSS reader for iphone is a great way to learn about the language. In this tutorial we’re going to create a rss reader that can also play podcasts. Such application will require us to download and parse a rss feed, display the data in a table and play a remote podcast. In this article I assume a minimal knowledge of cocoa programming, but you should be able to follow even if this is the first time you’re coding an iphone application.
Update: Fixed some issues and upgraded the code to IOS4
The final application will look like this:
In order to develop an iphone application, you need to install the cocoa touch sdk. You can download it from here if you haven’t already. Once you’ve done that, you’re ready to get started.
Project setup
Open up xcode, and create a new project of type Iphone → Application → Navigation based Application. Name the project RssReader.
The first thing we need to do is to create a class that will download and parse a rss feed. For this application we will use the MdnShow podcast available at http://feeds2.feedburner.com/TheMdnShow, but you can use your own feed if you want.
Part 1: Parsing the rss feed
In this part we are going to download and parse the rss feed.
Create a new ObjectiveC class named Parser inside the Classes folder.
This class will have one method that accepts a url parameter and a delegate. Our navigation controller will call this method and set itself as the delegate. Once the parser has finished downloading and parsing the xml feed, it will send a message back to our navigation controller. This is how the process looks from a 10k feet view:
The code for Parser.h is the following:
#import <Foundation/Foundation.h>
@protocol ParserDelegate <NSObject>
- (void)receivedItems:(NSArray *)theItems;
@end
@interface Parser : NSObject <NSXMLParserDelegate> {
id _delegate;
NSMutableData *responseData;
NSMutableArray *items;
NSMutableDictionary *item;
NSString *currentElement;
NSMutableString * currentTitle, * currentDate, * currentSummary, * currentLink, * currentPodcastLink;
}
@property (retain, nonatomic) NSMutableData *responseData;
@property (retain, nonatomic) NSMutableArray *items;
@property (retain, nonatomic) NSMutableString *currentTitle;
@property (retain, nonatomic) NSMutableString *currentDate;
@property (retain, nonatomic) NSMutableString *currentSummary;
@property (retain, nonatomic) NSMutableString *currentLink;
@property (retain, nonatomic) NSMutableString *currentPodcastLink;
- (void)parseRssFeed:(NSString *)url withDelegate:(id)aDelegate;
- (id)delegate;
- (void)setDelegate:(id)new_delegate;
@end
Note that we are keeping a reference of our _delegate and declaring the method receivedItems: on the top. That method must be implemented in the delegate class, we’ll do this later. responseData contains the xml data and items contains our items list. The other instance variables are used while parsing the xml data.
The implementation file looks as follow:
#import "Parser.h"
@implementation Parser
@synthesize items, responseData;
@synthesize currentTitle;
@synthesize currentDate;
@synthesize currentSummary;
@synthesize currentLink;
@synthesize currentPodcastLink;
- (void)parseRssFeed:(NSString *)url withDelegate:(id)aDelegate {
[self setDelegate:aDelegate];
responseData = [[NSMutableData data] retain];
NSURL *baseURL = [[NSURL URLWithString:url] retain];
NSURLRequest *request = [NSURLRequest requestWithURL:baseURL];
[[[NSURLConnection alloc] initWithRequest:request delegate:self] autorelease];
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
[responseData setLength:0];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[responseData appendData:data];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
NSString * errorString = [NSString stringWithFormat:@"Unable to download xml data (Error code %i )", [error code]];
UIAlertView * errorAlert = [[UIAlertView alloc] initWithTitle:@"Error loading content" message:errorString delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
[errorAlert show];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
self.items = [[NSMutableArray alloc] init];
NSXMLParser *rssParser = [[NSXMLParser alloc] initWithData:responseData];
[rssParser setDelegate:self];
[rssParser parse];
}
#pragma mark rssParser methods
- (void)parserDidStartDocument:(NSXMLParser *)parser {
}
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict{
currentElement = [elementName copy];
if ([elementName isEqualToString:@"item"]) {
item = [[NSMutableDictionary alloc] init];
self.currentTitle = [[NSMutableString alloc] init];
self.currentDate = [[NSMutableString alloc] init];
self.currentSummary = [[NSMutableString alloc] init];
self.currentLink = [[NSMutableString alloc] init];
self.currentPodcastLink = [[NSMutableString alloc] init];
}
// podcast url is an attribute of the element enclosure
if ([currentElement isEqualToString:@"enclosure"]) {
[currentPodcastLink appendString:[attributeDict objectForKey:@"url"]];
}
}
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName{
if ([elementName isEqualToString:@"item"]) {
[item setObject:self.currentTitle forKey:@"title"];
[item setObject:self.currentLink forKey:@"link"];
[item setObject:self.currentSummary forKey:@"summary"];
[item setObject:self.currentPodcastLink forKey:@"podcastLink"];
// Parse date here
NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
[dateFormatter setDateFormat:@"E, d LLL yyyy HH:mm:ss Z"]; // Thu, 18 Jun 2010 04:48:09 -0700
NSDate *date = [dateFormatter dateFromString:self.currentDate];
[item setObject:date forKey:@"date"];
[items addObject:[item copy]];
}
}
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string{
if ([currentElement isEqualToString:@"title"]) {
[self.currentTitle appendString:string];
} else if ([currentElement isEqualToString:@"link"]) {
[self.currentLink appendString:string];
} else if ([currentElement isEqualToString:@"description"]) {
[self.currentSummary appendString:string];
} else if ([currentElement isEqualToString:@"pubDate"]) {
[self.currentDate appendString:string];
NSCharacterSet* charsToTrim = [NSCharacterSet characterSetWithCharactersInString:@" \n"];
[self.currentDate setString: [self.currentDate stringByTrimmingCharactersInSet: charsToTrim]];
}
}
- (void)parserDidEndDocument:(NSXMLParser *)parser {
if ([_delegate respondsToSelector:@selector(receivedItems:)])
[_delegate receivedItems:items];
else
{
[NSException raise:NSInternalInconsistencyException
format:@"Delegate doesn't respond to receivedItems:"];
}
}
#pragma mark Delegate methods
- (id)delegate {
return _delegate;
}
- (void)setDelegate:(id)new_delegate {
_delegate = new_delegate;
}
- (void)dealloc {
[items release];
[responseData release];
[super dealloc];
}
@end
This code might look complex but in reality it’s not hard to understand. In the first part we are downloading the xml data with NSURLConnection. When the data has been successfully downloaded, the connectionDidFinishLoading: method is called. At that point we use NSXMLParser to parse the data that we have already downloaded. In theory could tell NSXMLParser to download the data itself by passing a url, but this process would block the user interface for the time that the data is being downloaded and processed, instead we want to show a spinner while the user is waiting. The rest of the code involves parsing the xml data. When NSXMLParser has finished its job, parserDidEndDocument: is called and we send our items back to the delegate class.
Part 2: Displaying our items to the user
Now we can finally start our creative work. Back in our RootViewController, there are 4 things we need to do:
- Instantiate a spinner so that we can show and hide it when necessary.
- Send a request to the Parser class to load and parse the data.
- Implement the method receivedItems: that will be called by our Parser.
- Configure the table view.
The following is how RootViewController.h should look like:
@interface RootViewController : UITableViewController {
UIActivityIndicatorView *activityIndicator;
NSArray *items;
}
@property (retain, nonatomic) UIActivityIndicatorView *activityIndicator;
@property (retain, nonatomic) NSArray *items;
@end
activityIndicator is a reference to our spinner, and items are the items returned by the Parser class. In the implementation file we need to import the Parser class, generate accessors methods and release our instance variables at the end:
#import "RootViewController.h"
#import "Parser.h"
@interface RootViewController (PrivateMethods)
- (void)loadData;
@end
@implementation RootViewController
@synthesize activityIndicator, items;
// Other code in the class
- (void)dealloc {
[activityIndicator release];
[items release];
[super dealloc];
}
@end
Note that we are declaring a private method loadData. We will use this method later. Now we’re ready to go through all the steps.
Step 1: Instantiate a spinner so that we can show and hide it when necessary
What we want to do is placing a spinner in the navigation bar in the top right. To do so we overwrite the viewDidLoad method:
- (void)viewDidLoad {
[super viewDidLoad];
UIActivityIndicatorView *indicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
indicator.hidesWhenStopped = YES;
[indicator stopAnimating];
self.activityIndicator = indicator;
[indicator release];
UIBarButtonItem *rightButton = [[UIBarButtonItem alloc] initWithCustomView:indicator];
self.navigationItem.rightBarButtonItem = rightButton;
[rightButton release];
}
Our spinner is an instance of UIActivityIndicatorView. There are many indicators available, but we want the white one.
Step 2: Send a request to the Parser class to load and parse the data
At this point we need to tell to the Parser class to download and parse the rss feed. We can do so by overwriting the viewDidAppear method:
- (void)viewDidAppear:(BOOL)animated {
[self loadData];
[super viewDidAppear:animated];
}
- (void)loadData {
if (items == nil) {
[activityIndicator startAnimating];
Parser *rssParser = [[Parser alloc] init];
[rssParser parseRssFeed:@"http://feeds2.feedburner.com/TheMdnShow" withDelegate:self];
[rssParser release];
} else {
[self.tableView reloadData];
}
}
We check to see if we already downloaded the items, and in that case we simply reload our table. If the items are nil, we show the spinner and call the parseRssFeed: method with the feed url.
Step 3: Implement the method receivedItems: that will be called by our Parser
When the Parser class is finished downloading and parsing the items, it will call the receivedItems: method:
- (void)receivedItems:(NSArray *)theItems {
items = theItems;
[self.tableView reloadData];
[activityIndicator stopAnimating];
}
Step 4: Configure the table view
At this point you can build and run the application, but the resulting table view would still be empty. We still have to tell our table view to use the items we downloaded. To do so, overwrite the following methods in RootViewController.m:
// Customize the number of rows in the table view.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [items count];
}
// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
// Configure the cell.
cell.textLabel.text = [[items objectAtIndex:indexPath.row] objectForKey:@"title"];
// Format date
NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
[dateFormatter setDateStyle:NSDateFormatterMediumStyle];
[dateFormatter setTimeStyle:NSDateFormatterNoStyle];
cell.detailTextLabel.text = [dateFormatter stringFromDate:[[items objectAtIndex:indexPath.row] objectForKey:@"date"]];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
return cell;
}
In tableView:numberOfRowsInSection: we simply return the number of items available. In tableView:cellForRowAtIndexPath: we set up the cell to show the title and the date of the item. If you build and run the application now, this is what you would see:
Unfortunately, if you click on an item nothing happens. We will fix this in the next part.
Part 3: Showing item details
When we click a row, we want to show a new window with all the feed details. We have to create a new controller called DetailController, along with a xib file that it will be our interface.
Select File → New file and select UIViewController subclass. Make sure that with XIB for user interface is checked. Name the class DetailController.
At this point I rename DetailController.xib to Detail.xib and I move it to the Resources folder.
Double click Detail.xib to open interface builder. Drag two text labels and an instance of UIWebView, and a button that we will use to start the podcast:
Now we need to declare some instance variables and one action in our DetailController. This is how DetailController.h should look like:
#import <UIKit/UIKit.h>
@interface DetailController : UIViewController {
NSDictionary *item;
IBOutlet UILabel *itemTitle;
IBOutlet UILabel *itemDate;
IBOutlet UIWebView *itemSummary;
}
@property (retain, nonatomic) NSDictionary *item;
@property (retain, nonatomic) IBOutlet UILabel *itemTitle;
@property (retain, nonatomic) IBOutlet UILabel *itemDate;
@property (retain, nonatomic) IBOutlet UIWebView *itemSummary;
- (id)initWithItem:(NSDictionary *)theItem;
- (IBAction)playPodcast:(id)sender;
@end
</UIKit>
Don't worry about <em>initWithItem:</em> for now. You may be wondering why we used a <em>UIWebView</em> for the summary of the item. The reason is that we want to display the text as html, and using <em>UIWebView</em> is perfect for that.
The implementation file looks as follow:
<pre name="code" class="objc">
#import "DetailController.h"
@implementation DetailController
@synthesize item, itemTitle, itemDate, itemSummary;
// other code
- (void)dealloc {
[item release];
[itemTitle release];
[itemDate release];
[itemSummary release];
[super dealloc];
}
@end
Now we can go back to interface builder (save first) and connect our labels to our instance variables, and the play button to our IBAction.
Back in xcode, it’s time to finish our application. Our RootViewController class will call a initWithItem: method in our DetailController class. This method will setup the title of the window and assign the instance of item. We also overwrite viewDidLoad to update the labels value and add a method to play the podcast. Add this code to DetailController.m:
- (id)initWithItem:(NSDictionary *)theItem {
if (self = [super initWithNibName:@"Detail" bundle:nil]) {
self.item = theItem;
self.title = [item objectForKey:@"title"];
}
return self;
}
/*
// The designated initializer. Override if you create the controller programmatically and want to perform customization that is not appropriate for viewDidLoad.
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
// Custom initialization
}
return self;
}
*/
- (void)viewDidLoad {
[super viewDidLoad];
self.itemTitle.text = [item objectForKey:@"title"];
NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease];
[dateFormatter setDateStyle:NSDateFormatterMediumStyle];
[dateFormatter setTimeStyle:NSDateFormatterNoStyle];
self.itemDate.text = [dateFormatter stringFromDate:[item objectForKey:@"date"]];
[self.itemSummary loadHTMLString:[item objectForKey:@"summary"] baseURL:nil];
}
- (IBAction)playPodcast:(id)sender {
NSURLRequest *request = [[NSURLRequest alloc]
initWithURL: [NSURL URLWithString: [item objectForKey:@"podcastLink"]]];
[self.itemSummary loadRequest:request];
[request release];
}
There’s still one thing we need to do. We haven’t told yet to our table view what to do when a user clicks on a row. To fix that, overwrite the tableView:didSelectRowAtIndexPath: method in RootViewController.m:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSDictionary *theItem = [items objectAtIndex:indexPath.row];
DetailController *nextController = [[DetailController alloc] initWithItem:theItem];
[self.navigationController pushViewController:nextController animated:YES];
[nextController release];
}
Remember to add the import statement for DetailController at the top of RootViewController.m. Build and run the application, you should now be able to play around and listen to podcasts.
Conclusion
Creating this application was a lot of work, but hopefully you’ve been able to follow the process. Feel free to download the source code and to study it. Also check the documentation if you want to know more about the classes we used. Finally feel free to leave a comment if something is unclear.
Download application source code
You should subscribe to the RSS Feed Here.









Comments
Nice tutorial!
Especially to noobs like me.
Great tutorial. Would love to find more resources like these and maybe ones that take it one level deeper and explain syntax and coding choices.
Jarrod
Where does this code belong? It is not very clear. I thought in the delegate but items is defined in Parser.
- (void)receivedItems:(NSArray *)theItems { items = theItems; [self.tableView reloadData]; [activityIndicator stopAnimating]; }I downloaded the source. It’s in the RootViewController.m
- (void)receivedItems:(NSArray *)theItems { …}
Is there an easy way to specify in which file each snippet should be placed?
Great job on the tutorial! Good Work!
Hey Mike, sorry about that, but glad you figured it out.
Downloaded the code. Application gets installed on simulator but in 2-3 seconds it closes. Any idea ?
“Downloaded the code. Application gets installed on simulator but in 2-3 seconds it closes.”
This happened to me too. I use iOS4 SDK xcode 3.2.3
Same problem as @kk and @Jane,
is there any way to fix this? thanks!
Same problem as @kk and @Jane,
is there any way to fix this? thanks!
bump, same problem with ios 4 sdk
however, great tutorial, thanks
Same here on the stalling/closing. Would love a fix, thanks for the wonderful tutorial!
Hey Guys, I’m currently on a 3 days trip. I’ll look into it as soon as I get home.
Thanks for the tutorial.
I get the warning: “class ‘Parser’ does not implement the ‘NSXMLParserDelegate’ protocol”
Unfortunately it does not work
Hi guys, the project was developed with the 3.1.3 sdk. I’m going to upgrade to the latest sdk asap and I’ll email each of you once I fix this.
Thanks for the tutorial, I was able to get it up and running perfectly on iOS4.
There were a few issues with the code as-is. The NSMLParserDelegate wasn’t being declared in the Parser class, there were null pointer exceptions due to the current* NSMutableStrings being out of scope and the date was being parsed wrong.
The first was easy to fix, I just declared the NSXML delegate in Parser.h:@interface Parser : NSObject {
I’m not sure about the technical details of the second, I thought it should work but the debugger said that the current* variables were “out of scope”. In any case I had seen this before and was able to work around it by creating properties for all of them and using them with the “dot” notation, like self.currentDate:
// Add properties in Parser.h
@property (retain, nonatomic) NSMutableString *currentTitle;
@property (retain, nonatomic) NSMutableString *currentDate;
@property (retain, nonatomic) NSMutableString *currentSummary;
@property (retain, nonatomic) NSMutableString *currentLink;
@property (retain, nonatomic) NSMutableString *currentPodcastLink;
// Synthesize them in Parser.m
Then synthesizing them like this:
@synthesize currentTitle;
@synthesize currentDate;
@synthesize currentSummary;
@synthesize currentLink;
@synthesize currentPodcastLink;
// Look for all occurrences of these and replace with self.currentWhatever.
Finally, for some reason the date field had a lot of newlines and spaces after it, I trimmed in the foundCharacters handler with:
} else if ([currentElement isEqualToString:@"pubDate"]) { [self.currentDate appendString:string]; NSCharacterSet* charsToTrim = [NSCharacterSet characterSetWithCharactersInString:@" \n"]; [self.currentDate setString: [self.currentDate stringByTrimmingCharactersInSet: charsToTrim]]; }For the first issue I meant:
@interface Parser : NSObject {
@Will, thank you. I have updated the code and now it’s working again!
@Will
What is this?
The first was easy to fix, I just declared the NSXML delegate in Parser.h:
@interface Parser : NSObject {
For the first issue I meant:
@interface Parser : NSObject {
Why are you writing this? what was wrong with it and what is the difference?
I tried posting angle brackets but they didn’t show up, I thought I made a mistake so tried posting it again.
Hi,
I noticed that the back button from the DetailView NavigationBar is missing. You could add that in by writing this code into your -(void)viewDidLoad method in RootViewCotroller.m :
UIBarButtonItem *backButton = [[UIBarButtonItem alloc] initWithTitle:@"Back" style:UIBarButtonItemStyleBordered target:nil action:nil]; self.navigationItem.backBarButtonItem = backButton; [backButton release];Cheers,
Razvan.
And there is already a charset for trimming whitespaces and newline characters, it’s called [NSCharacterSet whitespaceAndNewlineCharacterSet]. Sry for double posting :)
Is there a way to parse the image data from the url and use it in the tableview as well?
Thanks
Thanks for this tutorial. Very interesting.
I downloaded the source code but had to remove a few characters to get it to run. I changed this line in Parser.h: {
@interface Parser : NSObject
to this: {
@interface Parser : NSObject
…. something isn’t right there!!
So it’s not displaying text between angle brackets. After NSObject in the first line it should say “NSURLParserDelegate”, and at the same place in the second line it should just say “ParserDelegate”.
I’m finding that when I replace the feed address (http://feeds2.feedburner.com/TheMdnShow) with a different one (feed://scottishpoetrylibrary.podomatic.com/rss2.xml) it won’t work. Simulator shows error code 1002 (Unable to download xml data). The second address is from a working feed. I’m not clear on what the difference is. I mean, I can see the obvious differences in the addresses, but I can’t tell why that would prevent it from loading.
Ok, I am being dim. It’s because it actually works with Atom feeds, not RSS.
Apologies for four comments in a row…
This is a very good tutorial. I’m not very familiar with RSS feeds but I would like to post the entire article instead of the summary/description.
I can find the element name for the entire story.
Found several prospects: article, body, content. Nothing is quite working and I would like to know exactly what to use before I alter too many things.
Thanks,
I of course meant to say that I “cannot” find, the element name for the entire article
Upon further investigation I have no answer to my earlier question, although I have eliminated a lot of suspects.
My question, to state more clearly now is, how can i get the detail view to display an entire article, rather than the abbreviated version?
Thank you in advance,
Why is the amount of the text from the rss feed that is displayed so limited. I want to be able to expand the amount of text displayed.
Can anyone tell me how to do this?
I’m new to iphone dev, and on crushing deadline.
Thank you so much,
Sorry for the bother of my messages. The issue was changing from the element “description” to the element “content:encoded”.
Very well done tutorial, BTW.
Can someone tell me how to change the font in the UIWebView to Helvetica?
Thank you,
Ali W have you managed to expand the amount of text displayed from the rss feed? I have a problem in the table, no matter the font size, the last 2 or 3 letters are changed for “…”
Any idea?
Nice tutorial!
I’ve just started with xCode.
It runs perfectly.
Can you update it other tasks such as star, share, unread…
Thank you.
How can you push the xml data to a detail view without using tableview?
So When I run the app I get the titles but no date in the cells. Is this happening to anyone else? Why is this?
So When I run the app I get the titles but no date in the cells. Is this happening to anyone else? Why is this?
@LMason, are you using the same feed for your test?
To Anti,
I changed the lines for description in Parser.m to this:
} else if ([currentElement isEqualToString:@"content:encoded"]) {
[self.currentSummary appendString:string];
Sorry Anti,
I was rushing yesterday. I used the code above to replace the code for “description”. Sorry to not be more clear. (upgrading to 4.0 not going to well.)
Sorry Anti,
I was rushing yesterday. I used the code above to replace the code for “description”. Sorry to not be more clear. (upgrading to 4.0 not going to well.)
After upgrading my sdk to 4.0 my rss reader no longer is working.
Actually, it works with the MDN blog that is included in the tutorial, but it doesn’t work with the blog that I want to display, and which worked previously.
The RootViewController loads the title and the dividing lines of the tableView appear, but then it totally crashes.
The error I get is:
0×90c4eef6 <0010> jae 0×90c4ef06 <__kill26>
I blocked out this line in Parser.m and it will load on the simulator, albeit without the date (which needs to be there):
[item setObject:date forKey:@"date"]I thought there must be some difference between the rss feeds which work, and don’t work. I pulled source code from both however and cannot see a difference.
Source code in blog I want to use:
…. says, “Tell the truth, and shame the devil!”