RubyCocoaプログラミングの基本 ============================= このドキュメントは、RubyCocoaプログラミングする上で知っておくべき基本事項を紹介します。 RubyおよびCocoaの初歩をある程度学んでいる方が対象です。 対話型Rubyインタプリタを使って、実際に確認しながら読むと良いでしょう。 対話型Rubyインタプリタとしては以下のものが使えます。 * irbコマンド * CocoaRepl (RubyCocoa付属サンプルアプリケーション) * RubyConsole (RubyCocoa付属サンプルアプリケーション) 後半のGUI関連の確認には、irbよりもCocoaReplもしくはRubyConsoleが適しています。 RubyCocoaのロード ----------------- 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 # => # obj = OSX::NSObject.alloc.init p obj #=> # 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 #=> # p nstr.to_s #=> "NSString" nstr = OSX::NSString.stringWithString 'Hello world!' p nstr # => # "Hello world!" nstr = OSX::NSString.stringWithString(`pwd`.chop) nary = nstr.pathComponents p nary # => # ary = nary.to_a p ary # => [#, ...] 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 が定義されているので、それを使って変換できます。 メソッド名の決定方法 (1) ------------------------ files = Dir['/System/Library/Sounds/*.aiff'] files.each do |file| snd = OSX::NSSound.alloc snd = snd.initWithContentsOfFile_byReference(file, 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) このように書くことができます。 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 に登録されていないため、末尾に '?' をつけることが必要です。 メソッド名の決定方法 (2) - objc_send ------------------------------------ 長いメソッド名になると、メッセージセレクタのキーワードと引数の対応関係がわかりにくくなりがちです。 例えば、NSWindowの初期化は以下のようになります。 OSX::NSWindow.alloc.initWithContentRect_styleMask_backing_defer( frame, NSTitledWindowMask + NSResizableWindowMask, NSBackingStoreBuffered, false) このような場合、objc_send を使って可読性を高めることができます。 OSX::NSWindow.alloc. objc_send(:initWithContentRect, frame, :styleMask, NSTitledWindowMask + NSResizableWindowMask, :backing, NSBackingStoreBuffered, :defer, false) メソッドの引数は可能な限り変換する ---------------------------------- 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 オブジェクトに対してメッセージが送られます。 setterとしての"=" ----------------- NSSlider#setMaxValue のようなsetterは、"="でセットすることができます。 sldr = NSSlider.alloc.init p sldr.maxValue # => 1.0 sldr.maxValue = 100 p sldr.maxValue # => 100.0 Cocoa派生クラス --------------- ここまでは、既存の Cocoa クラスとそのインスタンスを使う方法を説明してきました。 ここからは、RubyCocoa アプリケーションを書く場合に必要となる、Cocoa 派生クラスの定義やそのインスタンスに関するトピックを扱います。 Cocoa の派生クラスはややトリッキーな実装により実現しているため、多少の制約や癖がありますが、それも含めて見ていくことにしましょう。 Cocoa派生クラスの定義 ---------------------- RubyCocoa における Cocoa の派生クラスの定義は、通常の Ruby での派生クラス定義と同様に書けます。 class MyController < OSX::NSObject ib_outlets :messageField def awakeFromNib @messageField.stringValue = '' end ib_action :greeting do |sender| @messageField.stringValue = 'Merry Christmas!' end end アウトレットの宣言 ------------------ アウトレットの宣言には ib_outlets (もしくはib_outlet)を使います。 アクションの定義・宣言 ---------------------- アクションの定義には ib_action を使います。 ib_action :buttonClicked do |sender| ... end Rubyのメソッドとして定義し、ib_action宣言することもできます。 ib_action :buttonClicked def buttonClicked(sender) ... end メソッドのオーバーライド ------------------------ 親クラスで定義されているメソッドをオーバーライドする場合でも、通常の Ruby のメソッド定義をするだけでオーバーライドできます。 class MyCustomView < OSX::NSView def drawRect(frame) super_drawRect(frame) end end オーバーライドしたメソッドの中でスーパークラスの同じメソッドを呼ぶ場合には、メソッド名に "super_" という接頭辞をつけて呼びます。 Cocoa 派生クラスのインスタンス生成 ---------------------------------- Cocoa 派生クラスのインスタンスを Ruby プログラムの中で生成する場合には、既存の Cocoa クラスの場合と同様に、 AppController.alloc.init のように書きます。Ruby でインスタンスを生成するときのクラスメソッドnewは使えません。 これにはいろいろな事情があるのですが、ここでは詳しい説明は省略します。 この制約は、インスタンス生成が、 * alloc (Objective-C 側) * alloc 内で Ruby オブジェクト生成 (ここで initialize が呼ばれる) という順番で行われることと深い関連があります。 インスタンス生成時の初期化コードはどこに書くべきか? ---------------------------------------------------- 一般に Ruby では initialize メソッドに初期化のコードを書きますが、Cocoa 派生クラスではあまり勧められません。 理由は先に述べたように、インスタンス生成時の initialize メソッドが呼ばれた時点では、Cocoa オブジェクトとしてはメモリが割り当てられただけで初期化が完了していないからです。 もっとも、Cocoa 側のメソッドを呼ばない限りにおいては、特に問題は発生しないと考えられます。 nib ファイルからロードされるような場合には、awakeFromNib メソッドで初期化するのがもっとも無難です。 実際に、Cocoa の派生クラスを定義する必要があるのも、このケースがもっとも多いのではないでしょうか。 その他の場合には、Cocoa の流儀で "init" 接頭辞を持つメソッドに書くのがよいでしょう。 メソッドが self を返すようにすることを忘れないでください。