[iOS] Protocol – Delegateパターン | Objective-C イベント伝達 その1

2012/06/26

12/07/06 追記
delegateのコードを修正しました。
delegateはretainでなくて、assignにした方が良いみたいです。
その場合は、deallocの中のreleaseも必要なくなります。

参考
>> Why are Objective-C delegates usually given the property assign instead of retain?

AがBを作り、BのdelegateにAをセットする。
BがretainでAをもつことにより、AとBが互いにretainで参照を持つことになります。
そうなってしまうと、BをreleaseしようとしてもAをretainしているためにreleaseできずにメモリリークするということのようです。

—————-

こんにちは。きんくまです。

今回から何回かに分けて、Objective-C でのイベント伝達の扱い方について書いてみます。

まずはじめに。イベント伝達って?

イベントはいろんなものが考えられますよね。

・ユーザーがボタンを押した
・ある値になった
・タイマーが完了した
・あるView(やViewController)を破棄して、別のView(ViewController)にきりかえるタイミング

こんな感じのイベントがおこったときに、それをあるオブジェクトから別のオブジェクトに知らせたいときがあります。それをイベント伝達とします。

今あるオブジェクトから別のオブジェクトへ伝えると書いたのですが、伝えられる先の数も2種類考えられます。

・1対1
・1対多(複数)

では具体的にはどうやってイベントを伝えるのでしょうか?

Objective-Cの3つのイベント伝達の仕組み

Objective-Cにはイベント伝達の仕組みが大きく分けて3つ用意されています。

・Protocol – Delegateパターン
・KVO (Key-Value Observing)
・NSNotification

それぞれに長所と短所があります。なので、目的に合わせて使用するのが良いと思います。
その比較については、それぞれの実装方法を書いたあとにまとめとしてやりたいと思います。

今回は、Protocol – Delegateパターンについて書きます。

Protocol – Delegateパターン

聞いたことのない人は何か新しいもののような気がするのですが、
UIKitを使っている人はたぶん既に一度は使っていると思います。

例えば、UITableViewDelegate, UITableViewDataSource, UIScrollViewDelegate などです。
UIScrollViewDelegateだったら、

– (void)scrollViewDidScroll:(UIScrollView *)scrollView
– (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView

などのメソッドを使ったことがあるんじゃないでしょうか。

使い方は

1. delegateプロパティにオブジェクトの参照を代入
2. オブジェクトのinterfaceのクラス名の後ろに<Protocol名> と追記 -> MyClass:NSObject<MyProtocol> みたいな
3. Protocolに宣言されているメソッドを実装部(.m)に書く

という感じです。

こういったProtocol -DelegateパターンはUIKitだから特別できるというのではなく、
Objective-Cのそもそもの機能を利用しています。
なので、自作することが可能です。

今のは使う側であり、通知される側ということになります。

では通知する側はどうすればよいのでしょうか。

Protocol – Delegateパターンの作り方

1. Protocolを定義する
2. 通知する側にインスタンス変数とプロパティでdelegateを用意
3. 通知したいイベントが発生したタイミングで、delegateのProtocolに定義されたメソッドを呼び出す

という感じになります。
実際に作ってみましょう。今回作成するのはこんな感じのものです。

window.rootViewController直下にKKViewControllerというものがあり、
その中にカスタムビューのKKCountPushView(緑の背景部分)というものがあります。

protocol_delegate_fig1

KKCountPushViewの中にはボタンがあり、押すたびに上のラベルに押した回数が表示されます。
現時点では、親のKKViewControllerと子供のKKCountPushViewには、イベント伝達の関係はありません。
子供のボタンの押した回数が変更した際に、親に伝達するようにこれから順番に実装していきます。

現時点のコードです。ここは大事なところでないので、飛ばしても大丈夫です。

KKCountPushView.h

#import <UIKit/UIKit.h>

@interface KKCountPushView : UIView
{
    int _pushCount;
    UIButton *_pushButton;
    UILabel *_countLabel;
}

@end

KKCountPushView.m


#import "KKCountPushView.h"

@interface KKCountPushView()
- (void)updateCountLabel:(int)count;
- (void)pushButtonTapped;
@end

@implementation KKCountPushView

- (void)dealloc
{
    [_pushButton release];
    [_countLabel release];
    [super dealloc];
}

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {

        self.backgroundColor = [UIColor colorWithRed:210/255.0 green:231/255.0 blue:159/255.0 alpha:1.0];

        _pushCount = 0;
        
        //button
        _pushButton = [[UIButton buttonWithType:UIButtonTypeRoundedRect] retain];
        _pushButton.frame = CGRectMake((220 - 100) * 0.5, 70, 100, 40);
        [_pushButton setTitle:@"PUSH ME" forState:UIControlStateNormal];
        [_pushButton addTarget:self action:@selector(pushButtonTapped) forControlEvents:UIControlEventTouchUpInside];
        [self addSubview:_pushButton];
        
        //label
        _countLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 30, 220, 20)];
        _countLabel.backgroundColor = [UIColor clearColor];
        [_countLabel setTextAlignment:UITextAlignmentCenter];
        [self addSubview:_countLabel];
        [self updateCountLabel:_pushCount];
    }
    return self;
}

- (void)updateCountLabel:(int)count
{
    _countLabel.text = [NSString stringWithFormat:@"%d times pushed", count];
}

- (void)pushButtonTapped
{
    _pushCount++;
    [self updateCountLabel:_pushCount];
}

@end

KKViewController.h

#import <UIKit/UIKit.h>

@class KKCountPushView;

@interface KKViewController : UIViewController
{
    KKCountPushView *_countPushView;
    UITextView *_logView;
}

@end

KKViewController.m

#import "KKViewController.h"
#import "KKCountPushView.h"

@interface KKViewController ()
- (void)log:(NSString *)logText;
@end

@implementation KKViewController

- (void)dealloc
{
    [_countPushView release];
    [_logView release];
    [super dealloc];
}

- (void)loadView
{
    [super loadView];
    UIView *myView = [[UIView alloc] init];
    myView.backgroundColor = [UIColor whiteColor];

    _countPushView = [[KKCountPushView alloc] initWithFrame:CGRectMake(50, 50, 220, 140)];
    [myView addSubview:_countPushView];

    _logView = [[UITextView alloc] initWithFrame:CGRectMake(0, 240, 320, 240)];
    _logView.editable = NO;
    [myView addSubview:_logView];

    self.view = myView;
    [myView release];
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self log:@"Hello"];
    [self log:@"Protocol - Delegate"];
}

- (void)log:(NSString *)logText
{
    NSString *newLog = [NSString stringWithFormat:@"%@\n%@", _logView.text, logText];
    _logView.text = newLog;
}

@end

1. Protocolを定義する

Protocolを定義します。
Protocolは、ヘッダーファイルとして独立して書いてもよいですし、
どこか別のヘッダーファイルに一緒に書いてもよいです。

複数にまたがるときは独立して書いた方がよいと思いますが、
今回はKKCountPushView.hだけが使うProtocolということで、KKCountPushView.hに足すことにします。

@class KKCountPushView;

@protocol KKCountPushViewDelegate <NSObject>
@optional 
- (void)countPushView:(KKCountPushView *)countPushView countChange:(int)pushCount;
@end

プロトコルのメソッドは好きな数定義することが可能です。
また、何を引数にしてイベント伝達にするかも自分で決められます。
どんなものが使いやすいかは、UIKitの各Delegateを参考にしてみるとよいかもしれません。
今回は、押した回数を引数に通知するようにしました。

また必ずしも実装しなくてよいメソッドは、@optional の下にまとめて書いておきましょう。

2. 通知する側にインスタンス変数とプロパティでdelegateを用意

KKCountPushViewにDelegateを追加します。変更がなかったその他のメソッドは省略します。

KKCountPushView.h

@interface KKCountPushView : UIView
{
    int _pushCount;
    UIButton *_pushButton;
    UILabel *_countLabel;
    id<KKCountPushViewDelegate> _delegate; //追加
}
@property (nonatomic, assign) id<KKCountPushViewDelegate> delegate; //追加

@end

KKCountPushView.m

@implementation KKCountPushView
@synthesize delegate = _delegate; //追加

- (void)dealloc
{
    [_pushButton release];
    [_countLabel release];
    [super dealloc];
}

3. 通知したいイベントが発生したタイミングで、delegateのProtocolに定義されたメソッドを呼び出す

ボタンを押した際にdelegateのメソッドを呼び出します。
_delegateがnilかどうかは、判定しなくても大丈夫です。
nilのメソッドを読んでもObjective-Cはnilを返すだけなので。

KKCountPushView.m

- (void)pushButtonTapped
{
    _pushCount++;
    [self updateCountLabel:_pushCount];

    //追加
    if ([_delegate respondsToSelector:@selector(countPushView:countChange:)]){
        [_delegate countPushView:self countChange:_pushCount];
    }
}

これで準備が整いました。早速親のKKViewControllerから使ってみます。

使ってみる

KKViewController.h

#import <UIKit/UIKit.h>
#import "KKCountPushView.h"

//クラス名の後ろにProtocolを追加している
@interface KKViewController : UIViewController<KKCountPushViewDelegate>
{
    KKCountPushView *_countPushView;
    UITextView *_logView;
}

@end

KKViewController.m

- (void)loadView
{
    [super loadView];

//省略

    _countPushView = [[KKCountPushView alloc] initWithFrame:CGRectMake(50, 50, 220, 140)];
    _countPushView.delegate = self; //追加
    [myView addSubview:_countPushView];

//省略
}

//Protocolメソッドを追加
- (void)countPushView:(KKCountPushView *)countPushView countChange:(int)pushCount
{
    [self log:[NSString stringWithFormat:@"count change : %d", pushCount]];
}

これで完成です。実際に動かすとこうなります。

protocol_delegate_fig2

Protocolを定義するとどういいんでしょ?

Protocolを定義しないでも、呼ぶ側が呼ばれる側のインスタンスを直接持って、呼ばれる側のメソッドを直接呼ぶことが可能です。
じゃあ、なんでこんなまわりくどいことをするんでしょうか?

答えは、それをすることで呼ぶ側と呼ばれる側の関係が疎結合になるからです。
つまり呼ばれる側はProtocolを実装さえしてれば誰でも構わないということになります。
今回作ったKKPushViewは、必要であれば別のところにそのまま持っていって使い回すことが可能になります。

このなるべく関係を薄くすることで、他のところでも使い回すことが可能になるというのは、結構大事なことかなと思ってたりします。

今回のプロジェクトデータ一式です。

※ 2012/07/22 コード修正しました
>> プロジェクトデータ一式


自作iPhoneアプリ 好評発売中!
フォルメモ - シンプルなフォルダつきメモ帳
ジッピー電卓 - 消費税や割引もサクサク計算!

ページトップへ戻る