スクリプトから
Array Controller にアクセスする

RubyCocoa のスクリプトから Array Controller にアクセスするには、Array Controller を outlet に結びつける。

ib_outlet :aryController

とスクリプトで、アウトレットを指定してから、Interface Builder で Array Controller を追加して、この outlet と結びつける。

これで、@aryController に対していろいろとスクリプトで処理していく。

実例があった方が後で見たときにわかりやすいと思って、サンプルとして作ったプログラムを元にしていく。つもりだったんだけど、そうならなかった。

Interface Builder で次のようなウィンドウを作った。

まあ、Core Data のアプリケーションみたいなデータベースのような形になっているけど、ここでは Array Controller の使い方だけなので、データの保存とかは考えないことにする。Add と Remove のボタンが2つあるのは、バインディングだけで追加と削除をするものと、スクリプトでするものを同時に実現するためにある。ちなみに上の2つをバインディング用にする。テーブルと Array Controller のバインディングについては、Array Controller Binding 1 を参照。key は次のものを用意した。

2つのボタンは、addButton、removeButton、Fetch ボタンは fetchButton とした。

ib_outlets :addButton, :removeButton, :fetchButton

要素を追加する

Array Controller に要素を追加するには、次の4つが使える。

addObject(object)

addObjects(array)

insertObject_atArrangedObjectIndex(object,index)

insertObjects_atArrangedObjectIndexes(array,NSIndexSet)

ここで、object は NSMutableDictionary オブジェクトで、Ruby の Hash でもいける。array は object の配列をつかう。addObject と addObjects Array Controller の配列の一番最後に追加される。insertObject_atArrangedObjectIndex はその時点での順番で index で指定したところに object を入れる。insertObjects_atArrangedObjectIndexes は指定した index の範囲のところに object の配列を入れる。この index の範囲は NSIndexSet オブジェクトで指定する。NSIndexSet オブジェクトは indexSetWithIndexesInRange(range) もしくは initWithIndexesInRange(range) を使って NSRange オブジェクトで range を指定するけど、NSRange オブジェクトは Ruby の配列で代用できるので、次のように書ける。

NSIndexSet.indexSetWithIndexesInRange([1,2])

NSIndexSet.alloc.initWithIndexesInRange([1,2])

ここで、[1,2] は要素1(先頭から2番目の要素)から要素2つということ。

まず、一つだけ要素を追加する。object というオブジェクトを作る。

NSMutableDictionary オブジェクトを作る場合。setValue_forKey(value,key) でそれぞれの key に値を割り当てていく。

object = NSMutableDictionary.alloc.init

object.setValue_forKey('Proud of You','title')

object.setValue_forKey('The Love Rocks','album')

object.setValue_forKey('Dreams Come True','artist')

でも、せっかく RubyCocoa なので Ruby の Hash オブジェクトで代用する。どちらでやっても結果は同じ。

object = Hash.new

object['title'] = 'Proud of You'

object['album'] = 'The Love Rocks'

object['artist'] = 'Dreams Come True'

最後に要素を追加する。

@aryController.addObject(object)

1番目(0番目の次)に追加する。

@aryController.insertObject_atArrangedObjectIndex(object,1)

でも、上でちょっと書いたけど RubyCocoa では Ruby の Hash を NSDictionary オブジェクトみたいに扱えるので、すっ飛ばして addObject(hash) としてしまうこともできる。

@aryController.addObject([{'title'=>'Proud of You','album'=>'Love Rocks','artist'=>'Dreams Come True'}])

次に、ary という object の配列を作って複数の要素を追加する。NSArray オブジェクトでもいいが Ruby の Array オブジェクトでもいけるので、ここではそうする。

ary = Array.new

ary[0] = NSMutableDictionary.alloc.init

ary[0].setValue_forKey('決戦は金曜日','title')

ary[0].setValue_forKey('The Swinging Star','album')

ary[0].setValue_forKey('Dreams Come True','artist')

ary[1] = NSMutableDictionary.alloc.init

ary[1].setValue_forKey('朝日の洗礼','title')

ary[1].setValue_forKey('DIAMOND15','album')

ary[1].setValue_forKey('Dreams Come True','artist')

もしくは Ruby の Hash で、

ary[0] = Hash.new

ary[0]['title'] = '決戦は金曜日'

ary[0]['album'] = 'The Swinging Star'

ary[0]['artist'] = 'Dreams Come True'

ary[1] = Hash.new

ary[1]['title'] = '朝日の洗礼'

ary[1]['album'] = 'DIAMOND15'

ary[1]['artist'] = 'Dreams Come True'

最後に要素を追加する。

@aryController.addObjects(ary)

1から2番目(0番目の次)に追加する(インデックスの1と2のところに追加する)。

@aryController.insertObjects_atArrangedObjectIndexes(ary,NSIndexSet.indexSetWithIndexesInRange([1,2]))

デフォルトでは、追加した後に追加した要素が選択されるようになっている(Interface Builder で選択しないように設定もできる)。

これは、スクリプトからも setSelectsInsertedObjects(true/false) というメソッドを使って設定できる。ただ、そうすると、Add ボタンを押して要素を追加して入力していくときにも選ばれないので自分で選ぶことになる。

もし、insertObjects_atArrangedObjectIndexes で要素を追加した場合など、追加された要素の index がわかっていれば、removeSelectionIndexes(NSIndexSet) で選ばれている状態を外すこともできる。これがいいのかどうかわからないが。

@aryController.removeSelectionIndexes(NSIndexSet.indexSetWithIndexesInRange([1,2]))

これで、このすぐ上で追加した要素の選ばれている状態を外すことができる。

でも次のように配列の長さを得れば、すべてが選択されてない状態にできる。

@aryController.removeSelectionIndexes(NSIndexSet.indexSetWithIndexesInRange([0,@aryController.arrangedObjects.length]))

これらをボタンに割り当てて実行すると、こんな感じ。

要素を得る

Array Controller の要素を得るには、index を指定するか、Predicate で検索をして絞り込んでから要素を得る。

Index を指定して要素を得る

Index で指定するには、setSelectionIndex(index)setSelectionIndexes(NSIndexSet) で要素を選択するか、テーブル上で直接要素を選ぶ。選んだら、selectedObjects で選択された要素を配列で得る。次の例では、先頭の要素から3つを取り出している。

@aryController.setSelectionIndexes(NSIndexSet.alloc.initWithIndexesInRange([0,3]))

@aryController.selectedObjects

ここで返ってくるオブジェクトは NSCFArray で要素は NSCFDictionary の配列となっている。ちなみに上で追加した3つの要素を取り出すと、こんな感じになっている。

#<NSCFArray [#<NSCFDictionary {#<NSCFString "title">=>#<NSCFString "Proud of You">, #<NSCFString "album">=>#<NSCFString "The Love Rocks">, #<NSCFString "artist">=>#<NSCFString "Dreams Come True">}>, #<NSCFDictionary {#<NSCFString "title">=>#<NSCFString "決戦は金曜日">, #<NSCFString "album">=>#<NSCFString "The Swinging Star">, #<NSCFString "artist">=>#<NSCFString "Dreams Come True">}>, #<NSCFDictionary {#<NSCFString "album">=>#<NSCFString "DIAMOND15">, #<NSCFString "artist">=>#<NSCFString "Dreams Come True">, #<NSCFString "title">=>#<NSCFString "朝日の洗礼">}>]>

例えば、これを Ruby で処理したいので、title、album、artist の3つの要素が入った配列の配列に直したい場合、NSArray は Ruby の Array オブジェクトとして、NSDictionary は Ruby の Hash として扱えるので、次のように書けばいい。

items = Array.new

@aryController.selectedObjects.each do |item|

items << [item['title'].to_s,item['album'].to_s,item['artist'].to_s]

end

こうすると、items は次のような Ruby の配列になる。

[["Proud of You", "The Love Rocks", "Dreams Come True"], ["決戦は金曜日", "The Swinging Star", "Dreams Come True"], ["朝日の洗礼", "DIAMOND15", "Dreams Come True"]]

Predicate で要素を得る

次に setFilterPredicate(NSPredicate) を使ってクエリー(検索?)を実行してから、その結果を arrangedObjects で得る方法。

まず、predicateWIthFormatNSPredicate オブジェクトを作る。Predicate のフォーマットに関しては、ここを参照。

SQL のクエリーみたいに =、!=、LIKE とか使える。でも LIKE のワイルドカードは ? と * だけで、% を使ったものは、BEGINSWITH、CONTAINS、ENDSWITH を使う。MATCHES を使うと正規表現での検索もできる(アップルのドキュメントではここを参照しろと書いてある)。これは %@ と %K が特別な意味を持つことと関係あるかも。%@ は object value の代わりに、%K は keyPath の代わりに使うと書いてある。でも、

request = NSPredicate.predicateWithFormat('%K like %@',"title","朝日の洗礼")

なんてすると title は integer じゃない、なんてエラーが返ってくる。しかたないので、次のようにした。

request = NSPredicate.predicateWithFormat('%K == %@',"title","朝日の洗礼")

なぜか、keyPath に %K を使うと、like が使えない。ということで、== を使った。これは、完全一致。

さて、この例では、要素は一つしか扱っていないが、album と title の両方で絞り込みたいときには次のようにする。

request = NSPredicate.predicateWithFormat('%K == %@ && %K == %@',"title","朝日の洗礼","album","DIAMOND15")

これを setFilterPredicate(NSPredicate) で実行する。

@aryController.setFilterPredicate(request)

これで、絞り込まれた要素を arrangedObjects で得る。

result = @aryController.arrangedObjects

上で追加した要素に対しての結果は次の通り。

#<NSArray [#<NSCFDictionary {#<NSCFString "title">=>#<NSCFString "朝日の洗礼">, #<NSCFString "album">=>#<NSCFString "DIAMOND15">, #<NSCFString "artist">=>#<NSCFString "Dreams Come True">}>]>

テーブル上ではこんな感じになってる。

この場合のように、Precidate を満たす結果が1つと分かっていれば NSArray の最初の要素なので、その title を得るには次のように書けばいい。

result[0]['title'].to_s

これで、"朝日の洗礼" が得られる。Ruby の文字列として処理しないと行けない場合 (String#match などを使う場合)でなければ、to_s はなくてもいい(NSString オブジェクトとして処理される)。

全部の要素を得たい場合は、predicate を nil にすればいい。

@aryController.setFilterPredicate(nil)

result = @aryController.arrangedObjects

要素を取り除く

要素を取り除くには、要素の index を元に removeObjectAtArrangedObjectIndex(index) または removeObjectsAtArrangedObjectIndexes(NSIndexSet) を使うか、要素を元に removeObject(object) または removeObjects(array) で直接取り除く、もしくは、index で指定して選ぶか直接テーブル上で要素を選んで、removeSelectedObjects を使って取り除く。

Index を指定して要素を一つ取り除く

@aryController.removeObjectAtArrangedObjectIndex(2)

NSIndexSet で指定した範囲の要素を取り除く

@aryController.removeObjectsAtArrangedObjectIndexes(NSIndexSet.alloc.initWithIndexesInRange([1,2]))

Predicate で得た要素を取り除く

request = NSPredicate.predicateWithFormat('%K like %@',"title","朝日の洗礼")

@aryController.setFilterPredicate(request)

result = @aryController.arrangedObjects

@aryController.removeObjects(result)

NSIndexSet で要素を選択して(直接テーブル上で選択してもいい)取り除く

まず、setSelectionIndexes(NSIndexSet) で選択(テーブル上で選択)する。

@aryController.setSelectionIndexes(NSIndexSet.alloc.initWithIndexesInRange([1,2]))

次に、selectedObjects で選択された要素を選んで、removeObjects で消す。

@aryController.removeObjects(@aryController.selectedObjects)

この最後のは、Remove をバインディングしたボタンで用意してあれば、そっちでやった方が速い。まあ、あくまでこれもできるよ、ということで。

すべての要素を取り除く

全部の要素を消したい場合は、Filter Predicate を nil にして arrangedObjectsremoveObjects で消せばいい。

@aryController.setFilterPredicate(nil)

@aryController.removeObjects(@aryController.arrangedObjects)

これとは別に、setSelectionIndexes(indexSet) で、indexSet に NSIndexSet を使って、全部のインデックスを指定してすべての要素を選択してから、remove(sender) で消してもいい。NSIndexSet オブジェクトは、indexSetWithIndexesInRange(range) で作る。range には、0 から Array Controller の要素の数までを指定する。

@aryController.setSelectionIndexes(NSIndexSet.indexSetWithIndexesInRange([0,@aryController.arrangedObjects.length]))

@aryController.remove(nil)

ちなみに、要素の数がかなり多いときには、この2つ目の方法の方が処理がかなり速かった。ただ、この2つ目の場合、tableViewSelectionDidChange(notification) という delegate メソッドを使って、テーブル上の選択が変わったかを読み取っていると、この処理の度に呼び出されるので、そこで何らかの対策をとらないと面倒なことになる。

でも実は、removeObjectsAtArrangedObjectIndexes(indexSet) を使えば何の問題もないんじゃないかと最近気づいた。

@aryController.removeObjectsAtArrangedObjectIndexes(NSIndexSet.indexSetWithIndexesInRange([0,@aryController.arrangedObjects.length]))

これで、スピードも問題なく、選択されてしまうという問題もなく削除されるはず。