25

I have what I thought to be a very simple protocol extension for my UIViewControllers providing the capability to dismiss a keyboard through a tap gesture. Here's my 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)
    }
}

The problem is that the above code throws a compile error on this line:

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

With the error message:

Argument of '#selector' refers to instance method 'on(tap:)' that is not exposed to Objective-C

with the suggestion to "fix it" by adding @objc before func on(tap: UITapGestureRecognizer)

Ok fine, I add the tag:

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

But then, it throws a different compile error on this newly added @objc tag with the error message:

@objc can only be used with members of classes, @objc protocols, and concrete extensions of classes

with the suggestion to "fix it" by removing the exact same tag I was just told to add.

I originally thought adding @objc before my protocol definition would solve any #selector problems but apparently that's not the case, and these cyclical error messages/suggestions aren't helping in the slightest. I've gone down a wild goose chase of adding/removing @objc tags everywhere, marking methods as optional, putting methods in the protocol's definition, etc.

It also doesn't matter what I put in the protocol definition Leaving the extension the same, the following example does not work nor does any combination of the declared methods in the protocol's definition:

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

This tricks me into thinking it works by compiling as a stand alone protocol, but the second I try to add it to a view controller:

class ViewController: UIViewController, KeyboardDismissing {}

it spits back the original error.

Can someone explain what I'm doing wrong and how I can compile this?

Note:

I've looked at this question but it is for Swift 2.2 not Swift 3 nor does the answer compile as soon as you create a view controller class that inherits from the protocol defined in the example.

I've also looked at this question but the answer uses NotificationCenter which is not what I'm after.

If there are any other seemingly duplicate questions, please let me know.

Community
  • 1
  • 1
Aaron
  • 6,466
  • 7
  • 37
  • 75
  • Duplicate of http://stackoverflow.com/questions/36184912/swift-2-2-selector-in-protocol-extension-compiler-error ? – Martin R Oct 28 '16 at 01:53
  • Good eye, I've tried incorporating that answer to no avail. I should point out that this is for Swift 3 and not Swift <=2.2. I've since edited the title and added the `Swift3` tag to reflect that. – Aaron Oct 28 '16 at 01:58
  • Whoever downvoted, can you care to explain? I'll edit my question to improve it if I know what's wrong with it. – Aaron Oct 28 '16 at 02:20
  • The reason for the downvote is that this _is_ a clear duplicate, if not of this, then of many other questions that have already been answered the same way. You are trying to inject Objective-C-callable functionality (selector, delegate method, whatever) into a class via a protocol extension. That's not going to work. – matt Oct 28 '16 at 02:27
  • It's a great pity but it's "just the way it is". Swift is magnificent, but it's still half a language. – Fattie Jan 23 '17 at 02:12

7 Answers7

43

Matt's answer is correct. However, I would just add that, if you are dealing with #selector to use from a NotificationCenter notification, you could try to avoid #selector by using the closure version.

Example:

Instead of writing:

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

you could write:

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
    }
}
Frederic Adda
  • 5,905
  • 4
  • 56
  • 71
  • 1
    Genius! Stumbled across this and after reading your answer I thought to myself: dude, you are using closures almost all the time, why haven't you just checked if theres a closure version to observe the notification center. now I can get rid of so many unnecessary functions which should really have been closure-observers – xxtesaxx May 17 '17 at 20:48
  • Thanks ;-) Glad I could be helpful! – Frederic Adda May 18 '17 at 03:35
  • So much better than using @objc and selector, because now I can put all functionality in a protocol. I didn't even realize that there is an addObserver with closure. – Flupp Sep 29 '17 at 11:14
  • Awesome, didn't realize there was closure version for notification. Too bad there are none for UIGestures class. – Caio Mar 11 '18 at 17:24
  • 1
    Doesn't this way gives memory leaks? I don't see any unregistering events. – J. Doe Feb 09 '19 at 14:59
  • This is solely to illustrate the fact that there is a closure-based version of the NotificationCenter addObserver method. Feel free to unregister from the notificationCenter with the removeObserver method. – Frederic Adda Feb 10 '19 at 15:55
  • 1
    I wish I could upvote you more than once, thanks, was actually sitting here thinking "there has to be a closure solution - this @objc stuff just doesn't feel right in extensions" – Andy Dent Feb 13 '19 at 09:09
  • It can lead to memory leaks if wouldn't be unregistered. – Nike Kov Jan 14 '20 at 07:06
18

This is a Swift protocol extension. Swift protocol extensions are invisible to Objective-C, no matter what; it knows nothing of them. But #selector is about Objective-C seeing and calling your function. That is not going to happen because your on(tap:) function is defined only in the protocol extension. Thus the compiler rightly stops you.

This question is one of a large class of questions where people think they are going to be clever with protocol extensions in dealing with Cocoa by trying to inject Objective-C-callable functionality (selector, delegate method, whatever) into a class via a protocol extension. It's an appealing notion but it's just not going to work.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • I understand and thanks for clarifying. I actually did have `addDismissalGesture()`, `on(tap: UITapGestureRecognizer)`, and `dismissKeyboard()` all declared in the protocol definition with combinations of just one of the methods to all three with and without `@objc` tags _and_ implemented in the extension but that was not working either. I intentionally left them out of my question for the sake of simplicity. The error remains if the are declared in the definition. – Aaron Oct 28 '16 at 02:27
  • I can only respond to the question you asked. If you rearrange your code significantly, that's another question. You asked why _this_ was not compiling and I believe I told you correctly why. – matt Oct 28 '16 at 02:28
  • 1
    BTW there is an open bug on this https://bugs.swift.org/browse/SR-492 But right now, this is the way it is. – matt Oct 28 '16 at 02:29
  • Thanks. I've since edited my question to better reflect what I'm after. – Aaron Oct 28 '16 at 02:40
  • My answer stands as written. Your strategy is doomed to failure, as I said in my comment, and I believe I've shown why. – matt Oct 28 '16 at 02:58
  • You're original answer before it was edited stated that "if you defined your on(tap:) function in the protocol itself, things would be different" but they were not different as I described in my edited question. I'll accept your edited answer and deduct that there is no way to mimic this keyboard functionality that I'm after via Swift protocols. – Aaron Oct 28 '16 at 03:19
4

As Matt said, you can't implement @objc methods in a protocol. Frédéric's answer covers Notifications, but what can you do about standard Selectors?

Let's say you have a protocol & extension, like so

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

This will generate an error, as we know. What we can do, is take advantage of callbacks.

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)

    }

}

Then, add an extension for the class you want to implement your protocol

extension ViewController: KeyboardHandler {

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

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

}

Instead of setting the action on creation, set it just after creation in the callback.

This way, you still get your desired functionality and can call the function in your class (ex. ViewController) without even seeing the callbacks!

Hayden Holligan
  • 1,822
  • 2
  • 19
  • 26
3

I have made another attempt, from another point of view. I use in many of my developments, a protocol to handle the style of UINavigationBar in a global way, from each of the UIViewController contained in it.

One of the biggest problems of doing this is the standard behavior to return to the previous UIViewController (pop) and dismiss a UIViewController shown in a modal way. Let's look at some code:

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

This is a very simplified (and slightly modified) version of the original protocol, although it will be worth explaining the example.

As you can see, a #selector is being set within a protocol extension. As we know, protocol extensions are not exposed to Objective-C and therefore this will generate an error.

My solution is to wrap the methods that handle the standard behaviors of all my UIViewController (pop and dismiss) in another protocol and extend UIViewController to it. Viewing this in 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)
    }
}

With this workaround, all UIViewController implementing the NavigationControllerCustomizable will immediately have the methods defined in NavigationControllerDefaultNavigable, with their default implementation, and therefore be accessible from Objective-C to create expressions of type #selector, without any type of error.

I hope this explanation can help someone.

Jorge Ramos
  • 891
  • 10
  • 21
3

Here's my idea: avoid mixing swift protocol & objc protocol. enter image description here

hstdt
  • 5,652
  • 2
  • 34
  • 34
0

@Frédéric Adda answer have the downside that you are responsible to unregister your observer, because it uses the block based way of adding an observer. In iOS 9 and later, the 'normal' way of adding an observer, will hold a weak reference to the observer and therefore the developer doesn't have to unregister the observer.

The following way will use the 'normal' way of adding an observer through protocol extensions. It uses a bridging class that will hold the selector.

Pro's:

  • You do not have the manually remove the observer
  • Typesafe way of using the NotificationCenter

Con's:

  • You have to call register manually. Do this once after self is fully initialized.

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

From my GitHub (you can't use the code through Cocoapods): https://github.com/Jasperav/JVGenericNotificationCenter

J. Doe
  • 12,159
  • 9
  • 60
  • 114
0

Here is a similar use-case, you can call a method through a selector without using @objc as in swift by using the dynamic keyword. By doing so, you are instructing the compiler to use dynamic dispatch implicitly.

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
    }
}
Abhijeet Rai
  • 119
  • 1
  • 5