Developing the initial UI for BirdWatch was relatively pain-free. After I put aside my fear and distrust of Interface Builder and figured out how everything fitted together with outlets and delegates it all came together quickly. This is roughly what it looked like:
Each search query in BirdWatch is an atom feed stored as an instance of PSFeed by the PubSub framework. When you click on the “macruby” query highlighted in blue above, the entries for that feed are sent to the table view on the right. Each entry in the feed is an instance of PSEntry and corresponds to one row in the table view. Each row is represented by the class Status:
class Status attr_accessor :image, :text def initialize(entry) @image = NSImage.imageNamed 'NSUser' @text = entry.content.plainTextString end end
The instance variables @image and @text populate the left and right cell of each table view row respectively.
These are all simple method calls to local resources so its no suprise this table view loads and scrolls very quickly. But its a little boring, lets see what happens when we add some more visual interest and interactivity.
Twitter Profile Pictures
My first priority was to add twitter profile pictures to spice things up and show at a glance the author of each status message. Unlike the status text, the profile image is not stored locally by the PubSub framework and will need to be downloaded from twitter. First we find the URL of the picture in the status xml. In this case I want a bigger version so I do a gsub on the string returned. Then I construct a NSURL instance and finally create and return the image:
class Status attr_accessor :image, :text def initialize(entry) links = entry.XMLRepresentation.nodesForXPath '//link[@type="image/png"]', error:nil href = links.first.attributeForName("href").stringValue.gsub '_normal.', '_bigger.' @image = NSImage.alloc.initByReferencingURL NSURL.URLWithString(url) @text = entry.content.plainTextString end end
At this point my application grinds to a halt and the beach ball of death becomes a constant companion. The initial loading time for each query is very slow, and trying to scroll through a large list of status messages is extremely painful. It seems downloading the profile images from twitter is blocking the UI and causing the application to become unresponsive.
Cocoa provides great support for asynchronous requests which we can leverage to download the profile images in the background and keep the app responsive. You may recognise some of the code below from the excellent hotcoca tutorials on the MacRuby site. The approach I’ll use here is to load the system-provided image when an instance of Status is first initialized. Then just before the cell is displayed on the screen I’ll kick off an asynchronous request to download the image from twitter, update the instance variable @image and reload the cell. Here is the method we will use in the table view delegate to start the request:
def tableView(view, willDisplayCell:cell, forTableColumn:column, row:row) if cell.image && cell.image.name == "NSUser" status = @entries[row] status.download do |data| status.image = NSImage.alloc.initWithData data view.reloadDataForRowIndexes NSIndexSet.indexSetWithIndex(row), columnIndexes:NSIndexSet.indexSetWithIndex(0) end end end
So first we are checking if this is an image cell and if the cell is still holding the placeholder image. Then we retrieve the status that corresponds to this cell and call the method “download” on that status. The download method takes a block which will return the downloaded data asynchronously. This is where MacRuby really shines: putting a simple DSL on top of a complicated Cocoa class. Here is what the Status class looks with the download method and friends added:
class Status attr_accessor :image, :text def initialize(entry) @image = NSImage.imageNamed 'NSUser' @text = entry.content.plainTextString @loading = false @data = NSMutableData.new @url = url entry end def url(entry) links = entry.XMLRepresentation.nodesForXPath '//link[@type="image/png"]', error:nil href = links.first.attributeForName("href").stringValue.gsub '_normal.', '_bigger.' NSURL.URLWithString href end def download(&block) return if @loading @loading = true @block = block request = NSURLRequest.requestWithURL @url NSURLConnection.alloc.initWithRequest request, delegate:self end def connection(connection, didReceiveResponse:response) @data.setLength 0 end def connection(connection, didReceiveData:data) @data.appendData data end def connection(connection, didFailWithError:error) @loading = false end def connectionDidFinishLoading(connection) @block.call @data @loading = false end end
Before a download is started the @loading instance variable is checked to ensure we don’t try and start an extra request for the same object. During the request the data returned is appended to the @data instance variable and when it has finished @data is passed back to the original block in the table view delegate.
With this code in place the interface is snappy no matter how terrible your internet connection is, which is important if you are living in rural New Zealand. It’s also a lot easier on the eyes to see profile pictures in full colour and makes the content much more engaging.
Now to deal with that status message text: where are the links? who is talking? how long ago?
That will have to wait for another blog post because you would be shocked if you knew how long this has taken me to write.
You can have a look at the entire BirdWatch application on github here: http://github.com/isaac/BirdWatch. I have simplified some of the code above for readability. Please feel free to fork away and send patches or open up an issue on GitHub if you come across any problems or have any feature requests. I recommend installing the latest MacRuby nightly before you try to build and run the app from Xcode. If you don’t have the latest MacRuby nightly installed you can download a version with MacRuby embedded in the app from here: http://github.com/isaac/BirdWatch/downloads.