web-dev-qa-db-ja.com

Swiftセレクタエラーを使用した3つのプロトコル拡張

私はUIViewControllersの非常に単純なプロトコル拡張機能だと思っていたものを、タップジェスチャーでキーボードを閉じる機能を提供しています。これが私のコードです:

_@objc protocol KeyboardDismissing { 
    func on(tap: UITapGestureRecognizer)
}

extension KeyboardDismissing where Self: UIViewController {

    func addDismissalGesture() {
        let tap = UITapGestureRecognizer(target: self, action: #selector(Self.on(tap:)))
        view.addGestureRecognizer(tap)
    }

    func on(tap: UITapGestureRecognizer) {
        dismissKeyboard()
    }

    func dismissKeyboard() {
        view.endEditing(true)
    }
}
_

問題は、上記のコードがこの行にコンパイルエラーをスローすることです。

_let tap = UITapGestureRecognizer(target: self, action: #selector(Self.on(tap:)))
_

エラーメッセージが表示されます:

「#selector」の引数は、Objective-Cに公開されていないインスタンスメソッド「on(tap :)」を参照しています

func on(tap: UITapGestureRecognizer)の前に_@objc_を追加して、「修正する」よう提案します

わかりました。タグを追加します。

_@objc func on(tap: UITapGestureRecognizer) {
    dismissKeyboard()
}
_

しかし、その後、この新しく追加された_@objc_タグに別のコンパイルエラーをスローし、エラーメッセージを表示します。

@objcは、クラスのメンバー、@ objcプロトコル、およびクラスの具象拡張でのみ使用できます

追加するように言われたまったく同じタグを削除して(= /// =)「修正」するよう提案しました

私は最初、プロトコル定義の前に_@objc_を追加することで_#selector_の問題を解決できると思っていましたが、明らかにそうではなく、これらの周期的なエラーメッセージ/提案はほとんど役に立ちません。 _@objc_タグをどこにでも追加/削除したり、メソッドをoptionalとしてマークしたり、プロトコルの定義にメソッドを配置したりといった、野生のガチョウの追跡を行ってきました。

また、プロトコル定義に何を入れてもかまいません。拡張機能をそのままにしておくと、次の例は機能せず、プロトコルの定義で宣言されたメソッドの組み合わせも機能しません。

_@objc protocol KeyboardDismissing { 
    func on(tap: UITapGestureRecognizer)
}
_

これにより、スタンドアロンプ​​ロトコルとしてコンパイルすることで機能するように思われますが、2番目に、それをビューコントローラに追加しようとします。

_class ViewController: UIViewController, KeyboardDismissing {}
_

元のエラーを返します。

誰かが私が間違っていることとこれをコンパイルする方法を説明できますか?

注:

私は この質問 を見てきましたが、Swift 2.2ではありませんSwift 3ではありません。また、回答がすぐにコンパイルされるわけではありません。例で定義されているプロトコルから継承するビューコントローラークラスを作成します。

私も この質問 を見ましたが、答えはNotificationCenterを使用しています。これは私が求めているものではありません。

他に重複しているように見える質問がある場合は、お知らせください。

22
Aaron

これはSwiftプロトコル拡張機能です。Swiftプロトコル拡張機能はObjective-Cからは見えません。何があってもそれは何も知りません。しかし_#selector_は、Objective-Cが関数を認識して呼び出すことを意味します。on(tap:)関数がプロトコル拡張で定義されているonlyであるため、これは起こりません。したがって、コンパイラーは正しく停止します。

この質問は、Objective-C呼び出し可能な機能(セレクター、デリゲートメソッドなど)をプロトコルを介してクラスに挿入しようとすることにより、Cocoaを処理する際にプロトコル拡張で巧妙になると考える大きなクラスの質問の1つです。拡張。それは魅力的な概念ですが、うまくいきません。

17
matt

マットの答えは正しいです。ただし、それを追加します。NotificationCenter通知から使用する#selectorを処理している場合は、クロージャバージョンを使用して#selectorを回避することができます。

例:

書く代わりに:

extension KeyboardHandler where Self: UIViewController {

    func startObservingKeyboardChanges() {

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillShow(_:)),
            // !!!!!            
            // compile error: cannot be included in a Swift protocol
            name: .UIKeyboardWillShow,
            object: nil
        )
    }

     func keyboardWillShow(_ notification: Notification) {
       // do stuff
    }
}

あなたは書くことができます:

extension KeyboardHandler where Self: UIViewController {

    func startObservingKeyboardChanges() {

        // NotificationCenter observers
        NotificationCenter.default.addObserver(forName: .UIKeyboardWillShow, object: nil, queue: nil) { [weak self] notification in
            self?.keyboardWillShow(notification)
        }
    }

    func keyboardWillShow(_ notification: Notification) {
       // do stuff
    }
}
41
Frédéric Adda

Mattが言ったように、プロトコルに@objcメソッドを実装することはできません。フレデリックの答えはNotificationsをカバーしていますが、標準のSelectorsに対して何ができますか?

次のようなプロトコルと拡張機能があるとします

protocol KeyboardHandler {
    func setupToolbar()
}

extension KeyboardHandler {
    func setupToolbar() {
        let toolbar = UIToolbar()
        let doneButton = UIBarButtonItem(title: "Done",
                                         style: .done,
                                         target: self,
                                         action: #selector(self.donePressed))

    }

    @objc func donePressed() {
        self.endEditing(true)
    }
}

私たちが知っているように、これはエラーを生成します。私たちにできることは、コールバックを利用することです。

protocol KeyboardHandler {
    func setupToolbar(callback: (_ doneButton: UIBarButtonItem) -> Void))
}

extension KeyboardHandler {
    func setupToolbar(callback: (_ doneButton: UIBarButtonItem) -> Void)) {
        let toolbar = UIToolbar()
        let doneButton = UIBarButtonItem(title: "Done",
                                         style: .done,
                                         target: self,
                                         action: nil

        callback(doneButton)

    }

}

次に、プロトコルを実装するクラスの拡張機能を追加します

extension ViewController: KeyboardHandler {

    func addToolbar(textField: UITextField) {
        addToolbar(textField: textField) { doneButton in
            doneButton.action = #selector(self.donePressed)
        }
    }

    @objc func donePressed() {
        self.view.endEditing(true)
    }

}

作成時にアクションを設定する代わりに、コールバックで作成直後に設定します。

このようにして、必要な機能を引き続き取得し、コールバックを確認することなく、クラスの関数(例:ViewController)を呼び出すことができます。

3
Hayden Holligan

別の観点から、私は別の試みをしました。私は多くの開発で、それに含まれるUINavigationBarのそれぞれから、グローバルな方法でUIViewControllerのスタイルを処理するプロトコルを使用しています。

これを行う際の最大の問題の1つは、以前のUIViewController(ポップ)に戻り、モーダルな方法で表示されるUIViewControllerを閉じる標準の動作です。いくつかのコードを見てみましょう:

public protocol NavigationControllerCustomizable {

}

extension NavigationControllerCustomizable where Self: UIViewController {
public func setCustomBackButton(on navigationItem: UINavigationItem) {
        let backButton = UIButton()
        backButton.setImage(UIImage(named: "navigationBackIcon"), for: .normal)
        backButton.tintColor = navigationController?.navigationBar.tintColor
        backButton.addTarget(self, action: #selector(defaultPop), for: .touchUpInside)
        let barButton = UIBarButtonItem(customView: backButton)
        navigationItem.leftBarButtonItem = barButton
    }
}

これは元のプロトコルを非常に簡略化した(そして少し変更した)バージョンですが、例を説明する価値はあります。

ご覧のとおり、プロトコル拡張内に#selectorが設定されています。ご存知のように、プロトコル拡張はObjective-Cに公開されていないため、エラーが発生します。

私の解決策は、すべてのUIViewController(ポップと却下)の標準動作を処理するメソッドを別のプロトコルでラップし、UIViewControllerをそれに拡張することです。コードでこれを表示する:

public protocol NavigationControllerDefaultNavigable {
    func defaultDismiss()
    func defaultPop()
}

extension UIViewController: NavigationControllerDefaultNavigable {
    public func defaultDismiss() {
        dismiss(animated: true, completion: nil)
    }

    public func defaultPop() {
        navigationController?.popViewController(animated: true)
    }
}

この回避策により、UIViewControllerを実装するすべてのNavigationControllerCustomizableは、NavigationControllerDefaultNavigableで定義されたメソッドをすぐにデフォルトの実装で持つため、Objective-Cからアクセスして式を作成できます。タイプ#selector、タイプのエラーなし。

この説明が誰かのお役に立てば幸いです。

3
Jorge Ramos

これが私のアイデアです:Swift protocol&objc protocol。 enter image description here

2
hstdt

Swift動的キーワードを使用することで、@ objcを使用せずにセレクターを介してメソッドを呼び出すことができます。これにより、コンパイラーに動的を使用するように指示しています。暗黙的にディスパッチします。

import UIKit

protocol Refreshable: class {

    dynamic func refreshTableData()

    var tableView: UITableView! {get set}
}

extension Refreshable where Self: UIViewController {

    func addRefreshControl() {
        tableView.insertSubview(refreshControl, at: 0)
    }

    var refreshControl: UIRefreshControl {
        get {
            let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self))
            if let control = _refreshControl[tmpAddress] as? UIRefreshControl {
                return control
            } else {
                let control = UIRefreshControl()
                control.addTarget(self, action: Selector(("refreshTableData")), for: .valueChanged)
                _refreshControl[tmpAddress] = control
                return control
            }
        }
    }
}

fileprivate var _refreshControl = [String: AnyObject]()

class ViewController: UIViewController: Refreshable {
    @IBOutlet weak var tableView: UITableView! {
        didSet {
            addRefreshControl()
        }
    }

    func refreshTableData() {
        // Perform some stuff
    }
}
0
Abhijeet Rai

@FrédéricAddaの回答には責任があるというマイナス面があります オブザーバーの登録を解除するには 。これは、オブザーバーを追加するブロックベースの方法を使用するためです。 iOS 9以降では、オブザーバーを追加する「通常の」方法は、オブザーバーへの弱い参照を保持するため、 開発者はオブザーバーの登録を解除する必要はありません

次の方法は、プロトコル拡張を通じてオブザーバーを追加する「通常の」方法を使用します。セレクターを保持するブリッジングクラスを使用します。

プロの:

  • オブザーバーを手動で削除していない
  • NotificationCenterを使用するタイプセーフな方法

短所:

  • 手動で登録を呼び出す必要があります。 selfが完全に初期化されたら、これを1回実行します。

コード:

/// Not really the user info from the notification center, but this is what we want 99% of the cases anyway.
public typealias NotificationCenterUserInfo = [String: Any]

/// The generic object that will be used for sending and retrieving objects through the notification center.
public protocol NotificationCenterUserInfoMapper {
    static func mapFrom(userInfo: NotificationCenterUserInfo) -> Self

    func map() -> NotificationCenterUserInfo
}

/// The object that will be used to listen for notification center incoming posts.
public protocol NotificationCenterObserver: class {

    /// The generic object for sending and retrieving objects through the notification center.
    associatedtype T: NotificationCenterUserInfoMapper

    /// For type safety, only one notification name is allowed.
    /// Best way is to implement this as a let constant.
    static var notificationName: Notification.Name { get }

    /// The selector executor that will be used as a bridge for Objc - C compability.
    var selectorExecutor: NotificationCenterSelectorExecutor! { get set }

    /// Required implementing method when the notification did send a message.
    func retrieved(observer: T)
}

public extension NotificationCenterObserver {
    /// This has to be called exactly once. Best practise: right after 'self' is fully initialized.
    func register() {
        assert(selectorExecutor == nil, "You called twice the register method. This is illegal.")

        selectorExecutor = NotificationCenterSelectorExecutor(execute: retrieved)

        NotificationCenter.default.addObserver(selectorExecutor, selector: #selector(selectorExecutor.hit), name: Self.notificationName, object: nil)
    }

    /// Retrieved non type safe information from the notification center.
    /// Making a type safe object from the user info.
    func retrieved(userInfo: NotificationCenterUserInfo) {
        retrieved(observer: T.mapFrom(userInfo: userInfo))
    }

    /// Post the observer to the notification center.
    func post(observer: T) {
        NotificationCenter.default.post(name: Self.notificationName, object: nil, userInfo: observer.map())
    }
}

/// Bridge for using Objc - C methods inside a protocol extension.
public class NotificationCenterSelectorExecutor {

    /// The method that will be called when the notification center did send a message.
    private let execute: ((_ userInfo: NotificationCenterUserInfo) -> ())

    public init(execute: @escaping ((_ userInfo: NotificationCenterUserInfo) -> ())) {
        self.execute = execute
    }

    /// The notification did send a message. Forwarding to the protocol method again.
    @objc fileprivate func hit(_ notification: Notification) {
        execute(notification.userInfo! as! NotificationCenterUserInfo)
    }
}

私のGitHubから(Cocoapodsからコードを使用することはできません): https://github.com/Jasperav/JVGenericNotificationCenter

0
J. Doe