Using MGTwitterEngine with MacRuby (or How to Build a Twitter Client with 37 Lines of Code)

Last week, the guys at PeepCode published their first screencast about MacRuby: Meet MacRuby. In it the presenter develops a simple Twitter client, which he actually calls "the world's simplest Mac Twitter client in Ruby" (at 21:05min in the screencast). While the screencast is great and the client itself is really quite simple, I was curious whether this is also the simplest way to do it. Looking at the accompanying code of the screencast, a lot of effort goes into connecting to the Twitter API and parsing of the returned XML. Twitter clients have been developed all over the place, so there should be a way to use some open source library and avoid most of this networking stuff.

My first approach was to install one of the Ruby libraries for Twitter such as Twitter4R or Twitter, but both seem not to work with version 0.4 of MacRuby. The cool thing with MacRuby is that it combines two worlds: Ruby and Cocoa. This brings in the broad range of Cocoa frameworks and with it MGTwitterEngine. MGTwitterEngine is an open source library to integrate Twitter into any Cocoa app (including those on the iPhone). So let's try to build a MacRuby app with it. As before, we assume that you have already installed Xcode 3.1 and MacRuby 0.4.

  1. Create a new MacRuby project in Xcode called "MacRubyTwitterMG".
  2. Checkout MGTwitterEngine:
    svn co http://svn.cocoasourcecode.com/MGTwitterEngine/
  3. Open MGTwitterEngine.xcodeproj in Xcode.
  4. Open the group Classes > MATwitterEngine in the source list. Then drag and drop all files beginning with MGTwitter as well as the complete groups Twitter NSXML Parsers and Categories to your MacRuby project (I created a group Vendor > MGTwitterEngine for it).
  5. Next we add a new Ruby class to the project. Right-click Classes and select New File.... Choose the template Ruby class, click Next, enter MainController.rb as the File Name and hit Finish.
  6. Add four attributes for username, password and status as well as the table view, in which we will show the tweets. Also add two methods for refreshing the timeline and sending updates.
    class MainController
      attr_accessor :username, :password, :status
      attr_writer :timelineTableView
    
      def refresh(sender)
      end
    
      def sendUpdate(sender)
      end
    end
    
  7. Open the file MainMenu.nib in Interface Builder. Put two text fields, one push button, one table view and a multi-line text field to the main window. The layout doesn't matter, mine looks like this:
  8. Drag and drop an instance of NSObject (the blue cube) from the IB library to your MainMenu.nib. Set its class to MainController.
  9. Ctrl-click the button, drag to Main Controller and connect it with the method refresh.
  10. Ctrl-click the Main Controller, drag to the table view and connect it with the attribute timelineTableView. The table view should have only one column (see above screenshot).
  11. Ctrl-click the multi-line text field, drag to Main Controller and connect it with the method sendUpdate.
  12. Select the multi-line text field, open the Attributes inspector (Cmd+1) and choose Sent On Enter Only for the attribute Action.
  13. Select the first text field, open the Bindings tab in the inspector (Cmd+4), activate Bind to and select Main Controller. Enter self.username in the Model Key Path field.
  14. Repeat the previous step for the second text field and set the binding to self.password.
  15. Repeat the previous step for the multi-line text field and set the binding to self.status.
  16. Save the nib file. If you get the following warning, click Review Issues, then select Mac OS X 10.5.x as the Deployment Target. This should "solve" the issues and you can save the file.

  17. Back in MainController.rb, we add the following method:
     def awakeFromNib
        @timeline = []
        @timelineTableView.dataSource = self
        @twitterEngine = MGTwitterEngine.alloc.initWithDelegate(self)
      end
    

    The method initializes an empty array for our timeline. It also sets the attribute datasource of the table view to self and initializes the MGTwitterEngine with self as the delegate.
  18. For the table view we need two simple methods:
     def numberOfRowsInTableView(view)
        @timeline.size
      end
    
      def tableView(view, objectValueForTableColumn:column, row:index) 
        @timeline[index]
      end
    

    The first method returns the number of rows in the table view, which we can get from the number of entries in our array. The second method returns the corresponding entry for each row.
  19. Next, we implement the method refresh:
      def refresh(sender)
        @twitterEngine.setUsername(username, password:password)
        @twitterEngine.getFollowedTimelineSinceID(0, startingAtPage:0, count:20)
      end
    

    We set username and password, then ask the engine to get our current timeline.
  20. MGTwitterEngine has five delegate methods (see below), but for now we're only interested in the tweets so we go with an implementation for statusesReceived:
     def statusesReceived(statuses, forRequest:identifier)
        @timeline = []
        statuses.each do |status|
          @timeline << status["user"]["name"] + ": " + status["text"]
        end
        @timelineTableView.reloadData
      end
    

    The parameter statuses is an array with a hash for each tweet. We iterate over it, take the user's name and the status text, concat it to a string and append it to our timeline array. Then we reload the data in the table view.
  21. The last thing is to send an update to Twitter. When the user hits enter in the multi-line text field, the method sendUpdate is invoked. We implement it as follows:
     def sendUpdate(sender)
        @twitterEngine.sendUpdate(sender.stringValue)
        sender.setTitleWithMnemonic("")
      end
    

    The parameter sender points to our text field, so we send its stringValue, i.e. the entered text, to Twitter and clear the text.
  22. In addition we need a small change to statusesReceived:
     def statusesReceived(statuses, forRequest:identifier)
        return if statuses.count == 1 and statuses.first["source_api_request_type"] == 5
    
        @timeline = [] 
        # ...
      end
    

    The reason for this is that the MGTwitterEngine calls this method after it has sent the status update. In this case, the parameter statuses contains only the new tweet, so that the initialization of @timeline in the next line would unintentionally clear the whole timeline. We therefore ignore this update and wait for the next click on Refresh to get the newest timeline.
  23. Finally, build the app and run it. Enter your credentials, click on Refresh and wait for your tweets to appear.

37 lines of code plus some magic in Interface Builder and we're done. Pretty simple, huh?

Aftermath

As said above, MGTwitterEngine provides five delegate methods in total:

def requestSucceeded(requestIdentifier)
end

def requestFailed(requestIdentifier, withError:error)
end

def statusesReceived(statuses, forRequest:identifier)
end

def directMessagesReceived(messages, forRequest:identifier)
end

def userInfoReceived(userInfo, forRequest:identifier)
end
For a real application, we would use at least the first three methods to do sufficient error handling, e.g. display an alert panel if the user has entered a wrong password. Furthermore we could also use these methods to show some status information, e.g. as a status label or an activity indicator like this:
  def refresh(sender)
    @indicator.startAnimation(sender)
    # Start engine
    # ...
  end

  def requestSucceeded(requestIdentifier)
    @indicator.stopAnimation(self)
  end

  def requestFailed(requestIdentifier, withError:error)
    @indicator.stopAnimation(self)
    # Show some error message
    # ...
  end

Git Repository

The complete project is available at GitHub. Feel free to fork. :)

Update 1 (29/May/2009)

If you encounter the problem that the password is not accepted although you've entered it correctly, try the following fix:

  1. Open MainMenu.nib in Interface Builder.
  2. Select the password field and open the Bindings inspector (Cmd + 4).
  3. Open Value and activate the option Continuously Updates Value.
  4. Optionally repeat the previous steps for the login field.
  5. Save. Rebuild.

Keywords: mac, programming, ruby

Added by Thomas Dohmke 432 days ago


Comments

Added by Geoffrey Grosenbach 431 days ago

Thanks for mentioning PeepCode!

I also considered using MGTwitterEngine, but the point of the screencast was to teach developers how MacRuby works. Doing our own posting and XML parsing was a great way to teach about delegates and how to use NSXMLDocument and other core classes. So the "simplest Twitter app" referred to the feature set, not the implementation.

But if I was going to build a production app, I would definitely use an existing library like MGTwitterEngine! As you've shown, it's a breeze to implement. Other Cocoa frameworks like BWToolkit can also be used easily with MacRuby.

Added by Francois Cocquemas 428 days ago

As a novice at MacRuby (and Cocoa, really), I followed your tutorial without trouble and it works great, so a million thanks!

I noticed two occurrences of MGTemplateEngine that you might want to replace by MGTwitterEngine. Also, a minor typo in point 8: "it's" instead of the possessive "its" (yeah, yeah, grammar freak).

Other than that, it worked perfectly as expected! Keep up the good work.

Added by Thomas Dohmke 428 days ago

@Francois: Thanks a lot for your comment. I've fixed both the misplaced MGTemplateEngine (which actually also exists, see MGTemplateEngine) and the grammar mistake.

Add a comment

Twitter

Follow us on Twitter:

Keywords