3. Automatically track changes with bindings
3.1 Untie Cocoa Bindings with Ruby
Cocoa Bindings were notoriously hard to learn, but it is easy to use them from Ruby. I think that some of Apple’s problems came from the wrong tool to teach about bindings. Perhaps you’ve heard of the ‘Golden Hammer’ anti-pattern. Apple’s is the graphical interface. Apple told everyone to use Interface Builder to configure their controller objects, and the only way to do that was by clicking from screen to screen in Interface Builder. After you’ve done it a while, you might eventually get used to it, but you can easily get lost, especially in tutorials that consist of pages and pages of screen shots.
But at the time, Apple really didn’t have a choice. The alternative would have been to make all the connections by writing code in Objective-C, and that would have required many pages of tedious and verbose code. But Ruby is a lot more expressive than Objective-C.
Here is an example online that created bindings in Ruby. You’ll find it on the examples page. The bindings example is called MailDemo. It’s a Ruby port of a Cocoa bindings tutorial by Scott Stevenson. In it, I’ve designed the user interface in Interface Builder, but instead of hooking up the controllers in IB, I exposed the user interface elements to my Ruby code and made all the connections there. On one page, you can see every connection used by this example.
def make_bindings @controllerAlias = with(ObjC::NSObjectController.alloc.init) {|c| c.set(:content => self) } @mailboxController = with(ObjC::NSArrayController.alloc.init) {|c| c.bind(:attribute => :contentArray, :object => @controllerAlias, :keyPath => "selection.mailboxes") c.set(:objectClass => Mailbox) @addMailboxButton.set(:target => c, :action => "add:") @deleteMailboxButton.set(:target => c, :action => "remove:") @mailboxTable[:title].bind(:attribute => :value, :object => c, :keyPath => "arrangedObjects.properties.title") options = {:NSDisplayPattern => "%{value1}@ Mailboxes"} @mailboxStatusLine.bind(:attribute => :displayPatternValue1, :object => c, :keyPath => "arrangedObjects.@count", :options => options.to_nsdictionary) } @emailController = with(ObjC::NSArrayController.alloc.init) {|c| c.bind(:attribute => :contentArray, :object => @mailboxController, :keyPath => "selection.emails") c.set(:objectClass => Email) @addEmailButton.set(:target => c, :action => "add:") @deleteEmailButton.set(:target => c, :action => "remove:") with(@emailTable) {|t| t[:address].bind(:attribute => :value, :object => c, :keyPath => "arrangedObjects.properties.address") t[:subject].bind(:attribute => :value, :object => c, :keyPath => "arrangedObjects.properties.subject") t[:date].bind(:attribute => :value, :object => c, :keyPath => "arrangedObjects.properties.date") } options = {:NSDisplayPattern => "%{value1}@ Emails"} @emailStatusLine.bind(:attribute => :displayPatternValue1, :object => c, :keyPath => "arrangedObjects.@count", :options => options.to_nsdictionary) @previewPane.bind(:attribute => :data, :object => c, :keyPath => "selection.properties.body") } end
What’s important here isn’t the syntax, it’s that every connection to our models and views is here in one place. There’s no need to tediously click through interface builder screens to check your connections.
But if you still want to make your connections in Interface Builder, you can still use Bindings with Ruby objects. In RubyObjC, you just have to be sure they are derived from ObjC::NSObject. See the online example for details.
Did you find an error? Is something missing? Post your comment or suggestion below!
Comments (1) post
Nice post,
Keep up the good work
Thanks for bringing this up