The question is: how can I do to make a generic version of UITapGestureRecognizer that I could use across my app.
If I don't want to pass any parameter, it is pretty straightforward:
class ClickListener: UITapGestureRecognizer {
var onClick : (() -> Void)? = nil
}
// MARK: UIView Extension
extension UIView {
func setOnClickListener(action :#escaping () -> Void){
let tapRecogniser = ClickListener(target: self, action: #selector(onViewClicked(sender:)))
tapRecogniser.onClick = action
self.addGestureRecognizer(tapRecogniser)
}
#objc func onViewClicked(sender: ClickListener) {
if let onClick = sender.onClick {
onClick()
}
}
}
As Sunneet Agrawal did.
The thing is, I sometimes need to pass parameters to this function.
For example, if I want to pass an Object, I would like to do so. And then I could reuse this function globally through the app.
Everything I tried didn't work, I can provide a bit of code like this:
class ClickListener<T: Any>: UITapGestureRecognizer {
var onClick : (() -> Void)? = nil
var clickedObject: Any
override init(onClick: (() -> Void), clickedObject: Any, target: self, action: ???){
self.onClick = onClick
self.clickedObject = clickedObject
super.init(target: self, action: ???)
}
}
extension UIView {
func setOnClickListener(action :#escaping () -> Void){
let tapRecogniser = ClickListener(// Init here)
tapRecogniser.onClick = action( // parameter here)
self.addGestureRecognizer(tapRecogniser)
}
#objc func onViewClicked(sender: ClickListener) {
if let onClick = sender.onClick {
onClick(// with parameter passed)
}
}
}
// Then in my VC
UIView.setOnclickListener(action: anyFunction, clickedObject: anyObject)
But I'm a bit lost right there.
First of all, the selector of the target/action pattern has two fixed forms:
A function without parameter
A function with one parameter representing the sender, the object which triggered the action.
But you can add properties in a subclass and pass the parameters in the (custom) init method.
A generic in a subclass fights the framework therefore a custom dictionary (you could use just Any, too) is the better choice.
For example beside target and action the class has an userInfo dictionary and a onClick closure. The init method calls super to call the designated initializer of the tap recognizer.
class ClickListener: UITapGestureRecognizer {
var onClick : (() -> Void)?
var userInfo: [String:Any]?
init(target: Any?, action: Selector?, userInfo: [String:Any]? = nil, onClick: (() -> Void)? = nil) {
self.userInfo = userInfo
self.onClick = onClick
super.init(target: target, action: action)
}
}
And you can use it
extension UIView {
func setOnClickListener(action :#escaping () -> Void){
let tapRecogniser = ClickListener(target: self, action: #selector(onViewClicked), userInfo: ["message":"Hello World"], onClick: action)
self.addGestureRecognizer(tapRecogniser)
}
#objc func onViewClicked(_ sender: ClickListener) {
if let userInfo = sender.userInfo,
let message = userInfo["message"] as? String {
print(message)
}
sender.onClick?()
}
}
A more generic implementation is to pass the userInfo in the onClick closure.
class ClickListener: UITapGestureRecognizer {
var onClick : (([String:Any]?) -> Void)?
var userInfo: [String:Any]?
init(target: Any?, action: Selector?, userInfo: [String:Any]? = nil, onClick: (([String:Any]?) -> Void)?) {
self.userInfo = userInfo
self.onClick = onClick
super.init(target: target, action: action)
}
}
extension UIView {
func setOnClickListener(userInfo: [String:Any], action :#escaping ([String:Any]?) -> Void){
let tapRecogniser = ClickListener(target: self, action: #selector(onViewClicked), userInfo: userInfo, onClick: action)
self.addGestureRecognizer(tapRecogniser)
}
#objc func onViewClicked(_ sender: ClickListener) {
sender.onClick?(sender.userInfo)
}
}
I have a UITableView with row where I added single tap and double tap gestures:
let doubleTap = UITapGestureRecognizer(target: self, action: "doubleTap:")
doubleTap.numberOfTapsRequired = 2
doubleTap.numberOfTouchesRequired = 1
let singleTap = UITapGestureRecognizer(target: self, action: "singleTap:")
singleTap.numberOfTapsRequired = 1
singleTap.numberOfTouchesRequired = 1
singleTap.requireGestureRecognizerToFail(doubleTap)
tableView.addGestureRecognizer(doubleTap)
tableView.addGestureRecognizer(singleTap)
Is there a way to reduce the time between when the first tap is made and when the gesture recognizer realize that it is a single tap and not a double tap?
I'm asking this because when I do a single tap, the new viewController appear quite late, giving a feeling that the app lags.
I found the answer on this link
The swift version:
class UIShortTapGestureRecognizer: UITapGestureRecognizer {
let tapMaxDelay: Double = 0.3
override func touchesBegan(touches: NSSet!, withEvent event: UIEvent!) {
super.touchesBegan(touches, withEvent: event)
delay(tapMaxDelay) {
// Enough time has passed and the gesture was not recognized -> It has failed.
if self.state != UIGestureRecognizerState.Ended {
self.state = UIGestureRecognizerState.Failed
}
}
}
}
With delay(delay: Double, closure:()->()):
class func delay(delay:Double, closure:()->()) {
dispatch_after(dispatch_time( DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC))), dispatch_get_main_queue(), closure)
}
Full Implementation of Markus's Swift 3 version of eladleb's original solution.
Create subclass file UIShortTapGestureRecogninzer
import UIKit
import UIKit.UIGestureRecognizerSubclass
class UIShortTapGestureRecognizer: UITapGestureRecognizer {
let tapMaxDelay: Double = 0.3 //anything below 0.3 may cause doubleTap to be inaccessible by many users
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
DispatchQueue.main.asyncAfter(deadline: .now() + tapMaxDelay) { [weak self] in
if self?.state != UIGestureRecognizerState.recognized {
self?.state = UIGestureRecognizerState.failed
}
}
}
}
Note: When adding UIGestureRecognizer only doubleTap needs to be of type UIShortTapGestureRecognizer & singleTap.require(toFail: doubleTap) is required.
func addBoth (views: UIView, selectorSingle: Selector, selectorDouble: Selector) {
let doubleTap:UIShortTapGestureRecognizer = UIShortTapGestureRecognizer(target: self, action: selectorDouble)
doubleTap.numberOfTapsRequired = 2
views.addGestureRecognizer(doubleTap)
let singleTap:UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: selectorSingle)
singleTap.numberOfTapsRequired = 1
singleTap.require(toFail: doubleTap)
views.addGestureRecognizer(singleTap)
}
Swift 5 implementation of Nico's accepted answer.
class UIShortTapGestureRecognizer: UITapGestureRecognizer {
var maximumTapLength: Double = 0.3
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
delay(delay: maximumTapLength) {
// Enough time has passed and the gesture was not recognized -> It has failed.
if self.state != .ended {
self.state = .failed
}
}
}
func delay(delay:Double, closure:#escaping ()->()) {
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: closure)
}
}
for future, full implementation by Howard Yang, here's links:
https://github.com/oney/SingleDoubleTapGestureRecognizer
let tap = SingleDoubleTapGestureRecognizer(target: self, singleAction: Selector("singleTap"), doubleAction: Selector("doubleTap"))
tap.duration = 0.8
view.addGestureRecognizer(tap)
https://github.com/oney/SingleDoubleTapGestureRecognizer/blob/master/Pod/Classes/SingleDoubleTapGestureRecognizer.swift
//
// SingleDoubleTapGestureRecognizer.swift
// SingleDoubleTapGestureRecognizer
//
// Created by Howard Yang on 08/22/2015.
// Copyright (c) 2015 Howard Yang. All rights reserved.
//
import UIKit
public class SingleDoubleTapGestureRecognizer: UITapGestureRecognizer {
var targetDelegate: SingleDoubleTapGestureRecognizerDelegate
public var duration: CFTimeInterval = 0.3 {
didSet {
self.targetDelegate.duration = duration
}
}
public init(target: AnyObject, singleAction: Selector, doubleAction: Selector) {
targetDelegate = SingleDoubleTapGestureRecognizerDelegate(target: target, singleAction: singleAction, doubleAction: doubleAction)
super.init(target: targetDelegate, action: Selector("fakeAction:"))
numberOfTapsRequired = 1
}
}
class SingleDoubleTapGestureRecognizerDelegate: NSObject {
var target: AnyObject
var singleAction: Selector
var doubleAction: Selector
var duration: CFTimeInterval = 0.3
var tapCount = 0
init(target: AnyObject, singleAction: Selector, doubleAction: Selector) {
self.target = target
self.singleAction = singleAction
self.doubleAction = doubleAction
}
func fakeAction(g: UITapGestureRecognizer) {
tapCount = tapCount + 1
if tapCount == 1 {
delayHelper(duration, task: {
if self.tapCount == 1 {
NSThread.detachNewThreadSelector(self.singleAction, toTarget:self.target, withObject: g)
}
else if self.tapCount == 2 {
NSThread.detachNewThreadSelector(self.doubleAction, toTarget:self.target, withObject: g)
}
self.tapCount = 0
})
}
}
typealias DelayTask = (cancel : Bool) -> ()
func delayHelper(time:NSTimeInterval, task:()->()) -> DelayTask? {
func dispatch_later(block:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(time * Double(NSEC_PER_SEC))),
dispatch_get_main_queue(),
block)
}
var closure: dispatch_block_t? = task
var result: DelayTask?
let delayedClosure: DelayTask = {
cancel in
if let internalClosure = closure {
if (cancel == false) {
dispatch_async(dispatch_get_main_queue(), internalClosure);
}
}
closure = nil
result = nil
}
result = delayedClosure
dispatch_later {
if let delayedClosure = result {
delayedClosure(cancel: false)
}
}
return result;
}
func cancel(task:DelayTask?) {
task?(cancel: true)
}
}
I'd rather recommend to use canBePrevented(by:) function, it takes in account amount of taps to perform and won't run you double-tap gesture recognizer unless first one is recognized/failed.
canBePrevented(by:)
Inspired by Howard Yang's implementation, Swift 5.1 using DispatchWorkItem:
public class SingleDoubleTapGestureRecognizer: UITapGestureRecognizer {
var targetDelegate: SingleDoubleTapGestureRecognizerDelegate
public var timeout: TimeInterval = 0.3 {
didSet {
self.targetDelegate.timeout = timeout
}
}
public init(target: AnyObject, singleAction: Selector, doubleAction: Selector) {
targetDelegate = SingleDoubleTapGestureRecognizerDelegate(target: target, singleAction: singleAction, doubleAction: doubleAction)
super.init(target: targetDelegate, action: #selector(targetDelegate.recognizerAction(recognizer:)))
}
}
class SingleDoubleTapGestureRecognizerDelegate: NSObject {
weak var target: AnyObject?
var singleAction: Selector
var doubleAction: Selector
var timeout: TimeInterval = 0.3
var tapCount = 0
var workItem: DispatchWorkItem?
init(target: AnyObject, singleAction: Selector, doubleAction: Selector) {
self.target = target
self.singleAction = singleAction
self.doubleAction = doubleAction
}
#objc func recognizerAction(recognizer: UITapGestureRecognizer) {
tapCount += 1
if tapCount == 1 {
workItem = DispatchWorkItem { [weak self] in
guard let weakSelf = self else { return }
weakSelf.target?.performSelector(onMainThread: weakSelf.singleAction, with: recognizer, waitUntilDone: false)
weakSelf.tapCount = 0
}
DispatchQueue.main.asyncAfter(
deadline: .now() + timeout,
execute: workItem!
)
} else {
workItem?.cancel()
DispatchQueue.main.async { [weak self] in
guard let weakSelf = self else { return }
weakSelf.target?.performSelector(onMainThread: weakSelf.doubleAction, with: recognizer, waitUntilDone: false)
weakSelf.tapCount = 0
}
}
}
}
Usage:
let singleDoubleTapRecognizer = SingleDoubleTapGestureRecognizer(
target: self,
singleAction: #selector(handleTapGesture),
doubleAction: #selector(handleDoubleTapGesture)
)
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#>)
}
With iOS 14 Apple has finally added this feature to UIKit. However, someone might still want to use this extension because Apple's method signature is suboptimal.
iOS 14:
extension UIControl {
func addAction(for controlEvents: UIControl.Event = .touchUpInside, _ closure: #escaping()->()) {
addAction(UIAction { (action: UIAction) in closure() }, for: controlEvents)
}
}
pre-iOS 14:
extension UIControl {
func addAction(for controlEvents: UIControl.Event = .touchUpInside, _ closure: #escaping()->()) {
#objc class ClosureSleeve: NSObject {
let closure:()->()
init(_ closure: #escaping()->()) { self.closure = closure }
#objc func invoke() { closure() }
}
let sleeve = ClosureSleeve(closure)
addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents)
objc_setAssociatedObject(self, "\(UUID())", sleeve, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
Usage:
button.addAction {
print("Hello, Closure!")
}
or:
button.addAction(for: .touchUpInside) {
print("Hello, Closure!")
}
or if avoiding retain loops:
self.button.addAction(for: .touchUpInside) { [unowned self] in
self.doStuff()
}
(Extension is included here: https://github.com/aepryus/Acheron)
Also note, in theory .primaryActionTriggered could replace .touchUpInside, but it seems to be currently bugged in catalyst, so I'll leave it as is for now.
Do Not Use This Answer, See Note Below
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
Swift 5
import UIKit
extension UIButton {
private func actionHandler(action:(() -> Void)? = nil) {
struct __ { static var action :(() -> Void)? }
if action != nil { __.action = action }
else { __.action?() }
}
#objc private func triggerActionHandler() {
self.actionHandler()
}
func actionHandler(controlEvents control :UIControl.Event, ForAction action:#escaping () -> Void) {
self.actionHandler(action: action)
self.addTarget(self, action: #selector(triggerActionHandler), for: control)
}
}
Older
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: .touchUpInside,
ForAction:{() -> Void in
print("Touch")
})
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")
}
Similar solution to those already listed, but perhaps lighter weight and doesn't rely on randomness to generate unique ids:
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(ObjectIdentifier(self).hashValue) + String(controlEvents.rawValue), sleeve,
objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
Usage:
button.add(for: .touchUpInside) {
print("Hello, Closure!")
}
Or if avoiding retain loops:
button.add(for: .touchUpInside) { [unowned self] in
self.doStuff()
}
This is now possible on iOS 14. You can pass a UIAction, which has a handler closure, when you create the UIButton:
let action = UIAction(title: "") { action in
print("Button tapped!")
}
UIButton(type: .system, primaryAction: action)
Or shorter:
UIButton(type: .system, primaryAction: UIAction(title: "") { action in
print("Button tapped!")
})
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.
#Armanoide solution is cool cause it uses trick with struct and static var inside it but it is not perfect if you're reusing one button a few times cause in this case action closure will always store the last handler.
I've fixed it for UIKitPlus library
import UIKit
extension UIControl {
private func actionHandler(action: (() -> Void)? = nil) {
struct Storage { static var actions: [Int: (() -> Void)] = [:] }
if let action = action {
Storage.actions[hashValue] = action
} else {
Storage.actions[hashValue]?()
}
}
#objc func triggerActionHandler() {
actionHandler()
}
func actionHandler(controlEvents control: UIControl.Event, forAction action: #escaping () -> Void) {
actionHandler(action: action)
addTarget(self, action: #selector(triggerActionHandler), for: control)
}
}
I put together a little extension for UIControl that will let you use closures for any action on any UIControl really easily.
You can find it here: https://gist.github.com/nathan-fiscaletti/8308f00ff364b72b6a6dec57c4b13d82
Here are some examples of it in practice:
Setting a Button Action
myButton.action(.touchUpInside, { (sender: UIControl) in
// do something
})
Detecting a Switch changing Values
mySwitch.action(.valueChanged, { (sender: UIControl) in
print("Switch State:", mySwitch.isOn)
})
Here is a generic swift 5 approach. It has a sender inside action block and eliminates adding action for same event twice
import UIKit
protocol Actionable {
associatedtype T = Self
func addAction(for controlEvent: UIControl.Event, action: ((T) -> Void)?)
}
private class ClosureSleeve<T> {
let closure: ((T) -> Void)?
let sender: T
init (sender: T, _ closure: ((T) -> Void)?) {
self.closure = closure
self.sender = sender
}
#objc func invoke() {
closure?(sender)
}
}
extension Actionable where Self: UIControl {
func addAction(for controlEvent: UIControl.Event, action: ((Self) -> Void)?) {
let previousSleeve = objc_getAssociatedObject(self, String(controlEvent.rawValue))
objc_removeAssociatedObjects(previousSleeve as Any)
removeTarget(previousSleeve, action: nil, for: controlEvent)
let sleeve = ClosureSleeve(sender: self, action)
addTarget(sleeve, action: #selector(ClosureSleeve<Self>.invoke), for: controlEvent)
objc_setAssociatedObject(self, String(controlEvent.rawValue), sleeve, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
extension UIControl: Actionable {}
Here is a fun variant to the answer by aepryus. My version uses Combine's Cancellable protocol to:
Support removing the registered closure.
Handle memory management thus avoiding the need to use objc_setAssociatedObject.
// Swift 5
import Combine
import UIKit
class BlockObject: NSObject {
let block: () -> Void
init(block: #escaping () -> Void) {
self.block = block
}
#objc dynamic func execute() {
block()
}
}
extension UIControl {
func addHandler(
for controlEvents: UIControl.Event,
block: #escaping () -> Void)
-> Cancellable
{
let blockObject = BlockObject(block: block)
addTarget(blockObject, action: #selector(BlockObject.execute), for: controlEvents)
return AnyCancellable {
self.removeTarget(blockObject, action: #selector(BlockObject.execute), for: controlEvents)
}
}
}
Usage:
let button = UIButton(type: .system)
// Add the handler
let cancellable = button.addHandler(for: .touchUpInside) {
print("Button pressed!")
}
// Remove the handler
cancellable.cancel()
Don't forget to store a reference to the Cancellable or else the handler will be immediately unregistered.
I change a little extension for UIControl that was posted #Nathan F.
here
I used objc_setAssociatedObject and objc_getAssociatedObject to get/set closure and i removed global static variable with all created buttons's keys.
So now event stored for each instance and released after dealloc
extension UIControl {
typealias Handlers = [UInt:((UIControl) -> Void)]
private enum AssociatedKey {
static var actionHandlers = "UIControl.actionHandlers"
}
/**
* A map of closures, mapped as [ event : action ] .
*/
private var actionHandlers: Handlers {
get {
return objc_getAssociatedObject(self, &AssociatedKey.actionHandlers) as? Handlers ?? [:]
}
set(newValue) {
objc_setAssociatedObject(self, &AssociatedKey.actionHandlers, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
}
You can find it here: https://gist.github.com/desyatov/6ed83de58ca1146d85fedab461a69b12
Here are some example:
myButton.action(.touchUpInside, { (sender: UIControl) in
// do something
})
Swift
After trying all the solutions, this one worked for me for all cases, even when the button in reusable table view cell
import UIKit
typealias UIButtonTargetClosure = UIButton -> ()
class ClosureWrapper: NSObject {
let closure: UIButtonTargetClosure
init(_ closure: UIButtonTargetClosure) {
self.closure = closure
}
}
extension UIButton {
private struct AssociatedKeys {
static var targetClosure = "targetClosure"
}
private var targetClosure: UIButtonTargetClosure? {
get {
guard let closureWrapper = objc_getAssociatedObject(self, &AssociatedKeys.targetClosure) as? ClosureWrapper else { return nil }
return closureWrapper.closure
}
set(newValue) {
guard let newValue = newValue else { return }
objc_setAssociatedObject(self, &AssociatedKeys.targetClosure, ClosureWrapper(newValue), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
func addTargetClosure(closure: UIButtonTargetClosure) {
targetClosure = closure
addTarget(self, action: #selector(UIButton.closureAction), forControlEvents: .TouchUpInside)
}
func closureAction() {
guard let targetClosure = targetClosure else { return }
targetClosure(self)
}
}
And then you call it like this:
loginButton.addTargetClosure { _ in
// login logics
}
Resource:
https://medium.com/#jackywangdeveloper/swift-the-right-way-to-add-target-in-uibutton-in-using-closures-877557ed9455
My solution.
typealias UIAction = () -> Void;
class Button: UIButton {
public var touchUp :UIAction? {
didSet {
self.setup()
}
}
func setup() -> Void {
self.addTarget(self, action: #selector(touchInside), for: .touchUpInside)
}
#objc private func touchInside() -> Void {
self.touchUp!()
}
}
Swift 4.2 for UIControl and UIGestureRecognizer, and and remove targets through swift extension stored property paradigm.
Wrapper class for the selector
class Target {
private let t: () -> ()
init(target t: #escaping () -> ()) { self.t = t }
#objc private func s() { t() }
public var action: Selector {
return #selector(s)
}
}
Protocols with associatedtypes so we can hide hide the objc_ code
protocol PropertyProvider {
associatedtype PropertyType: Any
static var property: PropertyType { get set }
}
protocol ExtensionPropertyStorable: class {
associatedtype Property: PropertyProvider
}
Extension to make the property default and available
extension ExtensionPropertyStorable {
typealias Storable = Property.PropertyType
var property: Storable {
get { return objc_getAssociatedObject(self, String(describing: type(of: Storable.self))) as? Storable ?? Property.property }
set { return objc_setAssociatedObject(self, String(describing: type(of: Storable.self)), newValue, .OBJC_ASSOCIATION_RETAIN) }
}
}
Let us apply the magic
extension UIControl: ExtensionPropertyStorable {
class Property: PropertyProvider {
static var property = [String: Target]()
}
func addTarget(for controlEvent: UIControl.Event = .touchUpInside, target: #escaping () ->()) {
let key = String(describing: controlEvent)
let target = Target(target: target)
addTarget(target, action: target.action, for: controlEvent)
property[key] = target
}
func removeTarget(for controlEvent: UIControl.Event = .touchUpInside) {
let key = String(describing: controlEvent)
let target = property[key]
removeTarget(target, action: target?.action, for: controlEvent)
property[key] = nil
}
}
And to the gestures
extension UIGestureRecognizer: ExtensionPropertyStorable {
class Property: PropertyProvider {
static var property: Target?
}
func addTarget(target: #escaping () -> ()) {
let target = Target(target: target)
addTarget(target, action: target.action)
property = target
}
func removeTarget() {
let target = property
removeTarget(target, action: target?.action)
property = nil
}
}
Example usage:
button.addTarget {
print("touch up inside")
}
button.addTarget { [weak self] in
print("this will only happen once")
self?.button.removeTarget()
}
button.addTarget(for: .touchDown) {
print("touch down")
}
slider.addTarget(for: .valueChanged) {
print("value changed")
}
textView.addTarget(for: .allEditingEvents) { [weak self] in
self?.editingEvent()
}
gesture.addTarget { [weak self] in
self?.gestureEvent()
self?.otherGestureEvent()
self?.gesture.removeTarget()
}
Here's a nice framework for doing this: HandlersKit. The biggest advantage is that you can access to the sender inside the closure without typecasting or optional unwrapping.
Example for UIButton:
import HandlersKit
let button = MyActivityIndicatorButton()
button.onTap { (sender: MyActivityIndicatorButton) in
sender.showActivityIndicator()
}
Example for UISwitch:
let switchView = UISwitch(frame: CGRect(x: 0.0, y: 0.0, width: 100.0, height: 50.0))
switchView.onChange { isOn in
print("SwitchView is: \(isOn)")
}
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.
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
)
}
}
class ViewController : UIViewController {
var aButton: UIButton!
var assignedClosure: (() -> Void)? = nil
override func loadView() {
let view = UIView()
view.backgroundColor = .white
aButton = UIButton()
aButton.frame = CGRect(x: 95, y: 200, width: 200, height: 20)
aButton.backgroundColor = UIColor.red
aButton.addTarget(self, action: .buttonTapped, for: .touchUpInside)
view.addSubview(aButton)
self.view = view
}
func fizzleButtonOn(events: UIControlEvents, with: #escaping (() -> Void)) {
assignedClosure = with
aButton.removeTarget(self, action: .buttonTapped, for: .allEvents)
aButton.addTarget(self, action: .buttonTapped, for: events)
}
#objc func buttonTapped() {
guard let closure = assignedClosure else {
debugPrint("original tap")
return
}
closure()
}
}
fileprivate extension Selector {
static let buttonTapped = #selector(ViewController.buttonTapped)
}
Then at some point in your app's lifecycle, you'll mutate the instances' closure. Here's an example
fizzleButtonOn(events: .touchUpInside, with: { debugPrint("a new tap action") })
Below extension is for add tap gesture to UIView's level, which will work on anything that based of UIView.
Note: I found this solution years ago on StackOverflow too, but now I can't seem to find the original source.
extension UIView {
// In order to create computed properties for extensions, we need a key to
// store and access the stored property
fileprivate struct AssociatedObjectKeys {
static var tapGestureRecognizer = "MediaViewerAssociatedObjectKey_mediaViewer"
}
fileprivate typealias Action = (() -> Void)?
// Set our computed property type to a closure
fileprivate var tapGestureRecognizerAction: Action? {
set {
if let newValue = newValue {
// Computed properties get stored as associated objects
objc_setAssociatedObject(self, &AssociatedObjectKeys.tapGestureRecognizer, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
get {
let tapGestureRecognizerActionInstance = objc_getAssociatedObject(self, &AssociatedObjectKeys.tapGestureRecognizer) as? Action
return tapGestureRecognizerActionInstance
}
}
// This is the meat of the sauce, here we create the tap gesture recognizer and
// store the closure the user passed to us in the associated object we declared above
public func addTapGestureRecognizer(action: (() -> Void)?) {
self.isUserInteractionEnabled = true
self.tapGestureRecognizerAction = action
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture))
tapGestureRecognizer.cancelsTouchesInView = false
self.addGestureRecognizer(tapGestureRecognizer)
}
// Every time the user taps on the UIImageView, this function gets called,
// which triggers the closure we stored
#objc fileprivate func handleTapGesture(sender: UITapGestureRecognizer) {
if let action = self.tapGestureRecognizerAction {
action?()
} else {
print("no action")
}
}
}
Usage example:
let button = UIButton()
button.addTapGestureRecognizer {
print("tapped")
}
let label = UILabel()
label.addTapGestureRecognizer {
print("label tapped")
}