Home » Swift » Adding a closure as target to a UIButton

Adding a closure as target to a UIButton

Posted by: admin November 30, 2017 Leave a comment

Questions:

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#>)
}
Answers:

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

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: UIControlEvents.TouchUpInside, 
 ForAction:{() -> Void in
     print("Touch")
 })

Questions:
Answers:

Similar solution to those already listed, but perhaps lighter weight:

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(format: "[%d]", arc4random()), sleeve, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
    }
}

Usage:

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

Questions:
Answers:

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

Questions:
Answers:

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.

Questions:
Answers:

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.

Questions:
Answers:

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

Questions:
Answers:

I find another solution to achieve the same effect. That is to use reactive cocoa.

let btn = UIButton()
btn.reactive
   .controlEvents(.touchUpInside)
   .observeValues{
     [weak self] // in case to avoid retain cycle
     button in // the button which trigger this closure
          print("show drop down") // actions you want to take
   }

The advantage of this solution is that it allows you to add how many actions you want in an intuitive way.