2015年3月17日火曜日

Delegateの仕組み

「委譲」などと訳されるObjective-Cのdelegate。(辞書で引くと「派遣」と出た)
正式な仕組みや意味的なものはともかく、何かのイベントが起こった時にそのメソッドが実行されるので、イベントハンドラーのようなものと考えてればいい。
書き方はわかったけど、根本的仕組みがよくわからんのでまとめ。

Objective-Cの場合

基本的使い方

ヘッダファイル.h のinterfaceのところで、○○Delegateと宣言してやらないといけないことが多い。
これは後で知ったんだけど、delegateメソッドを宣言してるプロトコルってやつなんだね。詳しくはプロトコルのところで。
例)
@interface DetailTableViewController : UITableViewController <MFMailComposeViewControllerDelegate, ADBannerViewDelegate, UITextFieldDelegate, UITextViewDelegate, UIActionSheetDelegate, CLLocationManagerDelegate, MKMapViewDelegate>

実装ファイル.m の中では、あらかじめ関連するクラスのdelegateプロパティにselfを指定する。(delegateメソッドの場所がその実装ファイル内にあるということ)
例)
    //returnキーでキーボードを隠すためのdelegate
    _titleTextField.delegate = self;
    _detailTextView.delegate = self;
    _mapView.delegate = self;
    
    //iAddelegate
    _iAdView.delegate = self;

同じ実装ファイル内にイベント発生時に呼ばれるdelegateメソッドを記述する。
メソッド名はそれぞれ決まっているので、どのイベントで何が呼ばれるかはあらかじめ調べておかないといけない。
例)
//textFieldreturnが押されたらキーボードを隠すdelegateメソッド
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
    [textField resignFirstResponder];
    return YES;
}

//textView編集終了時、キーボードを隠し、元データを更新するdelegateメソッド
- (BOOL)textViewShouldEndEditing:(UITextView *)textView
{
    _incidentData.text = _detailTextView.text; //元データ更新
    //ファイル保存
    [appDelegate writeFile];

    [textView endEditing:YES];
    return YES;
}

TableView関係のdelegateなどは、ヘッダファイルでの宣言、実装ファイルでのdelegateプロパティにselfの指定などは省略できるようだ。たぶんどこか見えないところでされてるんだとは思うが。
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.objects.count;
}

カスタムクラスでの独自Delegateの実装方法

DustというImageViewの独自クラスをタッチしたら、メインのGameViewControllerでラベルにメッセージを表示するコード。

Dustのヘッダファイルの記述

protocolの宣言と、delegateメソッド名の設定、delegateプロパティの設定をする。
(protocolを外部ファイルにしてある場合は、それを#importする)
//独自delegateの設定
@class Dust;
@protocol DustDelegate
- (void)dustTouched:(Dust *)dust;
@end

@interface Dust : UIImageView
@property (weak) id delegate; //独自delegate
@end

Dustの実装ファイルのdelegateメソッドを発生させるメソッドに、GameViewController内にdelegateメソッドが実装されていたらそれを呼ぶコードを書く。
respondsToSelector:メソッドは、:@selector()で指定したメソッドが存在するかの判定。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    //delegate先(GameViewController)にdustTouched:メソッドが実装されていれば、
    //自分(Dust)を引数にそいつを呼ぶ
    if ([self.delegate respondsToSelector:@selector(dustTouched:)]) {
        [self.delegate dustTouched:self];
    }
}

GameViewControllerのヘッダファイルでは特にdelegateに関する宣言なし。

実装ファイル内でDustのインスタンスを作る際にdelegateにself(GameViewController)を指定する。
    //Dustクラスを作る
    Dust *dust = [[Dust alloc]initWithImage:image];
    dust.delegate = self; //touch時に呼ばれるdelegateメソッドの場所の指示

実装ファイル内にdelegateメソッドを書く。以上。
//Dustがタッチされたら呼ばれるdelegate
- (void)dustTouched:(Dust *)dust
{
    _dustNameLabel.text = @"タッチされた";
}


仕組みの解説


Dust.hのprotocolの設定で、メソッドの引数に自分自身を指定していることと、プロパティで汎用のid型を使い、あらゆるクラスから呼ばれてもいいようにしているのが肝。

GameViewController.mで
dust.delegate = self;
としているので、dustのdelegateプロパティにはGameViewController自身が入る(②)

GameViewController内でDustインスタンスが作られ、画面でタッチされると、Dust内のtouchesBegan:〜メソッドが動く。(③)
if ([self.delegate respondsToSelector:@selector(dustTouched:)]) {〜}
によるチェックで、delegateプロパティに入っているのはGameViewController自身なので、当然dustTouched:メソッドは存在することになる。(④)

そしてDust.mのtouchesBegan:〜メソッド中の
[self.delegate dustTouched:self];
において、self(Dust)のdelegateプロパティに入っているクラス(GameViewController)内のdustTouched:メソッドを実行させている。(⑤)
引数にself(Dust)を渡してあるので、dustTouched:メソッドの中でもDustのいろいろなプロパティを使用することができる。(ここでは記述してないが)

つまり、GameViewControllerを親、Dustを子とした場合、親を指定して子を呼び、子の中で親にdelegateメソッドがあるかどうかをチェックし、あれば親の中のそのdelegateメソッドを実行しているわけだ。

dust.delegate = self;
というコードが単に「delegateメソッドの場所の指示」というわけではなく、dustのプロパティdelegateにGameViewController自身(正確にはそのポインタ)を入れているわけだ。
いろいろややこしいが。

他のイベント時に別のdelegateメソッドを発生させたかったら、Dustクラス中の記述(ヘッダ、実行ファイルとも)と、GameViewController中の対応するdelegateメソッドを記述すればいいわけだ。


Swiftの場合

protocolを宣言し、delegate変数にselfを入れてメソッドを呼び出すとかはObjective-Cと一緒。
以下はSpriteNode Coinをタッチしたら親ノードに書かれたdelegateメソッドを呼び出す例。

//delegate用プロトコルの宣言
protocol CoinDelegate: class {
    //delegateメソッドの定義
    func touchCoinBegan(coin: Coin)
    func touchCoinEnded(coin: Coin)
}

class Coin: SKSpriteNode {
    override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
        delegate?.touchCoinBegan(self)
    }
    
    override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
        delegate?.touchCoinEnded(self)
    }
}

なお、コードの中で
delegate?.touchCoinBegan(self)
は、Objective-Cの
if ([self.delegate respondsToSelector:@selector(dustTouched:)]) {
        [self.delegate dustTouched:self];
    }
にあたる部分。
やってることはちょっと違うんだけどね。
SwiftにもrespondsToSelectorメソッドはあるんだけど、まだ俺が書き方がわかんないし、オプショナル「?」で、変数delegateがnilじゃないか調べ、nilじゃなければtouchCoinBeganメソッドを実行させる方が簡単でSwiftらしい書き方のようだ。
プロトコル実装してればdelegate先にそのメソッドがないことはあり得ないしね。


んで、delegateを呼ぶ親ノード側。
ちゃんと最初にCoinDelegateプロトコルを使う旨最初に宣言。
Objective-Cの時は、なんか書かなくても動いちゃったけど、Swiftだと書かないとダメみたい。
class GameScene: SKScene, CoinDelegate { //自作delegateのプロトコルを宣言
    var 開始時刻:CFTimeInterval = 0
    var 射出力:CGFloat = 0
    var coin:Coin = Coin()
    
    override func didMoveToView(view: SKView) {
        coin = Coin(imageNamed: "10yen")
        coin.userInteractionEnabled = true 
        coin.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMaxY(self.frame))
        coin.setScale(0.5)
        coin.physicsBody = SKPhysicsBody(circleOfRadius: coin.size.width / 2)
        coin.delegate = self //delegate先をselfに
        self.addChild(coin)
    }

    //こっからがプロトコルに定義されたdelegateメソッド
    func touchCoinBegan(coin: Coin) {
        開始時刻 = CFAbsoluteTimeGetCurrent() //現在の時刻を得る(200111日からの経過秒数)
        射出力 = 0
    }
    
    func touchCoinEnded(coin: Coin) {
        let 経過時間:CGFloat = CGFloat(CFAbsoluteTimeGetCurrent() - 開始時刻)
        coin.physicsBody?.applyForce(CGVectorMake(経過時間, 0))
    }
}

0 件のコメント:

コメントを投稿