ラベル ヒレガス本 の投稿を表示しています。 すべての投稿を表示
ラベル ヒレガス本 の投稿を表示しています。 すべての投稿を表示

2010-12-02

設定変更を通知する - 環境設定パネルを作る #4 (MacBloggerGlass)

今回の記事の内容は、環境設定パネルで Blog ID を保存したときに(「Save」ボタンを押す)、アプリウィンドウに表示されているブログ一覧が変更された ID のものに自動的に変わるようにするための実装について。このような動作を実装する意図については、前回の記事(→「次は NSNotification に取り組む」)を参照してもらいたい。

変更点

実装の手順は以下の 4 つ。

  • (a) 通知の名前を文字列定数として定義する。
  • (b) 通知を送るオブジェクトで、通知を送るコードを追加する。
  • (c) 通知を受けるオブジェクトで、通知を処理するメソッド(通知ハンドラ)を定義する。
  • (d) 通知を受けるオブジェクトで、監視の登録のためのコードを追加する。
  • (e) 通知を受けるオブジェクトで、監視の登録解除のためのコードを追加する。

(a) はどこで定義しても構わないが、(b) と同じく通知を送るオブジェクトと一緒に定義する方が良いだろう。通知を受けるオブジェクトでは、送るオブジェクト(のクラス)のインタフェース部を #import することになる。

MacBloggerGlass の場合、通知を送るのが PreferenceController で、受けるのが AppController になる。

(b) の通知を送るタイミングは、環境設定パネルで「Save」ボタンが押されたとき。つまり、ボタンに対応するアクションメソッド内になる。

監視の登録 (d) については、少なくとも最初に環境設定パネルが開くより前でなければならない。確実なのは AppController が作られたときだ。AppController はアプリの MainMenu.xib にインスタンスとして登録されているから、アプリが起動した後は NIB がロードされる中で生成(というか復元)される。このタイミングは awakeFromNib で拾うことができる。一般にアプリの起動時に生成(初期化)されるオブジェクトなら init で登録すれば良い(ヒレガス本 Chapter 14 の例はこちらになっている)。また、登録の解除 (e) は dealloc で行う(これはヒレガス本の例と同じ)

実装

PreferenceController
インタフェース部: PreferenceController.h

追加したのは 1 行。クラスのインタフェースにふくまれるものではないので @interface {...} の外側に置かなければならない。こういう定数が増えたら、定数だけを独立させて別ファイルにすべきかも。

extern NSString * const MBGBlogIDChangeNotification;
実装部: PreferenceController.m

ヒレガス本からそのまま引き写し。

- (IBAction)saveSettings:(id)sender
{
    [...snip...]
    // send notification
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    NSDictionary *userInfo = [NSDictionary dictionaryWithObject:selectedBlog.blogID forKey:@"blogID"];
    [nc postNotificationName:MBGBlogIDChangeNotification
                      object:self
                    userInfo:userInfo];
    NSLog(@"Post notification with userInfo (%@)", userInfo);

    [self close];
}
AppController
実装部: AppController.m

通知ハンドラ handleBlogIDChange: は AppController.m だけから参照できる AppController のプライベートメソッドとした(ファイルローカルメソッドと呼ぶべきか?)。これについては荻原(2.0)本の「CHAPTER 09 カテゴリ」を参照のこと。

fetchPosts は GData クライアントライブラリを使って記事のフィードをリクエストするもの。これまで getFeed: というアクションメソッドの一部だったものを今回独立させた。これは、getFeed: と通知ハンドラの両方から呼べるようにするため。もっとも、今回の変更にあわせて、メインウィンドウから Blog ID を入力するフィールドと記事一覧を取得するためのボタンを取り除いたため、getFeed: は無用になってしまったが。

@interface AppController (PrivateMethods)
- (GDataServiceBase *)bloggerService;
- (void)fetchPosts;
- (void)renderHTML:(NSString *)content withBaseURL:(NSString *)baseURL;
- (void)handleBlogIDChange:(NSNotification *)note;
@end

@implementation AppController
[...snip...]
- (void)awakeFromNib
{
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    [nc addObserver:self selector:@selector(handleBlogIDChange:) name:MBGBlogIDChangeNotification object:nil];
}
@end

@implementation AppController (PrivateMethods)
[...snip...]
- (void)handleBlogIDChange:(NSNotification *)note
{
    NSLog(@"Received notification: %@", note);
    blogID = [[note userInfo] objectForKey:@"blogID"];

    [posts removeAllObjects];
    [self fetchPosts];
}
@end

プロトタイプ #0

右のスクリーンショットが今の MacBloggerGlass でこのブログを表示させた様子だ。NSSplitView を使い、上半分で記事一覧、下半分でその内容を表示させている。記事の表示には WebView に対して、取得した記事のフィードのコンテンツをただ文字列として流し込んでいるだけ。つまり、HTML としてきちんとしたドキュメントの構造になっていないし、もちろんスタイルシートや JavaScript も付いていない。ま、GData クライアントライブラリを使った RSS リーダのサンプルアプリといったところか。

今回のプロトタイプでは Blog ID の選択を環境設定に押し込めている。これは、ブログは高々 1 つしか見ることはないし、Blog ID を切り替える頻度は高くない、と想定したから。実際に動かしてみて思うことは、複数のブログを作っているなら他のブログも同時に見たいと思うかもしれない、ということ。つまりは、ブラウザのようにタブやウィンドウで複数のコンテンツを同時に開ける方が良いかもしれない。

ま、それは 1 つのブログをきちんと表示できるようになってから考えよう。せめて、GAE 版と同等の表示ができるようにしてからだ。

おまけ

予告(→「次は NSNotification に取り組む」)では「通知」に関連するドキュメントも探すと書いたが、時間切れで本当に探すだけになってしまった。まだ読めていない。リンクだけは「関連リンク」のところに挙げておく。

また、「通知」に関して書かれたドキュメントを検索していて便利そうなものを見つけた。Devpedia シリーズとでも呼べば良いのかな(URL の一部になっている)。右のスクリーンショットはそれを Safari で開いたところだ。

何が便利かと言うと、たとえば「Cocoa Core Competencies」なら、Cocoa アプリを作るために必要になるさまざまなトピックを一覧できること。また、それぞれについての簡単な説明(使い方をふくむ)が付いていて、より詳しいドキュメントへのリンクもある。ヒレガス本のように、アプリの作り方を順を追って説明するようなタイプのものではないが、実際にアプリを作るときにドキュメントを参照する出発点として役に立ちそう。

検索して見つかったのは以下の 3 つ。

参考文献

詳解 Objective-C 2.0
荻原 剛志
ソフトバンククリエイティブ ( 2008-05-28 )
ISBN: 9784797346800
Cocoa Programming for Mac OS X
Aaron Hillegass
Addison-Wesley Professional ( 2008-05-15 )
ISBN: 9780321503619

関連リンク

関連記事

2010-11-28

表示するブログの選択 - 環境設定パネルを作る #1 (MacBloggerGlass)

GAE 版 Blogger Glass では表示するブログの Blog ID を config.yaml という設定ファイルで指定するようになっている。また、実行時にユーザが(Google アカウント)でログインすることで、ユーザごとに Blog ID を指定できるように「Settings」画面も用意した(→「GAE アプリでユーザごとの設定を可能にする #5」)。

Mac 版では表示するブログの選択は「環境設定パネル(Preferences)」を開いて行う。GAE 版のように Blog ID を直接指定するのはメンドウなので、Google アカウントの ID をパスワードを入力することで、そのアカウントで作ったブログの一覧から選べるようにしたい。

今回は、そんな「環境設定パネル」でブログ一覧を取得し、表示するところまでを作り込む。

環境設定パネルを追加する

Cocoa アプリに対する「環境設定パネル」の設置手順は「ヒレガス本」の Chapter 12 〜 14 の内容となる。

ざっくり手順を並べると以下のようになる。

  1. 「環境設定パネル」を制御するコントローラを、NSWindowController を継承して作る。
  2. 適当な場所(AppContoller みたいなものを作っていればそこ)に「環境設定パネル」を表示させるアクションを作る。
  3. Interface Builder でメインメニューのメニュー項目(「アプリ名」>「Preferences...」)に「環境設定パネル」を表示させるアクションを接続する。
  4. 「環境設定パネル」用の NIB ファイルを作る(実際には XIB ファイル)。この NIB の File's Owner は「環境設定パネル」を制御するコントローラにする。
  5. Interface Builder でパネルを作る。
  6. 「環境設定パネル」を制御するコントローラに、パネルで使うアクション等を作り込む。
  7. 「環境設定パネル」の設定を User Defaults に書き込むようにする。
  8. User Defaults の値を参照するように、メインのアプリウィンドウのコードを変更する。

今回の記事の内容は、このうちの 6. までとなっている。7. と 8. は次回に持ち越し。

デザイン (意匠)

環境設定パネルの外観と簡単な使い方を以下に示す。

MacBloggerGlass ウィンドウデザイン
環境設定パネル
Google Account の ID (E-mailアドレス) とパスワードを入力し「Get Blog List」を押すと、下部の Blog for viewing にその Google アカウントで Blogger に作ったブログのタイトル一覧が現れる。アプリで表示させたいブログを選択し、「Save」ボタンを押して保存する。パネルを閉じるには左上のクローズボタンを押すか、ESC キーを叩く。

デザイン (設計)

Google アカウントの ID とパスワードを入力させて、ブログの一覧を取得するという機能は、GData ライブラリのパッケージにふくまれている Blogger 用サンプルにほぼそのままの形で作り込まれている。それをのまま流用しても良かったが(実際、GData を使った Google Data API へのアクセスの部分は流用している)、ここでは Cocoa Bindings を使ってみたい。つまり、「環境設定パネル」のコントローラ(PreferenceController クラスとした)を NSTableView のデータソースとするのではなく、モデルとなるオブジェクトを NSObjectController (あるいはその派生クラスたち)を介して、NSTableView と結びつけるようにしたい、ということ。

後述するように、モデルは、ブログごとの情報を抱えるオブジェクトと、その配列を抱えるオブジェクトの二段構えになっている。これは、Google Data API を経由してユーザのブログ一覧を取得した際に、GData ライブラリが生成するオブジェクトの構成をそのまま写し取ったものだ。

(GData ライブラリによるブログ一覧)
+---------------+
| GDataFeedBlog |
|---------------|            +----------------+
|- entries    --|----------->| GDataEntryBlog |
|  ...          | 1        * |----------------|
|               |            |  ...           |
+---------------+            +----------------+

GDataEntryBlog は個々のブログの ID やらタイトルやらといった情報を保持している。

この二段構えのモデル構造は以前の記事(→「Cocoa Bindings の使い方」)でダミーとして作ったモデルの構造と同じだ。あのサンプルアプリでは、二段構えモデルに対応するため、Cocoa Bindings のためのコントローラも NSObjectController と NSArrayController の二段構えとした。その構成をそのまま、今回の「環境設定パネル」でも用いる。

実装

モデル

モデルクラスは 2 つ。FeedBlog クラスは GData ライブラリが提供する GDataFeedBlog に対応するもので、EntryBlog クラスは同じく GDataEntryBlog に対応するものだ。どちらも対応する GData ライブラリのクラスを内部に抱える(has-a の関係)とともに、ビューで表示するための情報を抽出するためのクラスとして作った。抽出した情報(例: ブログのタイトル)は、主にビューから Cocoa Bindings の結びつきを介してアクセスするために用いる。

どちらのクラスにも、対応する GData ライブラリのオブジェクトをセットするためのメソッドを用意していて、そこで情報の抽出も行っている。

FeedBlog
//
//  FeedBlog.h
//  MacBloggerGlass

#import <Cocoa/Cocoa.h>
@class GDataFeedBlog;

@interface FeedBlog : NSObject {
    NSMutableArray *entries;
    GDataFeedBlog *gDataFeed;
}

@property (readonly) NSMutableArray *entries;
@property (readwrite, retain) GDataFeedBlog *gDataFeed;

@end
//
//  FeedBlog.m
//  MacBloggerGlass

#import "GData/GDataBlogger.h"
#import "FeedBlog.h"
#import "EntryBlog.h"

@implementation FeedBlog
- (id)init
{
    [super init];
    entries = [[NSMutableArray alloc] init];
    return self;
}

- (void)dealloc
{
    [gDataFeed release];
    [entries release];
    [super dealloc];
}

@synthesize entries;
@synthesize gDataFeed;
- (void)setGDataFeed:(GDataFeedBlog *)aFeed
{
    if (aFeed == gDataFeed) return;
    [gDataFeed release];
    gDataFeed = [aFeed retain];

    [entries removeAllObjects];

    EntryBlog *blog;
    GDataEntryBlog *entry;
    for (entry in [gDataFeed entries]) {
        blog = [[EntryBlog alloc] init];
        blog.gDataEntry = entry;        
        [entries addObject:blog];
    }
    NSLog(@"%d blogs was found.", [entries count]);
}
@end

setGDataFeed: で GDataFeedBlog の抱える配列から GDataEntryBlog の要素を取り出している for (var in array) { ... } という書き方が Objective-C 2.0 から導入された高速列挙 (fast enumeration) だ。Ruby や Python ではお馴染みのもの。配列の添字でループし、objectAtIndex: で配列にアクセスするよりもスッキリと書ける。array の部分には配列(NSArray)以外の集合(NSSet)や辞書(NSDictionary)も指定できる。詳しくは荻原(2.0)本 CHAPTER 08 の p.202 〜 206 あたりを参照のこと。

EntryBlog
//
//  EntryBlog.h
//  MacBloggerGlass

#import <Cocoa/Cocoa.h>
@class GDataEntryBlog;

@interface EntryBlog : NSObject {
    NSString *title;
    NSString *blogID;
    GDataEntryBlog *gDataEntry;
}

@property (readonly) NSString *title, *blogID;
@property (readwrite, retain) GDataEntryBlog *gDataEntry;

@end
//
//  EntryBlog.m
//  MacBloggerGlass

#import "GData/GDataBlogger.h"
#import "EntryBlog.h"

@interface EntryBlog (PrivateMethods)
- (NSString *)extractBlogID:(NSString *)identifier;
@end


@implementation EntryBlog
- (void)dealloc
{
    [gDataEntry release];
    [blogID release];
    [title release];
    [super dealloc];
}

@synthesize title, blogID;
@synthesize gDataEntry;
- (void)setGDataEntry:(GDataEntryBlog *)aEntry
{
    if (aEntry == gDataEntry) return;
    [gDataEntry release];
    gDataEntry = [aEntry retain];

    title = [[[gDataEntry title] stringValue] retain];
    blogID = [[self extractBlogID:[gDataEntry identifier]] retain];
}
@end

@implementation EntryBlog (PrivateMethods)

#pragma mark -
#pragma mark PrivateMethods
- (NSString *)extractBlogID:(NSString *)identifier
{
    if (!identifier || [identifier length] == 0) return nil;

    // GDataEntryBlog.identifier is in the format
    // "tag:blogger.com,1999:user-<user id>.blog-<blog id>"
    NSRange rangeBlog = [identifier rangeOfString:@"blog-"];
    int startPos = rangeBlog.location + rangeBlog.length;

    return [identifier substringFromIndex:startPos];
}

@end

EntryBlog.m の方で @interface EntryBlog (PrivateMethods) ... @end として宣言しているのは、このファイルからのみ参照するためのメソッドの定義だ。C でいう static 関数のようなもの。カテゴリの使い方の 1 つ。これも詳しくは荻原(2.0)本 CHAPTER 09 の p.227 〜 228 を参照のこと。

コントローラ

先に述べたように Cocoa Bindings のためのコントローラには NSObjectController と NSArrayController の 2 つを組み合わせて使う。左のスクリーンショットで Feed Observer となっているものが NSObjectController (のインスタンス)、Array Observer となっているものが NSArrayController (のインスタンス) だ。インスタンス名を Observer としているのは、どちらも KVO (Key-Value Observing) のため(だけ)に使っているからだ。

PreferenceController

環境設定パネルを表示するためのクラスで、NSWindowController を継承している。

GData を使ってブログ一覧を取得する部分は以前の記事(→「記事一覧を取得する」)で示したものとほとんど同じなので省略する。

以下に示すのは、ブログ一覧を取得するために GData ライブラリのサービスを呼び出した際に指定するコールバックメソッドだ。注意してほしいのは 102 と 104 行目。これは、KVO で キーに対応する変更通知を発行するための「作法」の 1 つ。103 行目で、feed が保持するオブジェクト(前述の FeedBlog のインスタンス)が抱える配列の内容が変更される。そのことを、PreferenceController に(「feed」というキーで)結びついているオブジェクト(今回の場合、それは Feed Observer という名前の NSObjectController のインスタンス)に通知している。

- (void)blogListTicket:(GDataServiceTicket *)ticket
      finishedWithFeed:(GDataFeedBlog *)aFeed
                 error:(NSError *)error
{
    if (error) {
        NSLog(@"ERROR in fetching the blog list: %@", error);
        return;
    }
    [self willChangeValueForKey:@"feed"];
    feed.gDataFeed = aFeed;
    [self didChangeValueForKey:@"feed"];
}

監視するオブジェクトの変更通知を受け取った Feed Observer (NSObjectController) は、今度は自分自身に(「entries」というキーで)結びついているオブジェクト(Array Observer という名前の NSArrayController のインスタンス)に変更通知を送る。それを受け取った Array Observer は、NSTableView (の中の NSTableColumn) に変更通知を送る。NSTableView は KVC によって結びついたオブジェクトのプロパティ(EntryBlog の title)を取得して、表示を更新する。このようにして、モデルの変更が Cocoa Bindings によって結びついたビューに伝播してゆくのだ。

次の展開

ブログ一覧を取得して表示できるようになっているが、まだ肝心の「Save」ボタンが機能しない。当然、メインウィンドウ(アプリ本体)とも連携していない。現段階では環境設定のためのパネルが単独で動いているだけ。

なので、次は「Save」ボタン(に対応するアクション)の実装と、アプリ本体とのつなぎ部分を実装することになる。

参考文献

詳解 Objective-C 2.0
荻原 剛志
ソフトバンククリエイティブ ( 2008-05-28 )
ISBN: 9784797346800
Cocoa Programming for Mac OS X
Aaron Hillegass
Addison-Wesley Professional ( 2008-05-15 )
ISBN: 9780321503619

関連記事

2010-11-26

Cocoa Bindings の使い方 #2 - Interface Builder 編

前回の記事(→「Cocoa Bindings の使い方」)では、Cocoa Bindings を使うことで、MVC のうち M (モデル) となるコードを書くだけでアプリが作れると書いた(標準部品とその振る舞いだけを使うアプリの場合)。ただし、その設定には Interface Builder に対する慣れが必要だ、とも書いた。

今回は、Interface Builder を使って Cocoa Bindings を設定する手順について書く。ネタ元は公式ドキュメント(Mac OS X Reference Library)の「Interface Builder User Guide」にある「Connections and Bindings」だ。

ヒレガス本の Chapter 8 でも、NSTableViewNSArrayController に結びつける手順が説明されている。サンプルアプリを作る手順をたどるハンズオンの資料としてはわかりやすいが、ツールについての説明が必要最小限にとどめられているため、自分でアプリを作ろうとすると情報が不足していることに気付く。上記のドキュメントはその足りない部分を補完してくれる。

「結びつき(Binding)」を作る

Connections and Bindings」の説明用に貼られているパネルのスクリーンショットは、(説明に使う)一部だけを開いてあるためダマされてしまうが、このパネルには似たような設定項目を持つセクションがいくつも並んでいる。

たとえば、右のスクリーンショットは、前回の記事で紹介したサンプルアプリの設定をしたときのものだが、複数のセクション(「Availability」とか「Controller Content Parameters」とか)に、右向きの三角が付いた項目がいくつもあることがわかる(「Availability」に「Editable」、「Content Controller Parameter」には「Filter Predicate」や「Selection Indexes」)。この右向き三角を開くとそれぞれに「Content Array」に表示されているのと同様な設定(「Bind to:」、「Controller Key」や「Model KeyPath」など)がある。

それぞれのセクションや項目がどういった「結びつき」を作るためのものなのかを知らないと、うっかり他の項目に「結びつき」を作ってしまうことになりかねない(前回のサンプルアプリでは実際そういう間違いをしてしまった)。これらの項目はオブジェクトのクラスによって変わるため、それぞれの項目がどういう振る舞いのためのものかは個別のクラスのリファレンスを参照するしかないようだ。一方で、それぞれの項目に対する設定のうち共通なものについては、「Connections and Bindings」の Table 7-1 に「Common binding attributes」としてまとめられている。

どの項目であれ、最低限、設定しなければならないのは「Bind to:」と「Model Key Path」の 2 つ。右のスクリーンショットでは、それぞれ Feed Controller、entries となっているものだ(この例では結びつける相手が NSObjectController のため「Controller Key」も指定している)。

「Bind to:」は文字通り、どのオブジェクトに結びつけるかを指定するもので、ドロップダウンリストを開くと Interface Builder のドキュメントウィンドウ(左のスクリーンショット)にあるオブジェクトの一覧が出る。先の例ではこの中の Feed Controller が選ばれている。

前回の記事に書いたとおり、このサンプルアプリでは NSArrayController が制御する配列(NSMutableArray)は別のコントローラ(NSObjectController のインスタンスで Feed Controller という名前にしてある)が制御するモデルオブジェクトが抱えるものだ。この配列に対する結びつきを指定しているのがこのパネルでの設定になる。

一方、Feed Controller 側と(配列を抱える)モデルオブジェクトとの結びつきも Interface Builder で設定している。右のスクリーンショットがその設定となる。ただ、こちらの設定は Cocoa Bindings とは異なり、NSObjectController が直接モデルオブジェクトを持つための設定だ(と思う)。ただ、そのアクセスに KVC が使われている。

「結びつき」のパターン

Connections and Bindings」では、結びつきのタイプを以下の 3 つのパターンに分類している。

  • Binding Directory to the Value (値に直接結びつける) [Figure 7-6]
  • Binding Through an Intermediate Object (中間オブジェクトを介して結びつける)
  • Binding to a Collection Objects (オブジェクトの集まりに結びつける) [Figure 7-9]

「中間オブジェクト」パターンについては、さらに 2 つに分けている。

  • Binding through another data object (別のデータオブジェクトを介して結びつける) [Figure 7-7]
  • Binding through an object controller (別のコントローラを介して結びつける) [Figure 7-8]

この分類にしたがえば、前回のサンプルアプリは、「別のコントローラを介して結びつける」と「オブジェクトの集まりに結びつける」の複合方式ということになる。

まとめ

前回のサンプルアプリはヒレガス本の手順を真似て、さらに「結びつき」を作るための試行錯誤を繰り返して作った。そうして動くモノができたものの、すっきりしない感じが残った。そのモヤモヤが「Connections and Bindings」を読むことで氷解した。とはいえ、最初にこのドキュメントに取り組んでいたとしても、すんなりと理解はできなかったに違いない。試行錯誤の後だからこその理解だと思う。

馴染みの薄い概念がふくまれていると、ドキュメントを読んでも表面的な理解で終わってしまう。そこが難しいところだよね。

ちなみに、この記事では「バインディング」「バインドする」とカタカナ表記で書くのが嫌で「結びつき」とか「結びつける」と書いてみた。Lisp 界隈で bind の訳語として定着している「束縛」を使わなかったのは、以前からなんというか語感が堅いと感じていたから。

ま、自分では、脳内で「bind」に変換して読んでいるんだから、どっちでも良いんだがね。他の人はどう感じるかな (・ω・)?

参考文献

Cocoa Programming for Mac OS X
Aaron Hillegass
Addison-Wesley Professional ( 2008-05-15 )
ISBN: 9780321503619

関連リンク

関連記事

2010-11-25

Cocoa Bindings の使い方

追記@2010-11-30

Cocoa Bindings の使い方については以下の後続記事も参照のこと。

ブログの記事一覧を表示するようなアプリでは、記事一覧のデータは、配列(のような何か)に蓄えることになるし、画面の表示は表形式(に似た何か)を使うことになるだろう。

Cocoa アプリでこれをシンプルに実現すると、配列 (NSMutableArray) に蓄えられたデータをテーブルビュー (NSTableView) に表示するものになる。MVC アーキテクチャで言うなら、配列がモデル(正確には配列に蓄えられたオブジェクトがモデル)で、テーブルビューが(その名の通り)ビューになる。

NSTableView にデータを表示させる場合、二通りの方法がある。1 つは、NSTableView のデータソースとなるクラスを定義する方法。具体的には、AppController というようなクラスを定義し(MVC の C)、そこで以下の 3 つのメソッドを定義するというもの。

  • - numberOfRowsInTableView:
  • - tableView:objectValueForTableColumn:row:
  • - tableView:setObjectValue:forTableColumn:row:

3 つ目のメソッドは表示のみのアプリなら必要ない。

この方法の欠点は、NSMutableArray の内容を NSTableView における表示と同期させるコードは、どれも良く似ているものになることだ。

良く似たものになるなら、その仕組みを共通化してフレームワークが提供してくれればありがたい。もちろん、そのためにはある種の「抽象化」が必要になり、利用に際しては一定の「作法」に従わなければならない。この「共通化された仕組み」が Cocoa Bindings であり、「抽象化」が KVC (Key-Value Coding)、「作法」が KVO (Key-Value Observing) ということになる。

サンプルアプリ

今回、サンプルとして作ったアプリのスクショがこれ(→)だ。機能的には(意匠的にも)ヒレガス本の Chapter 8 で作るもの(→ 「MVC と Cocoaバインディング」参照)と同等だが、こちらは、コードとして書いたのはモデルとなる 2 つのクラスのみで、ビューは当然として、コントローラも Cocoa フレームワークの提供するものだけで作られている。

モデル

まずはモデルを構成する 2 つのクラスを示す。これら 2 つは、Atom 形式のフィードから生成することを想定したものだ。Feed オブジェクトが Entry オブジェクトの配列を抱えるようになっている。

Entry の定義
//  Entry.h
#import <Cocoa/Cocoa.h>

@interface Entry : NSObject {
    NSString *title;
    NSDate *updated;
}

@property (readwrite, retain) NSString *title;
@property (readwrite, retain) NSDate *updated;

@end
//  Entry.m
#import "Entry.h"

@implementation Entry
- (id)init
{
    [super init];
    title = @"Hi!";
    updated = [[NSDate date] retain];
    return self;
}

- (void)dealloc
{
    [updated release];
    [title release];
    [super dealloc];
}

@synthesize title, updated;

- (void)setTitle:(NSString *)newTitle
{
    if (newTitle == title) return;
    [title release];
    title = [newTitle retain];
    self.updated = [[NSDate date] retain];
}

@end
Feed の定義
//  Feed.h
#import <Cocoa/Cocoa.h>

@interface Feed : NSObject {
    NSString *title;
    NSDate *updated;
    NSMutableArray *entries;
    int count;
}

@property (readwrite, retain) NSString *title;
@property (readwrite, retain) NSDate *updated;
@property (readwrite, retain) NSMutableArray *entries;
@property (readwrite) int count;


@end
//  Feed.m
#import "Feed.h"
#import "Entry.h"

@implementation Feed
- (id)init
{
    [super init];
    title = @"My Feed";
    updated = [[NSDate date] retain];
    entries = [[NSMutableArray alloc] init];
    [entries addObject:[[Entry alloc] init]];
    count = [entries count];
    return self;
}

- (void)dealloc
{
    [entries release];
    [updated release];
    [title release];
    [super dealloc];
}

@synthesize title, updated, entries;

- (void)setEntries:(NSMutableArray *)newArray
{
    if (newArray == entries) return;
    [entries release];
    entries = [newArray retain];
    self.count = [entries count];
}

@synthesize count;
@end
コントローラ

コントローラには NSObjectControllerNSArrayController を 1 つずつ使っている。これらのコントローラは Interface Builder から NIB (今は XIB)ファイルの中にインスタンス化し、設定を変更しているだけだ。コードは一切、書いていない。

これらコントローラの設定で肝となるのは、それぞれのコントローラが保持することになるモデルクラスとの対応づけだ。NSObjectController は 上述のFeed クラスのオブジェクトを保持する。NSArrayControllerEntry クラスのオブジェクトを生成し(Add ボタンのアクション)、消滅させる(Delete ボタンのアクション)。ただし、Entry オブジェクトたちを保持するのは Feed が抱えた配列だ。このため、NSArrayControllerFeed オブジェクトに対して要素となる Entry オブジェクトの追加、編集、削除を指示する必要がある。このとき用いられるのが Cocoa Bindings だ。

NSArrayController は配列を抱えた NSObjectController にバインドされることになる。

ビュー

スクショを見ればわかるように、いくつかのテキストフィールド(いずれも編集不可にしてある)とテーブルビュー(タイトル欄のみ編集可)、および 2 つのボタンから構成されている。いずれも Cocoa フレームワーク標準のビューたちだ。

テキストフィールドおよびテーブルビュー(のセル)は、Cocoa Bindings によって、2 つのコントローラのプロパティを経由してモデルと結びつけられている。このため、モデル(のプロパティ)が変更されれば、KVO の通知により自動的に変更がビューに反映されることになり、ビューによる変更はコントローラを経由してモデルに反映されるようになっている。

まとめ

MVC のうちモデルとなるクラスを書くだけで後は、Interface Builder の操作でアプリが作れてしまう。標準の部品(ビューとコントローラ)を使うアプリであれば簡単にできる。ただし、Interface Builder を使って Cocoa Bindings の設定するのには、ちょっと慣れが必要だ。似たような設定項目が多く、設定する場所を間違えたら、まったく動かない。今日もそれでかなり苦労した。

参考文献

詳解 Objective-C 2.0
荻原 剛志
ソフトバンククリエイティブ ( 2008-05-28 )
ISBN: 9784797346800
Cocoa Programming for Mac OS X
Aaron Hillegass
Addison-Wesley Professional ( 2008-05-15 )
ISBN: 9780321503619

関連リンク

関連記事

2009-11-05

MVC と Cocoaバインディング

ヒレガス本、Chapter 8 から Chapter 16 までのまとめ (ただし、Chapter 11 は除く)。

(p.124)
Over the next few chapters, you will create a full-featured application (・・・中略・・・) As this book progresses, you will add file saving, undo, user preferences, and printing capabilities.

これらの章では一つのアプリを継続して作る。少しずつ機能を追加していき、一般的なアプリに必要とされる機能を網羅した状態にまで作り込む (Chapter 27 ではさらに印刷機能が追加される)。

ここで作るアプリ自体に実用性はないが、備える機能はどうしてどうして立派なものだ。上記の引用にもある通り、GUI を使ったデータの入力・編集に始まり、編集時の Undo (と Redo)、ファイルへのセーブ(とロード)、環境設定用のパネル、そして GUI 部分の翻訳。機能を羅列すると入門書のサンプルプログラムとは思えないものだ。これは Cocoa と Objective-C の特性による。

実際にサンプルコードを打ち込みつつ、本に書かれた通りに作っていくと、打ち込むコード量に驚く。実現する機能に比べてあまりにも少ないから。↓がそのサンプルアプリだ。扱うデータはごく単純なものなので、何をするアプリなのかはウィンドウを見ればわかるだろう。

送信者 Macをプログラムする

このサンプルアプリを構築するために使われている主な技術を挙げてみよう。

各種 GUI 部品(ビュー)
ここでは表形式でデータを表示するための部品(table view)が主役になる。
ターゲット-アクション
GUI 部品(ビュー)とアプリのコードをつなぐ仕組み
キー値コーディングとキー値監視
オブジェクトのプロパティに対して、直接メソッドを呼び出すのではなく、名前を文字列で与えて間接的にアクセスする仕組み。
通知
あるオブジェクトの変更を他のオブジェクト群に知らせる仕組み
アンドゥマネージャ
undo と redo の仕組み
アーカイブ
データの保存(セーブ)と読み込み(ロード)の仕組み
ユーザ設定
ユーザ設定の保管と読み込みの仕組み
Info.plist
データを保存するファイルの拡張子やアプリのアイコンなど、付加情報の設定。

Cocoa のアプリは MVC アーキテクチャで作られることになる。フレームワークが提供するのは主にビュー(GUI 部品)、さらにビュー (V) とモデル (M)、コントローラ (C) をつなぐための仕組みだ。アプリプログラマはモデルとコントローラを書くことになるが、モデルとコントローラのためにもいろいろと便利な技術が用意されている (NSArrayController とか)。

MVC アーキテクチャは、1 つのシステムを 3 つの部分に分けて作ろうという設計思想だ。当然、それぞれの部分は他から独立していることが望ましい。独立性が高いほど、別々に作りやすくなるし、再利用性も高まる。一方で、最後は 1 つのシステムとして連携しなければならないのだから、間をつなぐ仕組みが必要。この「つなぐ仕組み」次第で 3 つの部分の設計(と実装)のしやすさが変わる。Cocoa で、この「つなぐ仕組み」として提供されているのが、「ターゲット-アクション」であり、キー値コーディング、キー値監視による「Cocoa バインディング」である。

「ターゲット-アクション」は、主にビュー(GUI 部品)とコントローラの「つなぎ」として使われていて(GUI 同士もこれでつなぐ)、一方「Cocoa バインディング」はモデルとコントローラの「つなぎ」になる。これらの特徴を端的に表現すると、「どこにつなぐかを実行時まで決めない」となる。実行時になってから決めるということは、コンパイル時には知らなくても良いということだ。だから、別々に作って、別々に配布・展開できる。Cocoa が提供するビューは、コントローラがどんなアクションを備えているかを知らない。Cocoa が提供するコントローラ(の再利用できる部分)は、モデルがどんなプロパティを持っているかを知らない。知らなくて良い。アクションやプロパティとの「つなぎ」はコンパイル時のリンクとは別の仕組みで伝えられるから。動的言語である Objective-C ならではの仕組みと言える。

「Cocoa バインディング」についてもっと知りたいんだが、ヒレガス本も荻原(2.0)本もこれについては詳しくはない。ここは原典というべき ADC ドキュメントに取り組むしかないようだ。このあたり(↓):

これの前提条件として、以下の文書を読め、と書いてある:

ちなみに *1 によれば、「Cocoaバインディング」が使えるようになったのは OSX の version 10.3 からだとのこと。Panther だ。

追記@2010-12-01

その後 Cocoa バインディングについて調べたり試したりしたことをまとめた記事をいくつか書いた。

関連記事