Cocoaサンプル - MVC

Xcode4を使ってMVCパターンに則った簡単なCocoaアプリーケーションのサンプルを作成します。

MVC(Model-View-Controller)パターン

MVCとは以下の3つの要素からなるアーキテクチャのことです。

Model

アプリケーションが利用するデータとその処理を行う部分です。データは隠蔽されていて操作自体は内部で完結しており、一般に再利用が可能。

View

データを可視化する部分。CocoaではNSViewクラスの派生クラスです。

Controller

ユーザーの操作を受け付けてModelとViewの制御を行う部分です。ModelとViewの仲介役ですので通常は再利用できないことが多いです。CocoaではこのクラスにOutletとActionを追加します。

MVCの相互関係

一般的な処理の流れは以下のようになります。

View ユーザー入力→ Controller 更新→ Model
View ←更新 Controller ←通知 Model

・ViewはControllerに指示を出す
・ControllerはModelとViewを制御する。
・ModelからみてControllerは間接的な相関関係があってもよい。状態が変化したことをControllerに知らせることができる。
※間接的な相関関係については後述

作成するサンプルは出来るだけ簡単なものにします。

仕様1:ウィンドウにボタンとラベルがあり、ラベルに0が表示されている。
仕様2:ボタンを押すと数字がカウントアップされてラベルに表示される

Viewを作成する

interface builderを利用して、ボタンとラベル(Static Text)を追加します。
Attributes InspectorでLabelのTitleを0にしておきます。

Controllerを作成する

NSObjectの派生クラスとして作成します。
Controller.h

#import <Foundation/Foundation.h>
@interface Controller : NSObject
{
    IBOutlet id label;
}
-(IBAction)onButtonClicked:(id)sender;
@end

Controller.m

#import "Controller.h"
@implementation Controller
- (id)init
{
    self = [super init];
    if (self) {
        // initialize
    }
    return self;
}
- (void) dealloc
{
    // finalize
}
-(IBAction)onButtonClicked:(id)sender
{
    [label setIntValue:1];
}
@end

ViewとControllerを関連付ける

Controllerオブジェクトをxibに追加します。

ユーザー入力

Viewからのイベントを受け取るために、ActionをControllerに追加して接続します。
具体的には、ボタンを押されたときのイベントハンドラをControllerクラスに追加して、

-(IBAction)onButtonClicked:(id)sender;

-(IBAction)onButtonClicked:(id)sender
{
    [label setIntValue:1];
}

Connections InspectorのReceived ActionsでボタンViewと接続します。この時点では仮実装としてラベルに1を表示することにします。

onButtonClicked <-- 接続-->  Push Button

Viewの更新

ControllerからViewを更新するには、OutletをControllerに作成してViewと接続します。
ラベルのOutletをControllerクラスのメンバに追加して、

IBOutlet id label;

Connections InspectorでStatic Text Viewと接続します。

label <-- 接続-->  Static Text

以上で、ViewとControllerを接続することができました。
この時点でビルド・実行してボタンを押すと1が表示されます。

Modelを作成する

データの操作、ここではカウントアップするクラスを追加します。
Model.h

#import <Foundation/Foundation.h>
@interface Model : NSObject
@property(readonly)int counter;
-(int)countUp;
@end

Model.m

#import "Model.h"
@implementation Model
@synthesize counter;
-(id)init{
    self = [super init];
    if (self) {
        counter = 0;
    }
    return self;
}
- (int)countUp
{
    ++counter;
    return counter;
}
@end

ModelとControllerを関連付ける

現時点ではControllerはModelと接続されていません。ControllerのメンバにModelクラスを追加します。
Controller.h

#import <Foundation/Foundation.h>
@class Model;  //  Modelとの接続
@interface Controller : NSObject
{
    IBOutlet id label;
    Model* model;  //  Modelとの接続
}
-(IBAction)onButtonClicked:(id)sender;
@end

さらに、イベントハンドラ内でModelとViewを更新します。
Controller.m

#import "Controller.h"
#import "Model.h"  //  Modelとの接続
@implementation Controller
- (id)init
{
    self = [super init];
    if (self) {
        model = [[Model alloc]init];  //  Modelとの接続
    }
    
    return self;
}
- (void) dealloc
{
    [model release];  //  Modelとの接続
}
-(IBAction)onButtonClicked:(id)sender
{
    int data = [model countUp]; // Modelの更新処理
    [label setIntValue:data];  // Viewの更新処理
}
@end

以上で実行すると、カウントアップされるので完成です。

ControllerとModelの接続にObserverパターンを利用する

この例ではControllerが直接ModelとViewを同じメソッドで更新していますが、Modelの処理が終わったことをControllerに通知したときにViewを更新するようなケースを考えてみます。

-(IBAction)onButtonClicked:(id)sender
{
    int data = [model countUp]; // ボタンを押したときはModelの更新処理だけ
}
-(void)Notified  // Modelから通知されたときに呼び出されるメソッド
{
    [label setIntValue:data];  // Viewの更新処理
}

ModelからControllerに通知する仕組みを入れたい場合、ModelクラスがControllerクラスを持ってしまうと再利用性に欠けてしまいます。

@interface Model: NSObject
{
    Controller* controller;  // ×これはあまり良くない
}

そこで初めの方で述べた間接的な相互関係を実現するのにObserverパターンを利用します。
実はNSObjectにはメンバの値を監視できるような仕組みがデフォルトで実装されているので、Observerパターンをすぐに実装することができます。メンバの値の変化を監視する方法に自動と手動の2つの実装方法がありますが、手動の方で実装してみることにします。

監視対象クラスにObserverを追加する

NSObjectに用意されているaddObserverメソッドを使います。

Controller.m

- (id)init
{
    self = [super init];
    if (self) {
        model = [[Model alloc]init];
        [model addObserver:self forKeyPath:@"counter" options:(NSKeyValueObservingOptionNew)  context:nil];
        
    }
    return self;
}
Modelから通知(Notify)された時に呼び出されるメソッドを追加する

監視対象の値が変化したことを知らせるためのメソッドとして、NSObjectのobserveValueForKeyPathメソッドが用意されているので追加します。
Controller.h

-(void)observeValueForKeyPath:(NSString *)keyPath
                     ofObject:(id)object
                       change:(NSDictionary *)change
                      context:(void *)context;

Controller.m

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if ([keyPath isEqual:@"counter"]) {
        int data = [model counter];
        [label setIntValue:data];  // Viewを更新
    }
}

次に観察対象Modelの実装をします。

値が変化したことを通知する

値が変化したことをObserverに対してNotifyするためのメソッドが、NSObjectのwillChangeValueForKey(値が変化する直前に実行する),didChangeValueForKey(値が変化した直後に実行)です。
Model.m

- (int)countUp
{
    [self willChangeValueForKey:@"counter"];
    ++counter; // 必ず変化する
    [self didChangeValueForKey:@"counter"];
    return counter;
}
手動で観察するための設定を追加する

これは単にNSObjectの仕様の問題なので直接Observerパターンとは関係ありません。観察対象の値が変化したことを手動で実装するために以下のメソッドを追加して、戻り値が必ずNOになるようにします。
Model.h

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey;

Model.m

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
    BOOL automatic = NO;
    
    if ([theKey isEqualToString:@"counter"]) {
        automatic=NO;
    } else {
        automatic=[super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}

以上で、ModelがカウントアップをControllerに通知しなおしてViewを更新するようになりました。

インターフェースを分離する

本来のObserverパターンはObserverとSubjectというinterfaceを用意して、その具象クラスで実装をします。IModelクラス、IControllerクラスを用意すると通知に必要な部分と本来の処理とを分離することができます。

Modelクラスの分離

automaticallyNotifiesObserversForKeyをIModel側に移します。
IModel.h

#import <Foundation/Foundation.h>
@class IController;
@interface IModel : NSObject
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey;
@end

IModel.m

#import "IModel.h"
@implementation IModel
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
    BOOL automatic = NO;
    
    if ([theKey isEqualToString:@"counter"]) {
        automatic=NO;
    } else {
        automatic=[super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}
@end

Model.h

#import <Foundation/Foundation.h>
#import "IModel.h"

@interface Model : IModel
@property(readonly)int counter;
-(int)countUp;
//+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey; //消す
@end
<||
Model.m
>|objc|
#import "Model.h"

@implementation Model
@synthesize counter;
-(id)init{
    self = [super init];
    if (self) {
        counter = 0;
    }
    return self;
}
- (int)countUp
{
    NSLog(@"countup");
    [self willChangeValueForKey:@"counter"];
    ++counter;
    [self didChangeValueForKey:@"counter"];
    return counter;
}
// automaticallyNotifiesObserversForKey// 削除
Controllerを分離する

こちらは再利用性を考慮しないので、特に分離しなくてもよいですが、TemplateMethodパターンなどと組み合わせて、IController側にobserveValueForKeyPathを移動して、Controllerでは分かりやすい名称のメソッドを改めて定義しています。
IController.h

#import <Foundation/Foundation.h>
@interface IController:NSObject
-(void)observeValueForKeyPath:(NSString *)keyPath
                     ofObject:(id)object
                       change:(NSDictionary *)change
                      context:(void *)context;
-(void)onModelChanged;
@end

IController.m

#import "IController.h"
@implementation IController
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    NSLog(@"obs");
    if ([keyPath isEqual:@"counter"]) {
        [self onModelChanged];
    }
}
-(void)onModelChanged
{
    // do nothing if base class
}
@end

Controller.h

#import <Foundation/Foundation.h>
#import "IController.h"
@class Model;
@interface Controller :IController
{
    IBOutlet id label;
    IBOutlet id window;
    Model* model;
}
-(IBAction)onButtonClicked:(id)sender;
-(void)onModelChanged;
@end

Controller.m

#import "Controller.h"
#import "Model.h"
@implementation Controller
- (id)init
{
    self = [super init];
    if (self) {
        model = [[Model alloc]init];
        [model addObserver:self forKeyPath:@"counter" options:(NSKeyValueObservingOptionNew)  context:nil];
    }
    return self;
}
- (void) dealloc
{
    [model release];
}
-(IBAction)onButtonClicked:(id)sender
{
    [model countUp];
}
-(void)onModelChanged
{
    int data = [model counter];
    [label setIntValue:data];
}
@end