ajrlewis
ajrlewis

Reputation: 3058

Protocol extension with button and selector

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!

Upvotes: 0

Views: 1596

Answers (2)

ajrlewis
ajrlewis

Reputation: 3058

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.

Upvotes: 3

entangled_photon
entangled_photon

Reputation: 457

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

Upvotes: -1

Related Questions