Introduction to HotCocoa's Mappings
A HotCocoa mapping defines a structure that sits on top of a particular Objective-C class and simplifies its usage in MacRuby.
This structure is defined in a simple, Ruby-based DSL (domain-specific language). The HotCocoa DSL includes syntax to aid in object instantiation, constant mapping, default options, customized methods, and delegate-method mappings. The only required section is object instantiation; the other four sections are only required if the Objective-C class in question requires it. Once defined, a mapping is registered with the HotCocoa::Mappings module, using the names of the mapping and the mapped Objective-C class.
The mappings that ship with HotCocoa are found in the
'lib/hotcocoa/mappings' directory of the source. You can easily define
your own mappings for classes by following the examples below. Place
mappings in files of your own, loading them after you load the
hotcocoa
library.
Basic Mapping Syntax
The basic syntax for defining a mapping is:
HotCocoa::Mappings.map name: CocoaClassName do
# mapping code...
end
To create a mapping, call the map
method on HotCocoa::Mappings
,
passing in a key-value pair to specify the constructor name and Cocoa
class to be mapped. Replace name:
with the desired name of the method
you want to be generated and made available to users of HotCocoa. For
example, the mapping definition for an NSButton
might be:
HotCocoa::Mappings.map button: NSButton do
# mapping code...
end
This creates a method on HotCocoa named button
, which will return
an instance of an NSButton
class. One caveat to note now is that
the class being mapped has to be available when you define the
mapping. NSButton
is available because HotCocoa loads the Cocoa
framework; if you want to map a class from AVFoundation
then you
will need to load AVFoundation
before you define any mappings.
You can create more than one mapping per Objective-C class. A good
reason to do this would be to provide different sets of defaults. As
an example,
Interface Builder
provides several "different" types of buttons in its Object Library,
but they are all instances of NSButton
that just have a different
default configuration.
Object Instantiation Method (required)
There are two methods, #init_with_options
and #alloc_with_options
,
that you can implement to support object instantiation. Define these
methods within the block that you pass to the map
method.
HotCocoa::Mappings.map button: NSButton do
def ,
.initWithFrame .delete(:frame)
end
end
As you can see from the method definition above, the
#init_with_options
method is provided with an instance of the class
that you declared in the mapping (NSButton
) which is created with
NSButton.alloc
. This implementation just calls the proper #init
method. This example calls #initWithFrame
and passes the options
value for the :frame
. The options hash is passed to this function
when you call the #button
method:
frame: [0,0,20,100]
Note that the method must delete any option that it consumes and must
return the mapped object. Every option used in the construction of the
object should be removed from the hash. Any options that are left in the hash
after begin processed by the instantiation methods will be dispatched
to the NSButton
instance.
The second method you can implement is:
HotCocoa::Mappings.map button: NSButton do
def
NSButton.alloc.initWithFrame .delete(:frame)
end
end
Here you are not provided with the alloc
'd object as the first
parameter, but simply the options hash. This is helpful for classes
that use class methods for instantiation, such as:
NSImage.imageNamed 'my_image'
You should implement either #init_with_options
or
#alloc_with_options
, but not both. If #alloc_with_options
exists,
it will be called and #init_with_options
will be ignored.
If you want a constructor to handle a block with extra behavior then
you will also need to implement #handle_block
for the mapping. Block
handling is done after the mapped instance has been initialized and
#handle_block
will be given the instance as a parameter. For
instance, HotCocoa uses this construct to encapsulate normal
application setup:
HotCocoa::Mappings.map application: NSApplication do
def opts
NSApplication.sharedApplication
end
def handle_block app
yield application
application.run
end
end
HotCocoa.application do |app|
app.delegate = self
end
Default Options (optional)
You can provide a hash of default options in the definition of your mapping. This is very useful for many Cocoa classes, because there are so many configuration options. Defaults are appended to the options hash that is passed into the constructor method if a value of the same key does not exist.
Supplying your defaults is simple. In the example below, if you
provide a :frame
, it will be used instead of CGRectZero
:
HotCocoa::Mappings.map button: NSButton do
defaults bezel: :rounded,
frame: CGRectZero,
layout: {}
end
A few of the defaults shown above are pretty important to UI classes;
specifically, :frame
and :layout
. The NSButton
example uses
frame: CGRectZero
. The CGRectZero
constant equals a rectangle of
[0,0,0,0]
. The layout: {}
part is important for using the
layout_view
classes, which are included in HotCocoa, it describes
where to put the UI element.
This default value for the layout is an empty hash, but if it's not
passed, the default value for the layout is actually nil
. If the
layout is nil
, the component is not included when the layout_view
computes the layout for the components. All of the UI mappings that ship with
HotCocoa provide an empty hash as a default :layout
.
Constant Mapping (optional, inherited)
Because constant names need to be globally unique in Objective-C, they can get very long. What the constant mapping provides in HotCocoa is the ability to use short symbol names and map them to the constant names that are scoped to the wrapped class. This is an example of mapping constants to represent button state:
HotCocoa::Mappings.map button: NSButton do
constant :state, {
on: NSOnState,
off: NSOffState,
mixed: NSMixedState
}
end
A constant map includes the key (:state
), followed by a hash which
maps symbols to actual constants. When you provide options to the
constructor method that match a constant key, it looks up the
corresponding value in that hash and replaces the value in the option
hash with the constant's value.
So, when you call:
:state => :on
It will be replaced with:
:state => NSOnState
You can have as many constant maps in each class as you need. Constant
maps are also inherited by subclasses. A constant map on NSView
is
also available on NSControl
and NSButton
(as they are subclasses).
If you provide a value for a constant key that is an array rather than
a single symbol, the constants in the array are OR'd with each
other. This is useful when the constants are masked. For NSWindow
's
mapping of style:
{ style: [:titled, :closable, :miniaturizable, :resizable] }
is equivalent to:
style = NSTitledWindowMask |
NSClosableWindowMask |
NSMiniaturizableWindowMask |
NSResizableWindowMask
Custom Methods (optional, inherited)
Custom methods are simply modules that are included in the instance; they provide idiomatic Ruby methods for the mapped Objective-C class instance. Providing custom methods in your mapping is easy:
HotCocoa::Mappings.map button: NSButton do
custom_methods do
def bezel= value
setBezelStyle(value)
end
def on?
state == NSOnState
end
end
end
In the first method, #bezel=
, we provide a better method name than
setBezelStyle
. Although we could provide idiomatic Ruby methods for
every Objective-C method, the number of these methods is huge. The
general principle is to customize where this provides something
better, not just syntactically better. Custom methods, like constant
mappings, are inherited by subclasses.
Constant Mappings and Custom Methods
In the last example, the #bezel=
method serves as a corresponding
method name to the constant map for bezel style:
constant :bezel, {
rounded: NSRoundedBezelStyle,
regular_square: NSRegularSquareBezelStyle,
thick_square: NSThickSquareBezelStyle,
thicker_square: NSThickerSquareBezelStyle,
disclosure: NSDisclosureBezelStyle,
shadowless_square: NSShadowlessSquareBezelStyle,
circular: NSCircularBezelStyle,
textured_square: NSTexturedSquareBezelStyle,
help_button: NSHelpButtonBezelStyle,
small_square: NSSmallSquareBezelStyle,
textured_rounded: NSTexturedRoundedBezelStyle,
round_rect: NSRoundRectBezelStyle,
recessed: NSRecessedBezelStyle,
rounded_disclosure: NSRoundedDisclosureBezelStyle
}
This way, you can easily create buttons of the provided bezel style:
:bezel => :circular
If you recall from the default options section (above), you can also
include default values that are constant mapped values (e.g.
:bezel => :rounded
is the default for a button). In this way,
constant mappings and custom methods work together to provide a vastly
better syntax for using constants in your code and simplifying the
code needed for an #init_with_options
method.
Delegate Method Mapping (optional)
Delegate method mapping is a little more complex then the prior
sections. Delegate methods are used pervasively in Cocoa to facilitate
customization of controls. Normally, what you need to do is implement
the methods that the control is looking for in a class of your
own. You would then set an instance of that class as the delegate of
the control, using setDelegate(instance)
.
In HotCocoa, we wanted to enable the use of Ruby blocks for delegate method calls, so the Objective-C code:
class MyDelegate
def windowWillClose sender
# perform something
end
end
window.setDelegate(MyDelegate.new)
is simplified to the Ruby code:
window.will_close do
# perform something
end
Notice that we do not have to worry about the sender
parameter
because the sender is window
.
To enable HotCocoa style delegation, you map individual delegate
methods to a symbol name, then map parameters that are passed to that
delegate method to the block parameters. For NSWindow
the definition
for delegating windowWillClose
, which passes no parameters to the
block, would be:
HotCocoa::Mappings.map window: NSWindow do
delegating 'windowWillClose:', to: :will_close
end
This creates a #will_close
method that accepts the block (as
above). For the sake of efficiency, it:
- creates an object
- adds the delegating method (
#windowWillClose
) as a method on that object's singleton class - stores the passed-in block inside that object
The generated #windowWillClose
method calls that block when Cocoa
calls the #windowWillClose
method. Each time a delegate method is
created, the object is set as the delegate (using #setDelegate
).
When a delegate needs to forward parameters to the block, the definition becomes a little more complex:
HotCocoa::Mappings.map window: NSWindow do
delegating 'window:willPositionSheet:usingRect:', to: :will_position_sheet, parameters: [:willPositionSheet, :usingRect]
end
The parameters:
list contains the corresponding selector name from
the Objective-C selector. Even though the delegate method normally has
three parameters (window
, willPositionSheet
, and usingRect
), the
block will only be passed the last two (because we already have the
first parameter). Using this method would look like:
window.will_position_sheet do |sheet, rect|
# ...
end
It's also possible to map a parameter, in cases where you have to invoke a more complex calling on the parameter:
HotCocoa::Mappings.map window: NSWindow do
delegating 'windowDidExpose:', to: :did_expose, parameters: ["windowDidExpose.userInfo['NSExposedRect']"]
end
Here we want to walk the first parameter's userInfo
dictionary, get
the NSExposedRect
rectangle, and pass it as a parameter to the
did_expose
block. Using this method would look like:
window.did_expose do | rect|
# ...
end
Each method for a delegate has to be mapped with an individual delegating call.
When To Make A Mapping
The best candidates for a new HotCocoa mappings are classes that require a lot of configuration. Though sometimes it is convenient to make a mapping just to take advantage of a mapping feature that HotCocoa provides, such as block-based delegation.
If you have mappings that you would like to share, feel free to open a pull request on Github.