Oct 26 2009

Poking Objective-C with a Testing Stick

4012929364_54af446306_bI’ve wandered back into Objective-C coding land recently. After spending so much time doing Ruby work I’ve gotten used to writing unit tests and using mock objects. To that end, I spent a bit of time figuring out OCUnit (built into XCode) and OCMock. I figured I’d write some of this down so I don’t forget for the next time I try to set this all up.

Build Results 1We’re going to work with a simple Cocoa Application that connects to an external web resource, in this case, Google Reader authentication. We’ll setup the testing bundle to run when our main application is built and create a mock object so we don’t hit the external resource on every test run. I’m going to be using XCode version 3.2 and create a Cocoa Application called UnitTesting. If you build and run this application you should see an empty window on screen. If you bring up the Build Results window (Build -> Build Results) you’ll see some information about the current build.

Add Unit Test TargetWith our app setup, we’ll start integrating the unit tests. Click on the Targets item in the project folder. Select Add -> New Target. Select Unit Test Bundle and press Next (Note, make sure you’re in the Cocoa and not the Cocoa Touch section when selecting Unit Test Bundle). I named my target Unit Tests and hit Finish.

At this point if you right click on the UnitTesting target and select Build “UnitTesting” everything should work correctly. If you right click on Unit Tests and select Build “Unit Tests” you should receive a build failure.

Add Test GroupLet’s add some tests. The first step is to create a new group to hold our test files. Right click on the UnitTesting item in the project draw and select Add -> New Group. Name the new group Tests. Now, right click on the Tests group and select Add -> New File…. You’ll want to add an Objective-C test case class (Note, make sure you’re in the Cocoa Class section of the new dialog and not the Cocoa Touch Class section or you’ll get an iPhone test class). Name the test GReaderTest.m and de-select the Also create “GReaderTest.h” option. The reason for this is that the headers are usually empty so there is no point in creating the extra file. You’ll also want to make sure you have the Unit Tests target selected.Add Test Case

When working with OCUnit you need to name each of your test classes SomethingTest. The trailing Test is required. In a similar vein, each of the tests themselves needs to start with test. So, something like testAuthentication.

Since we didn’t create a header file we’ll need to setup the interface for the test in the .m file.

#import <SenTestingKit/SenTestingKit.h>

@interface GReaderTest : SenTestCase
@end

@implementation GReaderTest
@end

Bundle Build FailureYou should be able to build the Unit Tests target now and have the build succeed. If the build doesn’t succeed, and you see a message about UIKit then you selected the Cocoa Touch unit test bundle instead of the Cocoa unit test bundle.

Adding dependenciesWith the unit tests building we can add them into our main build as a dependency. Right click on the UnitTesting target in the project drawer and select Get Info. In the General section add a new Direct Dependency for the Unit Tests target.

Now, when you press apple-B to build the project you should see your unit tests executed before the main build phase.

Build Results 2Ok, with everything setup we can start testing. First step, each test in this set will be using our GReader object. So, we’ll add setUp and tearDown methods that will be executed before and after each test, respectively.

#import <SenTestingKit/SenTestingKit.h>
#import "GReader.h"

@interface GReaderTest : SenTestCase {
    GReader *gr;
}
@end

@implementation GReaderTest
- (void)setUp {
    gr = [[GReader alloc] init];
}

- (void)tearDown {
    [gr release];
}
@end

This, of course, won’t execute as we haven’t created our GReader object yet. Let’s do that now.

Right click on the Classes group and select Add -> New File…. Add a new Objective-C class which is a subclass of NSObject. Call this new class GReader. The new class should be attached to both our main target and the Unit Tests target.

Now, for our little app, the first thing we’ll need to do is authenticate with Google Reader. There is a really good document on the Reader API from the pyrfeed project. We’ll need to post some specific data to a given end point and parse the response.

For our unit tests, we don’t actually want to hit the Google endpoint. There is too much time involved and we want our unit tests to be fast. So, we’ll need to do some mocking in order to verify the call is happening, but not actually make the call itself.

For this we’ll use OCMock. I’m going to add the test first and then we’ll add the OCMock.framework into the project. First, we need to import OCMock. This is done by adding #import <OCMock.h> to the beginning of the GReaderTest.m file.

The test is defined as follows.

- (void)testAuthentication {
    id mock = [OCMockObject partialMockForObject:gr];
    [[[mock stub] andCall:@selector(fakeAuthenticationPost:)
                 onObject:self] post:[OCMArg any]];
   
    [gr authenticateWithUsername:@"dan" password:@"password"];
}

- (NSString *)fakeAuthenticationPost:(NSString *)request {
    NSArray *d = [request componentsSeparatedByString:@"&"];
   
    STAssertTrue([d containsObject:@"Email=dan"], @"Username not set correclty into request");
    STAssertTrue([d containsObject:@"Passwd=password"], @"Password not set correctly in request");
   
    return @"SID=mysid\nLSID=mylsid\nAuth=myauth";
}

As you can probably tell, we’ve added two methods. There is only one test, testAuthentication but we needed a helper function to deal with the mock post call. Let’s take a look at what’s going on in these methods. We know that the GReader object will be making a call that we want to mock. So, we need to create a partial mock object that sits around our GReader object. This is done as: id mock = [OCMockObject partialMockForObject:gr];. With the mock created, we can stub out specific methods of the GReader object, this is called Method Swizzling in Objective-C land.

In order to swizzle the method we have [[[mock stub] andCall:@selector(fakeAuthenticationPost:) onObject:self] post:[OCMArg any]];. So, we create a new stub on our mock object. We then tell the stub to call the fakeAuthenticationPost defined in the current object whenever the post method is called on the GReader object. Our post method takes one parameter so we specify [OCMArg any] to allow any argument through.

Finally, we call the authenticateWithUsername:password: on the GReader object to kick off the authentication.

The second method we defined, fakeAuthenticationPost, will be called instead of the post method in the GReader object. To that end, it will receive the same parameter, the NSString that is pass to post. To be on the safe side, I’m verifying that we’re properly passing the required email and password fields in the string. I then fake some return data that is similar to a successful response from Google.

If we try to build the project at this point we’re going to get a lot of errors. First, since we haven’t added the OCMock framework and second, we haven’t created the authenticate or post methods for our GReader object.

First things first, let’s get the OCMock framework setup. To do that, you’ll need to download OCMock. You can grab the .dmg file off the OCMock pages. When you extract the archive you’ll see the OCMock.framework and the source directories. In order to keep everything in Git, I created a Framework directory in my UnitTesting directory. I then copied the OCMock.framework directory into this new Framework directory.

You could also install OCMock in the /Library/Frameworks directory but I like having it in Git as not all the developers may have OCMock installed.

Back in XCode, we need to add the framework to our project. Right click on the Frameworks group in the project directory. Select Add -> New Group. Name the new group Testing Frameworks. Note, this isn’t required, I just like the separation it provides. Finally, right click on Testing Frameworks and select Add -> Existing Frameworks... then press the Add Other… button. Navigate to the OCMock.framework directory you just copied into Frameworks directory and press Add. Make sure the new framework is hooked up to the Unit Tests target. Right click on the framework and hit Get Info in the general tab you can verify the targets.

Building the project at this point will get some warnings about missing functions and a crash executing the unit tests. In order to fix the crash we need to setup the build to copy the OCMock framework into our build directory. I’m not entirely sure why this is needed, something about one of the paths set in the framework, but it does get things working.

Copy Build PhaseRight click on the Unit Tests target and select Add -> New Build Phase -> New Copy Files Build Phase.

You need to set the Destination to Absolute Path and the Full Path to $(BUILT_PRODUCTS_DIR). Once the Copy Files phase is created drag the OCMock.framework from the Test Frameworks group into the copy phase. The copy phase needs to be placed between Compile Sources and Link Binary With Library phases.

Built Products dirAt this point, we should be able to execute our build again and we’ll get a failure [NSProxy doesNotRecognizeSelector:post]. This is good. This means our mock is setup correctly and is trying to hook into our non-existant post method.

Let’s go create a couple methods so things compile correctly. First we update the GReader.h file as follows.

@interface GReader : NSObject
- (void)authenticateWithUsername:(NSString *)username password:(NSString *)password;
- (NSString *)post:(NSString *)request;
@end

And the implementation.

#import "GReader.h"
@implementation GReader
- (void)authenticateWithUsername:(NSString *)username password:(NSString *)password {
    [self post:[NSString stringWithFormat:@"Email=%@&Passwd=%@", username, password]];
}

- (NSString *)post:(NSString *)request {
    return @"My post data";
}
@end

With that in place our build should succeed. You can make sure that things are working correctly by modifying [gr authenticateWithUsername:@"dan" password:@"password"]; to something similar to [gr authenticateWithUsername:@"stan" password:@"password"]; and you should see a build failure.

That’s it. We have the build system setup and the mock objects hooked up. We can now continue down our TDD path. You can see an example project that I’ve uploaded to GitHub.

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

Jul 21 2009

Toolbars with HotCocoa

3407707273_c05017779fI had the opportunity to add a toolbar to my Silver Lining app the other day and though I’d write down how I did it. This would, probably, have been easier if I had an understanding of the Cocoa Toolbar.

The main thing that kept tripping me up is that you don’t add toolbar items directly to the toolbar. Each item you want to add to the toolbar is given an identifier (HotCocoa will generate one from the label if needed). Then, when you create your toolbar, you specify which toolbar items are in the toolbar by default and which items are available to be put in the bar.

Once the toolbar is created we don’t just append it to the window with the normal << operator. We need to use toolbar= to assign the toolbar to the window.

silverlining_toolbarThe toolbar we’re going to create is fairly simple. We want a button, with image, on the left for reloading and a search field on the right. We’ll put a flexible spacer in the middle to put them on opposite sides of the screen. (Note, there is currently a bug in HotCocoa where our specified items will be to the right and the spacer to the left. Things may not look correct until fixed.)

reload_item = toolbar_item(:label => "Reload",
                           :image => image(:named => "reload"))
reload_item.on_action { reload_instances }

The first toolbar item we create is the reload button. We set the image on the button to a file in the resources folder called reload.png. When the button is clicked, we want to execute the reload_instances method.

search_item = toolbar_item(:identifier => "Search") do |si|
  search = search_field(:frame => [0, 0, 250, 30],
                        :layout => {:align => :right, :start => false})
  search.on_action { |sender| filter_instances(search) }

  si.view = search
end

The second item is the search box. You’ll notice we’re setting the :identifier instead of the :label as we did for the reload button. The difference being, the label will appear below the button and we don’t want Search appearing below the search box. The reload button will have an identifier of Reload created for it by HotCocoa.

With the search toolbar item in hand we create a search_field. The search field is then assigned as the view in the search toolbar item using si.view=.

@toolbar = toolbar(:default => [reload_item, :flexible_space, search_item]) do |tb|
  win.toolbar = tb
end

Finally, we create our toolbar. This is done with the toolbar method. We want our default set of items to be the reload_item, :flexible_space and search_item. We then assign the toolbar to our window with win.toolbar = tb.

There are a few default toolbar items you can use, similar to :flexible_space. They are:

  • :separator
  • :space
  • :flexible_space
  • :show_colors
  • :show_fonts
  • :customize
  • :print

That’s all folks. Have fun.

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

Jun 11 2009

MacRuby and NSError

Categories: Computers, Programming
Tags: ,

Just a note for other people that might be cracking their heads trying to meld MacRuby and NSError ** parameters. The answer was already sitting on my computer, I just never through to look at the MacRuby Core Data XCode templates, but that’s beside the point. For those of you searching, you just need to do the following.

      error = Pointer.new_with_type(‘@’)
      unless managed_object_context.save(error)
        NSLog error[0].inspect
        NSApplication.sharedApplication.presentError(error[0])
      end
 

The NSLog error[0].inspect will output #<NSError:0x80057a640>. Exactly what we want.

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

May 25 2009

Heating up with HotCocoa Part III

img_5183In the words of Homer Simpsons, “forward not backwards, upwards not forwards and always twirling, twirling, twirling towards freedom”. With that, we’re back for part III of my HotCocoa tutorial. For those of you just joining the party, you’ll probably want to take a look at part I and part II.

If you’d like to grab a copy of the code, it’s getting a bit big to post all of it in one go, you can grab the tar ball here. The file contains all of the HotCocoa files along with the sprite image that I’ve shamelessly niced from PostRank.

picture-12When we last left off we’d created the basic layout for our application with our button and table views setup. With this installment, we’re going to go a step further and get a fully working application. We’re going to use the feed entered into the text field to query PostRank to get the current posts in the feed along with there PostRank and metric information. I’m going to be skipping over sections of code that haven’t changed from part I or part II for the sake of brevity.

Note, we’re going to be using JSON when working with the PostRank APIs. There is a bug in MacRuby, as of revision 1594, where JSON.parse would crash MacRuby. You’ll need to apply the patch attached to ticket 257 in order run this application.

OK, let’s go.

POSTRANK_URL_BASE = "http://api.postrank.com/v2"
APPKEY = "appkey=Postie"

All of our calls to PostRank will use the same URL prefix and we’ll need to provide our appkey. I’ve placed both of these into constants.

vert << scroll_view(:layout => {:expand => [:width, :height]}) do |scroll|
  scroll.setAutohidesScrollers(true)

  pr_column = column(:id => :postrank, :title => )
  pr_column.setDataCell(PostRankCell.new)
  pr_column.setMaxWidth(34)
  pr_column.setMinWidth(34)
           
  info_column = column(:id => :data, :title => )
  info_column.setDataCell(PostCell.new)
           
  scroll << @table = table_view(:columns => [pr_column, info_column],
                                :data => []) do |table|
    table.setRowHeight(PostCell::ROW_HEIGHT)
    table.setUsesAlternatingRowBackgroundColors(true)
    table.setGridStyleMask(NSTableViewSolidHorizontalGridLineMask)                            
    table.setDelegate(self)
    table.setDoubleAction(:table_clicked)
  end
end

I’ve made one layout modification which was to add an extra column to our table to display the PostRank for each post. The PostRank column and post data columns use custom cell formatters so we can get the layout we want. I also wanted to constrain the PostRank column to a set size, 34 pixels seemed to look good. In order to use my custom formatters I use setDataCell on the column objects. The parameter to setDataCell is an instantiated instance of our formatter class. I have two classes, PostRankCell and PostCell for the PostRank and post columns respectively.

Along with the column changes we’re also setting a default height on the table rows as defined in the PostCell class. We set the Postie instance as the delegate for the table so we can receive the tableView(table, heightOfRow:row) callback (thanks @macruby for the pointer). The last addition to the table is to hookup the double click action with @table.setDoubleAction(:table_clicked). The parameter is the name of the method that will be called, as a symbol.

def table_clicked
  url = NSURL.URLWithString(@table.dataSource.data[@table.clickedRow][:data][:link])
  NSWorkspace.sharedWorkspace.openURL(url)
end

When a table row is double clicked we want to open the corresponding posts page in the users brower. We’ll be storing the link in the data attached to our table. We can get the clicked row with @table.clickedRow and access the link with @table.dataSource.data[@table.clickedRow][:data][:link]. We then create a NSURL with this string. The created URL object is passed to NSWorkspace.sharedWorkspace.openURL(url) causing the page to open in the browser.

def tableView(table, heightOfRow:row)
  metrics = @table.dataSource.data[row][:data][:metrics].keys.length
   
  num_rows = (metrics / PostCell::NUM_METRICS_PER_ROW) + 1
  num_rows -= 1 if metrics > 0 && (metrics % PostCell::NUM_METRICS_PER_ROW) == 0
  num_rows = 0 if metrics == 0

  num_rows * PostCell::ROW_HEIGHT + PostCell::ROW_HEIGHT  # 2nd row height for the title
end

picture-2The tableView(table, heightOfRow:row) callback is triggered each time the table is rendered out to determine the height for a given row. In the case of Postie we’re going to display the post title on the first line and the metrics on subsequent lines. I’ve constrained the metrics to allow a maximum of 6 metrics on each line. All of the metrics are stored in a :metrics key of the data attached to our table. Both the number of metrics and a row and the row height are constants stored in the PostCell class.

def load_feed
  @table.data = []

  str = @feed_field.stringValue
  unless str.nil? || str =~ /^s*$/
    fetch_feed(str)
  end
end

load_feed has been updated to empty our tables data by assigning a new array and, assuming we’ve received a valid feed, call fetch_feed to start retrieving the feed data.

There are a few different ways we could go about querying the data from the PostRank APIs. We could use Net::HTTP, Curb, NSXMLDocument or, as I’ve done, NSURLConnection. The reason I used NSURLConnection is so that I can have the requests run asynchronously. As well, the UI won’t block as we’re off fetching the data. A handy feature when you want things to remain responsive.

Let’s take a quick look at the wrapper class I’ve put around NSURLConnection before looking at fetch_feed. The reason I created a wrapper is that NSURLConnection, because it’s asynchronous, works through callbacks. I’m going to need to query three different PostRank APIs and take different actions for each query. Instead of trying to do some magic in the callbacks, I’ve created a wrapper class that accepts a block. The block is called when the data has been successfully retrieved. (The wrapper just spits out an error if something goes wrong, the block is never called.) For example, to download the Google.ca homepage we could do:

DataRequest.new("http://google.ca") do |data|
  NSLog "Data: #{data}"
end
class DataRequest
  def get(url, &blk)
    @buf = NSMutableData.new
    @blk = blk
    req = NSURLRequest.requestWithURL(NSURL.URLWithString(url))
    NSURLConnection.alloc.initWithRequest(req, delegate:self)
  end
 
  def connection(conn, didReceiveResponse:resp)
    @buf.setLength(0)
  end
 
  def connection(conn, didReceiveData:data)
    @buf.appendData(data)
  end

  def connection(conn, didFailWithError:err)
    NSLog "Request failed"
  end
 
  def connectionDidFinishLoading(conn)
    @blk.call(NSString.alloc.initWithData @buf, encoding:NSUTF8StringEncoding)
  end
end

As NSURLConnection executes it returns data to the application. We’re storing this data in a NSMutableData object called @buf. The callbacks we’re interested in are:

connection(conn, didReceiveResponse:resp)
Called when we receive a response from the server. This can be called multiple times if there are any server redirects in place. We reset the length of our data buffer each time this callback is called.
connection(conn, didReceiveData:data)
Called each time data is received from the server. This can be called multiple times and we just append the data to our buffer each time.
connection(conn, didFailWithError:err)
Called if there is an error retrieving the data. We, basically, just ignore the error. You’d probably want to do something sane in your application.
connectionDidFinishLoading(conn)
Called when all of the data has been retrieved from the remote server. Since we’re not working with binary data I convert the NSMutableData to an NSString using the initWithData:encoding method. Note the use of alloc on the NSString. If you try to use new you’ll, like me, spend the next 30 minutes trying to figure out why your application is crashing.
def fetch_feed(url)
  DataRequest.new.get("#{POSTRANK_URL_BASE}/feed/info?id=#{url}&#{APPKEY}") do |data|
    feed_info = JSON.parse(data)
    unless feed_info.has_key?(‘error’)
      DataRequest.new.get("#{POSTRANK_URL_BASE}/feed/#{feed_info['id']}?#{APPKEY}") do |data|
        feed = JSON.parse(data)
        feed[‘items’].each do |item|
          post_data = {:title => item[‘title’], :link => item[‘original_link’], :metrics => {}}
          @table.dataSource.data << {:data => post_data,
                                     :postrank => {:value => item[‘postrank’],
                                                   :color => item[‘postrank_color’]}}
          DataRequest.new.get("#{POSTRANK_URL_BASE}/entry/#{item['id']}/metrics?#{APPKEY}") do |data|
            metrics = JSON.parse(data)
            metrics[item[‘id’]].each_pair do |key, value|
              next if key == ‘friendfeed_comm’ || key == ‘friendfeed_like’
              post_data[:metrics][key.to_sym] = value
            end
            @table.reloadData
          end
        end
      end
    end
  end
end

Most of the code in fetch_feed should probably be refactored into Feed, Post and Metrics classes, but, for the tutorial, I’m not going to bother.

You can see we’re doing three successive data requests. The first is to the Feed Info API. From this call we can retrieve the feed_hash which allows us to uniquely identify our feed in the PostRank system. By default all the PostRank API calls will return the data in JSON format. We could, and I did this initially use format=xml and NSXMLDocument.initWithContentsOfURL to pullback and parse all the data (the problem being, metrics only responds in JSON).

Now, as long as the query to Feed Info didn’t return an error we use the id to access the Feed API. The Feed API will return the posts in the given feed. The default is to return 10 posts which works for our purposes. We could, if we wished, add a button to retrieve the next set of posts from the API using the start and num parameters.

With the feed in hand we’re interested in the items attribute. This is an array of the posts in the feed. Using these items we can start to create our table data. For each item we’re going to create two hashes of data, one for each column of our table. The PostRank column will contain the :postrank and :postrank_color and the post column will contain the :title, :link and :metrics.

Finally, we query the metrics API for each post to retrieve the metrics data. The metrics API will provide us with a hash with a single key based on our post’s ID. Under this key we receive a hash containing the metric source names and the values. We’re skipping friendfeed_comm and friendfeed_like as they’ve been renamed to ff_comments and ff_links and only remain as legacy.

Once we’ve got all the metrics source information packed into our post_data hash we call @table.reloadData so everything gets rendered properly.

Since the calls to DataRequest are asynchronous, we have to call reload inside the metrics block. This guarantees the table will be reloaded after we’ve received our data.

With that out of the way, we’re onto our formatting cells. In order to get our custom table display we need to subclass NSCell and override the drawInteriorWithFrame(frame, inView:view) where we can layout our cell as desired.

class PostRankCell < NSCell
  def drawInteriorWithFrame(frame, inView:view)
    m = objectValue[:color].match(/#(..)(..)(..)/)
    NSColor.colorWithCalibratedRed(m[1].hex/ 255.0, green:m[2].hex/255.0, blue:m[3].hex/255.0, alpha:100).set
    NSRectFill(frame)
 
    rank_frame = NSMakeRect(frame.origin.x + (frame.size.width / 2)12,
                            frame.origin.y + (frame.size.height / 2)8, frame.size.width, 17)
 
    objectValue[:value].to_s.drawInRect(rank_frame, withAttributes:nil)
  end
end

The PostRankCell is pretty simple. We parse the provided PostRank colour, which comes as #ffffff into separate red, green and blue values. These values are passed to NSColor.colorWithCalibratedRed(red, green:green, blue:blue, alpha:alpha) in order to create a NSColor object representing our PostRank colour. We need to divided each value by 255 as colorWithCalibratedRed:green:blue:alpha: expects a value between 0.0 and 1.0. Once we’ve got our colour we call set to make that colour active and, using NSRectFill we fill then entire frame with the provided postrank_color.

I’m, kinda, sorta, centering the PostRank values in the column so we need to create a NSRect to specify the box where we want to draw the numbers. This is done by calling NSMakeRect and providing the x, y, width and height values for the rectange. Once we’ve got our NSRect in hand we call drawInRect(rank_frame, withAttributes:nil) on the PostRank value. This will draw the string in the rectangle specified. We could set extra attributes on the string but, I don’t need any, so I just leave it nil.

You’ll notice I’m using objectValue in a few places. objectValue is a NSCell method that will return the value assigned to this cell as retrieved based on the column key from our table data source.

class PostCell < NSCell
  ROW_HEIGHT = 20
  NUM_METRICS_PER_ROW = 6
  SPRITE_SIZE = 16
 
  @@sprites = {:default => 0, :blogines => 16, :reddit => 32, :reddit_votes => 32,
      :technorati => 48, :magnolia => 64, :digg => 80, :twitter => 96, :comments => 112,
      :icerocket => 128, :delicious => 144, :google => 160, :pownce => 176, :views => 192,
      :bookmarks => 208, :clicks => 224, :jaiku => 240, :digg_comments => 256,
      :diigo => 272, :feecle => 288, :brightkite => 304, :furl => 320, :twitarmy => 336,
      :identica => 352, :ff_likes => 368, :blip => 384, :tumblr => 400,
      :reddit_comments => 416, :ff_comments => 432}
  @@sprite = nil

  def drawInteriorWithFrame(frame, inView:view)
    unless @@sprite
      bundle = NSBundle.mainBundle
      @@sprite = NSImage.alloc.initWithContentsOfFile(bundle.pathForResource("sprites", ofType:"png"))
      @@sprite.setFlipped(true)
    end

    title_rect = NSMakeRect(frame.origin.x, frame.origin.y + 1, frame.size.width, 17)
    metrics_rect = NSMakeRect(frame.origin.x, frame.origin.y + ROW_HEIGHT, frame.size.width, 17)

    title_str = "#{objectValue[:title]}"
    title_str.drawInRect(title_rect, withAttributes:nil)

    count = 0
    orig_x_orign = metrics_rect.origin.x
   
    objectValue[:metrics].each_pair do |key, value|
      s = metrics_rect.size.width
      metrics_rect.size.width = SPRITE_SIZE
     
      y = if @@sprites.has_key?(key)
        @@sprites[key.to_sym]
      else
        0
      end
      r = NSMakeRect(0, y, SPRITE_SIZE, SPRITE_SIZE)
      @@sprite.drawInRect(metrics_rect, fromRect:r,
                          operation:NSCompositeSourceOver, fraction:1.0)
      metrics_rect.origin.x += 21
      metrics_rect.size.width = s - 21
       
      "#{value}".drawInRect(metrics_rect, withAttributes:nil)
      s = "#{value}".sizeWithAttributes(nil)
      metrics_rect.origin.x += s.width + 15
     
      count += 1
      if count == NUM_METRICS_PER_ROW
        metrics_rect.origin.y += ROW_HEIGHT
        metrics_rect.origin.x = orig_x_orign
        count = 0
      end
    end
  end
end

PostRankCell is similar to PostCell in that we're basically creating bounding rectangles and drawing into them. The extra little bit we're doing here is loading up a NSImage which is our sprite set and using that to pull out all of the individual service icons. NSImage makes it easy to work with our sprite image by providing drawInRect(rect, fromRect:from_rect, operation:op, fraction:val). drawInRect:fromRect:operation:fraction: draws into the rectangle defined by rect retrieving the pixels in your NSImage that are inside the from_rect. I'm using NSCompositeSourceOver because some of my images are semi-transparent. The fraction parameter is a the alpha setting for the image.

With that, well, you'll probably need to download the source code to see it all in one file, you should have a working application that will query PostRank for a feed and display the posts and metrics for the feed.

As for the next installment. I've got a few things I still want to do, including: tabbing between widgets, submitting the text field on return, a progress indicator as the feed information is being retrieved and adding a tabbed interface to allow showing feed, post and top post information. I'm not sure which of these things I'll tackle next. There are a few helper methods I want to try to add to HotCocoa from this article that I'll probably do first. So, until next time.

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

May 23 2009

Heating up with HotCocoa Part II

img_4292Welcome back. In part I we created a basic HotCocoa application and took a look at the created files and some of their workings. In part II we’re going get our UI laid out and hooked up.

We’ll be creating a text field, button, scroll view and a table. When the button is pressed, for now, the content of the text field will be appended to the table view.

A good thing to keep in mind when hacking around with MacRuby and HotCocoa is that the methods available in the OS X frameworks you’re using are available in Ruby. This means, if you don’t know how to do something, the Apple documentation is awesome at trying to figure out the right methods use.

HotCocoa doesn’t have wrappers for all of the methods we need so, sometimes, we’ll drop down and call the Cocoa methods directly. Hopefully as HotCocoa matures more of these methods will have wrappers in Ruby land. I’ve created few patches, here, here, here and here for some of the wrappings I thought would be handy as I worked on this portion of our tale.

I’m going to start off with the complete listing of the code. Don’t worry if this doesn’t make any sense at the moment as we’re going to go through all of the relevant portions. Some of this should look familiar from the basic HotCocoa generated code seen in part I.

require ‘hotcocoa’

class Postie
  include HotCocoa

  def start
    application(:name => "Postie") do |app|
      app.delegate = self
      window(:size => [640, 480], :center => true, :title => "Postie", :view => :nolayout) do |win|
        win.will_close { exit }

        win.view = layout_view(:layout => {:expand => [:width, :height],
                                           :padding => 0, :margin => 0}) do |vert|
          vert << layout_view(:frame => [0, 0, 0, 40], :mode => :horizontal,
                              :layout => {:padding => 0, :margin => 0,
                                          :start => false, :expand => [:width]}) do |horiz|
            horiz << label(:text => "Feed", :layout => {:align => :center})
            horiz << @feed_field = text_field(:layout => {:expand => [:width]})
            horiz << button(:title => ‘go’, :layout => {:align => :center}) do |b|
              b.on_action { load_feed }
            end
          end

          vert << scroll_view(:layout => {:expand => [:width, :height]}) do |scroll|
            scroll.setAutohidesScrollers(true)
            scroll << @table = table_view(:columns => [column(:id => :data, :title => )],
                                          :data => []) do |table|
               table.setUsesAlternatingRowBackgroundColors(true)
               table.setGridStyleMask(NSTableViewSolidHorizontalGridLineMask)                            
            end
          end
        end
      end
    end
  end

  def load_feed
    str = @feed_field.stringValue
    unless str.nil? || str =~ /^\s*$/
      @table.dataSource.data << {:data => str}
      @table.reloadData
    end
  end
end

Postie.new.start

I’m going to skip the parts that we saw in part I and just mention the changes and new additions in part II.

window(<em>:size => [640, 480], :center => true</em>, :title => "Postie", :view => :nolayout)

Instead of using the :frame => [100, 100, 500, 500] as seen in part I, I prefer to use :size => [640, 480] and :center => true to set the window with a starting size of 640×480 position in the center of the screen.

You’ll also notice the addition of :view => :nolayout tacked on the end of the window method. This isn’t strictly necessary but saves the creation of an object we’re just going to destroy anyway. By default when a window is created a LayoutView will be created and appended to the window. I’m going to be creating my own layout and overriding the created one so I’m just telling the window to skip the creation of the default view.

Before we dig into the next chunk of code lets take a little diversion to look at layout_view. The layout_view is one of the basic building blocks for organizing your application layout. The layout_view method will create a LayoutView object which is a subclass of NSView. Any Cocoa methods available for an NSView can be called on a LayoutView.

When working with layout_view there are a few parameters we’re interested in. The first, similar to window is :frame. As with window the :frame parameter allows us to set the frame position and size for the view. Note, if you don’t set a :frame then, it appears, that the view may have 0 size. This can be changed by the children placed in the view but not always. I spent a while trying to figure out why removing the :frame => [0, 0, 0, 40] from the code above caused my label, text field and button to disappear.

A handy little trick when working with layout_view is to run your application in the $DEBUG mode of Ruby. When $DEBUG is active each layout will have a red border.

To execute your application in $DEBUG you can do:

titania:Postie dj2$ macruby -d lib/application.rb 

Other parameters we’re using for the layout_view calls are :mode, :margin, :spacing and :layout.

:mode
Lets us specify if this view has a :vertical or :horizontal layout. The default layout is :vertical.
:margin
Allows us to specify a margin size for the layout. The provided value is a single integer which will be applied to top, bottom, left and right margins of the view.
:spacing
Allows us to set the spacing for items placed into the view. The value is a single integer.

The last option we’re going to look at is :layout. The layout option isn’t restricted to just layout_view calls and is available on all of the other widgets I’ve created so far.

The :layout hash will be turned into a LayoutOptions object. The available keys are: :start, :expand, :padding, :[left | right | bottom | top]_padding and :align.

:start
Signifies if the view is packed at the start or end of the packing view. I’ll admit, I don’t really know what that means. I’m stealing it from the LayoutOption docs. It appears, in my limited fiddling, that setting it to false causes your subviews to end up at the top of the layout. The default value is true.
:expand
Specifies how the view will grow when the window is resized. The available options are: :height, :width, [:height, :width] and nil. The default setting is nil.
:*padding
Allows you to set the padding around the view. The padding values are specified as a float with a default of 0.0.
:align
Allows us to specify the alignment of the view as long as it isn’t set to :expand in the other direction. The available options are: :left, :center, :right, :top and :bottom.

With that out of the way, back to our code. As you can see by the layout image, our layout isn’t overly complicated. All of the layout is handled by two layout views and a scroll view.

win.view = layout_view(:layout => {:expand => [:width, :height], :padding => 0}) do |vert|

postie_layoutWe start by creating the main window view. If you remember, we created the window with :view => :nolayout so there is currently no view in our window. We assign the new layout_view to the win.view instead of using << to pack it into the view. We remove the padding on the view and set the expand to [:width, :height] so the view will fill the entire window and resize correctly.

Since we didn’t specify a :mode the main window will layout its packed widgets in a vertical fashion.

As each layout view is created you can attach a block to the layout_view call. The block will be called with the LayoutView object that was just created. This makes it easy to pack subviews into the views as they’re created.

vert << layout_view(:frame => [0, 0, 0, 40], :mode => :horizontal,
                    :layout => {:padding => 0, :start => false, :expand => [:width]}) do |horiz|

Next we pack a horizontal view (:mode => :horizontal) to hold the label, text field and button. We set the view to :expand => [:width] so we’ll only get horizontal expansion and maintain the height specified in our :frame parameter. You’ll notice we’re setting a :frame on this layout_view. If we don’t have this parameter it appears that the view will be drawn with 0 height. Effectively making the view invisible.

horiz << label(:text => "Feed", :layout => {:align => :center})
horiz << @feed_field = text_field(:layout => {:expand => [:width]})
horiz << button(:title => ‘go’, :layout => {:align => :center}) do |b|
  b.on_action { load_feed }
end

Into the horizontal layout we pack our label, text_field and button. For both the label and button we’re specifying an :align => :center to line them up with the center of the text field. The only item we’re setting an expand on is the text_field. The other two widgets will maintain their positions and sizes when the window is resized.

When we create the button we attach a block for the on_action callback. This will be triggered when the button is pressed. In our case we’re just calling the load_feed method.

vert << scroll_view(:layout => {:expand => [:width, :height]}) do |scroll|
  scroll.setAutohidesScrollers(true)

With the horizontal view out of the way we create a scroll_view and pack it into our main vertical view. We want the scroll view to :expand => [:width, :height] so it fills the entire window on resize. There is currently no exposed HotCocoa sugar for auto hiding the scrollbars so we drop down and call setAutohidesScrollers to set the scrollbars to auto hide.

scroll << @table = table_view(:columns => [column(:id => :data, :title => )],
                              :data => []) do |table|
  table.setUsesAlternatingRowBackgroundColors(true)
  table.setGridStyleMask(NSTableViewSolidHorizontalGridLineMask)                            
end

The last view we pack is a table_view. We’re again dropping down to Cocoa for the setUsesAlternatingRowBackgroundColors and setGridStyleMask.

Now, the table_view itself. The table view is more complicated then the other widgets we’ve looked at in that it requires column and data source information.

The column information is provided by the :columns => [column(:id => :data, :title => '')] parameter. You provide an array of column objects which define the table columns. The :title will be displayed at the top of a column. If you don’t provide a :title the default is Column. We also providing an :id parameter to our column method. This parameter will be passed when we’re accessing our data source, and, if you’re using the default data source, used to access the specific column information. We’ll dig into columns and their relations to data sources in a moment.

There are two ways to provide data source information. You can either pass an array, which is what we’re doing here or provide your own data source object.

If you decide to define your own data source then it must respond to numberOfRowsInTable(tableView) => integer and tableView(view, objectValueForTableColumn:column, row:i) => string.

If you opt to use the default data source then you provide an array of hashes. The keys into the hash are the :id values set when we created our columns.

picture-11Hopefully an example will make this a bit clearer. Lets say we want to create a table with three columns, name, age and sex. We would define our table as:

@table = table_view(:columns => [column(:id => :name, :title => ‘Name’),
                                 column(:id => :age, :title => ‘Age’),
                                 column(:id => :sex, :title => ‘Sex’)],
                    :data => [{ :name => ‘Betty Sue’, :age => 29, :sex => ‘F’ },
                              { :name => ‘Brandon Oberon’, :age => 0.005, :sex => ‘M’ },
                              { :name => ‘Sally Joe’, :age => 48, :sex => ‘F’}])

With the table view finished all that’s left is to define the load_feed method. For this first iteration we’re just going to take the content of the feed text field and load it into our table view.

def load_feed
  str = @feed_field.stringValue
  unless str.nil? || str =~ /^\s*$/
    @table.dataSource.data << {:data => str}
    @table.reloadData
  end
end

Postie V1We use the Cocoa stringValue method to retrieve the value set into the text field. As long as it isn’t blank we append a new hash (with our :data key as defined in our column setup) into the @table.dataSource.data. We then call @table.reloadData to get the table to re-display its content.

That’s it. You should be able to run the application, enter some text in the text field, hit go and see the text appear in the table view. As you add more table rows, scrollbars should appear and you can scroll in the table as needed.

The third part of this series will be, hopefully, pulling real data back from PostRank and displaying it in a custom cell in our table.

Update: Part III is now available.

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]

Next Page »