Swift の Protocol Extension 内で Selector を呼び出す
検証環境:
Xcode 11.6
Swift 5.2.4
Swift の Protocol Extension 内では @objc
をつけたメソッドを実装しても #selector
で呼び出しすることはできません。
例えば以下のコードでは NotificationCenter の引数で selector を指定しています。
protocol KeyboardDetector {} extension KeyboardDetector where Self: UIViewController { func startObservingKeyboardChanges() { NotificationCenter.default.addObserver( self, selector: #selector(onKeyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil) } @objc func onKeyboardWillShow(_ notification: Notification) { // do stuff } }
しかし selector を指定する部分で、
Argument of '#selector' refers to instance method 'onKeyboardWillShow' that is not exposed to Objective-C
というエラーが、また @objc のメソッド定義部分にも
@objc can only be used with members of classes, @objc protocols, and concrete extensions of classes
というエラーになってしまいます。
どうやら Protocol Extension 機能は Objective-C からは見えないようです。
なんとか使うことはできないのかとググってみたところ、Objective-C の Associated Objects を使って実装する方法があったので、ちょっと強引ですがそれで実装してみます。
Swift の Protocol Extension 内で Selector を呼び出す
まず NotificationCenter の引数に渡す Observer 用クラスを作ります。
@objc
メソッドはこの中に定義し、実際の処理は外部からクロージャで渡すようにします。
class NotificationObserver { let closure: (_ notification: Notification) -> Void init(closure: @escaping (_ notification: Notification) -> Void) { self.closure = closure } @objc func invoke(_ notification: Notification) { closure(notification) } }
通常 NotificationCenter の Observer には self
(UIViewController) を渡すところですが、今回は @objc
メソッドを定義した NotificationObserver
クラスのインスタンスを関数スコープ内で生成し、NotificationCenter の Observer として渡します。
このままだと Observer インスタンスが関数終了時に消滅してしまいますが、これを回避するために、objc_setAssociatedObject
を使って保持するようにします。
protocol KeyboardDetector {} extension KeyboardDetector where Self: UIViewController { func startObservingKeyboardChanges() { let observer = NotificationObserver { [weak self] notification in self?.onKeyboardWillShow(notification) } NotificationCenter.default.addObserver( observer, selector: #selector(NotificationObserver.invoke(_:)), name: UIResponder.keyboardWillShowNotification, object: nil) objc_setAssociatedObject( self, "[\(arc4random())]", observer, .OBJC_ASSOCIATION_RETAIN) } func onKeyboardWillShow(_ notification: Notification) { // do stuff } }
以下が参考にした記事です。
こちらでは UIControl の addTarget メソッドの引数に渡す selector の例です。