Swift 2.2 #selector in protocol extension compiler error
Asked Answered
H

5

26

I've got a protocol extension it used to work perfectly before swift 2.2.

Now I have a warning that tells me to use the new #selector, but if I add it

no method declared with Objective-C Selector.

I tried to reproduce the issue in this few lines of code, that can be easily copy and paste also into playground

  protocol Tappable {
    func addTapGestureRecognizer()
    func tapGestureDetected(gesture:UITapGestureRecognizer)
}

extension Tappable where Self: UIView {
    func addTapGestureRecognizer() {
        let gesture = UITapGestureRecognizer(target: self, action:#selector(Tappable.tapGestureDetected(_:)))
        addGestureRecognizer(gesture)
    }
}

class TapView: UIView, Tappable {
    func tapGestureDetected(gesture:UITapGestureRecognizer) {
        print("Tapped")
    }
}

There is also a suggestion to append to that method in the protocol @objc, but if I do it asks me also to add it to the class that implements it, but once I add the class doesn't conform to the protocol anymore, because it doesn't seems to see the implementation in the protocol extension.
How can I implement this correctly?

Huang answered 23/3, 2016 at 17:30 Comment(4)
Could you please post a compilable example? How is panGestureDetected declared?Collis
I'm going to add it tomorrow, thank you SulthanHuang
@Huang Change Pannable.panGestureDetected(_:) . put the name of the class where panGestureDetected is declared instead of Pannable.whatever just put YourClass.panGestureDetected(_:)Appointive
Leo Dabus: Doing that means that I need to create a protocol extension for each tapGestureDetected implementation that I have. @Collis I updated with full code or it can be downloaded here dropbox.com/s/0bn1u9ndnzk1c2e/Selector.playground.zip?dl=0Huang
C
24

I had a similar problem. here is what I did.

  1. Marked the protocol as @objc.
  2. Marked any methods I extended with a default behavior as optional.
  3. Then used Self. in the #selector.

    @objc public protocol UpdatableUserInterfaceType {
      optional func startUpdateUITimer()
      optional var updateInterval: NSTimeInterval { get }
      func updateUI(notif: NSTimer)
    }
    
    public extension UpdatableUserInterfaceType where Self: ViewController {
    
      var updateUITimer: NSTimer {
        return NSTimer.scheduledTimerWithTimeInterval(updateInterval, target: self, selector: #selector(Self.updateUI(_:)), userInfo: nil, repeats: true)
      }
    
      func startUpdateUITimer() {
        print(updateUITimer)
      }
    
      var updateInterval: NSTimeInterval {
        return 60.0
      }
    }
    
Commonage answered 24/3, 2016 at 13:52 Comment(4)
@Andrea, Cool! Glad I could help.Commonage
I get a "Use of unresolved identifier 'Self'. My protocol is not marked with public.Apulia
Ugh, doesn't work when we have associatedtype requirementsOperand
this still has alert messenger: Non-@objc method does not satisfy optional requirement of '@objc' protocolAardvark
G
17

You can create a property which is a Selector... Example:

protocol Tappable {
    var selector: Selector { get }
    func addTapGestureRecognizer()
}

extension Tappable where Self: UIView {
    func addTapGestureRecognizer() {
        let gesture = UITapGestureRecognizer(target: self, action: selector)
        addGestureRecognizer(gesture)
    }
}

class TapView: UIView, Tappable {
    var selector = #selector(TapView.tapGestureDetected(_:))

    func tapGestureDetected(gesture:UITapGestureRecognizer) {
        print("Tapped")
    }
}

The error stops to show and it is not more necessary to set your protocol and class with the @objc decorator.

This solution is not the most elegant, but looks ok until now.

Gentian answered 15/4, 2016 at 20:51 Comment(2)
Yes, not that awesome but way better than putting @objc everywhere, particularly if you are "inheriting" from a chain of other protocols with their default implementations. Suddenly you end up changing your whole code base just because of that selector.Adjacency
how to call protocol own function as selector, i don’t want to declare selector in class extending protocol…Herne
C
7

This answer is quite similar to Bruno Hecktheuers, but instead of having everyone that wants to conform to the "Tappable" protocol implement the variable "selector", we choose to pass it as a parameter to the addTapGestureRecognizer function:

protocol Tappable {
    func addTapGestureRecognizer(selector selector: Selector)
    func tapGestureDetected(gesture:UITapGestureRecognizer)
}

extension Tappable where Self: UIView {
    func addTapGestureRecognizer(selector selector: Selector)
        let gesture = UITapGestureRecognizer(target: self, action: selector)
        addGestureRecognizer(gesture)
    }
}

class TapView: UIView, Tappable {    
    func tapGestureDetected(gesture:UITapGestureRecognizer) {
        print("Tapped")
    }
}

and then just pass the selector wherever it is used:

addTapGestureRecognizer(selector: #selector(self.tapGestureDetected(_:)))

This way we avoid having the ones implementing this protocol having to implement the selector variable and we also avoid having to mark everyone using this protocol with "@objc". Feels like this approach is less bloated.

Chive answered 16/6, 2016 at 12:35 Comment(1)
What if you want to provide a default implementation of tapGestureDetected in the protocol extension itself, not in the concrete class that implements the protocol?Impend
W
4

Here is a working example using Swift 3. It uses a standard Swift protocol without the need for any @objc decorations and a private extension to define the callback function.

protocol PlayButtonPlayable {

    // be sure to call addPlayButtonRecognizer from viewDidLoad or later in the display cycle
    func addPlayButtonRecognizer()
    func handlePlayButton(_ sender: UITapGestureRecognizer)

}

fileprivate extension UIViewController {
    @objc func _handlePlayButton(_ sender: UITapGestureRecognizer) {
        if let playable = self as? PlayButtonPlayable {
            playable.handlePlayButton(sender)
        }
    }
}

fileprivate extension Selector {
    static let playTapped =
        #selector(UIViewController._handlePlayButton(_:))
}

extension PlayButtonPlayable where Self: UIViewController {

    func addPlayButtonRecognizer() {
        let playButtonRecognizer = UITapGestureRecognizer(target: self, action: .playTapped)
        playButtonRecognizer.allowedPressTypes = [ NSNumber(value: UIPressType.playPause.rawValue as Int) ]
        view.addGestureRecognizer(playButtonRecognizer)
    }

}
Wold answered 14/6, 2017 at 20:16 Comment(0)
R
0

I happened to see this in the side bar, I recently had this same issue.. Unfortunately, due to Objective-C runtime limitations you cannot use @objc on protocol extensions, I believe this issue was closed early this year.

The issue arises because the extension is added after the conformance of the protocol, therefor there is no way to guarantee that conformance to the protocol is met. That said, it is possible to call a method as a selector from anything that subclasses NSObject and conforms to the protocol. This is most often done with delegation.

This implies you could create an empty wrapper subclass that conforms to the protocol and use the wrapper to call its methods from the protocol that are defined in the wrapper, any other undefined methods from the protocol can be passed to the delegate. There are other similar solutions that use a private extension of a concrete class such as UIViewController and define a method that calls the protocol method but these are also tied to a particular class and not a default implementation of a particular class that happens to conform to the protocol.

Realize that you are trying to implement a default implementation of a protocol function that uses another of it's own protocol functions to define a value for it's own implementation. whew!

Protocol:

 public protocol CustomViewDelegate {
     func update()
     func nonDelegatedMethod()
}

View:

Use a delegate, and define a wrapper method to safely unwrap the delegate’s method.

class CustomView: UIView {

    let updateButton: UIButton = {
        let button = UIButton(frame: CGRect(origin: CGPoint(x: 50, y: 50), size: CGSize(width: 150, height: 50)))
        button.backgroundColor = UIColor.lightGray
        button.addTarget(self, action: #selector(doDelegateMethod), for: .touchUpInside)
        return button
    }()

    var delegate:CustomViewDelegate?

    required init?(coder aDecoder: NSCoder) {
        fatalError("Pew pew, Aghh!")
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        addSubview(updateButton)
    }

    @objc func doDelegateMethod() {
        if delegate != nil {
           delegate!.update()
        } else {
           print("Gottfried: I wanted to be a brain surgeon, but I had a bad habit of dropping things")
        }
     }


   }

ViewController:

Conform the View Controller to the view’s delegate: and implement the protocol’s method.

class ViewController: UIViewController, CustomViewDelegate {

    let customView = CustomView(frame: CGRect(origin: CGPoint(x: 100, y: 100), size: CGSize(width: 200, height: 200)))

    override func viewDidLoad() {
        super.viewDidLoad()
        customView.backgroundColor = UIColor.red
        customView.delegate = self //if delegate is not set, the app will not crash
        self.view.addSubview(customView)
    }

    // Protocol -> UIView Button Action -> View Controller's Method
    func update() {
        print("Delegating work from View that Conforms to CustomViewDelegate to View Controller")
    }

    //Protocol > View Controller's Required Implementation
    func nonDelegatedMethod() {

       //Do something else 

   }
}

Note that the view controller only had to conform to the delegate and did not set the selector of some property of the view, this separates the view (and it's protocol) from view controller.

You already have a UIView named TapView that inherits from UIView and Tappable so your implementation could be:

Protocol:

protocol TappableViewDelegate {
    func tapGestureDetected(gesture:UITapGestureRecognizer)
}

TappableView:

class TappableView: UIView {

    var delegate:TappableViewDelegate?

    required init?(coder aDecoder: NSCoder) {
        fatalError("Pew pew, Aghh!")
    }

    override init(frame: CGRect) {
        super.init(frame: frame)

        let gesture = UITapGestureRecognizer(target: self, action: #selector(doDelegateMethod(gesture:)))
        addGestureRecognizer(gesture)
    }

    @objc func doDelegateMethod(gesture:UITapGestureRecognizer) {
        if delegate != nil {
            delegate!.tapGestureDetected(gesture: gesture)
        } else {
            print("Gottfried: I wanted to be a brain surgeon, but I had a bad habit of dropping things")
        }
    }

}

ViewController:

class ViewController: UIViewController, TappableViewDelegate {

    let tapView = TappableView(frame: CGRect(origin: CGPoint(x: 100, y: 100), size: CGSize(width: 200, height: 200)))

    override func viewDidLoad() {
        super.viewDidLoad()
        tapView.backgroundColor = UIColor.red
        tapView.delegate = self
        self.view.addSubview(tapView)
    }

    func tapGestureDetected(gesture: UITapGestureRecognizer) {
        print("User did tap")
   }

}
Reaves answered 18/11, 2017 at 11:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.