Swift programmatically create function for button with a closure

13,943

Solution 1

Create your own UIButton subclass to do this:

class MyButton: UIButton {
    var action: (() -> Void)?

    func whenButtonIsClicked(action: @escaping () -> Void) {
        self.action = action
        self.addTarget(self, action: #selector(MyButton.clicked), for: .touchUpInside)
    }

    // Button Event Handler:
    // I have not marked this as @IBAction because it is not intended to
    // be hooked up to Interface Builder       
    @objc func clicked() {
        action?()
    }
}

Substitute MyButton for UIButton when you create buttons programmatically and then call whenButtonIsClicked to set up its functionality.

You can also use this with UIButtons in a Storyboard (just change their class to MyButton) and then call whenButtonIsClicked in viewDidLoad.

@IBOutlet weak var theButton: MyButton!

var count = 0

override func viewDidLoad() {
    super.viewDidLoad()

    // be sure to declare [unowned self] if you access
    // properties or methods of the class so that you
    // don't create a strong reference cycle
    theButton.whenButtonIsClicked { [unowned self] in
        self.count += 1
        print("count = \(self.count)")
    }

A much more capable implementation

Recognizing the fact that programmers might want to handle more events than just .touchUpInside, I wrote this more capable version which supports multiple closures per UIButton and multiple closures per event type.

class ClosureButton: UIButton {
    private var actions = [UInt : [((UIControl.Event) -> Void)]]()

    private let funcDict: [UInt : Selector] = [
        UIControl.Event.touchCancel.rawValue:       #selector(eventTouchCancel),
        UIControl.Event.touchDown.rawValue:         #selector(eventTouchDown),
        UIControl.Event.touchDownRepeat.rawValue:   #selector(eventTouchDownRepeat),
        UIControl.Event.touchUpInside.rawValue:     #selector(eventTouchUpInside),
        UIControl.Event.touchUpOutside.rawValue:    #selector(eventTouchUpOutside),
        UIControl.Event.touchDragEnter.rawValue:    #selector(eventTouchDragEnter),
        UIControl.Event.touchDragExit.rawValue:     #selector(eventTouchDragExit),
        UIControl.Event.touchDragInside.rawValue:   #selector(eventTouchDragInside),
        UIControl.Event.touchDragOutside.rawValue:  #selector(eventTouchDragOutside)
    ]

    func handle(events: [UIControl.Event], action: @escaping (UIControl.Event) -> Void) {
        for event in events {
            if var closures = actions[event.rawValue] {
                closures.append(action)
                actions[event.rawValue] = closures
            } else {
                guard let sel = funcDict[event.rawValue] else { continue }
                self.addTarget(self, action: sel, for: event)
                actions[event.rawValue] = [action]
            }
        }
    }

    private func callActions(for event: UIControl.Event) {
        guard let actions = actions[event.rawValue] else { return }
        for action in actions {
            action(event)
        }
    }

    @objc private func eventTouchCancel()       { callActions(for: .touchCancel) }
    @objc private func eventTouchDown()         { callActions(for: .touchDown) }
    @objc private func eventTouchDownRepeat()   { callActions(for: .touchDownRepeat) }
    @objc private func eventTouchUpInside()     { callActions(for: .touchUpInside) }
    @objc private func eventTouchUpOutside()    { callActions(for: .touchUpOutside) }
    @objc private func eventTouchDragEnter()    { callActions(for: .touchDragEnter) }
    @objc private func eventTouchDragExit()     { callActions(for: .touchDragExit) }
    @objc private func eventTouchDragInside()   { callActions(for: .touchDragInside) }
    @objc private func eventTouchDragOutside()  { callActions(for: .touchDragOutside) }
}

Demo

class ViewController: UIViewController {

    var count = 0

    override func viewDidLoad() {
        super.viewDidLoad()

        let button = ClosureButton(frame: CGRect(x: 50, y: 100, width: 60, height: 40))
        button.setTitle("press me", for: .normal)
        button.setTitleColor(.blue, for: .normal)

        // Demonstration of handling a single UIControl.Event type.
        // If your closure accesses self, be sure to declare [unowned self]
        // to prevent a strong reference cycle
        button.handle(events: [.touchUpInside]) { [unowned self] _ in
            self.count += 1
            print("count = \(self.count)")
        }

        // Define a second handler for touchUpInside:
        button.handle(events: [.touchUpInside]) { _ in
            print("I'll be called on touchUpInside too")
        }

        let manyEvents: [UIControl.Event] = [.touchCancel, .touchUpInside, .touchDown, .touchDownRepeat, .touchUpOutside, .touchDragEnter,
             .touchDragExit, .touchDragInside, .touchDragOutside]

        // Demonstration of handling multiple events
        button.handle(events: manyEvents) { event in
            switch event {
            case .touchCancel:
                print("touchCancel")
            case .touchDown:
                print("touchDown")
            case .touchDownRepeat:
                print("touchDownRepeat")
            case .touchUpInside:
                print("touchUpInside")
            case .touchUpOutside:
                print("touchUpOutside")
            case .touchDragEnter:
                print("touchDragEnter")
            case .touchDragExit:
                print("touchDragExit")
            case .touchDragInside:
                print("touchDragInside")
            case .touchDragOutside:
                print("touchDragOutside")
            default:
                break
            }
        }

        self.view.addSubview(button)
    }
}

Solution 2

If you don't want to do anything "questionable" (i.e., using Objective-C's dynamic capabilities, or adding your own touch handlers, etc.) and do this purely in Swift, unfortunately this is not possible.

Any time you see #selector in Swift, the compiler is calling objc_MsgSend under the hood. Swift doesn't support Objective-C's dynamicism. For better or for worse, this means that in order to swap out the usage of this selector with a block, you'd probably need to perform some black magic to make it work, and you'd have to use Objective-C constructs to do that.

If you don't have any qualms about doing "yucky dynamic Objective-C stuff", you could probably implement this by defining an extension on UIButton, and then associate a function to the object dynamically using associated objects. I'm going to stop here, but if you want to read more, NSHipster has a great overview on associated objects and how to use them.

Solution 3

This one will work ! Make sure you don't alter the tag for buttons

extension UIButton {
private func actionHandleBlock(action:(()->())? = nil) {
    struct __ {
        var closure : (() -> Void)?
        typealias EmptyCallback = ()->()
        static var action : [EmptyCallback] = []
    }
    if action != nil {
       // __.action![(__.action?.count)!] = action!
        self.tag = (__.action.count)
        __.action.append(action!)
    } else {
        let exe = __.action[self.tag]
        exe()
    }
}

@objc private func triggerActionHandleBlock() {
    self.actionHandleBlock()
}

func addAction(forControlEvents control :UIControlEvents, ForAction action:@escaping () -> Void) {
    self.actionHandleBlock(action: action)
    self.addTarget(self, action: #selector(triggerActionHandleBlock), for: control)
}

}

Share:
13,943

Related videos on Youtube

Foobar
Author by

Foobar

Just a random person who like to code and make apps.

Updated on October 05, 2022

Comments

  • Foobar
    Foobar over 1 year

    In Swift you can create a function for a button like this:

    button.addTarget(self, action: #selector(buttonAction), forControlEvents: .TouchUpInside)
    

    However is there a way I can do something like this:

    button.whenButtonIsClicked({Insert code here})
    

    That way I do not even have too declare an explicit function for the button. I know I can use button tags but I would prefer to do this instead.

    • vacawama
      vacawama almost 8 years
      In my answer, I show how you can do it with a subclass of UIButton.
  • Foobar
    Foobar almost 8 years
    I don't really want to use Objective-C. Luckily, it turned out there was a better solution that utilized the IOS "style" better!
  • HaveNoDisplayName
    HaveNoDisplayName over 7 years
    add some description to your answer.
  • holex
    holex almost 6 years
    it'd feel a better logic, if you use @IBAction modifier instead of @objc.
  • vacawama
    vacawama almost 6 years
    @holex, @IBAction is for functions you are going to connect in Interface Builder. @objc is for selectors so even though @IBAction includes @objc, I feel @objc is more appropriate here since clicked is an internal method and not intended to be connected through IB.
  • holex
    holex almost 6 years
    there is no convention about such exclusivity at all, it'd be just used by IB as well; but in this case @IBAction modifier'd indicate to the reader that the actual method is a real event-handler here, rather than a generic @objc modifier for a generic callback method (e.g. notifications, timers, etc...)
  • vacawama
    vacawama almost 6 years
    @holex, I respectfully suggest that we agree to disagree on this point. If I were to make it an @IBAction, I’m convinced that I would get questions about how to connect that method to buttons in the Storyboard.
  • holex
    holex almost 6 years
    we can disagree, I'm happy with that – but in general marking a method with @IBAction indicates that the method is an event handler and will not mark only an exclusive connection between the code and IB; you could use it for only such purpose but that is not general and you cannot expect only such use-case automatically from @IBAction.
  • vacawama
    vacawama almost 6 years
    @holex, To me, @IBAction means that I want to expose that method to Interface Builder for possible connections. In this case, I do not want Interface Builder to see this method because it is not for Interface Builder’s use.
  • holex
    holex almost 6 years
    I understood that it is your convention, I have accepted that – but what I'm trying to emphasise is that @IBAction means that the method could be exposed in IB but not only for exposing it in IB; generally it marks event-handlers with all advantages of an event handler: like e.g. you can get know optionally the sender or the event as well, if needed anytime later – while marking a designated event-handler with a more generic @objc modifier makes the event-handler hidden and dumb in your code; and giving such advice to others could be a bad practice or habit.
  • vacawama
    vacawama almost 6 years
    @holex, thanks, I now finally understand your point. I frankly never considered that @IBAction might indicate to my fellow programmers (or to me at a later date) that the method so marked was an event handler. @IBAction's original or primary purpose was to make it available to Interface Builder as can be seen by its affect in the tools, but I can see how marking event handlers with @IBAction is further communication about the intent of the function.
  • vacawama
    vacawama almost 6 years
    In this one particular case, I feel the confusion caused by suggesting that the click method can be hooked up in Interface Builder (which we don't really want anyone considering/doing/trying) outweighs the benefit of documenting/identifying the event handler nature of click.
  • holex
    holex almost 6 years
    as you wish... I am still keeping my opinion about that is a bad practice to 'generalise' an event-handler this way.