RubyCocoa プログラミング †
irb †
RubyCocoa を試してみるために、いきなりアプリケーションを作るのは大変なので、まずは irb で試してみましょう。 ターミナルを立ち上げて、以下のコマンドを打ち込みます。
$ irb
ライブラリのロード †
RubyCocoa のライブラリは以下のようにロードします。
require 'osx/cocoa'
システム音を鳴らしてみる †
まずは、動いた実感を味わうために、システム音を鳴らしてみましょう。
names = Dir['/System/Library/Sounds/*.aiff'].grep(/([^\/]+)\.aiff/){ |i| $1 }
OSX::NSSound.soundNamed(names[0]).play
OSX::NSSound.soundNamed(names[1]).play
以降は、RubyCocoa の動作の理解の助けになると思われる例を挙げていきます。 説明の中で #=> の右側は実行結果として表示される文字列です。
Cocoa クラス †
p OSX::NSObject #=> OSX::NSObject obj = OSX::NSObject.description p obj #=> #<OSX::NSCFString:0x220c2e class='NSCFString' id=0x1103c10> obj = OSX::NSObject.alloc.init p obj #=> #<OSX::NSObject:0x1cad6 class='NSObject' id=0x5930c0>
RubyCocoa では、Cocoa クラスは OSX モジュール配下のクラスとして定義されています。 Cocoa クラスは、Ruby のクラスであると同時に Cocoa のオブジェクトとしても振る舞います。
Cocoa オブジェクトの生成 †
Cocoa オブジェクトの生成には、Cocoa の各クラスのメソッドをそのまま使います。
obj = OSX::NSObject.alloc.init str = OSX::NSString.stringWithString 'hello' str = OSX::NSString.alloc.initWithString 'world'
生成された Cocoa オブジェクトは、RubyCocoa 内部で OSX::ObjcID というクラスのオブジェクトに包まれています。 通常、OSX::ObjcID クラスの存在を意識する必要はありません。
オーナーシップとメモリ管理 †
OSX::ObjcID のインスタンスは、かならず自分が包んでいる Cocoa オブジェクトのオーナーシップを持ちます。 オーナーシップは、OSX::ObjcID のインスタンスが GC に掃除されるときに自動的になくなります。 したがって、RubyCocoa ではオーナーシップの管理を気にする必要はありません。 また、OSX::ObjcID というクラスの存在を意識する必要もありません。
str = OSX::NSString.stringWithString 'hello' str = OSX::NSString.alloc.initWithString 'world'
上の2つの例は、Objective-C ではオーナーシップを発生させるかさせないかという違いがありますが、オーナーシップを気にする必要のない RubyCocoa では大して違いはありません。 retain、release、autorelease などのメソッドは、基本的に呼ぶ必要がありませんし、NSAutoreleasePool を作る必要もありません。
- Cocoa オブジェクトの生成は、Cocoa クラスの生成メソッドを呼ぶ
- Cocoa オブジェクトは作りっぱなしでよく、メモリ管理は不要
メソッドの返す値 †
nstr = OSX::NSString.description
p nstr #=> #<OSX::NSCFString:0x1ca90 class='NSCFString' id=0x593200>
p nstr.to_s #=> "NSString"
nstr = OSX::NSString.stringWithString 'Hello world!'
p nstr # => #<OSX::NSCFString:0x1c9c8 class='NSCFString' id=0x593400
p nstr.to_s # => "Hello world!"
nstr = OSX::NSString.stringWithString(`pwd`.chop)
nary = nstr.pathComponents
p nary # => #<OSX::NSCFArray:0x1c8ec class='NSCFArray' id=0x593660>
ary = nary.to_a
p ary # => [#<OSX::NSCFString:0x1c216 class='NSCFString' id=0x593a10>, ...]
ary.map! {|i| i.to_s }
p ary # => ["/", "Users", "hisa", "src", "ruby", "osxobjc"]
これらの例から推測できるように、RubyCocoa では NSString や NSArray などの Objective-C オブジェクトを返すメソッドを Cocoa オブジェクトとして返します。 積極的に対応する Ruby のオブジェクト (例えば String や Array) などには変換しません。 文字列と配列に関しては、to_s や to_a が定義されているので、それを使って変換することができます。
メソッド名の決定方法 †
files = `ls /System/Library/Sounds/*.aiff`.split files.each do |file| snd = OSX::NSSound.alloc snd = snd.objc_send(:initWithContentsOfFile, file, :byReference, true) snd.play sleep 0.25 while snd.isPlaying? end
上の例は、さきほど示した音を鳴らす例の別バージョンです。 Objective-C のメッセージセレクタと引数を Ruby 風に表記する別の方法を示しています。
RubyCocoa では、
[obj control:a textview:b doCommandBySelector:c];
に対応する、いくつかの呼び出し方法が用意されています。
基本は、メッセージセレクタの ':' を '_' に置き換えたものが Ruby 側でのメソッド名となります。
obj.control_textview_doCommandBySelector_(a, b, c)
ただし、最後の '_' は省略することができるので、
obj.control_textview_doCommandBySelector(a, b, c)
このように書くことができます。
また、長いメソッド名の場合など、メッセージセレクタのキーワードと引数の対応関係がわかりにくいため、次のような呼び出し方法も用意しています。
obj.objc_send(:control, a, :textview, b, :doCommandBySelector, c)
BOOL を返すメソッドの場合には、メソッド名の最後に '?' を付けてください。 RubyCocoa では、'?' の有無でメソッドが論理値を返すものかどうか判断しています。 付けない場合には、Objective-C が返した数値 (0:NO, 1:YES) が返りますが、これらの値は Ruby の論理値としてはどちらも真になるので注意してください。
nary = OSX::MyArray.alloc.init
p nary.contains("hoge") # => 0
p nary.contains?("hoge") # => false
nary.add("hoge")
p nary.contains("hoge") # => 1
p nary.contains?("hoge") # => true
ただし、AppKit などのあらかじめ BridgeSupport にすべてのメソッド定義が登録されているフレームワークを使う場合には、末尾に '?' がなくても論理値を返すかどうか判断できるため、末尾の '?' は不要です。 自分で定義したクラスの場合には、メソッド定義は BridgeSupport に登録されていないため、末尾に '?' をつけることが必要です。
メソッドの引数は可能な限り変換する †
OSX::NSString.stringWithString 'hello'
のように、引数の値として Objective-C オブジェクトを取るメソッドを呼び出す場合には、Ruby オブジェクトをそのまま渡しても、出来る限り変換を試みます。
メソッド名が重複するときに使う接頭辞 "oc_" †
klass = OSX::NSObject.class p klass #=> Class klass = OSX::NSObject.oc_class p klass #=> OSX::NSObject
Object#class のように、Ruby と Objective-C でメソッド名 (セレクタ) が全く同じ場合には、Ruby のメソッドが呼ばれます。 このような場合には、メソッド名の頭に "oc_" という接頭辞をつけると、Objective-C オブジェクトに対してメッセージが送られます。
Cocoa クラスの派生クラスとそのインスタンス †
ここまでは、既存の Cocoa クラスとそのインスタンスを使う方法を扱いました。 ここからは、RubyCocoa アプリケーションを書く場合に必要となる、Cocoa 派生クラスの定義やそのインスタンスに関するトピックを扱います。 Cocoa の派生クラスはややトリッキーな実装により実現しているため、多少の制約や癖がありますが、それも含めて見ていくことにしましょう。
Cocoa 派生クラスの定義 †
InterfaceBuilder で作成した GUI 定義ファイル (nibファイル) の中で設定した、Cocoa クラスは派生クラスとして定義します。 例えば、Cocoa の入門書やチュートリアルなどで最初の方に出てくる MVC のコントローラは、
class AppController < OSX::NSObject
ib_outlets :messageField
def buttonClicked(sender)
@messageField.setStringValue 'Merry Christmas!'
end
end
のように定義します。 RubyCocoa における Cocoa の派生クラスの定義は、このように通常の Ruby での派生クラス定義と同様に書けます。
アウトレット †
nib ファイル中でクラスに設定したアウトレットは、派生クラスの定義の中で、
ib_outlets :rateField, :amountField
と書きます。 ib_outlets は、実際には Module#attr_writer と同じです。
メソッドのオーバーライド †
親クラスで定義されているメソッドをオーバーライドする場合でも、通常の Ruby のメソッド定義をするだけでオーバーライドできます。
class MyCustomView < OSX::NSView
def drawRect(frame)
super_drawRect(frame)
end
end
オーバーライドしたメソッドの中でスーパークラスの同じメソッドを呼ぶ場合には、メソッド名に "super_" という接頭辞をつけて呼びます。
Cocoa 派生クラスのインスタンス生成 †
Cocoa 派生クラスのインスタンスを Ruby プログラムの中で生成する場合には、既存の Cocoa クラスの場合と同様に、
AppController.alloc.init
のように書きます。
Ruby でのもっとも一般的な書き方である、
AppController.new
を使うことはできません。
これにはいろいろな事情があるのですが、ここでは詳しい説明は省略します。 この制約は、インスタンス生成が、
- alloc (Objective-C 側)
- alloc 内で Ruby オブジェクト生成 (ここで initialize が呼ばれる)
という順番で行われることと深い関連があります。
インスタンス生成時の初期化コードはどこに書くべきか? †
一般に Ruby では initialize メソッドに初期化のコードを書きますが、Cocoa 派生クラスではあまり勧められません。 理由は先に述べたように、インスタンス生成時の initialize メソッドが呼ばれた時点では、Cocoa オブジェクトとしてはメモリが割り当てられただけで初期化が完了していないからです。 もっとも、Cocoa 側のメソッドを呼ばない限りにおいては、特に問題は発生しないと考えられます。
nib ファイルからロードされるような場合には、awakeFromNib メソッドで初期化するのがもっとも無難です。 実際に、Cocoa の派生クラスを定義する必要があるのも、このケースがもっとも多いのではないでしょうか。
その他の場合には、Cocoa の流儀で "init" 接頭辞を持つメソッドに書くのがよいでしょう。 メソッドが self を返すようにすることを忘れないでください。
原著者: 藤本尚邦, <hisa at fobj.com>