Swift programmatically create function for button with a closure
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 UIButton
s 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)
}
}
Related videos on Youtube
Comments
-
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 almost 8 yearsIn my answer, I show how you can do it with a subclass of
UIButton
.
-
-
Foobar almost 8 yearsI don't really want to use Objective-C. Luckily, it turned out there was a better solution that utilized the IOS "style" better!
-
HaveNoDisplayName over 7 yearsadd some description to your answer.
-
holex almost 6 yearsit'd feel a better logic, if you use
@IBAction
modifier instead of@objc
. -
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 sinceclicked
is an internal method and not intended to be connected through IB. -
holex almost 6 yearsthere 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 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 almost 6 yearswe 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 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 almost 6 yearsI 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 thesender
or theevent
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 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 almost 6 yearsIn 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 ofclick
. -
holex almost 6 yearsas you wish... I am still keeping my opinion about that is a bad practice to 'generalise' an event-handler this way.