Cocoaはやっぱり!
Cocoa Binding
バインディングって何なのさ

今回のテーマ

今回のテーマは、Mac OS X 10.3 Pantherで新たに追加された「 Cocoa Binding 」です。まずは、これがどんなものかを簡単に説明していきます。

推奨環境 この解説は、以下の環境を前提に作成し、動作確認等を行っています。ご確認ください。

改版履歴

Cocoa Bidingとは何か?

Mac OS X 10.3でCocoaのフレームワークには、Cocoa Binding ( ココアバインディング ) という機構が追加され、それに伴ってNSControllerを始めとするいくつかのクラスやプロトコルなども追加されました。ここでは、このCocoa Bindingについて説明していきます。

そもそも「 Cocoa Bindingって何か? 」ということですが、簡単に言うと「 異なるオブジェクト間で様々な値を同期するための仕組みを提供するもの 」ということになります。...と、こんな抽象的な説明ではピンと来ないかもしれませんので、具体例を見てもらいましょう。

注:bindingというのは、bind、つまり「束ねる」とか「結びつける」とかが一般的な意味で、異なるオブジェクトの値を常に同じ値にするための意味として使っています。紙を束ねるバインダーもこのbindです。

カレンダーソフトにおけるバインディング

バインディングの説明として月間カレンダーをサンプルに説明をしていきます。ユーザインターフェイスは以下のようなものを考えます。yearのところに表示したい年、monthのところに表示したい月を入力すると、imageのところにカレンダーのイメージが表示されます。

このカレンダーを実現するために、「 年と月を与えると、月間カレンダーのイメージを生成する 」クラスを準備します。名前を「 Calendar 」クラスとします。以下のように、インスタンスとして、カレンダーのイメージを保持するimage、表示中の年と月の値を保持するyearとmonthを持ちます。メソッドとしては、カレンダーイメージを取得するimageと、年を変更するsetYear :、年を取得するyear、月を変更するsetMonth :、月を取得するmonthを用意します。

このアプリケーションで、ユーザがユーザがyearのところに「 2005 」のように年の値を入力としたとします。すると、Calendarクラスのyearの値も「 2005 」に変えなければいけません。つまり、setYear : メソッドを呼び出して、yearを変更する処理をどこかに書く必要があります。また、yearが変更されたということは、Calendarクラスのimageも連動して変更されることになりますので、それをimageメソッドを使って取得して、ユーザインターフェイスのimageにセットして画面に表示させる必要があります。

このように、ユーザインターフェイスとCalndarクラスという異なるオブジェクトの間で、値を同じに保つという同期処理を行う必要があります。もちろん、monthについても同様の同期処理が必要です。同期処理を行うことで、下図のようにオブジェクトがリンクして値を共有しているように動作させることが可能になります。

さらに、このアプリケーションに「 終了時に年と月の値を初期設定ファイルに保存し、次回起動時には、その値を読み込んで前回の終了時と同じ状態に復帰させる 」という機能を追加するとします。このためには、初期設定を扱うNSUserDefaultsとの間ともyearとmonthの値を同期する必要がでてきます。

アプリケーションの規模が大きくなってきて、扱うオブジェクトが多くなると、同期するオブジェクトも増え、同期の処理の書き忘れなどでバグを抱えてしまう可能性もあります。同期の処理というのは、やっていることは値のコピーという単純な処理であることが多く、また、アプリケーションを作るたびに同じようなコードを書くこともしばしばです。Cocoa Bindingというのは、このオブジェクト間の値の同期を自動化するもので、退屈なコーディンクからプログラマーを解放してくれる仕組みなのです。

アプリケーションには様々な形態がありますので、Cocoa Bindingにも様々な同期の機構を持っています。ここで説明したのは極一部の例でしかありません。その辺りも順を追って説明していきます。

WebViewのバインディングを使ってみる

さて、やはり実際にアプリケーションを作って体験してみないと実感がわかないかもしれませんので、バインディングを実際に使ってアプリケーションを作ってみましょう。簡単なWebブラウザをバインディング機構を使って作ってみます。

●URLをバインドする

まず、Cocoa Applicationのプロジェクトを作ります。WebKit.frameworkをプロジェクトに追加して、MainMenu.nibをInterface Builderで開きます。そして、ウィンドウにWebViewとURLを入力するためのNSTextFieldを以下のように配置します。

ここで値を同期させたいのは「 WebViewが表示しているページのURL 」と「 NSTextFieldで表示しているURLの文字列 」です。ということで、NSTextFieldをWebViewにバインドする手順を説明しましょう。

NSTextFieldを選択して、Infoパネルの「 Bindings 」を開きます…(1)。すると、沢山の設定項目が表示されますが、一番上にある「 Value 」のところを開きます…(2)。このValueというのはNSTextFieldのValue、つまり表示している文字列のことです。この操作でバインド元を選んでいるわけです。これを何かにバインドするには、まずBindのチェックボックスをオンにします…(3)。ここからはバインド先の指定になります。「 Bind To 」のところに現在バインド可能なものが表示されますので、メニューから「 WebView 」を選びます…(4)。すると、「 Model Key Path 」のところに、WebViewの持っている属性の一覧がでてきます。ここから「 mainFrameURL 」を選択します…(5)。mainFrameURLというのは、表示中のページのURLのことです(mainFrameという修飾がついているのは、フレーム分割されているページの場合にその一番上のフレームを示すためです)。

これで、WebViewのmainFrameURLの値とNSTextFieldのValueの値がバインドされて、値が同期されることになりました。では、実行して、NSTextFieldにURLを入力してリターンキーを押してみてください。これでWebViewにそのページが読み込まれるはずです。また、表示されたページのリンクをクリックして別のページにジャンプしてみてください。NSTextFieldのURLも連動して変化するはずです。NSTextFieldのValueが変化したときには、それがWebViewへ伝えられ、逆に、WebViewのmainFrameURLが変化したときも、それがNSTextFieldに伝えられるというのが自動的に行われていることが分かると思います。

これを、バインド機構を使わずに行うとどうなるか書いてみましょう。NSTextFieldからWebViewへの通知は、NSTextFieldのアクションをWebViewの takeStringURLFrom : に繋ぐことで実現します。WebViewからNSTextFieldへの通知は、WebViewのWebFrameLoadDelegateアウトレットにこのデリゲートのインスタンスを接続して、webView : didStartProvisionalLoadForFrame : を実装しておきます。このメソッド呼ばれたらURLが分かりますので、自分自身にURLの文字列をセットするという処理を行います。やっていることは、結局のところ「 値の同期 」なのですが、片方はアクション、もう片方はデリゲートによる通知という手法になっています。

●Webページのアイコンをバインドする

もう1つ例を書いてみましょう。Webページのアイコンを表示するというものです。まず、NSImageViewをアイコンを表示する程度の小さい大きさで配置します。WebViewのmainFrameIconという値にはWebページのアイコンが格納されていますので、これとNSImageのimageをバインドしましょう。やり方は先程と同様で、NSImageViewでvalueのところからバインドの設定を行います。バインドする先はWebViewのmainFrameIconにします。

これで、アイコンつきのWebページを表示読み込ませれば、NSImageViewにはそのアイコンが表示されるはずです。バインド機構を使わない場合は、WebFrameLoadDelegateのwebView : didReceiveIcon : forFrame :メソッドでNSImageViewを更新する必要があります。

●ページタイトルをバインドする

Webブラウザのウィンドウのタイトル部分にはWebページのタイトルが表示されるのが一般的です。これもバインドで実現できます。NSWindowのtitleとWebViewのmainFrameTitleをバインドすることで、ページが読み込まれたときに、ウィンドウタイトルが自動的に書き換わるようになります。バインディングは、NSViewに限定されたものではなくて、その他のクラスにも適用できるわけです。

●ボタンのenabledの値をバインドする

今までは、URL、アイコン、タイトルといった目に見えるデータばかりをバインドしてきましたが、ボタンのenabledの値のようなものもバインドすることができます。Webブラウザの戻るボタンは、履歴の先頭にあるページを表示中は使用不可能にしなければいけませんが、この制御もバインドにより自動化することが出来ます。

WebViewには、canGoBackという値を持っていて、戻ることができるときのみYESになります。ですので、戻るボタン(NSButton)を配置して、こボタンのenabledをcanGoBackにバインドすれば、自動的にenabledの値の制御が可能になるわけです。同様に、canGoForwardを使えば、進むボタンの制御も同様に可能になります。

●値の変換 ( Value Transformer )

ダウンロードの経過表示を表すプログレスバーも追加してみましょう。NSProgressIndicatorを配置して、valueをWebViewの経過の値を保持しているestimatedProgressにバインドします。estimatedProgressの値は、0.0〜1.0ですので、プログレスバーのMinimum RangeとMaximum Rangeの属性もこれに合わせておきます。これで、経過表示は行われるようになりますが、さらに、読み込み完了時にはプログレスバーを隠すようにしてみます。

NSProgressIndicatorのBidingsの項目にhiddenがあります。これを、WebViewのisLoadingとバインドします。しかし、isLoadingは読み込み中がYESなので、バインドするだけでは、読み込み中にhiddenがYESになって隠れてしまうという逆の動作になってしまいます。この場合は、Infoパネルの中にある「 Value Transformer 」を使います。Value Transformerというのは、オブジェクト間で値を同期する際に、値の変換を行うための仕掛けで、NSNegateBooleanというのは名前のとおり、論理値の否定ですので、YESのときにNO、NOのときにはYESに変換してくれるものです。

このように、バインド機構には値の変換の仕組みも持っています。値の変換を指定しない場合は、バインドしているインスタンスの種類に応じたデフォルトの変換が行われます。一番最初に例として説明した、URLのバインディングについて見てみると、NSTextFieldのValueはNSStringで、WebViewのmainFrameURLはNSURLですが、デフォルトの値変換が行われているため何も指定しなくても動いているわけです。

●複数インスタンスの同時バインド

Webブラウザには、現在表示中のページの詳しい情報を表示するための情報ウィンドウを持っているものがあります。この情報ウィンドウを作ってみましょう。下図のようにURLとタイトルを表示するとします。

この場合も、今までと全く同じ手順でバインドができます。WebViewのmainFrameURLやtitleは、複数のインスタンスからバインドされることになるわけですが、この場合も全く問題なく同期処理が行われます。

今回の場合は、WebViewが持っているmainFrameURLの値がマスターデータで、このマスターデータに変更があったときには、全てのバインド元に変更通知が行われます。ユーザがURLを入力した場合は、URL入力を行うNSTextFieldからマスターデータに変更依頼を行ないます。そして、マスターデータの変更が行われ、その結果として全てのバインド先への通知が行われることになります。

このように、バインディングを使うと複数のインスタンスをも同期できることが分かっていただけたかと思います。アプリケーションが複雑化すればするほど、同期を行うインスタンスが増えて、それを正しいタイミングでもれなく同期するためのコードが増えることになりますが、バインディングによって、そういったコードは書かなくてもよくなるわけです。また、バインド元とバインド先の間では、値を同期するだけの仕組みだけで繋がっているため、お互いの依存性は非常に低く保たれるというメリットもあります。