0

I have some protocol:

@objc protocol SomeProtocol { } 

that I extend for UIViewController instances. In this extension I want to create and add a button, whose selector is also defined in the protocol:

extension SomeProtocol where Self: UIViewController {

    func addSomeButton() {
        let someButton = UIButton()
        someButton.addTarget(self, #selector(someButtonPressed), for: .touchUpInside)
        view.addSubview(someButton)
    }

    @objc func someButtonPressed() {
    }

}

However, I get the error @objc can only be used with members of classes, @objc protocols, and concrete extensions of classes at the definition of someButtonPressed.

Is there any way to achieve this using protocols?

Thanks in advance for any suggestions!

Cal
  • 422
  • 6
  • 20
ajrlewis
  • 2,968
  • 3
  • 33
  • 67
  • why not `extension UIViewController` ?? tried `@objc protocol` – Shehata Gamal Jan 11 '19 at 22:59
  • @Sh_Khan Hi. It was an `@objc` protocol (fixed the typo now). I'm trying to move to Swift protocols for doing things like this, so that I don't have a very length `UIViewController` with lots of extensions. Do you know of a way to do this, please? – ajrlewis Jan 11 '19 at 23:04

2 Answers2

3

A workaround is adding a closure sleeve to the UIButton instead of a target action as in shown here https://stackoverflow.com/a/41438789/5058116 and copied below for convenience.

typealias Closure = () -> ()

///
class ClosureSleeve {
    let closure: Closure
    init(_ closure: @escaping Closure) {
        self.closure = closure
    }
    @objc func invoke () {
        closure()
    }
}

extension UIControl {
    func addAction(for controlEvents: UIControl.Event = .touchUpInside, _ closure: @escaping Closure) {
        let sleeve = ClosureSleeve(closure)
        addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents)
        objc_setAssociatedObject(self, String(format: "[%d]", arc4random()), sleeve, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
    }
}

Then simply replace:

someButton.addTarget(self, #selector(someButtonPressed), for: .touchUpInside)

with:

someButton.addAction { [weak self] in
        self?.someButtonPressed()
    }

and hey presto.

ajrlewis
  • 2,968
  • 3
  • 33
  • 67
-1

You need to supply a Selector requirement in the protocol. This is because you can only apply the @objc attribute to an NSObject. The only reason to mark a protocol @objc is for optional methods.

@objc protocol SomeProtocol {
    var action: Selector { get }
} 

Change the extension to this:

extension SomeProtocol where Self: UIViewController {
    func addSomeButton() {
        let someButton = UIButton()
        someButton.addTarget(self, action: action, for: .touchUpInside)
        view.addSubview(someButton)
    }
}

Now, this works:

extension UIViewController: SomeProtocol {
    @objc func buttonPressed(sender: UIButton) {
        print("Button pressed")
    }
    var action: Selector {
        return #selector(buttonPressed(sender:))
    }
}

Usage:

let vc: myViewController: MyViewController!
func doSomething() {
  vc.addSomeButton()
}