Расширение протокола Swift 3 с использованием ошибки выбора

У меня есть то, что я считал очень простым расширением протокола для моего UIViewController, предоставляющего возможность отклонять клавиатуру с помощью жестов tap. Здесь мой код:

@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' относится к методу экземпляра 'on (tap:)', который не подвергается Objective-C

с предложением "исправить", добавив @objc до func on(tap: UITapGestureRecognizer)

Хорошо, я добавляю тег:

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

Но затем он генерирует другую ошибку компиляции в этом недавно добавленном теге @objc с сообщением об ошибке:

@objc может использоваться только с членами классов, протоколами @objc и конкретными расширениями классов

с предложением "исправить", удалив тот же самый тег, который мне просто сказали добавить.

Первоначально я думал добавить @objc, прежде чем мое определение протокола решит любые проблемы #selector, но, по-видимому, это не так, и эти циклические сообщения об ошибках/предложения нисколько не помогают. Я отправился на дикую охоту на гусей, добавляя/удаляя теги @objc всюду, методы маркировки как optional, вводя методы в определение протокола и т.д.

Также не имеет значения, что я ввел в определение протокола. Оставляя расширение одинаковым, следующий пример не работает и не сочетает объявленные методы в определении протокола:

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

Это заставляет меня думать, что он работает, компилируя в качестве автономного протокола, но второй я пытаюсь добавить его в контроллер представления:

class ViewController: UIViewController, KeyboardDismissing {}

он отбрасывает исходную ошибку.

Может кто-нибудь объяснить, что я делаю неправильно, и как я могу скомпилировать это?

Примечание:

Я рассмотрел этот вопрос, но для Swift 2.2 не Swift 3 и компиляция ответа сразу после создания класса контроллера вида, который наследуется от протокол, определенный в примере.

Я также рассмотрел этот вопрос, но ответ использует NotificationCenter, который не является тем, что я после.

Если есть какие-то другие, казалось бы, повторяющиеся вопросы, пожалуйста, дайте мне знать.

Ответ 1

Это расширение протокола Swift. Быстрые расширения протокола невидимы для Objective-C, независимо от того, что; он ничего не знает о них. Но #selector примерно Objective-C видит и вызывает вашу функцию. Этого не произойдет, потому что ваша функция on(tap:) определена только в расширении протокола. Таким образом, компилятор правильно останавливает вас.

Этот вопрос является одним из большого класса вопросов, когда люди думают, что они будут умны с расширениями протоколов при работе с Cocoa, пытаясь вставить Objective-C -callable функциональность (селектор, метод делегирования, что угодно) в класс через расширение протокола. Это привлекательное понятие, но оно просто не сработает.

Ответ 2

Мэтт ответ правильный. Однако я хотел бы просто добавить, что если вы имеете дело С#selector для использования из уведомления NotificationCenter, вы можете попытаться избежать #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
    }
}

Ответ 3

Как сказал Мэтт, вы не можете реализовать методы @objc в протоколе. Ответ Frédéric охватывает 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), даже не видя обратных вызовов!

Ответ 4

Я сделал еще одну попытку, с другой точки зрения. Я использую во многих своих разработках протокол для обработки стиля UINavigationBar глобальным способом, из каждого содержащегося в нем UIViewController.

Одной из самых больших проблем при этом является стандартное поведение для возврата к предыдущему 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, без каких-либо ошибок.

Я надеюсь, что это объяснение может помочь кому-то.

Ответ 5

Вот моя идея: избегать смешивания Swift Protocol & протокол объекта. enter image description here

Ответ 6

Ответ @Frédéric Adda имеет обратную сторону: вы несете ответственность за отмену регистрации своего наблюдателя, поскольку он использует блочный способ добавления наблюдателя. В iOS 9 и более поздних версиях "нормальный" способ добавления наблюдателя будет содержать слабую ссылку на наблюдателя, и поэтому разработчику не нужно отменять регистрацию наблюдателя.

Следующий способ будет использовать "нормальный" способ добавления наблюдателя через расширения протокола. Он использует мостовой класс, который будет содержать селектор.

Pro-х:

  • Вы не имеете вручную удалить наблюдателя
  • Типизированный способ использования NotificationCenter

Con-х:

  • Вы должны позвонить в регистр вручную. Сделайте это один раз после того, как self полностью инициализирован.

Код:

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