1RubyCocoaプログラミングの基本
2=============================
3
4このドキュメントは、RubyCocoaプログラミングする上で知っておくべき基本事項を紹介します。
5RubyおよびCocoaの初歩をある程度学んでいる方が対象です。
6対話型Rubyインタプリタを使って、実際に確認しながら読むと良いでしょう。
7対話型Rubyインタプリタとしては以下のものが使えます。
8
9* irbコマンド
10* CocoaRepl   (RubyCocoa付属サンプルアプリケーション)
11* RubyConsole (RubyCocoa付属サンプルアプリケーション)
12
13後半のGUI関連の確認には、irbよりもCocoaReplもしくはRubyConsoleが適しています。
14
15
16RubyCocoaのロード
17-----------------
18
19RubyCocoa のライブラリは以下のようにロードします。
20
21    require 'osx/cocoa'
22
23
24システム音を鳴らしてみる
25------------------------
26
27まずは、動いた実感を味わうために、システム音を鳴らしてみましょう。
28
29    names = Dir['/System/Library/Sounds/*.aiff'].
30              grep(/([^\/]+)\.aiff/){ |i| $1 }
31    OSX::NSSound.soundNamed(names[0]).play
32    OSX::NSSound.soundNamed(names[1]).play
33
34以降は、RubyCocoa の動作の理解の助けになると思われる例を挙げていきます。
35説明の中で #=> の右側は実行結果として表示される文字列です。
36
37
38Cocoaクラス
39------------
40
41    p OSX::NSObject  #=> OSX::NSObject
42    obj = OSX::NSObject.description
43    p obj       # => #<OSX::NSCFString:0x220c2e class='NSCFString' id=0x1103c10>
44    obj = OSX::NSObject.alloc.init
45    p obj  #=> #<OSX::NSObject:0x1cad6 class='NSObject' id=0x5930c0>
46
47RubyCocoa では、Cocoa のクラスは OSX モジュール以下に定義されています。
48Cocoa クラスは、Ruby のクラスであると同時に Cocoa のオブジェクトとしても振る舞います。
49
50
51Cocoa オブジェクトの生成
52------------------------
53
54Cocoa オブジェクトの生成には、Cocoa の各クラスのメソッドをそのまま使います。
55
56    obj = OSX::NSObject.alloc.init
57    str = OSX::NSString.stringWithString 'hello'
58    str = OSX::NSString.alloc.initWithString 'world'
59
60生成された Cocoa オブジェクトは、RubyCocoa 内部で OSX::ObjcID というクラスのオブジェクトに包まれています。
61通常、OSX::ObjcID クラスの存在を意識する必要はありません。
62
63
64オーナーシップとメモリ管理
65--------------------------
66
67OSX::ObjcID のインスタンスは、かならず自分が包んでいる Cocoa オブジェクトのオーナーシップを持ちます。
68オーナーシップは、OSX::ObjcID のインスタンスが GC に掃除されるときに自動的になくなります。
69したがって、RubyCocoa ではオーナーシップの管理を気にする必要はありません。
70また、OSX::ObjcID というクラスの存在を意識する必要もありません。
71
72    str = OSX::NSString.stringWithString 'hello'
73    str = OSX::NSString.alloc.initWithString 'world'
74
75上の2つの例は、Objective-C ではオーナーシップを発生させるかさせないかという違いがありますが、オーナーシップを気にする必要のない RubyCocoa では大して違いはありません。
76retain、release、autorelease などのメソッドは、基本的に呼ぶ必要がありませんし、NSAutoreleasePool を作る必要もありません。
77
78* Cocoa オブジェクトの生成は、Cocoa クラスの生成メソッドを呼ぶ
79* Cocoa オブジェクトは作りっぱなしでよく、メモリ管理は不要
80
81
82メソッドの返す値
83----------------
84
85    nstr = OSX::NSString.description
86    p nstr       #=> #<OSX::NSCFString:0x1ca90 class='NSCFString' id=0x593200>
87    p nstr.to_s  #=> "NSString"
88
89    nstr = OSX::NSString.stringWithString 'Hello world!'
90    p nstr       # => #<OSX::NSCFString:0x1c9c8 class='NSCFString' id=0x593400
91    p nstr.to_s  # => "Hello world!"
92
93    nstr = OSX::NSString.stringWithString(`pwd`.chop)
94    nary = nstr.pathComponents
95    p nary  # => #<OSX::NSCFArray:0x1c8ec class='NSCFArray' id=0x593660>
96
97    ary = nary.to_a
98    p ary  # => [#<OSX::NSCFString:0x1c216 class='NSCFString' id=0x593a10>, ...]
99
100    ary.map! {|i| i.to_s }
101    p ary  # => ["/", "Users", "hisa", "src", "ruby", "osxobjc"]
102
103これらの例から推測できるように、RubyCocoa では NSString や NSArray などの Objective-C オブジェクトを返すメソッドを Cocoa オブジェクトとして返します。
104積極的に対応する Ruby のオブジェクト (例えば String や Array) には変換しません。
105文字列と配列に関しては、to_s や to_a が定義されているので、それを使って変換できます。
106
107
108メソッド名の決定方法 (1)
109------------------------
110
111    files = Dir['/System/Library/Sounds/*.aiff']
112    files.each do |file|
113      snd = OSX::NSSound.alloc
114      snd = snd.initWithContentsOfFile_byReference(file, true)
115      snd.play
116      sleep 0.25 while snd.isPlaying?
117    end
118
119上の例は、さきほど示した音を鳴らす例の別バージョンです。
120Objective-C のメッセージセレクタと引数を Ruby 風に表記する別の方法を示しています。
121
122RubyCocoa では、
123
124    [obj control:a textview:b doCommandBySelector:c];
125
126に対応する、いくつかの呼び出し方法が用意されています。
127
128基本は、メッセージセレクタの ':' を '_' に置き換えたものが Ruby 側でのメソッド名となります。
129
130    obj.control_textview_doCommandBySelector_(a, b, c)
131
132ただし、最後の '_' は省略することができるので、
133
134    obj.control_textview_doCommandBySelector(a, b, c)
135
136このように書くことができます。
137
138BOOL を返すメソッドの場合には、メソッド名の最後に '?' を付けてください。
139RubyCocoa では、'?' の有無でメソッドが論理値を返すものかどうか判断しています。
140付けない場合には、Objective-C が返した数値 (0:NO, 1:YES) が返りますが、これらの値は Ruby の論理値としてはどちらも真になるので注意してください。
141
142    nary = OSX::MyArray.alloc.init
143    p nary.contains("hoge")   # => 0
144    p nary.contains?("hoge")  # => false
145    nary.add("hoge")
146    p nary.contains("hoge")   # => 1
147    p nary.contains?("hoge")  # => true
148
149ただし、AppKit などのあらかじめ BridgeSupport にすべてのメソッド定義が登録されているフレームワークを使う場合には、末尾に '?' がなくても論理値を返すかどうか判断できるため、末尾の '?' は不要です。
150自分で定義したクラスの場合には、メソッド定義は BridgeSupport に登録されていないため、末尾に '?' をつけることが必要です。
151
152
153メソッド名の決定方法 (2) - objc_send
154------------------------------------
155
156長いメソッド名になると、メッセージセレクタのキーワードと引数の対応関係がわかりにくくなりがちです。
157例えば、NSWindowの初期化は以下のようになります。
158
159    OSX::NSWindow.alloc.initWithContentRect_styleMask_backing_defer(
160      frame,
161      NSTitledWindowMask + NSResizableWindowMask,
162      NSBackingStoreBuffered,
163      false)
164
165このような場合、objc_send を使って可読性を高めることができます。
166
167    OSX::NSWindow.alloc.
168      objc_send(:initWithContentRect, frame,
169                          :styleMask, NSTitledWindowMask + NSResizableWindowMask,
170                            :backing, NSBackingStoreBuffered,
171                              :defer, false)
172
173
174
175メソッドの引数は可能な限り変換する
176----------------------------------
177
178    OSX::NSString.stringWithString 'hello'
179
180のように、引数の値として Objective-C オブジェクトを取るメソッドを呼び出す場合には、Ruby オブジェクトをそのまま渡しても、出来る限り変換を試みます。
181
182
183メソッド名が重複するときに使う接頭辞 "oc_"
184------------------------------------------
185
186    klass = OSX::NSObject.class
187    p klass  #=> Class
188    klass = OSX::NSObject.oc_class
189    p klass  #=> OSX::NSObject
190
191Object#class のように、Ruby と Objective-C でメソッド名 (セレクタ) が全く同じ場合には、Ruby のメソッドが呼ばれます。このような場合には、メソッド名の頭に "oc_" という接頭辞をつけると、Objective-C オブジェクトに対してメッセージが送られます。
192
193
194setterとしての"="
195-----------------
196
197NSSlider#setMaxValue のようなsetterは、"="でセットすることができます。
198
199     sldr = NSSlider.alloc.init
200     p sldr.maxValue   # => 1.0
201     sldr.maxValue = 100
202     p sldr.maxValue   # => 100.0
203
204
205Cocoa派生クラス
206---------------
207
208ここまでは、既存の Cocoa クラスとそのインスタンスを使う方法を説明してきました。
209ここからは、RubyCocoa アプリケーションを書く場合に必要となる、Cocoa 派生クラスの定義やそのインスタンスに関するトピックを扱います。
210Cocoa の派生クラスはややトリッキーな実装により実現しているため、多少の制約や癖がありますが、それも含めて見ていくことにしましょう。
211
212
213Cocoa派生クラスの定義
214----------------------
215
216RubyCocoa における Cocoa の派生クラスの定義は、通常の Ruby での派生クラス定義と同様に書けます。
217
218    class MyController < OSX::NSObject
219      ib_outlets :messageField
220
221      def awakeFromNib
222        @messageField.stringValue = ''
223      end
224
225      ib_action :greeting do |sender|
226        @messageField.stringValue = 'Merry Christmas!'
227      end
228    end
229
230
231アウトレットの宣言
232------------------
233
234アウトレットの宣言には ib_outlets (もしくはib_outlet)を使います。
235
236
237アクションの定義・宣言
238----------------------
239
240アクションの定義には ib_action を使います。
241
242    ib_action :buttonClicked do |sender|
243      ...
244    end
245
246Rubyのメソッドとして定義し、ib_action宣言することもできます。
247
248    ib_action :buttonClicked
249    def buttonClicked(sender)
250      ...
251    end
252
253
254メソッドのオーバーライド
255------------------------
256
257親クラスで定義されているメソッドをオーバーライドする場合でも、通常の Ruby のメソッド定義をするだけでオーバーライドできます。
258
259    class MyCustomView < OSX::NSView
260      def drawRect(frame)
261        super_drawRect(frame)
262      end
263    end
264
265オーバーライドしたメソッドの中でスーパークラスの同じメソッドを呼ぶ場合には、メソッド名に "super_" という接頭辞をつけて呼びます。
266
267
268Cocoa 派生クラスのインスタンス生成
269----------------------------------
270
271Cocoa 派生クラスのインスタンスを Ruby プログラムの中で生成する場合には、既存の Cocoa クラスの場合と同様に、
272
273    AppController.alloc.init
274
275のように書きます。Ruby でインスタンスを生成するときのクラスメソッドnewは使えません。
276これにはいろいろな事情があるのですが、ここでは詳しい説明は省略します。
277この制約は、インスタンス生成が、
278
279* alloc (Objective-C 側)
280* alloc 内で  Ruby オブジェクト生成 (ここで initialize が呼ばれる)
281
282という順番で行われることと深い関連があります。
283
284
285インスタンス生成時の初期化コードはどこに書くべきか?
286----------------------------------------------------
287
288一般に Ruby では initialize メソッドに初期化のコードを書きますが、Cocoa 派生クラスではあまり勧められません。
289理由は先に述べたように、インスタンス生成時の initialize メソッドが呼ばれた時点では、Cocoa オブジェクトとしてはメモリが割り当てられただけで初期化が完了していないからです。
290もっとも、Cocoa 側のメソッドを呼ばない限りにおいては、特に問題は発生しないと考えられます。
291
292nib ファイルからロードされるような場合には、awakeFromNib メソッドで初期化するのがもっとも無難です。
293実際に、Cocoa の派生クラスを定義する必要があるのも、このケースがもっとも多いのではないでしょうか。
294
295その他の場合には、Cocoa の流儀で "init" 接頭辞を持つメソッドに書くのがよいでしょう。
296メソッドが self を返すようにすることを忘れないでください。
297