it-swarm.com.de

Swift 3-Protokollerweiterung mit Wahlfehler

Ich habe eine meiner Meinung nach sehr einfache Protokollerweiterung für meine UIViewControllers, die die Möglichkeit bietet, eine Tastatur durch Tippen auf eine Geste zu schließen. Hier ist mein 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)
    }
}

Das Problem ist, dass der obige Code einen Kompilierungsfehler in dieser Zeile auslöst:

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

Mit der Fehlermeldung:

Das Argument von '#selector' bezieht sich auf die Instanzmethode 'on (tap :)', die nicht für Objective-C verfügbar ist

mit dem Vorschlag, das Problem zu beheben, indem Sie @objc vor func on(tap: UITapGestureRecognizer) hinzufügen

Ok, ich füge den Tag hinzu: 

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

Dann wirft es jedoch einen anderen Kompilierungsfehler in diesem neu hinzugefügten @objc-Tag mit der Fehlermeldung:

@objc kann nur mit Klassenmitgliedern, @objc-Protokollen und konkreten Erweiterungen von Klassen verwendet werden

mit dem Vorschlag, das Problem zu beheben, indem das exakt gleiche Tag entfernt wird, das mir gerade hinzugefügt wurde.

Ich dachte ursprünglich, dass das Hinzufügen von @objc vor meiner Protokolldefinition alle #selector-Probleme lösen würde, aber anscheinend ist dies nicht der Fall, und diese zyklischen Fehlermeldungen/Vorschläge helfen nicht im geringsten. Ich habe eine wilde Verfolgungsjagd gemacht, @objc-Tags überall hinzuzufügen/zu entfernen, Methoden als optional zu markieren, Methoden in die Definition des Protokolls zu setzen, usw.

Es spielt auch keine Rolle, was ich in die Protokolldefinition stecke. Wenn die Erweiterung gleich bleibt, funktioniert das folgende Beispiel nicht und auch keine Kombination der deklarierten Methoden in der Protokolldefinition:

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

Das bringt mich dazu zu denken, dass es funktioniert, indem es als eigenständiges Protokoll kompiliert wird, aber das zweite versuche ich, es einem View-Controller hinzuzufügen:

class ViewController: UIViewController, KeyboardDismissing {}

es spuckt den ursprünglichen Fehler zurück.

Kann jemand erklären, was ich falsch mache und wie ich das kompilieren kann?

Hinweis:

Ich habe mir diese Frage angesehen, aber es ist für Swift 2.2 nicht Swift 3 und die Antwort wird nicht kompiliert, sobald Sie eine View-Controller-Klasse erstellen, die das im Beispiel definierte Protokoll erbt.

Ich habe mir auch diese Frage angesehen, aber die Antwort verwendet NotificationCenter, was ich nicht suche.

Wenn es andere scheinbar doppelte Fragen gibt, lass es mich wissen.

16
Aaron

Dies ist eine Erweiterung des Swift-Protokolls. Swift-Protokollerweiterungen sind für Objective-C nicht sichtbar, egal was passiert; es weiß nichts von ihnen. Bei #selector geht es darum, dass Objective-C Ihre Funktion sieht und aufruft. Das wird nicht passieren, da Ihre on(tap:)-Funktion in der Protokollerweiterung only definiert ist. Der Compiler stoppt also zu Recht.

Diese Frage gehört zu einer großen Klasse von Fragen, bei denen die Leute denken, sie würden mit Protokollerweiterungen im Umgang mit Cocoa klug sein, indem sie versuchen, Objective-C-aufrufbare Funktionen (Selector, Delegate-Methode, was auch immer) über ein Protokoll in eine Klasse zu injizieren Erweiterung. Es ist eine ansprechende Vorstellung, aber es wird einfach nicht funktionieren.

10
matt

Matts Antwort ist richtig. Ich möchte jedoch hinzufügen, dass Sie, wenn Sie sich mit #selector befassen, um von einer NotificationCenter-Benachrichtigung aus zu verwenden, versuchen, #selector zu vermeiden, indem Sie die Abschlussversion verwenden.

Beispiel:

Anstatt zu schreiben:

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

du könntest schreiben:

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

Wie Matt gesagt hat, können Sie keine @objc-Methoden in einem Protokoll implementieren. Die Antwort von Frédéric umfasst Notifications, aber was können Sie mit dem Standard Selectors tun?

Nehmen wir an, Sie haben ein Protokoll und eine Erweiterung

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

Dies wird bekanntermaßen zu einem Fehler führen. Was wir tun können, ist, Callbacks zu nutzen. 

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)

    }

}

Fügen Sie dann eine Erweiterung für die Klasse hinzu, die Sie Ihr Protokoll implementieren möchten

extension ViewController: KeyboardHandler {

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

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

}

Anstatt die Aktion für die Erstellung festzulegen, setzen Sie sie im Callback direkt nach der Erstellung. 

Auf diese Weise erhalten Sie immer noch Ihre gewünschte Funktionalität und können die Funktion in Ihrer Klasse (z. B. ViewController) aufrufen, ohne die Rückrufe zu sehen!

2
Hayden Holligan

Ich habe aus einem anderen Blickwinkel einen weiteren Versuch unternommen. Ich verwende in vielen meiner Entwicklungen ein Protokoll, um den Stil von UINavigationBar auf globale Art und Weise von jeder der darin enthaltenen UIViewController zu behandeln.

Eines der größten Probleme dabei ist das standardmäßige Verhalten, um zur vorherigen UIViewController (pop) zurückzukehren und eine UIViewController, die auf modale Weise angezeigt wird, zu verwerfen. Schauen wir uns einen Code an:

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

Dies ist eine sehr vereinfachte (und leicht modifizierte) Version des ursprünglichen Protokolls, obwohl es sich lohnt, das Beispiel zu erläutern.

Wie Sie sehen, wird ein #selector innerhalb einer Protokollerweiterung gesetzt. Wie wir wissen, sind Protokollerweiterungen nicht für Objective-C verfügbar. Daher wird ein Fehler generiert.

Meine Lösung besteht darin, die Methoden, die das Standardverhalten aller meiner UIViewController (pop und demiss) behandeln, in ein anderes Protokoll zu packen und UIViewController darauf zu erweitern. Anzeigen dieses im 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)
    }
}

Bei dieser Problemumgehung werden für alle UIViewController, die die NavigationControllerCustomizable implementieren, sofort die in NavigationControllerDefaultNavigable definierten Methoden mit ihrer Standardimplementierung definiert. Sie können daher von Objective-C aus aufgerufen werden, um Ausdrücke vom Typ #selector ohne Fehler zu erstellen.

Ich hoffe, diese Erklärung kann jemandem helfen.

2
Jorge Ramos

Hier ist meine Idee: Vermeiden Sie das Mischen von Swift protocol & objc protocol. enter image description here

1
hstdt

@ Frédéric Adda answer hat den Nachteil, dass Sie die Registrierung des Beobachters aufheben müssen , weil er die blockbasierte Methode zum Hinzufügen eines Beobachters verwendet. In iOS 9 und höher enthält der "normale" Weg, einen Beobachter hinzuzufügen, einen schwachen Verweis auf den Beobachter. Daher muss der Entwickler den Beobachter nicht abmelden .

Auf die folgende Weise wird ein "normaler" Weg zum Hinzufügen eines Beobachters über Protokollerweiterungen verwendet. Es verwendet eine Überbrückungsklasse, die den Selektor enthalten wird.

Pro's: 

  • Sie müssen den Beobachter nicht manuell entfernen
  • Typesafe Verwendung des NotificationCenter

Con's: 

  • Sie müssen die Registrierung manuell anrufen. Tun Sie dies einmal, nachdem self vollständig initialisiert ist.

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

Von meinem GitHub aus (Sie können den Code nicht über Cocoapods verwenden): https://github.com/Jasperav/JVGenericNotificationCenter

0
J. Doe