Mike Ash wrote a post on how to probe Cocoa with PyObjC. Instead of using PyObjC (a Python-Objective-C Bridge, much like RubyCocoa), you can also use MacRuby. This post shows his examples in MacRuby 0.5 beta 2.

MacRuby is not a bridge, but instead runs directly on top of Objective-C, using the same infrastructure. This means if you create a string in MacRuby, and pass it on to an Objective-C method, it doesn’t have to create a proxy object, but can instead use the same object:

>> str = "a string"
=> "a string"
>> str.class
=> NSMutableString

(Strings are mutable by default in Ruby)

Basics

Fire up an interactive MacRuby shell by typing macirb in Terminal.

To load a framework, you just call framework and pass it the Framework you’d like to load:

>> framework 'Foundation'
=> true

(true means it was successfully loaded, false would mean it has already been loaded)

And then you can send call methods on these classes just you would like in Ruby:

>> NSFileManager.defaultManager
=> #<NSFileManager:0x200025120>

(Note that you can omit the parentheses if it is syntactically unambiguous)

As mentioned, MacRuby objects are also Objective-C objects, so no need for proxying anything:

>> NSFileManager.defaultManager.class.ancestors
=> [NSFileManager, NSObject, Kernel]

(NSObject is the base class for everything in MacRuby. In normal Ruby it’d just be Object)

The method call syntax is very similar to Objective-C:

>> NSFileManager.defaultManager.displayNameAtPath('/')
=> "Macintosh HD"
>> NSFileManager.defaultManager.fileAttributesAtPath('/', traverseLink: true)
=> {
      "NSFileOwnerAccountID" => 0,
      "NSFileSystemFileNumber" => 2,
      "NSFileExtensionHidden" => false,
      "NSFileSystemNumber" => 234881026,
      "NSFileSize" => 1360,
      "NSFileGroupOwnerAccountID" => 80,
      "NSFileOwnerAccountName" => "root",
      "NSFileCreationDate" => #<__NSCFDate:0x20042dd20>,
      "NSFilePosixPermissions" => 1021,
      "NSFileType" => "NSFileTypeDirectory",
      "NSFileGroupOwnerAccountName" => "admin",
      "NSFileReferenceCount" => 38,
      "NSFileModificationDate" => #<__NSCFDate:0x20040e140>
    }

(I’ve reformatted the output for better readability)

Errors

Some Cocoa methods expect pointers to pointers as parameters. This can be achieved via the Pointer class:

>> error = Pointer.new(:object)
=> #<Pointer:0x20040e5a0>
>> string = NSString.stringWithContentsOfFile('/', encoding: NSUTF8StringEncoding, error: error)
=> nil
>> error[0]
=> #<NSError:0x2002c45c0>
>> error[0].description
=> "Error Domain=NSCocoaErrorDomain Code=257 UserInfo=0x2002c4600 \"The file “Macintosh HD” couldn’t be opened because you don’t have permission to view it.\" Underlying Error=(Error Domain=NSPOSIXErrorDomain Code=13 \"The operation couldn’t be completed. Permission denied\")"

Arrays and Dictionaries

You can use arrays in exactly the same way as you would in Ruby, but keep in mind that Ruby creates NSMutableArrays by default:

>> error = Pointer.new(:object)
=> #<Pointer:0x200444840>
>> data = NSPropertyListSerialization.dataWithPropertyList(['hello', 'world'], format: NSPropertyListXMLFormat_v1_0, options: 0, error: error)
=> #<NSCFData:0x2004416e0>
>> NSString.alloc.initWithData data, encoding: NSUTF8StringEncoding
=> "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<array>\n\t<string>hello</string>\n\t<string>world</string>\n</array>\n</plist>\n"

Same with hashes (or dictionaries, as they’re called in Objective-C):

>> error = Pointer.new(:object)
=> #<Pointer:0x200444840>
>> data = NSPropertyListSerialization.dataWithPropertyList({ 'key' => 'value', 'key2' => 'value2' }, format: NSPropertyListXMLFormat_v1_0, options: 0, error: error)
=> #<NSCFData:0x20043fe60>
>> NSString.alloc.initWithData data, encoding: NSUTF8StringEncoding
=> "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>key</key>\n\t<string>value</string>\n\t<key>key2</key>\n\t<string>value2</string>\n</dict>\n</plist>\n"

Custom Frameworks

Just load them with framework by providing the full path:

>> framework '/System/Library/Frameworks/WebKit.framework'
=> true
>> WebView.alloc.init
=> #<WebView:0x200459aa0>
>> framework File.expand_path('~/Library/Frameworks/JSON.framework')
=> true

AppKit

AppKit is supported as well:

>> framework 'AppKit'
=> true
>> NSApplicationLoad()
=> true
>> window = NSWindow.alloc.init
=> #<NSWindow:0x2002e6220>

(Note that in this case you have to use the parentheses, otherwise MacRuby won’t know you want to call a method)

And you can use it to easily manipulate images:

>> image = NSImage.alloc.initWithContentsOfFile('penguins.jpg')
=> #<NSImage:0x20040ffa0>
>> scaledImage = NSImage.alloc.initWithSize(NSMakeSize(32, 32))
=> #<NSImage:0x20040f4c0>
>> scaledImage.lockFocus
=> #<NSImage:0x20040f4c0>
>> image.drawInRect(NSMakeRect(0, 0, 32, 32), fromRect: NSZeroRect, operation: NSCompositeCopy, fraction: 1.0)
=> #<NSImage:0x20040ffa0>
>> scaledImage.unlockFocus
=> #<NSImage:0x20040f4c0>
>> scaledImage.TIFFRepresentation.writeToFile('/tmp/penguins_thumb.tiff', atomically: true)
=> true

Conclusion

MacRuby combines the flexibility of Ruby with the power of Cocoa, making it an excellent tool to not only explore the Cocoa APIs and frameworks, but also to create complete OS X applications that run without the performance hit of a bridge. Even better, MacRuby supports Ahead-Of-Time compilation, allowing you to create applications that can be run by anyone on OS X, without the need to install any extra libraries first.

Questions, comments, constructive criticism?
JavaScript Array filter
Fork me on GitHub