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?, ac
The associatedObject and wrapping and pointers and import of ObjectiveC are unnecessary, at least in Swift 3. This works great and is much more Swift-y. Feel free to throw a typealias in there for () -> ()
if you find it more readable, bit I find it easier to read the block signature directly.
import UIKit
class BlockButton: UIButton {
fileprivate var onAction: (() -> ())?
func addClosure(_ closure: @escaping () -> (), for control: UIControlEvents) {
self.addTarget(self, action: #selector(actionHandler), for: control)
self.onAction = closure
}
dynamic fileprivate func actionHandler() {
onAction?()
}
}
According to n13's solution, I made a swift3 version.
Hopes it could help some people like me.
import Foundation
import UIKit
import ObjectiveC
var ActionBlockKey: UInt8 = 0
// a type for our action block closure
typealias BlockButtonActionBlock = (_ sender: UIButton) -> Void
class ActionBlockWrapper : NSObject {
var block : BlockButtonActionBlock
init(block: @escaping BlockButtonActionBlock) {
self.block = block
}
}
extension UIButton {
func block_setAction(block: @escaping BlockButtonActionBlock, for control: UIControlEvents) {
objc_setAssociatedObject(self, &ActionBlockKey, ActionBlockWrapper(block: block), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
self.addTarget(self, action: #selector(UIButton.block_handleAction), for: .touchUpInside)
}
func block_handleAction(sender: UIButton, for control:UIControlEvents) {
let wrapper = objc_getAssociatedObject(self, &ActionBlockKey) as! ActionBlockWrapper
wrapper.block(sender)
}
}
This is not necessarily a "hooking," but you can effectively achieve this behavior 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")
}
UIButton inherits from UIControl, which handles the receiving of input and forwarding to the selection. According to the docs, the action is "A selector identifying an action message. It cannot be NULL." And a Selector is strictly a pointer to a method.
I would think that given the emphases Swift seems to be placing on Closures, this would be possible, but this doesnt seem to be the case.
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).
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.
The general approach for anything you think should be in the libraries but isn't: Write a category. There's lots of this particular one on GitHub but didn't find one in Swift so I wrote my own:
=== Put this in its own file, like UIButton+Block.swift ===
import ObjectiveC
var ActionBlockKey: UInt8 = 0
// a type for our action block closure
typealias BlockButtonActionBlock = (sender: UIButton) -> Void
class ActionBlockWrapper : NSObject {
var block : BlockButtonActionBlock
init(block: BlockButtonActionBlock) {
self.block = block
}
}
extension UIButton {
func block_setAction(block: BlockButtonActionBlock) {
objc_setAssociatedObject(self, &ActionBlockKey, ActionBlockWrapper(block: block), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
addTarget(self, action: "block_handleAction:", forControlEvents: .TouchUpInside)
}
func block_handleAction(sender: UIButton) {
let wrapper = objc_getAssociatedObject(self, &ActionBlockKey) as! ActionBlockWrapper
wrapper.block(sender: sender)
}
}
Then invoke it like this:
myButton.block_setAction { sender in
// if you're referencing self, use [unowned self] above to prevent
// a retain cycle
// your code here
}
Clearly this could be improved, there could be options for the various kinds of events (not just touch up inside) and so on. But this worked for me. It's slightly more complicated than the pure ObjC version because of the need for a wrapper for the block. Swift compiler does not allow storing the block as "AnyObject". So I just wrapped it.