Swift でデリゲートを実装する方法
Swift プログラミング
Swift でデリゲートパターンを実装する方法を整理してみました。
Objective-C でお馴染みのデリゲートメソッドの実装を任意にする方法についても紹介します。
オブジェクトが何か処理をするとき、別のオブジェクトに処理の状況を伝える方法の一つにデリゲート があります。
デリゲートパターンは、処理をするオブジェクトに別のオブジェクトを設定しておいて、状況に応じて設定しておいたオブジェクトのメソッドを呼び出してもらうようにします。
呼び出してもらうメソッドは設計時にプロトコルで定義しておくのが普通です。また、レシーバーにはメソッド呼び出しを行うオブジェクトそのものを登録して、メソッドの処理状況を自分で受け取る方法がよく採られます。
このようなデリゲートは Objective-C でよく使われる手法でしたが、Swift でも同じように利用できます。
Swift のデリゲートパターン
Swift のデリゲートパターンは、Objective-C と同じようにプロトコルを活用して実現します。
たとえば、何かデータを解析するParserクラス クラスを作るとします。このクラスが処理の状況をデリゲートで別の(任意の)オブジェクトに伝えられるようにします。
プロトコルの定義
まずは処理状況をどのメソッドに通知するかの取り決めとして、ParserDelegateプロトコル を定義します。
protocol ParserDelegate {
// Parser クラスの初期化が終わった後に追加の初期化を行う機会を与えます。
func parserDidInitialize(sender:Parser)
}
たとえば、このようにparserDidInitialize:メソッド を定義します。
デリゲートが Parser クラスに設定されたときには、初期化後にこのメソッドを呼び出して追加の初期化処理を自由にしてもらう機会を与えることにします。
クラスの定義
デリゲートで使うメソッドを決めたら、それを考慮してParserクラス を設計します。
class Parser {
// デリゲートの設定は任意なので Optional 型で保持します。
weak var delegate: ParserDelegate?
// 初期化時にデリゲートを受け取り、設定します。
// 指定しなくても良いように Optional で受け取ります。
init(delegate: ParserDelegate?) {
self.delegate = delegate
// 初期化が終わったところで、デリゲートのメソッドを呼び出します。
self.delegate?.parserDidInitialize(self)
}
}
今回はデリゲートを設定しなくてもいいように設計しています。
デリゲートが持つべきメソッドはParserDelegate型 なので、それをインスタンスが保持できるようにしていますが、Optional 型にして、設定しないことも可能にします。
そして、イニシャライザではデリゲートとして使うオブジェクトを受け取ります。ここでも Optional 型にして、必要がなければnil
を渡せるようにします。
これで設定に関する部分は完成です。
あとは適切なタイミングで、所定のデリゲートメソッドを呼び出すコードを実装します。
今回は「初期化終了時にparserDidInitialize:メソッド を呼び出すことにするので、イニシャライザの最後に次のコードを記載してあります。
self.delegate?.parserDidInitialize(self)
ここのself.delegate?
のところは、意味合い的には「delegateプロパティ
に値が設定されていれば」となります。
値が設定されているときに限って、その後に記載したメソッドが呼び出されるので、これで「デリゲートが設定されているときに限って所定のメソッドを実行する」という表記が可能です。
Objective-C では[self.delegate parserDidInitialize:self]
と書いていた部分がこれに当たります。Objective-C では nil の場合は何もしないという特徴があったため、普段と特に変わりなく書けていました。Swift では「?」がひとつ必要になります。
実装を省略できるデリゲートメソッドを用意する
デリゲートメソッドを用意するときに、状況を知らせるために用意するけれど、レシーバー側が必ずしも必要としない(不要なときは実装を省略できる)メソッドを用意したくなる場合があります。
Swift のプロトコルで定義したメソッドは、標準では実装を省略できません。ただし Objective-C 互換のプロトコルにすることで、省略可能なメソッドをプロトコルに定義できます。
プロトコルを Objective-C 互換にする
プロトコルを Objective-C 互換にするには、プロトコルの定義の先頭に@objcディレクティブ を記載します。
そうすることで、実装が任意なメソッドを定義できるようになります。任意なメソッドはoptional
を先頭に書き加えます。
たとえば、パース処理が終わったときに呼び出すデリゲートメソッドparserDidEnd:
を先ほどのParserDelegateプロトコル
に追加して、それを必要に応じて実装できるようにしたい場合は次のようになります。
// プロトコルの定義に @objc を追加します。
@objc protocol ParserDelegate {
func parserDidInitialize(sender:Parser)
// Parser の処理が終わったときに呼び出されるメソッドを追加します。
// 必要がなければ実装しなくて済むように、定義の冒頭に optional を指定しています。
optional func parserDidEnd(sender:Parser)
}
こうすることで、実装しなくても良いメソッドparserDidEnd:
を定義できました。
なお、上記のようにoptional
を指定したとき、プロトコルに@objcディレクティブ
を指定していないと'optional' can only applied to members of an @objc protocol
というエラーになります。
ちなみにこのとき、次のようなエラーメッセージが表示されます。
Method cannot be marked @objc because the type of the parameter cannot be represented in Objective-C
これは、定義に optional を指定したメソッドは Objective-C 互換にする必要があるのですが、このメソッドの定義の中で Objective-C では使えないものが使われているのが原因です。
今回の場合は、メソッドの引数で使っているParserクラス が Objective-C 互換ではないため、このエラーが発生しています。
デリゲートで使うクラスを Objective-C 互換にする
クラスを Objective-C に対応させる方法は、次の 2 つがあります。
- クラスの定義に@objcディレクティブ を追加する
- クラスを
NSObject
から派生させる
今回はNSObjectクラス の機能は必要ないので、前者の「定義に@objcディレクティブ を追加する」方法で対応してみることにします。
併せて、新たにparseメソッド
を追加して、その最後に、先ほど追加した任意のデリゲートメソッドparserDidEnd:
を呼び出すコードを追記しておくことにします。
// クラスの定義に @objc を追加して Objective-C 互換にします。
@objc class Parser {
weak var delegate: ParserDelegate?
init(delegate: ParserDelegate?) {
self.delegate = delegate
self.delegate?.parserDidInitialize(self)
}
// 追加したデリゲートメソッドを呼び出すメソッドをひとつ追加します。
func parse() {
// 最後にデリゲートメソッドを呼び出します。
// レシーバーが登録され、メソッドが実装されている場合に限り実行します。
self.delegate?.parserDidEnd?(self)
}
}
ところで、今回追加したデリゲートメソッドの呼び出しコードでは、self.delegate?
に加えて、デリゲートメソッドそのものにも「?」が付けられています。
self.delegate?.parserDidEnd?(self)
これは、今回のデリゲートメソッドはoptional
で定義してあるため、レシーバー (self.delegate) が実装していない可能性があるためです。
メソッドが実装されていない場合は、メソッドの実装がnil
になるため「メソッドが実装されている(nil ではない)ときに限ってメソッドを実行する」という意味合いになります。
具体的にはparserDidEnd?
という形で扱うことになります。つまり今回の定義であれば ((Parser)->Void)? ということになります。
Objective-C では if ([self.delegate respondsToSelector:@selector(parserDidEnd:)]) [self.delegate parserDidEnd:self] という書き方をしていた部分がこれに当たります。こちらの場合は Swift の方が簡単に書けますね。必須なメソッドとほとんど同じように書けるところが嬉しいです。