protocolと関連させたクラスの定義方法

最近またiPhone触り始めて、新たに知ったことなどがあったのでメモ。
delegateを持ち、自分で定義したコールバックを行うクラスの定義方法」について。

ヘッダでの宣言

例としてNSXMLParserをラップしてXMLからある要素を5回検出したときにコールバックを呼び出すようなクラスを考える。
こんなカンジで書く。

#import <Cocoa/Cocoa.h>


@protocol MyXMLParserDelegate;

- (void)found5statuses;

@end



@interface MyXMLParser : NSObject {
    int num;
    id <MyXMLParserDelegate> delegate;
}

@property (retain, nonatomic) id <MyXMLParserDelegate> delegate;

- (void)parse;

@end

当然だけど"@interface"の宣言と"@protocol"を上下逆に書いてしまうと、protocol宣言していないのに"@interface"の中でMyXMLParserDelegateが登場してしまいコンパイラに怒られてしまう。

実装

@implementation MyXMLParser

@synthesize delegate;

- (id)init {
    if (self = [super init]) {
        num = 0;
    }

    return self;
}

- (void)parse {
    NSLog(@"parse");
    NSURL *url = [NSURL URLWithString:@"http://twitter.com/statuses/public_timeline.xml"];
    NSXMLParser *parser = [[[NSXMLParser alloc] initWithContentsOfURL:url] autorelease];
    [parser setDelegate:self];
    [parser parse];
}

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qualifiedName attributes:(NSDictionary *)attributeDict {
    if ([elementName isEqualToString:@"status"]) {
        num++;
        if (num % 5 == 0) {
            [delegate found5statuses];
        }
    }
}

@end
  • initをオーバーライドして変数を初期化
  • インスタンスメソッドparserでNSXMLParserのparseを実行
  • delegateにselfを指定して自らdelegateメソッドを書いてイベントを受け取る
  • 特定の条件で自クラスのdelegateプロパティに対しMyXMLParserDelegateで定義したコールバックメソッドを呼び出す(その前にdelegatenilチェックすべきだろうけど)

呼び出す側

GUIでのイベントを受け取って動くメソッドという前提で考えておくけれども。

#import <Cocoa/Cocoa.h>
#import "MyXMLParser.h"


@interface Hoge : NSObject <MyXMLParserDelegate> {

}

- (IBAction)hoge:(id)sender;

@end
#import "Hoge.h"


@implementation Hoge

- (IBAction)hoge:(id)sender {
    NSLog(@"hoge");

    MyXMLParser *parser = [[[MyXMLParser alloc] init] autorelease];
    [parser setDelegate:self];
    [parser parse];
}

- (void)found5statuses {
    NSLog(@"found5statuses");
}

@end

setDelegateする時点で、HogeクラスがMyXMLParserDelegateを実装していないと警告を出してもらえる。ので必ずHoge.hで "" と書くことになる。そうするとMyXMLParserDelegateのメソッドを実装していないとまた警告してもらえる。
…というように、そこそこ安全にプログラミングできるようになる。

Delegateメソッドで関連するインスタンスを引数に渡す場合

上記の例はdelegateメソッドに引数が無かった。
けど場合によってはどのMyXMLParserから受けたコールバックか分かるようにそのインスタンスを引数に渡したいこともある。
その場合にどう書くか。

#import <Cocoa/Cocoa.h>


@protocol MyXMLParserDelegate

- (void)found5statuses:(MyXMLParser *)parser;

@end



@interface MyXMLParser : NSObject {
    int num;
    id <MyXMLParserDelegate> delegate;
}

@property (retain, nonatomic) id <MyXMLParserDelegate> delegate;

- (void)parse;

@end

のようにすると、MyXMLParserの@interface宣言の前なので解決できない。@interfaceと@protocolを逆にしてもダメ。
で、どうすればいいのかというと、以下のように書けば良いらしい。

#import <Cocoa/Cocoa.h>


@protocol MyXMLParserDelegate;


@interface MyXMLParser : NSObject {
    int num;
    id <MyXMLParserDelegate> delegate;
}

@property (retain, nonatomic) id <MyXMLParserDelegate> delegate;

- (void)parse;

@end


@protocol MyXMLParserDelegate

- (void)found5statuses:(MyXMLParser *)parser;

@end

先に"MyXMLParserDelegate"というのがあるよ、とだけ宣言、その後でメソッド宣言を書いてやる。こうすれば目的の動作が実現できるようだ。

#import "MyXMLParser.h"


@implementation MyXMLParser

@synthesize delegate;

- (id)init {
    if (self = [super init]) {
        num = 0;
    }

    return self;
}

- (void)parse {
    NSLog(@"parse");
    NSURL *url = [NSURL URLWithString:@"http://twitter.com/statuses/public_timeline.xml"];
    NSXMLParser *parser = [[[NSXMLParser alloc] initWithContentsOfURL:url] autorelease];
    [parser setDelegate:self];
    [parser parse];
}

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qualifiedName attributes:(NSDictionary *)attributeDict {
    if ([elementName isEqualToString:@"status"]) {
        num++;
        if (num % 5 == 0) {
            [delegate found5statuses:self];
        }
    }
}

@end
#import "Hoge.h"


@implementation Hoge

- (IBAction)hoge:(id)sender {
    NSLog(@"hoge");

    MyXMLParser *parser1 = [[[MyXMLParser alloc] init] autorelease];
    MyXMLParser *parser2 = [[[MyXMLParser alloc] init] autorelease];
    [parser1 setDelegate:self];
    [parser2 setDelegate:self];
    [parser1 parse];
    [parser2 parse];
}

- (void)found5statuses:(MyXMLParser *)parser {
    NSLog(@"found5statuses by %@", parser);
}

@end