12

I have a set of view controllers which will have a Menu bar button. I created a protocol for those viewControllers to adopt. Also, I've extended the protocol to add default functionalities.

My protocol looks like,

protocol CenterViewControllerProtocol: class {

    var containerDelegate: ContainerViewControllerProtocol? { get set }

    func setupMenuBarButton()
}

And, the extension looks like so,

extension CenterViewControllerProtocol where Self: UIViewController {

    func setupMenuBarButton() {
        let barButton = UIBarButtonItem(title: "Menu", style: .Done, target: self, action: "menuTapped")
        navigationItem.leftBarButtonItem = barButton
    }

    func menuTapped() {
        containerDelegate?.toggleSideMenu()
    }
}

My viewController adopts the protocol -

class MapViewController: UIViewController, CenterViewControllerProtocol {

    weak var containerDelegate: ContainerViewControllerProtocol?

    override func viewDidLoad() {
        super.viewDidLoad()

        setupMenuBarButton()
    }
}

I got the button to display nicely, but when I click on it, the app crashes with

[AppName.MapViewController menuTapped]: unrecognized selector sent to instance 0x7fb8fb6ae650

If I implement the method inside the ViewController, it works fine. But I'd be duplicating the code in all viewControllers which conform to the protocol.

Anything I'm doing wrong here? Thanks in advance.

4 Answers4

0

It seems like using protocol extensions are not supported at this point in time. According to fluidsonic's answer here:

In any case all functions you intend to use via selector should be marked with dynamic or @objc. If this results in an error that @objc cannot be used in this context, then what you are trying to do is simply not supported."

In your example, I think one way around this would be to create a subclass of UIBarButtonItem that calls a block whenever it is tapped. Then you could call containerDelegate?.toggleSideMenu() inside that block.

Matt Lathrop
  • 16
  • 1
  • 3
0

This compiles but crash also in Xcode7.3 Beta so finally you should use a ugly super class as target of the action, that i suppose that it's what you and me are trying to avoid.

BuguiBu
  • 1,507
  • 1
  • 13
  • 18
0

This is an old question but I also ran into the same issue and came up with a solution which may not be perfect but it's the only way I could think of.

Apparently even in Swift 3, it's not possible to set a target-action to your protocol extension. But you can achieve the desired functionality without implementing your func menuTapped() method in all your ViewControllers that conforms to your protocol.

first let's add new methods to your protocol

protocol CenterViewControllerProtocol: class {

    var containerDelegate: ContainerViewControllerProtocol? { get set }

    //implemented in extension
    func setupMenuBarButton()
    func menuTapped()

    //must implement in your VC
    func menuTappedInVC()
}

Now change your extention like this

extension CenterViewControllerProtocol where Self: UIViewController {

        func setupMenuBarButton() {
            let barButton = UIBarButtonItem(title: "Menu", style: .Done, target: self, action: "menuTappedInVC")
            navigationItem.leftBarButtonItem = barButton
        }

        func menuTapped() {
            containerDelegate?.toggleSideMenu()
        }
    }

Notice now button's action is "menuTappedInVC" in your extension, not "menuTapped" . And every ViewController that conforms to CenterViewControllerProtocol must implement this method.

In your ViewController,

class MapViewController: UIViewController, CenterViewControllerProtocol {

    weak var containerDelegate: ContainerViewControllerProtocol?

    override func viewDidLoad() {
        super.viewDidLoad()

        setupMenuBarButton()
    }

    func menuTappedInVC()
    {
      self.menuTapped()
    }

All you have to do is implement menuTappedInVC() method in your VC and that will be your target-action method. Within that you can delegate that task back tomenuTapped which is already implemented in your protocol extension.

Rukshan
  • 7,902
  • 6
  • 43
  • 61
0

I think you can wrap Target-Action to make Closure from them and then use it in similar way I have used Target-Action for UIGestureRecognizer

protocol SomeProtocol { 
    func addTouchDetection(for view: UIView)
}

extension SomeProtocol {

    func addTouchDetection(for view: UIView) {

        let tapGestureRecognizer = UITapGestureRecognizer(callback: { recognizer in

           // recognizer.view
        })
        view.addGestureRecognizer(tapGestureRecognizer)
    }
}

// MARK: - IMPORTAN EXTENSION TO ENABLE HANDLING GESTURE RECOGNIZER TARGET-ACTIONS AS CALLBACKS
extension UIGestureRecognizer {

    public convenience init(callback: @escaping (_ recognizer: UIGestureRecognizer) -> ()) {
        let wrapper = CallbackWrapper(callback)
        self.init(target: wrapper, action: #selector(CallbackWrapper.callCallback(_:)))

        // retaint callback wrapper
        let key = UnsafeMutablePointer<Int8>.allocate(capacity: 1);
        objc_setAssociatedObject(self, key, wrapper, .OBJC_ASSOCIATION_RETAIN)
    }

    class CallbackWrapper {
        var callback : (_ recognizer: UIGestureRecognizer) -> ();
        init(_ callback: @escaping (_ recognizer: UIGestureRecognizer) -> ()) {
            self.callback = callback;
        }
        @objc public func callCallback(_ recognizer: UIGestureRecognizer) {
            self.callback(recognizer);
        }
    }

}
Michał Ziobro
  • 10,759
  • 11
  • 88
  • 143