Adding a closure as target to a UIButton
Asked Answered
B

19

71

I have a generic control class which needs to set the completion of the button depending on the view controller.Due to that setLeftButtonActionWithClosure function needs to take as parameter a closure which should be set as action to an unbutton.How would it be possible in Swift since we need to pass the function name as String to action: parameter.

func setLeftButtonActionWithClosure(completion: () -> Void)
{
    self.leftButton.addTarget(<#target: AnyObject?#>, action: <#Selector#>, forControlEvents: <#UIControlEvents#>)
}
Buskined answered 18/9, 2014 at 18:4 Comment(1)
See Hejazi answer for an updated iOS14+ swift 5 concise solutionMotorize
U
36

Do Not Use This Answer, See Note Below

NOTE: like @EthanHuang said "This solution doesn't work if you have more than two instances. All actions will be overwrite by the last assignment." Keep in mind this when you develop, i will post another solution soon.

If you want to add a closure as target to a UIButton, you must add a function to UIButton class by using extension

Swift 5

import UIKit    
extension UIButton {
    private func actionHandler(action:(() -> Void)? = nil) {
        struct __ { static var action :(() -> Void)? }
        if action != nil { __.action = action }
        else { __.action?() }
    }   
    @objc private func triggerActionHandler() {
        self.actionHandler()
    }   
    func actionHandler(controlEvents control :UIControl.Event, ForAction action:@escaping () -> Void) {
        self.actionHandler(action: action)
        self.addTarget(self, action: #selector(triggerActionHandler), for: control)
    }
}

Older

import UIKit

extension UIButton {
    private func actionHandleBlock(action:(() -> Void)? = nil) {
        struct __ {
            static var action :(() -> Void)?
        }
        if action != nil {
            __.action = action
        } else {
            __.action?()
        }
    }
    
    @objc private func triggerActionHandleBlock() {
        self.actionHandleBlock()
    }
    
    func actionHandle(controlEvents control :UIControlEvents, ForAction action:() -> Void) {
        self.actionHandleBlock(action)
        self.addTarget(self, action: "triggerActionHandleBlock", forControlEvents: control)
    }
}

and the call:

 let button = UIButton()
 button.actionHandle(controlEvents: .touchUpInside, 
 ForAction:{() -> Void in
     print("Touch")
 })
Urushiol answered 1/12, 2015 at 9:29 Comment(11)
Perfect, this is a nice solution.Forelimb
This is amazing. How does setting the struct work? Is that a global struct or something? Why does its "action" value persist between function calls? I made a version for myself with renamed functions, to clarify what's going on for myelf, posted below.Goldberg
You missed a closing brace in the UIButton extension.Macedo
This solution doesn't work if you add action handlers to multiple UIControlEvents on the same UIButton. The 2nd one will overwrite the handler for the first one.Baptista
This solution doesn't work if you have more than two instances. All actions will be overwrite by the last assignment.Frida
this might not be the best solution as the closure might cause memory leak, am I right?Direction
@MohsenShakiba how does it cause a memory leak?Francium
@WalterMartinVargas-Pena in this solution you are storing the closure which might hold strong reference to self in a static variable. and it causes memory leak. you can try it for yourselfDirection
@MohsenShakiba you are correct that a memory leak should not occur. And it definitely could have been documented.Francium
what if the closure is made as a weak varPamplona
Wow, just realized this doesn't work with more than two buttons. This should not be the accepted answer!Fahey
N
177

With iOS 14 Apple has finally added this feature to UIKit. However, someone might still want to use this extension because Apple's method signature is suboptimal.

iOS 14:

extension UIControl {
    func addAction(for controlEvents: UIControl.Event = .touchUpInside, _ closure: @escaping()->()) {
        addAction(UIAction { (action: UIAction) in closure() }, for: controlEvents)
    }
}

pre-iOS 14:

extension UIControl {
    func addAction(for controlEvents: UIControl.Event = .touchUpInside, _ closure: @escaping()->()) {
        @objc class ClosureSleeve: NSObject {
            let closure:()->()
            init(_ closure: @escaping()->()) { self.closure = closure }
            @objc func invoke() { closure() }
        }
        let sleeve = ClosureSleeve(closure)
        addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents)
        objc_setAssociatedObject(self, "\(UUID())", sleeve, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
    }
}

Usage:

button.addAction {
    print("Hello, Closure!")
}

or:

button.addAction(for: .touchUpInside) {
    print("Hello, Closure!")
}

or if avoiding retain loops:

self.button.addAction(for: .touchUpInside) { [unowned self] in
    self.doStuff()
}

(Extension is included here: https://github.com/aepryus/Acheron)

Also note, in theory .primaryActionTriggered could replace .touchUpInside, but it seems to be currently bugged in catalyst, so I'll leave it as is for now.

Neilla answered 3/1, 2017 at 7:53 Comment(17)
I like this solution, but changed "UIButton" to "UIControl" so that it will be available on all controls. :)Mutual
@JohnFowler I switched it to UIControl and improved the method signature to make use of Swifts closure as last parameter syntax advantage.Neilla
can you explain pls why we need last line? objc_setAssociatedObject ...Tamikatamiko
@BohdanSavych when this add method is called an instance of ClosureSleeve is created. We want that instance to be retained for the lifetime of the UIControl. The obj_setAssociatedObject does just that; it is essentially adding a strong property to the control at runtime.Neilla
Does accessing self. properties could create retain cycle?Sweaty
Yes, use unowned or weak self.Booklet
This works and does not create the issue by overriding previous action. Should be the accepted answer.Vola
You can also extend UIBarButtonItem if you want to use it for bar buttons (sorry for formatting): extension UIBarButtonItem { func addAction(_ closure: @escaping ()->()) { let sleeve = ClosureSleeve(closure) target = sleeve action = #selector(ClosureSleeve.invoke) objc_setAssociatedObject(self, String(format: "[%d]", arc4random()), sleeve, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN) } }Ignacioignacius
RE: retain cycles. I believe you may also have to weakly retain the button. [weak self, weak button] in { ... }Salyers
@Salyers It's not necessary if the button doesn't have a strong pointer back to the parent. (Although, if the button is added as a subview and isn't needed after it gets removed it won't hurt for it to be declared weak since it will be retained while existing on its superview. If the button is something that appears and disappears then it would be deinit'ed when removed from the superview.)Neilla
@Salyers I see what you are saying now. Yes, if you access the button itself from within the closure (to change its title for example), you'd need to add [weak button] to the closure header also. Thanks for the point.Neilla
The ClosureSleeve needs to be marked @objc and inherit from NSObject or you will get a runtime exception if you access the allTargets property on the control: static Set_unnconditionallyBridgeFromObjectiveC(_:)Salyers
@Salyers Thanks for the info. I separated your edit out since for most people it won't be necessary, but it is great to have the information included if needed.Neilla
Is arc4random() guaranteed to return a unique number every time? This may not be safe to use if you're calling addAction more than once on the same instance though, since two random numbers could theoretically collide. Isn't it safer to instead use UUID() for the associated reference key to guarantee uniqueness?Jagir
It creates strong reference. So when you are going to use that button again It call nth times.Danaus
@Jagir Incorporated your suggestion.Neilla
@YannSteph (finally) incorporated your suggestion.Neilla
U
36

Do Not Use This Answer, See Note Below

NOTE: like @EthanHuang said "This solution doesn't work if you have more than two instances. All actions will be overwrite by the last assignment." Keep in mind this when you develop, i will post another solution soon.

If you want to add a closure as target to a UIButton, you must add a function to UIButton class by using extension

Swift 5

import UIKit    
extension UIButton {
    private func actionHandler(action:(() -> Void)? = nil) {
        struct __ { static var action :(() -> Void)? }
        if action != nil { __.action = action }
        else { __.action?() }
    }   
    @objc private func triggerActionHandler() {
        self.actionHandler()
    }   
    func actionHandler(controlEvents control :UIControl.Event, ForAction action:@escaping () -> Void) {
        self.actionHandler(action: action)
        self.addTarget(self, action: #selector(triggerActionHandler), for: control)
    }
}

Older

import UIKit

extension UIButton {
    private func actionHandleBlock(action:(() -> Void)? = nil) {
        struct __ {
            static var action :(() -> Void)?
        }
        if action != nil {
            __.action = action
        } else {
            __.action?()
        }
    }
    
    @objc private func triggerActionHandleBlock() {
        self.actionHandleBlock()
    }
    
    func actionHandle(controlEvents control :UIControlEvents, ForAction action:() -> Void) {
        self.actionHandleBlock(action)
        self.addTarget(self, action: "triggerActionHandleBlock", forControlEvents: control)
    }
}

and the call:

 let button = UIButton()
 button.actionHandle(controlEvents: .touchUpInside, 
 ForAction:{() -> Void in
     print("Touch")
 })
Urushiol answered 1/12, 2015 at 9:29 Comment(11)
Perfect, this is a nice solution.Forelimb
This is amazing. How does setting the struct work? Is that a global struct or something? Why does its "action" value persist between function calls? I made a version for myself with renamed functions, to clarify what's going on for myelf, posted below.Goldberg
You missed a closing brace in the UIButton extension.Macedo
This solution doesn't work if you add action handlers to multiple UIControlEvents on the same UIButton. The 2nd one will overwrite the handler for the first one.Baptista
This solution doesn't work if you have more than two instances. All actions will be overwrite by the last assignment.Frida
this might not be the best solution as the closure might cause memory leak, am I right?Direction
@MohsenShakiba how does it cause a memory leak?Francium
@WalterMartinVargas-Pena in this solution you are storing the closure which might hold strong reference to self in a static variable. and it causes memory leak. you can try it for yourselfDirection
@MohsenShakiba you are correct that a memory leak should not occur. And it definitely could have been documented.Francium
what if the closure is made as a weak varPamplona
Wow, just realized this doesn't work with more than two buttons. This should not be the accepted answer!Fahey
G
24

You can effectively achieve this by subclassing UIButton:

class ActionButton: UIButton {
    var touchDown: ((button: UIButton) -> ())?
    var touchExit: ((button: UIButton) -> ())?
    var touchUp: ((button: UIButton) -> ())?

    required init?(coder aDecoder: NSCoder) { fatalError("init(coder:)") }
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupButton()
    }

    func setupButton() {
        //this is my most common setup, but you can customize to your liking
        addTarget(self, action: #selector(touchDown(_:)), forControlEvents: [.TouchDown, .TouchDragEnter])
        addTarget(self, action: #selector(touchExit(_:)), forControlEvents: [.TouchCancel, .TouchDragExit])
        addTarget(self, action: #selector(touchUp(_:)), forControlEvents: [.TouchUpInside])
    }

    //actions
    func touchDown(sender: UIButton) {
        touchDown?(button: sender)
    }

    func touchExit(sender: UIButton) {
        touchExit?(button: sender)
    }

    func touchUp(sender: UIButton) {
        touchUp?(button: sender)
    }
}

Use:

let button = ActionButton(frame: buttonRect)
button.touchDown = { button in
    print("Touch Down")
}
button.touchExit = { button in
    print("Touch Exit")
}
button.touchUp = { button in
    print("Touch Up")
}
Gripsack answered 28/6, 2016 at 21:14 Comment(1)
It would be more efficient to only add the target when the matching closure is setHafnium
B
10

This is now possible on iOS 14. You can pass a UIAction, which has a handler closure, when you create the UIButton:

let action = UIAction(title: "") { action in
    print("Button tapped!")
}

UIButton(type: .system, primaryAction: action)

Or shorter:

UIButton(type: .system, primaryAction: UIAction(title: "") { action in
    print("Button tapped!")
})
Baronetage answered 10/7, 2020 at 16:25 Comment(0)
A
9

Similar solution to those already listed, but perhaps lighter weight and doesn't rely on randomness to generate unique ids:

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

extension UIControl {
    func add (for controlEvents: UIControlEvents, _ closure: @escaping ()->()) {
        let sleeve = ClosureSleeve(closure)
        addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents)
        objc_setAssociatedObject(self, String(ObjectIdentifier(self).hashValue) + String(controlEvents.rawValue), sleeve,
                             objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
    }
}

Usage:

button.add(for: .touchUpInside) {
    print("Hello, Closure!")
}

Or if avoiding retain loops:

button.add(for: .touchUpInside) { [unowned self] in
    self.doStuff()
}
Arietta answered 13/3, 2018 at 14:44 Comment(1)
Capture 'self' was never used warningDanaus
G
3

This is basically Armanoide's answer, above, but with a couple slight changes that are useful for me:

  • the passed-in closure can take a UIButton argument, allowing you to pass in self
  • the functions and arguments are renamed in a way that, for me, clarifies what's going on, for instance by distinguishing a Swift closure from a UIButton action.

    private func setOrTriggerClosure(closure:((button:UIButton) -> Void)? = nil) {
    
      //struct to keep track of current closure
      struct __ {
        static var closure :((button:UIButton) -> Void)?
      }
    
      //if closure has been passed in, set the struct to use it
      if closure != nil {
        __.closure = closure
      } else {
        //otherwise trigger the closure
        __. closure?(button: self)
      }
    }
    @objc private func triggerActionClosure() {
      self.setOrTriggerClosure()
    }
    func setActionTo(closure:(UIButton) -> Void, forEvents :UIControlEvents) {
      self.setOrTriggerClosure(closure)
      self.addTarget(self, action:
        #selector(UIButton.triggerActionClosure),
                     forControlEvents: forEvents)
    }
    

Much props to Armanoide though for some heavy-duty magic here.

Goldberg answered 15/4, 2016 at 15:24 Comment(1)
If I add two different button with different closure on one screen, all button will execute the closure from the last created button. Am I doing wrong or there is something to handle that?Eirena
S
3

Here is a fun variant to the answer by aepryus. My version uses Combine's Cancellable protocol to:

  1. Support removing the registered closure.
  2. Handle memory management thus avoiding the need to use objc_setAssociatedObject.
// Swift 5

import Combine
import UIKit

class BlockObject: NSObject {
    let block: () -> Void

    init(block: @escaping () -> Void) {
        self.block = block
    }

    @objc dynamic func execute() {
        block()
    }
}

extension UIControl {
    func addHandler(
        for controlEvents: UIControl.Event,
        block: @escaping () -> Void)
        -> Cancellable
    {
        let blockObject = BlockObject(block: block)
        addTarget(blockObject, action: #selector(BlockObject.execute), for: controlEvents)

        return AnyCancellable {
            self.removeTarget(blockObject, action: #selector(BlockObject.execute), for: controlEvents)
        }
    }
}

Usage:

let button = UIButton(type: .system)

// Add the handler
let cancellable = button.addHandler(for: .touchUpInside) {
    print("Button pressed!")
}

// Remove the handler
cancellable.cancel()

Don't forget to store a reference to the Cancellable or else the handler will be immediately unregistered.

Sisely answered 27/7, 2020 at 15:11 Comment(0)
M
3

I change a little extension for UIControl that was posted @Nathan F. here

I used objc_setAssociatedObject and objc_getAssociatedObject to get/set closure and i removed global static variable with all created buttons's keys. So now event stored for each instance and released after dealloc

extension UIControl {
    
    typealias Handlers = [UInt:((UIControl) -> Void)]
    
    private enum AssociatedKey {
        static var actionHandlers = "UIControl.actionHandlers"
    }

    /**
     * A map of closures, mapped as  [ event : action ] .
     */

    private var actionHandlers: Handlers {
        get {
            return objc_getAssociatedObject(self, &AssociatedKey.actionHandlers) as? Handlers ?? [:]
        }
        set(newValue) {
            objc_setAssociatedObject(self, &AssociatedKey.actionHandlers, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
        }
    }
}

You can find it here: https://gist.github.com/desyatov/6ed83de58ca1146d85fedab461a69b12

Here are some example:

myButton.action(.touchUpInside, { (sender: UIControl) in
    // do something
})
Meredith answered 23/4, 2021 at 18:28 Comment(0)
N
2

@Armanoide solution is cool cause it uses trick with struct and static var inside it but it is not perfect if you're reusing one button a few times cause in this case action closure will always store the last handler.

I've fixed it for UIKitPlus library

import UIKit

extension UIControl {
    private func actionHandler(action: (() -> Void)? = nil) {
        struct Storage { static var actions: [Int: (() -> Void)] = [:] }
        if let action = action {
            Storage.actions[hashValue] = action
        } else {
            Storage.actions[hashValue]?()
        }
    }

    @objc func triggerActionHandler() {
        actionHandler()
    }

    func actionHandler(controlEvents control: UIControl.Event, forAction action: @escaping () -> Void) {
        actionHandler(action: action)
        addTarget(self, action: #selector(triggerActionHandler), for: control)
    }
}
Nanaam answered 7/10, 2019 at 0:42 Comment(0)
K
2

I put together a little extension for UIControl that will let you use closures for any action on any UIControl really easily.

You can find it here: https://gist.github.com/nathan-fiscaletti/8308f00ff364b72b6a6dec57c4b13d82

Here are some examples of it in practice:

Setting a Button Action

myButton.action(.touchUpInside, { (sender: UIControl) in
    // do something
})

Detecting a Switch changing Values

mySwitch.action(.valueChanged, { (sender: UIControl) in
    print("Switch State:", mySwitch.isOn)
})
Kester answered 16/10, 2019 at 17:49 Comment(0)
M
2

Here is a generic swift 5 approach. It has a sender inside action block and eliminates adding action for same event twice

import UIKit

protocol Actionable {
    associatedtype T = Self
    func addAction(for controlEvent: UIControl.Event, action: ((T) -> Void)?)
}

private class ClosureSleeve<T> {
    let closure: ((T) -> Void)?
    let sender: T

    init (sender: T, _ closure: ((T) -> Void)?) {
        self.closure = closure
        self.sender = sender
    }

    @objc func invoke() {
        closure?(sender)
    }
}

extension Actionable where Self: UIControl {
    func addAction(for controlEvent: UIControl.Event, action: ((Self) -> Void)?) {
        let previousSleeve = objc_getAssociatedObject(self, String(controlEvent.rawValue))
        objc_removeAssociatedObjects(previousSleeve as Any)
        removeTarget(previousSleeve, action: nil, for: controlEvent)

        let sleeve = ClosureSleeve(sender: self, action)
        addTarget(sleeve, action: #selector(ClosureSleeve<Self>.invoke), for: controlEvent)
        objc_setAssociatedObject(self, String(controlEvent.rawValue), sleeve, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
    }
}

extension UIControl: Actionable {}
Melantha answered 22/5, 2020 at 11:35 Comment(0)
L
1

Swift

After trying all the solutions, this one worked for me for all cases, even when the button in reusable table view cell

import UIKit

typealias UIButtonTargetClosure = UIButton -> ()

class ClosureWrapper: NSObject {
    let closure: UIButtonTargetClosure
    init(_ closure: UIButtonTargetClosure) {
       self.closure = closure
    }
}

extension UIButton {

private struct AssociatedKeys {
    static var targetClosure = "targetClosure"
}

private var targetClosure: UIButtonTargetClosure? {
    get {
        guard let closureWrapper = objc_getAssociatedObject(self, &AssociatedKeys.targetClosure) as? ClosureWrapper else { return nil }
        return closureWrapper.closure
    }
    set(newValue) {
        guard let newValue = newValue else { return }
        objc_setAssociatedObject(self, &AssociatedKeys.targetClosure, ClosureWrapper(newValue), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    }
}

func addTargetClosure(closure: UIButtonTargetClosure) {
    targetClosure = closure
    addTarget(self, action: #selector(UIButton.closureAction), forControlEvents: .TouchUpInside)
}

   func closureAction() {
       guard let targetClosure = targetClosure else { return }
       targetClosure(self)
   }
}

And then you call it like this:

loginButton.addTargetClosure { _ in

   // login logics

}

Resource: https://medium.com/@jackywangdeveloper/swift-the-right-way-to-add-target-in-uibutton-in-using-closures-877557ed9455

Longspur answered 26/2, 2018 at 15:49 Comment(0)
C
1

My solution.

typealias UIAction = () -> Void;

class Button: UIButton {

    public var touchUp :UIAction? {
        didSet {
            self.setup()
        }
    }

    func setup() -> Void {
        self.addTarget(self, action: #selector(touchInside), for: .touchUpInside)
    }

    @objc private func touchInside() -> Void {
        self.touchUp!()
    }

}
Corner answered 17/9, 2018 at 17:57 Comment(0)
B
1

Swift 4.2 for UIControl and UIGestureRecognizer, and and remove targets through swift extension stored property paradigm.

Wrapper class for the selector

class Target {

    private let t: () -> ()
    init(target t: @escaping () -> ()) { self.t = t }
    @objc private func s() { t() }

    public var action: Selector {
        return #selector(s)
    }
}

Protocols with associatedtypes so we can hide hide the objc_ code

protocol PropertyProvider {
    associatedtype PropertyType: Any

    static var property: PropertyType { get set }
}

protocol ExtensionPropertyStorable: class {
    associatedtype Property: PropertyProvider
}

Extension to make the property default and available

extension ExtensionPropertyStorable {

    typealias Storable = Property.PropertyType

    var property: Storable {
        get { return objc_getAssociatedObject(self, String(describing: type(of: Storable.self))) as? Storable ?? Property.property }
        set { return objc_setAssociatedObject(self, String(describing: type(of: Storable.self)), newValue, .OBJC_ASSOCIATION_RETAIN) }
    }
}

Let us apply the magic

extension UIControl: ExtensionPropertyStorable {

    class Property: PropertyProvider {
        static var property = [String: Target]()
    }

    func addTarget(for controlEvent: UIControl.Event = .touchUpInside, target: @escaping () ->()) {
        let key = String(describing: controlEvent)
        let target = Target(target: target)
        addTarget(target, action: target.action, for: controlEvent)
        property[key] = target
    }

    func removeTarget(for controlEvent: UIControl.Event = .touchUpInside) {
        let key = String(describing: controlEvent)
        let target = property[key]
        removeTarget(target, action: target?.action, for: controlEvent)
        property[key] = nil
    }
}

And to the gestures

extension UIGestureRecognizer: ExtensionPropertyStorable {

    class Property: PropertyProvider {
        static var property: Target?
    }

    func addTarget(target: @escaping () -> ()) {
        let target = Target(target: target)
        addTarget(target, action: target.action)
        property = target
    }

    func removeTarget() {
        let target = property
        removeTarget(target, action: target?.action)
        property = nil
    }
}

Example usage:

button.addTarget {
    print("touch up inside")
}
button.addTarget { [weak self] in
    print("this will only happen once")
    self?.button.removeTarget()
}
button.addTarget(for: .touchDown) {
    print("touch down")
}
slider.addTarget(for: .valueChanged) {
    print("value changed")
}
textView.addTarget(for: .allEditingEvents) { [weak self] in
    self?.editingEvent()
}
gesture.addTarget { [weak self] in
    self?.gestureEvent()
    self?.otherGestureEvent()
    self?.gesture.removeTarget()
}
Biogenesis answered 13/10, 2018 at 19:26 Comment(1)
I found what causes the error it is this keys objc_getAssociatedObject(self, String(describing: type(of: Storable.self))) You should change them into something like &AssociatedKeys.storable and AssociatedKeys should be struct with static field var storable. Otherwise there are problem with accessing this property object and many times it returns nil as string can be interpereted differently as the pointer!Clynes
M
1

Here's a nice framework for doing this: HandlersKit. The biggest advantage is that you can access to the sender inside the closure without typecasting or optional unwrapping.

Example for UIButton:

import HandlersKit

let button = MyActivityIndicatorButton()
button.onTap { (sender: MyActivityIndicatorButton) in
    sender.showActivityIndicator()
}

Example for UISwitch:

let switchView = UISwitch(frame: CGRect(x: 0.0, y: 0.0, width: 100.0, height: 50.0))
switchView.onChange { isOn in
    print("SwitchView is: \(isOn)")
}
Mistakable answered 13/2, 2020 at 15:46 Comment(0)
N
0

I have started to use Armanoide's answer disregarding the fact that it'll be overridden by the second assignment, mainly because at first I needed it somewhere specific which it didn't matter much. But it started to fall apart.

I've came up with a new implementation using AssicatedObjects which doesn't have this limitation, I think has a smarter syntax, but it's not a complete solution:

Here it is:

typealias ButtonAction = () -> Void

fileprivate struct AssociatedKeys {
  static var touchUp = "touchUp"
}

fileprivate class ClosureWrapper {
  var closure: ButtonAction?

  init(_ closure: ButtonAction?) {
    self.closure = closure
  }
}

extension UIControl {

  @objc private func performTouchUp() {

    guard let action = touchUp else {
      return
    }

    action()

  }

  var touchUp: ButtonAction? {

    get {

      let closure = objc_getAssociatedObject(self, &AssociatedKeys.touchUp)
      guard let action = closure as? ClosureWrapper else{
        return nil
      }
      return action.closure
    }

    set {
      if let action = newValue {
        let closure = ClosureWrapper(action)
        objc_setAssociatedObject(
          self,
          &AssociatedKeys.touchUp,
          closure as ClosureWrapper,
          .OBJC_ASSOCIATION_RETAIN_NONATOMIC
        )
        self.addTarget(self, action: #selector(performTouchUp), for: .touchUpInside)
      } else {        
        self.removeTarget(self, action: #selector(performTouchUp), for: .touchUpInside)
      }

    }
  }

}

As you can see, I've decided to make a dedicated case for touchUpInside. I know controls have more events than this one, but who are we kidding? do we need actions for every one of them?! It's much simpler this way.

Usage example:

okBtn.touchUp = {
      print("OK")
    }

In any case, if you want to extend this answer you can either make a Set of actions for all the event types, or add more event's properties for other events, it's relatively straightforward.

Cheers, M.

Naaman answered 10/11, 2016 at 6:49 Comment(0)
S
0

One more optimisation (useful if you use it in many places and don't want to duplicate call to objc_setAssociatedObject). It allows us to not worry about a dirty part of objc_setAssociatedObject and keeps it inside ClosureSleeve's constructor:

class ClosureSleeve {
    let closure: () -> Void

    init(
        for object: AnyObject,
        _ closure: @escaping () -> Void
        ) {

        self.closure = closure

        objc_setAssociatedObject(
            object,
            String(format: "[%d]", arc4random()),
            self,
            objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN
        )
    }

    @objc func invoke () {
        closure()
    }
}

So your extension will look a tiny bit cleaner:

extension UIControl {
    func add(
        for controlEvents: UIControlEvents,
        _ closure: @escaping ()->()
        ) {

        let sleeve = ClosureSleeve(
            for: self,
            closure
        )
        addTarget(
            sleeve,
            action: #selector(ClosureSleeve.invoke),
            for: controlEvents
        )
    }
}
Shumpert answered 18/5, 2017 at 16:47 Comment(7)
This seems to have the exact same number of calls to objc_setAssociatedObject as above.Neilla
Hi @Neilla there is only one subtle difference - let's say you want to use it for UIBarButtonItem an UIButton. In your implementation you'll need to create a sleeve and call the objc_setAssociatedObject(self, String(format: "[%d]", arc4random()), sleeve, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN) twice. In my improvement you don't need to call the later (it's handled by the ClosureSleeve's constructor)Shumpert
If you are attaching the same ClosureSleeve to two different controls, you actually want to call it twice, so that each has a strong reference to the ClosureSleeve and it doesn't get released until each of your two controls are also released.Neilla
@Neilla I'm not sure if I get you right... I didn't mean to attach ClosureSleeve to two different objects (it's not even possible in my solution as you're attaching it in the constructor). Please check my updated answerShumpert
@Neilla what I mean is you can now use ClosureSleeve in other classes as well and don't worry about the dirty part of objc_setAssociatedObject outside of itShumpert
Oh! I understand now. That makes much more sense.Neilla
@Neilla glad to hear that! Actually it's a small improvement to your original solution. Is it originally yours? If it is I don't mind if you use my answer to improve yours 😊Shumpert
H
0
class ViewController : UIViewController {
  var aButton: UIButton!

  var assignedClosure: (() -> Void)? = nil

  override func loadView() {
    let view = UIView()
    view.backgroundColor = .white

    aButton = UIButton()
    aButton.frame = CGRect(x: 95, y: 200, width: 200, height: 20)
    aButton.backgroundColor = UIColor.red

    aButton.addTarget(self, action: .buttonTapped, for: .touchUpInside)

    view.addSubview(aButton)
    self.view = view
  }

  func fizzleButtonOn(events: UIControlEvents, with: @escaping (() -> Void)) {
    assignedClosure = with
    aButton.removeTarget(self, action: .buttonTapped, for: .allEvents)
    aButton.addTarget(self, action: .buttonTapped, for: events)
  }

  @objc func buttonTapped() {
    guard let closure = assignedClosure else {
      debugPrint("original tap")
      return
    }
    closure()
  }
} 

fileprivate extension Selector {
  static let buttonTapped = #selector(ViewController.buttonTapped)
}

Then at some point in your app's lifecycle, you'll mutate the instances' closure. Here's an example

fizzleButtonOn(events: .touchUpInside, with: { debugPrint("a new tap action") })
Hyacinthia answered 26/2, 2018 at 16:48 Comment(0)
B
0

Below extension is for add tap gesture to UIView's level, which will work on anything that based of UIView.

Note: I found this solution years ago on StackOverflow too, but now I can't seem to find the original source.

extension UIView {
    
    // In order to create computed properties for extensions, we need a key to
    // store and access the stored property
    fileprivate struct AssociatedObjectKeys {
        static var tapGestureRecognizer = "MediaViewerAssociatedObjectKey_mediaViewer"
    }
    
    fileprivate typealias Action = (() -> Void)?
    
    // Set our computed property type to a closure
    fileprivate var tapGestureRecognizerAction: Action? {
        set {
            if let newValue = newValue {
                // Computed properties get stored as associated objects
                objc_setAssociatedObject(self, &AssociatedObjectKeys.tapGestureRecognizer, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
            }
        }
        get {
            let tapGestureRecognizerActionInstance = objc_getAssociatedObject(self, &AssociatedObjectKeys.tapGestureRecognizer) as? Action
            return tapGestureRecognizerActionInstance
        }
    }
    
    // This is the meat of the sauce, here we create the tap gesture recognizer and
    // store the closure the user passed to us in the associated object we declared above
    public func addTapGestureRecognizer(action: (() -> Void)?) {
        self.isUserInteractionEnabled = true
        self.tapGestureRecognizerAction = action
        let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture))
        tapGestureRecognizer.cancelsTouchesInView = false
        self.addGestureRecognizer(tapGestureRecognizer)
    }
    
    // Every time the user taps on the UIImageView, this function gets called,
    // which triggers the closure we stored
    @objc fileprivate func handleTapGesture(sender: UITapGestureRecognizer) {
        if let action = self.tapGestureRecognizerAction {
            action?()
        } else {
            print("no action")
        }
    }
    
}

Usage example:

let button = UIButton()
button.addTapGestureRecognizer {
    print("tapped")
}
        
let label = UILabel()
label.addTapGestureRecognizer {
    print("label tapped")
}           
Blowy answered 21/4, 2021 at 4:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.