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

Pantherで新たに導入されたCocoaバインディングというメカニズムがあるのですが、mkinoさんの運営されているサイト「HDMT(HAPPY Macintosh Developing TIME!)」のBBSでNorikoさんが書き込まれたスレッド「バインディングで目から鱗ーTabelViewなんて不要だったのね。」に触発され、紹介されていたCocoaバインディングの解説サイトとmkinoさんのサイトのAppleドキュメントの翻訳をもとに自分でもサンプルプログラムを作ってみました。
サンプルを作る過程を示すことでみなさんのお役に立てれば幸いです。また、私の理解内容に誤りがある場合もありますので、そのときはご遠慮なくご指摘ください。

なお、サンプルを作成するにあたって有益な情報をいただいたmkinoさん、Norikoさん、および「Cocoaハヤッパリ!」の鶴薗さんに感謝いたします。m(__)m

【参考にしたサイト】

「HAPPY Macintosh Developing TIME!」(mkinoさんのサイト)
http://homepage.mac.com/mkino2/

「Cocoaはやっぱり!」(鶴薗さんのサイト)
http://www.big.or.jp/~crane/cocoa/

「MacDevCenter.com- The Cocoa Controller Layer [Apr. 06, 2004]」
http://www.macdevcenter.com/pub/a/mac/2004/04/06/cocoa.html

このドキュメント内にはMVC(Model View Controller), KVC(Key-Value Coding), KVO(Key-Value Observing)という言葉が出てきますが、これらの概念についてはについては上記サイトを参照してください。

【作成環境】

このサンプルは以下の環境で作成しています。

Mac OS X v10.3.3
Xcode v1.2

1.サンプルの概要

このサンプルプログラムは書籍情報の一覧を表示・更新するもので、以下のような画面を持ちます。

画面は上下2つのパートに分かれていて、上半分が一覧の表示領域(リスト上で更新も可能)で、下半分が上部の一覧で現在選択されている書籍の詳細情報を表示・更新する部分です。また、上部のボタンでは一覧への追加・削除を行います。
これはAppleドキュメント「Cocoa Bindings: Creating a Master-Detail Interface」でMaster-Detail Interfaceと呼ばれているものです。
このサンプルでは、上部のリスト上および下部の詳細情報の各パート双方でデータ更新が可能になっており、どちらかで更新を行った場合はその内容をもう一方に自動的に反映する必要があります。

2.主なクラス・インスタンス間の関係

このサンプルを構成する主なクラス・インスタンス間の関係の概略図を以下に示します。(クリックすると拡大表示されます)

注:Model枠内のNSMutableArrayは実際にはNSArrayController内で自動的に生成されます。(2004/06/04追記)

このうち、色付けされているクラスが今回コーディングを行ったもので、残りはすべてInterface Builder(以下IB)上での設定で実現されているものです。
なお、書籍を表すクラスUCBookは、このサンプルのように単純なモデルデータの場合FoundationのNSMutableDictionaryでも十分で、わざわざクラスを起こす必要はないのですが、Cocoaバインディングの基礎となっているKVC(キー・バリュー・コーディング)の概略をつかむために作成しています。(このサンプルでの習得が終わったらクラスUCBookをNSMutableDictionaryに変更して実験してみてください。それこそデータの保存等を考えなければ、このサンプルの機能範囲であればコーディングはいっさい不要ということになります!!)
また、File's OwnerのDelegateとなっているUCAppControllerは、Cocoaバインディングと直接は関係ないのですが、アプリケーションの起動/終了時にモデルデータの読み込み/保存を行うために作成したクラスで、このページ内では作成しません。別途データの読み込み/保存機能の追加として説明する予定です。

3.プロジェクトの作成

参照した解説サイトではドキュメントベースのアプリケーションで作成するようになっていますが、複数のドキュメントを開く必要もないのでここではCocoa Applicationのプロジェクトを作成します。

4.モデルクラス(UCBook)の作成(M-V-CのM)

書籍情報は以下の3つの属性を持つこととします。

属性 アクセスキー
タイトル title
作者 author
コメント comment

表のアクセスキーというのはKVCで各属性にアクセスするときのキーです。
書籍情報を表すクラスとして以下のクラスを定義します。

UCBook.h : 書籍クラスのインターフェース

@interface UCBook : NSObject
{
    // インスタンス変数
    NSString    *_pstrTitle;            // タイトル
    NSString    *_pstrAuthor;           // 作者
    NSString    *_pstrComment;          // コメント
}

// 初期化
- (id)init;

// 後処理
- (void)dealloc;

//
// アクセサ(キー・バリュー・コーディング準拠)
//
// タイトル用
- (NSString *)title;
- (void)setTitle:(NSString *)pstrTitle;

// 作者用
- (NSString *)author;
- (void)setAuthor:(NSString *)pstrAuthor;

// コメント用
- (NSString *)comment;
- (void)setComment:(NSString *)pstrComment;

@end

定義内容は、個々の属性を保持するインスタンス変数とそれにアクセスためのアクセサメソッドを定義したものです。
この中で重要なのはアクセサメソッドの名前で、Cocoaバインディングが要求するKVCに準拠するためには以下の形式でなければなりません。

-<Key> :<Key>で指定される属性値の取得用メソッド
_is<Key> :<Key>で指定される属性値の取得用メソッド(属性値によりボタンの状態に反映する等BOOL値を返すもの?)
-set<Key>: :<Key>で指定される属性値の設定用メソッド(<Key>の先頭は大文字とする)

※<Key>は属性に付与したアクセスキー

例えば、タイトルという属性値(NSString型とする)に割り当てたアクセスキーが"title"であった場合、取得・設定用メソッド名は以下のようになります。


// 取得メソッド
- (NSString *)title;

// 設定用メソッド
- (void)setTitle:(NSString *)pstrTitle;

この規則に則っていることで、Cocoaバインディングを実現するために導入されたNSControllerクラス(実際はそのサブクラス)のインスタンスは、モデルとビューとの対応付けとしてIBで設定されたキー文字列からモデルオブジェクトにアクセスするメソッド名を自動生成して呼び出すことができるようになります。
つまり、KVCに準拠することで汎用のコントローラクラスに手を入れなくても、IBの設定で属性に対応するキーを指定するだけでアプリケーション毎に異なるモデルとビューを結びつけられるようになるわけです。

また、上記アクセサメソッドがインプリメントされていなくても、以下の命名規則のいずれかに従ったインスタンス変数が存在すれば、コントローラはメソッド経由ではなくこれらのインスタンス変数に直接アクセスすることで属性値の参照/設定を行うことができます。(詳細はmkinoさんのサイトの和訳記事参照

  _<key> or _is<Key> or <Key> or is<Key>

余談: メソッド名やインスタンス変数の実行時の動的検索なんて、Objective-Cの持つ(オブジェクト指向言語での)動的バインディングのメカニズムやクラスやメソッド(セレクタ)自体もオブジェクトとして扱える等、柔軟性のある言語仕様とランタイム環境のメリットを十二分に活かしている感じですね。C++ではこうはいかないでしょう。

次にインプリメントファイルは以下のように定義します。

UCBook.m : 書籍クラスのインプリメント

@implementation UCBook

//
// 初期化処理
//
- (id)init
{
    // 親クラス呼び出し
    self = [super init];
    if ( self == nil ) {
        return nil;
    }

    // 各メンバ変数初期化(新規作成時のデフォルト値)
    _pstrTitle   = [[NSString alloc] initWithCString:"Untitled"];
    _pstrAuthor  = [[NSString alloc] initWithCString:"Unknown"];
    _pstrComment = [[NSString alloc] initWithCString:""];

    return self;
}

//
// 後処理
//
- (void)dealloc
{
    if ( _pstrTitle != nil ) {
        [_pstrTitle release];
    }
    if ( _pstrAuthor != nil ) {
        [_pstrAuthor release];
    }
    if ( _pstrComment != nil ) {
        [_pstrComment release];
    }

    [super dealloc];
}

//
// タイトル取得
//
- (NSString *)title
{
    return _pstrTitle;
}

//
// タイトル設定
//
- (void)setTitle:(NSString *)pstrTitle
{
    [_pstrTitle release];
    _pstrTitle = [pstrTitle retain];
}

    *** 以下作者、コメントについても同様のアクセサメソッドを作成する ***

@end

4.ビューの作成(M-V-CのV)

IBで以下のようにウィンドウにコントロールを配置します。

5.コントローラの作成(M-V-CのC)

いよいよM-V-Cパラダイムの最後の要素、コントローラを作成します。
今まではアプリケーション毎に独自のコントローラクラスを作成し、シコシココーディングをしていたわけですが、Panther以降ではCocoaバインディングメカニズムとして汎用のコントローラクラスが追加され、これを使ってIB上の操作でモデルとビューとの関係づけができるようになりました。
コントローラ関連クラスには以下のものがあります。

NSController コントローラ関連クラスの基底クラス
NSObjectController 単一のモデルオブジェクトを管理するコントローラクラス
NSArrayController 複数のモデルオブジェクトを管理するコントローラクラス
NSUserDefaultsController ユーザデフォルトファイルとのやり取りを管理するコントローラクラス

このサンプルでは複数の書籍オブジェクトを扱うのでNSArrayControllerを使用します。

XcodeではInterface Builderパレットに以下のようなコントローラのペインが追加されました。(右端の">>"をクリックすると表示されるメニューで"Controllers"を選びます)

アイコンは左からそれぞれNSUserDefaultsController, NSObjectController, NSArrayControllerです。
今回はNSArrayControllerを使用しますのでパレットの右端のアイコンをnibウィンドウ上にドロップします。(インスタンス化)

ドロップ後はインスタンスの名前をNSArrayControllerからBookControllerに変更しておきます。

6.コントローラとモデルのバインディング

作成したコントローラインスタンスを選択しそのinfoパネルの「Attributes」で以下の操作を行います。

(1) 「Object Class Name:」をデフォルトのNSMutableDictionaryから今回使用するモデルのクラスであるUCBookに変更する。

すなわちここではこのコントローラが扱うべきモデルデータが何のクラスのインスタンスの配列であるかを指定しているわけです。

(2) 「keys」リストに以下のキーを追加する。
title
author
comment

ここでは(1)で指定したクラスインスタンスの持つ各属性にアクセスするときのキー文字列を指定しています。
これにより「4.モデルクラス(UCBook)の作成」で述べたように、(1)で指定したクラスインスタンスの各属性にアクセスするメソッド名を動的に生成してアクセスできるようになります。

なお、nibファイル保存時に以下のダイアログが表示されますが、「Save in 10.2 Format」をクリックしてそのまま保存します。

7.ビューとコントローラのバインディング

(1)Masterインターフェース部のバインディング

●NSTableViewのバインディング

まず、Masterインターフェース部のNSTableViewとコントローラのバインディングを行います。
今まではNSTableViewへデータを表示させるにはデータソースの設定で行っていましたが、CocoaバインディングではNSTableViewの中の個々のカラムに対するコントローラとのバインディングの設定という形で行います。
従って、まずNSTableView自体ではなく、内部のタイトル部のカラムを選択状態にします。

次にInfoパネルの「Bindings」を開きます。

いろいろな項目が並んでいますが、表示する値に関するのは「Value」ですのでValueの項目をクリックします。(その他の項目については別途値の変換の実験のところで扱います)

クリックするとパネル内がValueの設定画面に切り替わります。
ここで以下の設定を行います。

「Bind」ボタンにチェックを入れてこのビューがコントローラとバインドされることを示します。(明にチェックしなくても「Binding to:」の設定を行う事で自動的にチェックされます)
「Binding to:」に先ほど作成した「BookController」を選択します。
「Controller Key:」に「arrangedObjects」を選択します。ここで指定するのはNSArrayControllerから値を取り出すメソッドそのものです。
ここはNSTableViewのカラム、すなわち複数データを表示するViewの設定なのでコントローラがNSMutableArrayで保持している全データを取得するメソッド「arrangedObjects」を指定します。
「Mdel Key Path:」にこのカラムに表示するモデルオブジェクトの属性値のキー「title」を選択します。
これは「Controller Key:」で指定したメソッドで取得したオブジェクトから目的の属性値を取得するためのキーとなります。

以下同様に作者のカラムについてもバインディングの設定を行います。

●エントリの追加/削除ボタンのアクション設定

NSTableViewのバインディングが完了したら、次にエントリの追加/削除ボタンのアクションの設定を行います。

「Add」ボタンアクションのターゲットとしてBookControllerを指定し、NSArrayControllerが持っているアクションメソッド「add:」に接続します。
「Remove」ボタンも同様にBookControllerの「remove:」メソッドに接続します。
これによりボタンのクリックでコントローラに対してエントリの追加・削除の指示が出せるようになります。

エントリの追加/削除ボタンとコントローラのバインディング

例えば削除ボタンの場合、NSTableView上で何も選択されていない状態では削除するべき対象が存在しないということなのでボタンを無効にしたほうがよいわけですが、Cocoaバインディングではこのような制御もIB上のバインディングの設定で行えるようになっています。
ボタンの状態をコントローラで制御できるようにするにはまずボタンを選択し、InfoパネルのBindingsを開きます。

ボタンの状態をコントローラとバインドするにはAvailabilityの中のenabledを選択します。

ここで以下の設定を行います。

「Bind」ボタンにチェックを入れてこのボタンがコントローラとバインドされることを示します。(明にチェックしなくても「Binding to:」の設定を行う事で自動的にチェックされます)
「Binding to:」に先ほど作成した「BookController」を選択します。
「Controller Key:」に「canAdd」を選択します。
このメソッドはコントローラがデータの追加操作を行えるか否かを返すものです。このメソッドの返す値によりボタンの有効/無効が決まります。
追加ボタンの有効性はモデルデータの値に依存する訳ではないので「Mdel Key Path:」には何も指定しません。

以下同様に削除ボタンについてもバインディングの設定を行います。削除ボタンの場合「Controller Key:」に指定する値は「canRemove」になります。

(2)Detailインターフェース部のバインディング

次にDetailインターフェース部の各NSTestFieldとコントローラのバインディングを行います。
NSTableViewのカラム同様に、タイトルのテキストフィールドを選択してInfoパネルの「Bindings」を開きます。
こちらも表示する値のバインディングなのでValueの項目をクリックします。

ここで以下の設定を行います。

「Bind」ボタンにチェックを入れてこのビューがコントローラとバインドされることを示します。(明にチェックしなくても「Binding to:」の設定を行う事で自動的にチェックされます)
「Binding to:」に先ほど作成した「BookController」を選択します。
「Controller Key:」に「selection」を選択します。
先程述べたようにここで指定するのはNSArrayControllerから値を取り出すメソッドそのもので、「selection」はリスト上で現在選択されているオブジェクトを取得するものです。
「Mdel Key Path:」にこのカラムに表示するモデルオブジェクトの属性値のキー「title」を選択します。

以下同様に作者、コメントのフィールドについてもバインディングの設定を行います。

8.とりあえず走らせてみる

以上でCocoaバインディングに関して必要なコーディングとIBでの設定は完了しましたのでビルドして走らせてみましょう。
いかがでしょうか。ほとんどコーディングなしでIB上の設定だけでここまで動くのはちょっと感動的です。Norikoさんが「バインディングで目から鱗」と言われた理由、納得いたしました。

次はCocoaバインディングからは離れますが、このサンプルプログラムのモデルデータのファイルへの保存/読み込み機能を組み込みたいと思います。

サンプルプログラムのダウンロード(1.4MB)


一覧に戻る