Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Hooking up UIButton to closure? (Swift, target-action)

I want to hook up a UIButton to a piece of code – from what I have found, the preferred method to do this in Swift is still to use the addTarget(target: AnyObject?, action: Selector, forControlEvents: UIControlEvents) function. This uses the Selector construct presumably for backwards compatibility with Obj-C libraries. I think I understand the reason for @selector in Obj-C – being able to refer to a method since in Obj-C methods are not first-class values.

In Swift though, functions are first-class values. Is there a way to connect a UIButton to a closure, something similar to this:

// -- Some code here that sets up an object X  let buttonForObjectX = UIButton()   // -- configure properties here of the button in regards to object // -- for example title  buttonForObjectX.addAction(action: {() in     // this button is bound to object X, so do stuff relevant to X  }, forControlEvents: UIControlEvents.TouchUpOutside) 

To my knowledge, the above is currently not possible. Considering that Swift looks like it's aiming to be a quite functional, why is this? The two options could clearly co-exist for backwards compatibility. Why doesn't this work more like onClick() in JS? It seems that the only way to hook up a UIButton to a target-action pair is to use something that exists solely for backwards compatibility reasons (Selector).

My use case is to create UIButtons in a loop for different objects, and then hook each up to a closure. (Setting a tag / looking up in a dictionary / subclassing UIButton are dirty semi-solutions, but I'm interested in how to do this functionally, ie this closure approach)

like image 297
rafalio Avatar asked Jul 25 '14 18:07

rafalio


1 Answers

You can replace target-action with a closure by adding a helper closure wrapper (ClosureSleeve) and adding it as an associated object to the control so it gets retained.

This is a similar solution to the one in n13's answer. But I find it simpler and more elegant. The closure is invoked more directly and the wrapper is automatically retained (added as an associated object).

Swift 3 and 4

class ClosureSleeve {     let closure: () -> ()      init(attachTo: AnyObject, closure: @escaping () -> ()) {         self.closure = closure         objc_setAssociatedObject(attachTo, "[\(arc4random())]", self, .OBJC_ASSOCIATION_RETAIN)     }      @objc func invoke() {         closure()     } }  extension UIControl {     func addAction(for controlEvents: UIControlEvents = .primaryActionTriggered, action: @escaping () -> ()) {         let sleeve = ClosureSleeve(attachTo: self, closure: action)         addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents)     } } 

Usage:

button.addAction {     print("Hello") } 

It automatically hooks to the .primaryActionTriggered event which equals to .touchUpInside for UIButton.

like image 128
Marián Černý Avatar answered Sep 21 '22 05:09

Marián Černý