使用选择器错误的Swift 3协议扩展

时间:2016-10-28 00:43:25

标签: ios objective-c swift swift3 protocols

我认为我的istringstream istr(str); char token; istr >> token; string t; t.push_back(token); istr >> token; while (token != ' ' && token != '+' && token != '-') { t.push_back(token); istr >> token; } 提供了通过轻击手势解除键盘功能的非常简单的协议扩展。这是我的代码:

UIViewController

问题是上面的代码在这一行引发编译错误:

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

显示错误消息:

  

'#selector'的参数是指没有暴露给Objective-C的实例方法'on(tap :)'

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

之前添加@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)
}

它吐出原来的错误。

有人可以解释我做错了什么以及如何编译它?

注意:

我看过this question,但它是针对Swift 2.2而不是Swift 3,一旦创建了一个继承自示例中定义的协议的视图控制器类,答案就会编译。

我也看了this question,但答案使用的是class ViewController: UIViewController, KeyboardDismissing {} ,这不是我所追求的。

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

7 个答案:

答案 0 :(得分:35)

马特的回答是正确的。但是,我只想补充一点,如果您要处理 #selector 以使用NotificationCenter通知,您可以尝试使用闭包版本来避免 #selector 。< / p>

示例:

而不是写作:

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

答案 1 :(得分:10)

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

这个问题是一大类问题之一,人们认为通过尝试将Objective-C可调用功能(选择器,委托方法,等等)注入类中来处理Cocoa时,他们会变得很聪明。通过协议扩展。这是一个吸引人的想法,但它不会起作用。

答案 2 :(得分:3)

从另一个角度来看,我又做了一次尝试。我在我的许多开发中使用了一个协议,以全局的方式处理UINavigationBar的样式,包含在其中的每个UIViewController

执行此操作的最大问题之一是返回到上一个UIViewController(pop)的标准行为,并以模态方式关闭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(pop和dismiss)标准行为的方法包装在另一个协议中,并将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 ,不会出现任何类型的错误。

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

答案 3 :(得分:2)

正如Matt所说,你不能在协议中实现@objc方法。 Frédéric的答案涵盖Notifications,但您对标准Selectors有什么看法?

假设你有一个协议&amp;扩展,像这样

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 :(得分:0)

@FrédéricAdda的回答是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

答案 5 :(得分:0)

同样的情况,一个固定的通报问题

这是我的主意:避免混合使用swift协议和objc协议。 enter image description here

答案 6 :(得分:0)

这是一个类似的用例,您可以通过选择器调用方法,而无需像通过使用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
    }
}