Расширение протокола 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