The Project Page for this articles project is on github at http://github.com/Machx/FeedViewer you can grab the source code with git by entering the following command in terminal....
git clone git://github.com/Machx/FeedViewer.git
The PubSub Framework is one of those lesser known Frameworks that Apple introduced in Leopard. It has the ability to parse RSS & ATOM feeds and automatically generate KVO compliant Objective-C objects which you can inspect & enumerate through in your Application. You can even get access to Safari's or Mail.apps RSS feeds and display them in your application. Recently on StackOverflow someone was complaining that there really wasn't an example project showing how you can use PubSub.
So what i've decided to do here is setup a minimal project showing how you can efficiently use PubSub to retrieve and display a RSS feed and its contents. I tried to use a bare minimum of custom objects and instead rely on PubSub's classes to automatically generate the objects that should be displayed in the view part of the app, this way you write less code and can more easily extend the code to fit your needs in your own application. I should note that in this example I've decided to take full advantage of Grand Central Dispatch and a 10.6 only API. Here my intent is to take you through a semi-tutorial to show you what you need to do to setup this project. So let's start building this app...
First Create a new Cocoa Application Project in Xcode and call it FeedViewer or whatever you want to call it
The next obvious thing we need to do is add the PubSub framework to our app, so right click on Frameworks->Linked Frameworks and go to Add->Existing Frameworks. Find PubSub.Framework and add it. Now create a new Cocoa class which will be our App Controller. Add the following ivars & methods in the header file.
The PSFeed *newsFeed; will both contain information about our RSS Feed and contain the RSS entries once we've started a refresh. The NSOperationQueue *rssQueue; is technically unnecessary here, but to reduce the overall code Im going to use a bit of Grand Central Dispatch + Cocoa magic and a 10.6 only API which let's us register for a Notification & execute a block on another thread from NSOperationQueue. Thanks to the 10.6 API's this is pretty easy & should be done in the background to not interrupt the main UI thread. You could accomplish the same thing on 10.5, but you'd still need an NSOperationQueue, and you'd have to register for a notification on PSFeed and when you see a feed is done refreshing create an NSOperation subclass object, put it on the queue and (again) check for when the NSOperation object is finished executing. The NSError *feedError; is for presenting an error to our users so they have some feedback in the case something goes wrong.The Method -(IBAction)startFeedRefresh:(id)sender; is the only method we really need here for starting the refresh on the PSFeed. Lastly the id psNotification will be used to retain an object that Cocoa gives us for receiving notifications, you'll understand this more later on.
In AppController.m i'll explain things slightly out of order just so you know the flow of the code. First the obvious -init stuff...
Most of this doesn't need explanation except for 2 things. First if you've never seen [rssQueue setName:@"com.FeedViewer.rssQueue"]; you should. In 10.6 Apple rewrote NSOperation and NSOperationQueue so they now use Grand Central Dispatch, essentially an Objective-C wrapper around the lower level GCD API's. Plus NSOperation(Queue) provides some nice additional functionality, and it a great fit for how to do threading in Objective-C (See my article on Grand Central Dispatch if you want more info.) Grand Central Dispatch uses Queues as its primary means of receiving blocks to take in and dispatch threads. And for debugging purposes its nice to set a name for these queues so you know which Queues are yours and what the queues are doing. You can name your Queues what you want, but Apple encourages reverse style DNS naming like above.
The other thing is I am using here is a constant kAppleRSSNewsFeed which is defined as
This could have been accomplished by using #define as well, but I am generally a fan of doing it this way so it doesn't clutter your method list in Xcode with a bunch of #defines that you don't care about most of the time. Now for our -(IBAction)startFeedRefresh:(id)sender method...
That's it! This -refresh method is asynchronous (which Apple encourages), so most of the work is done elsewhere. In this case in the -awakeFromNib method...
-(void)awakeFromNib
{
NSNotificationCenter *notifyCenter = [NSNotificationCenter defaultCenter];
self.psNotification = [notifyCenter addObserverForName:PSFeedRefreshingNotification
object:newsFeed
queue:rssQueue
usingBlock:^(NSNotification *arg1) {
if ([newsFeed isRefreshing]) {
return;
}
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
if (nil != feedError) {
[NSApp presentError:feedError];
return;
}
//inform our KVO Controllers that we now have
//RSS entries to display
[self willChangeValueForKey:@"newsFeed"];
[self didChangeValueForKey:@"newsFeed"];
}];
}];
}
First off I apologize for how bad the formatting of the first few lines of code look in this method, here on this blog, I am trying to not make the code stretch out horizontally for too long. Anyway all we are doing here is registering for a notification on the (PSFeed *) newsFeed object for the PSFeedRefreshingNotification notification. Also we are giving it a NSOperationQueue object (our rssQueue), and a block to execute. This API is equivalent to registering for the notification and inside -observeValueForKeyPath:ofObject:change:context: we created an NSOperation object and then added it to a NSOperationQueue, instead we are doing this and registering for the notification all with 1 API.
It still feels odd assigning a value to -addObserver... especially since if you've been doing this in 10.4 and 10.5 this method has a void return type ( - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context.) However you need to retain this id object given to you, otherwise you'll leak in non-Garbage Collected code and your block will never be called under Garbage Collection, I presume because its being collected by the Garbage Collector before the notification is ever sent out.
The first thing we need to do right away is to check if the feed is still refreshing, if it is then there is no point in doing anything else except immediately returning. If we aren't refreshing anymore then the PSFeed is done refreshing and we can then go back to the main thread and send out any notifications, etc to finish up the process. In this case all that needs to be done is sending KVO notifications that PSFeed has changed and that any controller objects that are doing bindings work for us, should update themselves.
This is all that is necessary on the code side of things, the rest will be done in KVO with Cocoa Bindings in the XIB. All we need for this is 2 controllers a NSArrayController to bind to the PSFeeds entries array and a NSObjectController to bind to the selected PSEntry object's content object which is a PSContent object. I've created a quick and short video explaining what's going on...
So now you understand what's going on in the UI. To give an overview of the entire app this is what happens. We tell our PSFeed objet to refresh its entires. When we receive PubSub's notification, a check is done to see if we are refreshing, if we are then we immediately return, if not then we send KVO notifications to let our bindings controllers update. Our NSArrayController updates and our NSTableColumns which are bound to NSArrayController update and populate the table. When the Table has a selection our NSObjectController updates with the PSContent object and a NSTextView which binds to the plainTextString property updates and displays the content for the selected PSEntry object.
Here are some useful links
Publication Subscription Programming GuidePublication Subscription Framework Reference
Short and Sweet. Now get using RSS in your apps!
Hello, I am using your code to setup a basic rss reader mac app.
I have a function where I want to change the URL of the feed I am subscribing to. (kAppleRSSNewsFeed)
However, the URL property of the newsFeed object is read-only and if I alloc and init a new newsFeed object, I am getting the following error:
Cannot update for observer for the key path "newsFeed.entries" from , most likely because the value for the key "newsFeed" has changed without an appropriate KVO notification being sent. Check the KVO-compliance of the AppController class.
Can you help me with how I can change the URL of the newsfeed and repopulate my bound controls?
1 comment:
Hello, I am using your code to setup a basic rss reader mac app.
I have a function where I want to change the URL of the feed I am subscribing to. (kAppleRSSNewsFeed)
However, the URL property of the newsFeed object is read-only and if I alloc and init a new newsFeed object, I am getting the following error:
Cannot update for observer for the key path "newsFeed.entries" from , most likely because the value for the key "newsFeed" has changed without an appropriate KVO notification being sent. Check the KVO-compliance of the AppController class.
Can you help me with how I can change the URL of the newsfeed and repopulate my bound controls?
Thanks
Post a Comment