MacRuby Tutorial
Getting Started
Welcome to the MacRuby Tutorial. In this tutorial you will learn the basic things you need in order to develop with Mac OS X frameworks using MacRuby.
This tutorial is not about learning Ruby, or Objective-C. There are many resources about that on the web. You don't need to know Objective-C to follow this tutorial though.
MacRuby, as a port of the official Ruby code base, should run your already existing Ruby code. Just take into account that it is based on the 1.9 version which presents some incompatibilities with the current stable version, 1.8.
Please follow the instructions described in InstallingMacRuby in order to install MacRuby on your machine.
If you want to check out sample examples before starting the tutorial, you will find some in the /Developer/Examples/Ruby/MacRuby directory (or you can read them from your browser).
Loading Frameworks
The first thing you have to do in your MacRuby script is to load the framework you want to use.
If you plan to work on a Cocoa application, you will need the Cocoa framework. You can load it using the framework method:
$ /usr/local/bin/macirb --simple-prompt >> framework 'Cocoa' => true
When you load a framework, MacRuby will also automatically load all of its dependencies. For example, both Foundation and AppKit will be loaded for you if you load Cocoa.
A framework can be written in C, Objective-C, or both. The C definitions, such as structures, constants, enumerations, functions, and more, will also be available when you call the framework method.
You can read a complete list of frameworks that ship with Mac OS X here.
Accessing Classes
It is very easy to use an Objective-C class from MacRuby. You just have to refer to it as if it was a Ruby class. For example, to access the NSSound class:
$ /usr/local/bin/macirb --simple-prompt >> framework 'Cocoa' => true >> NSSound.ancestors => [NSSound, Object, NSObject, Kernel]
In MacRuby, all classes, including Ruby core classes, always inherit from NSObject, the root class of mostly all Objective-C classes.
>> Regexp.ancestors => [Regexp, Object, NSObject, Kernel]
This means that every object in MacRuby responds to methods defined in the NSObject class.
For example,
>> s = "foo" => "foo" >> s.respond_to?(:upcase) => true >> s.upcase => "FOO"
Can also be written using the equivalent methods from NSObject.
>> s.respondsToSelector(:upcase) => true >> s.performSelector(:upcase) => "FOO"
To get a list of methods on a MacRuby object just call #methods on it. You will see that objects in MacRuby respond to more methods than standard Ruby.
$ ruby -ve "p ''.methods.size" ruby 1.8.6 (2007-09-24 patchlevel 111) [universal-darwin9.0] 143 $ /usr/local/bin/macruby -ve "p ''.methods.size" MacRuby version 0.1 (ruby 1.9.0 2008-02-18 revision 0) [i686-darwin9.2.0] 564
If you come from RubyCocoa, since all classes in MacRuby automatically inherit from NSObject, it is therefore not necessary to explicitly subclass NSObject.
# RubyCocoa class MyController < OSX::NSObject # ... end # MacRuby class MyController # ... end
Sending Messages
When you call a method on an object in Ruby, the interpreter sends that as a message which includes the name of the method and separately the arguments. Messages in Objective-C are referred as selectors. Selectors are a little bit different than genuine Ruby messages in the sense that method arguments can have a name (or key) which is also part of the selector.
For example, let's imagine a Person Objective-C class, that responds to 3 simple selectors. Assuming that the Person class is properly defined (we will see that later), here is how you would call the selectors from Objective-C:
Person *person = [Person new]; // create an instance of the Person class. [person name]; // send selector 'name'. [person setName:name]; // send selector 'setName:', passing the 'name' variable as an argument. [person setFirstName:first lastName:last]; // send selector 'setFirstName:lastName:', // passing both 'first' and 'last' variables as arguments.
Sending selectors in MacRuby is just as easy as in Objective-C. Here is the same example, but converted to MacRuby:
person = Person.new person.name person.setName(name) person.setFirstName(first, lastName:last)
If you're used to write RubyCocoa code, the first 3 lines might not be new to you, but the last one most probably is. This represents the new MacRuby syntax to define keyed arguments.
You can use both key:value or :key => value to define keyed arguments. For instance, the last line can also be written as:
person.setFirstName first, :lastName => last
When calling a method using this syntax, MacRuby will actually reconstruct the full method name and send it to the object.
A few notes:
- The order of the keys is significant, because it is used to build the selector.
- Multiple arguments can have the same key, as in Objective-C.
- If the selector is not found, a Hash object is built and sent instead, to stay compatible with Ruby code that expects receiving one.
Let's take a more concrete example, creating an NSWindow object. In Objective-C:
NSWindow *window = [[NSWindow alloc] initWithContentRect:frame styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:false];
Here is the MacRuby equivalent:
window = NSWindow.alloc.initWithContentRect frame, styleMask:NSBorderlessWindowMask, backing:NSBackingStoreBuffered, defer:false
To compare, here is the RubyCocoa version, which doesn't support keyed arguments:
window = NSWindow.alloc.initWithContentRect_styleMask_backing_defer( frame, NSBorderlessWindowMask, NSBackingStoreBuffered, false)
To call setter methods on Objective-C objects you normally call a method like setName with the name as the argument. MacRuby provides a facility which allows the use of standard attribute writer methods:
person.name = name # send selector 'setName:', passing the 'name' variable
And the same goes for predicate methods, you can for example call loaded? instead of isLoaded:
framework 'foundation' NSBundle.mainBundle.loaded? # send selector 'isLoaded'
Defining Methods
Now that we know how to send Objective-C messages, let's learn how to create methods from Ruby.
Here is how the Person class that we introduced in the previous section could be implemented with MacRuby:
class Person def name @name end def setName(name) @name = name end def setFirstName(first, lastName:last) @name = "#{first} #{last}" end end
You can see that the setFirstName:lastName: method is defined in the same way we called it.
During your development with Objective-C frameworks, you may have to subclass an existing Objective-C class to override or add new behaviors. Subclassing an Objective-C class in MacRuby is just like subclassing any Ruby class. For example, here is how to create an NSView subclass that performs some custom drawing:
class HelloView < NSView def drawRect(rect) # Set the window background to transparent NSColor.clearColor.set NSRectFill(bounds) # Draw the text in a shade of red and in a large system font attributes = { NSForegroundColorAttributeName => NSColor.redColor, NSFontAttributeName => NSFont.boldSystemFontOfSize(48.0) } str = "Hello, Ruby Baby" str.drawAtPoint(NSPoint.new(0, 0), withAttributes:attributes) end end # ... # Then, later, the custom view can be instantiated and added as the window's content view: window.contentView = HelloView.alloc.initWithFrame(frame)
When you override an Objective-C method and want to call the super implementation, you can just use super as if you were overriding a Ruby method.
class MyObject # Redefine NSObject's initializer. def init # Call the super initializer. if super # ... # You must always return self in an NSObject initializer. self end end end o = MyObject.new # Shortcut to MyObject.alloc.init
Primitive Objects
Thanks to the design of MacRuby, Objective-C objects can be passed to Ruby with no conversion, and conversely Ruby objects can be passed into Objective-C methods without needing to be converted.
Ruby defines its own set of primitive classes, String, Array and Hash, which also exist in Cocoa, being respectively NSString, NSArray and NSDictionary.
In MacRuby, the Ruby primitives classes have been re-modeled on top of their Cocoa equivalents. For example, String does not exist anymore as a class, but as a pointer to NSMutableString. Additionally, the existing String interface has been re-implemented on NSString.
It means that all strings that you create in MacRuby are Cocoa strings, and therefore respond to the NSString interface. There is no conversion or data loss when you pass them to an underlying C or Objective-C API that expects an NSString. And you can call any method of String on an NSString too.
$ macirb >> String => NSMutableString >> "foo".class => NSCFString >> "foo".class.ancestors => [NSCFString, NSMutableString, NSString, Comparable, Object, NSObject, Kernel] >> "foo".upcase # calling String#upcase => "FOO" >> "foo".uppercaseString # calling NSString#uppercaseString => "FOO"
An interesting detail of this behavior is that frameworks that define methods on the Cocoa primitive classes on the fly will also share the same functionality with the Ruby primitive classes as well. If you look closer the previous code snippet, you will see the following:
attributes = { NSForegroundColorAttributeName => NSColor.redColor, NSFontAttributeName => NSFont.boldSystemFontOfSize(48.0) } str = "Hello, Ruby Baby" str.drawAtPoint(NSPoint.new(0, 0), withAttributes:attributes)
This code passes a Ruby Hash object to the [NSString -drawAtPoint:attributes:] method, which expects an NSDictionary. Since Hash implements the NSDictionary API, it works! Notice you also have drawAtPoint:attributes: on NSString, and thus on all Ruby strings.
Since Cocoa types can be either mutable and immutable, if you try to call a method that is supposed to modify its receiver on an immutable object, a runtime exception will be raised. By default, strings, arrays or hashes that you create in MacRuby are mutable.
$ macirb >> 'foo'.capitalize! => "Foo" >> NSMutableString.stringWithString('foo').capitalize! => "Foo" >> NSString.stringWithString('foo').capitalize! RuntimeError: can't modify immutable string from (irb):3:in `capitalize!' from (irb):3 from /usr/local/bin/macirb:12:in `<main>'
Accessing Static APIs
Many Mac OS X framework APIs are not introspectable because they are static, but thanks to the BridgeSupport project, static APIs can be called from MacRuby.
The following API types are available:
- CoreFoundation types (CFType)
- C structures
- C opaque types
- C enumerations
- C and Objective-C constants (including preprocessor-defined constants)
- C functions (including inline functions)
- Objective-C informal protocols
As an example, you can access the NSRect, NSPoint and NSSize C structures in MacRuby as if these were real classes, and call C functions on them:
$ /usr/local/bin/macirb --simple-prompt >> framework 'foundation' => true >> point = NSPoint.new(1, 2) => #<NSPoint x=1.0 y=2.0> >> point.x += 1 => 2.0 >> rect = NSRect.new(point, NSZeroSize) => #<NSRect origin=#<NSPoint x=2.0 y=2.0> size=#<NSSize width=0.0 height=0.0>> >> point.x -= 1 => 1.0 >> NSEqualRects(rect, NSRect.new(point, NSZeroSize)) => false >> point.x += 1 => 2.0 >> NSEqualRects(rect, NSRect.new(point, NSZeroSize)) => true
Mentioning structures, there are different ways of creating them. You can either manually allocate them using #new, use one of the helper functions part of Cocoa, or pass a Ruby array:
>> r = NSRect.new(NSPoint.new(1, 2), NSSize.new(3, 4)) => #<NSRect origin=#<NSPoint x=1.0 y=2.0> size=#<NSSize width=3.0 height=4.0>> >> NSEqualRects(r, NSMakeRect(1, 2, 3, 4)) => true >> NSEqualRects(r, [1, 2, 3, 4]) => true >> NSEqualRects(r, [[1, 2], [3, 4]]) => true
Some Cocoa classes are toll-free bridged with corresponding Core Foundation types (CFTypes). You can safely call Core Foundation functions and pass objects.
>> CFStringGetLength('foo') => 3 >> url = CFURLCreateWithString(nil, "http://apple.com", nil) => #<NSURL:0x1492230> >> url.fileURL? => false >> CFURLHasDirectoryPath(url) => false >> CFEqual(url, NSURL.URLWithString("http://apple.com")) => true
In some occasions you will want to load bridge support files that you personally generated using gen_bridge_metadata(1). To do that, you can use the Kernel#load_bridge_support_file method.
A typical use case is when you want to access enumerated constants from a scriptable dictionary in order to control an application using the Scripting Bridge framework.
$ sdef /Applications/iTunes.app | sdp -fh --basename ITunes $ gen_bridge_metadata -c '-I.' ITunes.h > ITunes.bridgesupport $ grep enum ITunes.bridgesupport | head -n 10 <enum name='ITunesEKndAlbumListing' value='1799449698'/> <enum name='ITunesEKndCdInsert' value='1799570537'/> <enum name='ITunesEKndTrackListing' value='1800696427'/> <enum name='ITunesEPlSFastForwarding' value='1800426310'/> <enum name='ITunesEPlSPaused' value='1800426352'/> <enum name='ITunesEPlSPlaying' value='1800426320'/> <enum name='ITunesEPlSRewinding' value='1800426322'/> <enum name='ITunesEPlSStopped' value='1800426323'/> <enum name='ITunesERptAll' value='1800564801'/> <enum name='ITunesERptOff' value='1800564815'/> $ /usr/local/bin/macirb --simple-prompt >> load_bridge_support_file 'ITunes.bridgesupport' => main >> ITunesEKndAlbumListing => 1799449698 >> ITunesEKndCdInsert => 1799570537 >>
Starting a New Project
Note: this part of the tutorial is also available as a screencast. Note: the tutorial uses the ib_outlet and ib_action syntax which has been deprecated since MacRuby 0.3. Read below for the new syntax.
The easiest way to start a new MacRuby project is to use Xcode.
Click on File, New Project, then select MacRuby Application in the projects list.
You should then get a new empty MacRuby project. You will see that the project contains two files:
- main.m, a small C file, which contains the traditional main entry point function. The function just calls the MacRuby initializer, passing the path of the Ruby script that will be loaded once the runtime has been initialized.
- rb_main.rb: this is the Ruby script that will be executed once your application is starting. This file loads the Cocoa framework, all other .rb (Ruby) files present in your application bundle, then calls the NSApplicationMain function of the Application kit. This will enter the Cocoa run-loop.
You can build and run the project, it will show you an empty window.
To add functionality, click on on File, New File, then select Empty File in Project. Name your file MyController.rb, then paste the following code to it.
class MyController < NSWindowController attr_writer :button def clicked(sender) puts "Button clicked!" end end
This code defines a subclass of NSWindowController with 2 methods, button= and clicked, which will be respectively mapped as an Interface Builder outlet and action. Outlets are references to user-interface elements, while actions are methods that will be called when a certain action occurs.
Note: attr_accessor can also be used to generate an Interface Builder outlet.
Note: to make sure a method will be recognized by Interface Builder as an action, it must have only one argument and the argument must be named sender. Other methods will not be recognized.
Right now, your Ruby code is completely disconnected from the interface. Do not forget to save the MyController.rb file, then double-click on MainMenu.nib, which is under the Resources folder of the sidebar. This will open Interface Builder.
First, let's instantiate our class. In the Library pane, drag-and-drop an NSObject item to the main window. Then, make sure you selected it, and open the inspector pane (click on Window, then Document Info). In the Object Identity tab, select MyController as the object class.
You will then see that IB knows about your class, and especially the outlet and action defined earlier.
Drag an NSButton item from the Library pane to your window. Then, connect it to the MyController object, by clicking on it while maintaining the control key, dragging the mouse over the object, then releasing the button. A translucent small window will appear, showing you the object's actions, and there you can select clicked:.
For the clarity of this tutorial, we are not going to connect the button outlet, as it is not required.
You can now save the MainMenu.nib file, then go back in Xcode, build and run. Your new window should appear, and when you click on the button, you should see in the Console window the Button clicked message. Congratulations!
Using MacRuby in an Existing Project
TODO
Writing C Extensions
TODO

