web-dev-qa-db-fra.com

Extension du protocole Swift 3 avec erreur de sélection

J'ai ce que je pensais être une extension de protocole très simple pour ma UIViewControllers, offrant la possibilité de fermer un clavier par un geste de tapotement. Voici mon code:

@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)
    }
}

Le problème est que le code ci-dessus renvoie une erreur de compilation sur cette ligne:

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

Avec le message d'erreur:

L'argument de '#selector' fait référence à la méthode d'instance 'on (tap :)' qui n'est pas exposée à Objective-C

avec la suggestion de "réparer" en ajoutant @objc avant func on(tap: UITapGestureRecognizer)

Ok bien, j'ajoute le tag: 

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

Mais ensuite, il génère une erreur de compilation différente sur cette nouvelle balise @objc avec le message d'erreur suivant:

@objc ne peut être utilisé qu'avec des membres de classes, des protocoles @objc et des extensions concrètes de classes

avec la suggestion de "résoudre le problème" par en retirant exactement la même balise qu'on m'a juste dit d'ajouter.

Au départ, je pensais que l'ajout de @objc avant la définition de mon protocole résoudrait tous les problèmes de #selector, mais apparemment, ce n'est pas le cas, et ces messages/suggestions d'erreur cycliques n'aident en rien. Je me suis laissé aller à une folle poursuite en ajoutant/supprimant des balises @objc partout, en marquant des méthodes comme étant optional, en plaçant des méthodes dans la définition du protocole, etc.

Peu importe également ce que je mets dans la définition du protocole. En laissant l'extension identique, l'exemple suivant ne fonctionne pas et aucune combinaison des méthodes déclarées dans la définition du protocole:

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

Cela me conduit à penser que cela fonctionne en compilant en tant que protocole autonome, mais dès que j'essaye de l'ajouter à un contrôleur de vue:

class ViewController: UIViewController, KeyboardDismissing {}

il crache l'erreur d'origine.

Quelqu'un peut-il expliquer ce que je fais mal et comment je peux le compiler?

Remarque:

J'ai examiné cette question mais c'est pour Swift 2.2 et non pour Swift 3, et la réponse ne se compile pas dès que vous créez une classe de contrôleur de vue qui hérite du protocole défini dans l'exemple.

J'ai aussi regardé cette question mais la réponse utilise NotificationCenter qui n'est pas ce que je recherche.

S'il y a d'autres questions qui semblent faire double emploi, veuillez me le faire savoir.

16
Aaron

C'est une extension de protocole Swift. Les extensions de protocole Swift sont invisibles pour Objective-C, quoi qu'il arrive; il n'en sait rien. Mais #selector concerne Objective-C qui voit et appelle votre fonction. Cela ne se produira pas car votre fonction on(tap:) est définie seulement dans l'extension de protocole. Ainsi, le compilateur vous arrête à juste titre.

Cette question fait partie d'une grande catégorie de questions où les gens pensent qu'ils vont utiliser intelligemment les extensions de protocole pour traiter avec Cocoa en essayant d'injecter une fonctionnalité Objectable-C-callable (sélecteur, méthode de délégation, etc.) dans une classe via un protocole. extension. C'est une notion séduisante mais elle ne va tout simplement pas au travail.

10
matt

La réponse de Matt est correcte. Cependant, je voudrais simplement ajouter que, si vous utilisez #selector à utiliser à partir d’une notification NotificationCenter, vous pouvez essayer d’éviter/ #selector en utilisant la version de fermeture.

Exemple:

Au lieu d'écrire:

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
    }
}

tu pourrais écrire:

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
    }
}
34
Frédéric Adda

Comme Matt l'a dit, vous ne pouvez pas implémenter les méthodes @objc dans un protocole. La réponse de Frédéric couvre Notifications, mais que pouvez-vous faire avec la variable Selectors standard?

Disons que vous avez un protocole et une extension, comme si

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)
    }
}

Cela générera une erreur, comme nous le savons. Ce que nous pouvons faire, c'est tirer parti des rappels. 

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)

    }

}

Ensuite, ajoutez une extension pour la classe que vous souhaitez implémenter votre protocole

extension ViewController: KeyboardHandler {

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

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

}

Au lieu de définir l'action à la création, définissez-la juste après la création dans le rappel. 

De cette façon, vous obtenez toujours la fonctionnalité souhaitée et pouvez l'appeler dans votre classe (ex. ViewController) sans même voir les rappels!

2
Hayden Holligan

J'ai fait une autre tentative, d'un autre point de vue. J'utilise dans beaucoup de mes développements, un protocole pour gérer le style de UINavigationBar de manière globale, à partir de chacun des UIViewController qu'il contient.

L'un des plus gros problèmes de ce type de comportement est le comportement standard qui consiste à revenir à la précédente UIViewController (pop) et à ignorer une UIViewController affichée de manière modale. Regardons un code:

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
    }
}

Ceci est une version très simplifiée (et légèrement modifiée) du protocole original, bien que cela vaille la peine d’expliquer cet exemple.

Comme vous pouvez le constater, un #selector est défini dans une extension de protocole. Comme nous le savons, les extensions de protocole ne sont pas exposées à Objective-C, ce qui génère une erreur.

Ma solution consiste à envelopper les méthodes qui gèrent les comportements standard de tous mes UIViewController (pop and ignore) dans un autre protocole et à étendre UIViewController à celui-ci. Voir ceci dans le code:

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)
    }
}

Avec cette solution de contournement, toutes les UIViewController implémentant NavigationControllerCustomizable auront immédiatement les méthodes définies dans NavigationControllerDefaultNavigable, avec leur implémentation par défaut, et seront donc accessibles depuis Objective-C pour créer des expressions de type #selector , sans aucun type d'erreur.

J'espère que cette explication peut aider quelqu'un.

2
Jorge Ramos

@ Frédéric Adda répond que l'inconvénient est que vous êtes responsable pour annuler l'enregistrement de votre observateur }, _, car il utilise la méthode par bloc consistant à ajouter un observateur. Dans iOS 9 et les versions ultérieures, la méthode "normale" consistant à ajouter un observateur tiendra une référence faible à l'observateur et donc à { le développeur ne doit pas annuler l'enregistrement de l'observateur }.

La méthode suivante utilisera la méthode "normale" consistant à ajouter un observateur via des extensions de protocole. Il utilise une classe de pontage qui contiendra le sélecteur.

Avantages: 

  • Vous n'avez pas le supprimer manuellement l'observateur
  • Typeafe moyen d'utiliser NotificationCenter

Les inconvénients: 

  • Vous devez appeler le registre manuellement. Faites cela une fois que self est complètement initialisé.

Code: 

/// 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)
    }
}

Depuis mon GitHub (vous ne pouvez pas utiliser le code via Cocoapods): https://github.com/Jasperav/JVGenericNotificationCenter

0
J. Doe