読者です 読者をやめる 読者になる 読者になる

定食屋おろポン

おろしポン酢と青ネギはかけ放題です

Swift時代のDelegate通知

Objective-C時代のDelegate

今は昔、Objective-Cの時代ではOwnerへの通知にDelegateを使用していました。 言語仕様としてDelegate機構が用意されているわけでも、Delegateは通知のための機構であるというわけでもなく、CocoaフレームワークDelegateパターンが通知に多用されていた、ということです。

Delegateの実装としては、概ね下記のようになっています。


通知を受け取るReceiverと、通知を送るSenderがいます。

Senderはプロパティとしてdelegateをもっており、delegateプロパティにはReceiverがセットされます。

また、Senderのヘッダ内で、「Delegateがどのような通知に対して処理が出来るのか」 = 「Delegateが受け取ることができるメソッド」 が定義されます。 定義には、NSObjectクラスのカテゴリか、プロトコルを使用します。 プロトコルで定義するメソッドには、「Required: Delegateに実装されていないといけないメソッド」と「Optional: 通知は送るが、実装されていなくてもよいメソッド」の二種類があります。

Receiverでは、定義されたメソッドを実装し、通知を受け取ったときの処理を記述します。

通知を送る処理はSenderで実装します。単純にdelegateプロパティにメッセージを送るだけなので、こんなかんじです。

[self.delegate someMethod];

ただし、delegateプロパティ = Receiver がsomeMethodを実装していない場合、存在しないメソッドを呼び出すこととなり、クラッシュを引き起こします。 当然クラッシュは好ましくないため、respondsToSelector:を使い、メソッドを呼ぶ前にメソッドの存在確認を行なうのが通例です。

if ([self.delegate respondsToSelector:@selector(someMethod)]) {
    [self.delegate someMethod];
}

これが典型的なObjective-CDelegateの使用例となります。


Swift時代のDelegate

まず、SwiftでもObjective-CスタイルのDelegateの実装は可能です。

コードで示します。

// Protocolの定義
@objc protocol SampleDelegate {
    func requiredMethod() -> ()
    optional func optionalMethod() -> ()
}

// Receiver: 通知を受け取る方
class Receiver :SampleDelegate { //SampleDelegateプロトコルに準拠していることを示す
    let sender :Sender = Sender()

    init() {
        // Senderのdelegateに自分を指定
        sender.delegate = self

        // senderに通知を送ってもらう
        sender.notify1()
        sender.notify2()
    }

    // SampleDelegateプロトコルに準拠しているので、`requiredMethod`を実装しないとコンパイルできない
    func requiredMethod() {
        println("required method was called")
    }

    // `optionalMethod`はoptionalなので実装しなくてもよい
}

// Sender: 通知を送る方
class Sender {
    // delegate にはSampleDelegateプロトコルに準拠したオブジェクトかnilが入る
    weak var delegate :SampleDelegate? = nil

    // requiredなメソッドを呼び出す
    func notify1() {
        self.delegate?.requiredMethod()
    }

    // optionalなメソッドを呼び出す
    func notify2() {
        self.delegate?.optionalMethod?() // メソッド呼び出しに?が付いているのがキモ
    }
}

プロトコル定義に@objc属性が付いているのが嫌なかんじです。 これは、SwiftプロトコルではOptionalを指定できないために付けています。*1

Swiftのみで完結させようとすると、プロトコルに定義したメソッドがすべてRequiredになってしまうということですね。

カジュアルにDelegateメソッドをバンバン追加していると、「これ全部実装しないといけないのかよ」となり、使い勝手が非常に悪いです。

UIScrollViewDelegateの定義なんてこんなんですよ。これでもマシなほうですけど。

@protocol UIScrollViewDelegate<NSObject>

@optional

- (void)scrollViewDidScroll:(UIScrollView *)scrollView;                                               // any offset changes
- (void)scrollViewDidZoom:(UIScrollView *)scrollView NS_AVAILABLE_IOS(3_2); // any zoom scale changes

// called on start of dragging (may require some time and or distance to move)
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView;
// called on finger up if the user dragged. velocity is in points/millisecond. targetContentOffset may be changed to adjust where the scroll view comes to rest
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset NS_AVAILABLE_IOS(5_0);
// called on finger up if the user dragged. decelerate is true if it will continue moving afterwards
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;

- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView;   // called on finger up as we are moving
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;      // called when scroll view grinds to a halt

- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView; // called when setContentOffset/scrollRectVisible:animated: finishes. not called if not animating

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView;     // return a view that will be scaled. if delegate returns nil, nothing happens
- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view NS_AVAILABLE_IOS(3_2); // called before the scroll view begins zooming its content
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale; // scale between minimum and maximum. called after any 'bounce' animations

- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView;   // return a yes if you want to scroll to the top. if not defined, assumes YES
- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView;      // called when scrolling animation finished. may be called immediately if already at top

@end

これが全てRequiredだったら..と考えるとゾッとします。 メソッドの中身は空でもいいけど、とにかく全部実装してあげないとコンパイルが通らないんですからね。

今後

当面は@objcを使っていればよいのですが、今後どうなっていくんでしょうか。

ニュービーなので実際のところはよくわかりませんが、Objective-CDelegateが多用されているのは、Cocoaフレームワークで多用されていて慣れ親しんでいるからだろうと思います。

今後、Objective-Cの領域が徐々に狭くなっていくだろうことが想像されます。その中で、こういった通知の仕組みもSwiftフレンドリーな書き方に置き換わっていくんでしょうか。