Referencing the Objective-C selector of a method
01 何が問題だったのか
Objective-C 由来の API では、ターゲットにメッセージを送る相手のメソッドを セレクタ(Selector) として渡す場面が多くあります。ところが Swift 2 では、Selector 型が期待される文脈に文字列リテラルを書くことでセレクタを表現する仕組みになっており、これが非常にエラーを起こしやすいものでした。
control.sendAction("doSomething:", to: target, forEvent: event)
この書き方にはいくつかの問題があります。
セレクタ文字列のスペルミスを検出できない
コンパイラは文字列の中身までは検査しないため、実在しないメソッド名を書いてもコンパイルは通ってしまい、実行時にメッセージが届かず不可解な挙動になります。セレクタの綴りが正しく整った形式になっているかさえチェックされません。
Swift名と Objective-C セレクタの対応が非自明
SE-0005 による Objective-C API の自動リネームが導入されたことで、Swift から見えるメソッド名と実際の Objective-C セレクタは機械的には一致しなくなりました。さらに @objc(...) 属性で個別に別名を付けることもできるため、Swift 側のメソッドを文字列セレクタとして書き下すには、開発者が頭の中で命名変換を再現しなければならず、間違いの元になります。
extension MyApplication {
@objc(jumpUpAndDown:)
func doSomething(sender: AnyObject?) { ... }
}
このようなケースでは、Swift のメソッド名 doSomething から実際のセレクタ jumpUpAndDown: を手で導き出す必要があり、コードを読む側にとってもリンクが追いにくいという問題がありました。
02 どのように解決されるのか
セレクタを文字列リテラルで書く代わりに、メソッドへの参照からセレクタを構築する新しい式 #selector を導入します。#selector にはセレクタを得たい Swift のメソッドを名前で渡します。コンパイラはそれが実在する @objc メソッドであることを静的に検証したうえで、対応する Objective-C セレクタへ変換してくれます。
control.sendAction(#selector(MyApplication.doSomething), to: target, forEvent: event)
@objc(jumpUpAndDown:) のように Objective-C 側で別名が与えられていても、開発者は Swift のメソッド名 doSomething を書くだけで済みます。命名変換はコンパイラが行うため、Swift 名と Objective-C セレクタのずれを意識する必要がなくなります。
引数ラベルでオーバーロードを区別する
SE-0021 のコンパウンド名と組み合わせることで、同名のメソッドも引数ラベル込みで一意に指定できます。
let sel = #selector(UIView.insertSubview(_:atIndex:))
// produces the Selector "insertSubview:atIndex:"
それでも曖昧な場合は、as による型キャストで対象メソッドを絞り込めます。#selector の中身に書けるのは型やインスタンス・クラスメンバを . で繋いだ参照と、末尾に付けられる as による型注釈のみです。メソッド呼び出しを含む任意の式は書けないため、#selector の中で副作用が発生することはないと保証されます。
let sel = #selector(((UIView.insertSubview(_:at:)) as (UIView) -> (UIView, Int) -> Void))
文字列リテラルからのセレクタ構築は非推奨に
#selector の導入とあわせて、文字列リテラルを Selector 型の文脈に書く従来の書き方は非推奨となり、将来的には取り除かれる想定です。どうしても文字列からセレクタを作りたい場合は、Selector("insertSubview:atIndex:") のように Selector のイニシャライザを明示的に呼ぶ形に書き換えます。既存コードに対しては、マイグレータが #selector 形式または明示的イニシャライザ呼び出しへの変換を支援します。