【问题标题】:Swift 3 protocol extension using selector errorSwift 3 协议扩展使用选择器错误
【发布时间】:2017-03-10 19:52:28
【问题描述】:

我有一个我认为是我的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)
}

这让我误以为它是通过编译为独立协议来工作的,但第二次我尝试将它添加到视图控制器:

class ViewController: UIViewController, KeyboardDismissing {}

它吐回原来的错误。

谁能解释我做错了什么以及如何编译它?

注意:

我查看了this question,但它适用于 Swift 2.2 而不是 Swift 3,也不会在您创建一个继承自示例中定义的协议的视图控制器类后立即编译答案。

我也看过this question,但答案使用NotificationCenter,这不是我想要的。

如果还有其他看似重复的问题,请告诉我。

【问题讨论】:

  • 好眼光,我尝试合并该答案无济于事。我应该指出,这是针对 Swift 3 而不是 Swift Swift3 标签来反映这一点。
  • 谁投了反对票,你能解释一下吗?如果我知道问题出在哪里,我会编辑我的问题以改进它。
  • 投反对票的原因是这明显的重复,如果不是这个,那么许多其他问题已经以同样的方式得到了回答。您正在尝试通过协议扩展将 Objective-C 可调用功能(选择器、委托方法等)注入到类中。那是行不通的。
  • 很遗憾,但它“就是这样”。 Swift 很棒,但它仍然是一门语言。

标签: ios objective-c swift swift3 protocols


【解决方案1】:

马特的回答是正确的。但是,我只想补充一点,如果您正在处理 #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
    }
}

【讨论】:

  • 天才!偶然发现了这一点,在阅读了您的答案后,我心想:伙计,您几乎一直在使用闭包,为什么不检查是否有闭包版本来观察通知中心。现在我可以摆脱这么多不必要的功能,这些功能本来应该是闭包观察者
  • 谢谢 ;-) 很高兴我能提供帮助!
  • 比使用@objc 和选择器要好得多,因为现在我可以将所有功能放在一个协议中。我什至没有意识到有一个带有闭包的 addObserver。
  • 这种方式不会造成内存泄漏吗?我没有看到任何取消注册的事件。
  • 我希望我能不止一次地为你投票,谢谢,实际上是坐在这里想“必须有一个闭包解决方案 - 这个 @objc 的东西在扩展中感觉不对”
【解决方案2】:

这是一个 Swift 协议扩展。 Swift 协议扩展对 Objective-C 是不可见的,无论如何;它对它们一无所知。但是#selector 是关于 Objective-C 看到和调用你的函数。这不会发生,因为您的on(tap:) 函数在协议扩展中定义。因此编译器正确地阻止了你。

这个问题是一大类问题之一,人们认为他们将通过尝试将 Objective-C 可调用功能(选择器、委托方法等)注入到一个类中来巧妙地处理 Cocoa 的协议扩展通过协议扩展。这是一个吸引人的想法,但它只是行不通。

【讨论】:

  • 我理解并感谢您的澄清。实际上,我确实在协议定义中声明了 addDismissalGesture()on(tap: UITapGestureRecognizer)dismissKeyboard(),并结合了所有三种方法中的一种,有和没有 @objc 标签 and 在中实现扩展,但这也不起作用。为了简单起见,我故意将它们排除在我的问题之外。如果在定义中声明了,错误仍然存​​在。
  • 我只能回答你问的问题。如果你重新排列你的代码,那是另一个问题。您问为什么 this 没有编译,我相信我正确地告诉了您原因。
  • 顺便说一句,bugs.swift.org/browse/SR-492 有一个开放的错误,但现在,就是这样。
  • 谢谢。我已经编辑了我的问题,以更好地反映我所追求的。
  • 我的回答是书面的。正如我在评论中所说,你的策略注定要失败,我相信我已经说明了原因。
【解决方案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)
        }
    }
    

    使用此解决方法,所有实现NavigationControllerCustomizableUIViewController 将立即拥有NavigationControllerDefaultNavigable 中定义的方法及其默认实现,因此可以从Objective-C 访问以创建#selector 类型的表达式,没有任何类型的错误。

    我希望这个解释可以帮助别人。

    【讨论】:

    • 这对我不起作用。与选择器相同的错误。
    【解决方案5】:

    这是我的想法:避免混合使用 swift 协议和 objc 协议。

    【讨论】:

      【解决方案6】:

      @Frédéric Adda 答案的缺点是您负责 to unregister your observer,因为它使用基于块的方式添加观察者。在 iOS 9 及更高版本中,添加观察者的“正常”方式将持有对观察者的弱引用,因此 the developer doesn't have to unregister the observer

      以下方式将使用通过协议扩展添加观察者的“正常”方式。它使用一个包含选择器的桥接类。

      专业人士:

      • 您没有手动删除观察者
      • 使用 NotificationCenter 的类型安全方式

      缺点:

      • 您必须手动调用注册。在 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

      【讨论】:

        【解决方案7】:

        这是一个类似的用例,您可以通过选择器调用方法,而无需像在 swift 中那样使用 dynamic 关键字来使用 @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
            }
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-08-21
          • 1970-01-01
          • 2016-07-11
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多