How to add parameters to a generic UITapGestureRecognizer? - ios

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)
}
}

Related

How to remove passed closure from UIViewController

sorry maybe title not so much informative, so here is my problem
I want to create ThemeManager and apply to all screens, theme can be changed in the app, thats why I added closureList which will fire and update all related screens
class ThemeManager {
static let shared = ThemeManager()
private(set) var theme: Theme
private var bindedList: [()->Void] = []
private init () {
self.theme = AppGreenTheme()
}
func apply(theme: Theme) {
self.theme = theme
}
func bind(closure: #escaping ()->Void) {
bindedList.append(closure)
}
func bindAndFire(closure: #escaping ()->Void) {
bind(closure: closure)
closure()
}
}
here is how I want to use it from any UIViewController, or any UIView
ThemeManager.shared.bindAndFire {
// here I will get theme changes and update my screen
}
so I wanted to know, in this case will I create reference cycle for UIViewController, or UIView, and which is the best approach to remove closures from the list after parent UIViewController or UIView, will be removed from memory.
Its safe as long as you pass your UIViewController as a weak reference, like so
ThemeManager.shared.bindAndFire { [weak self] in
guard let strongSelf = self else { return }
// here I will get theme changes and update my screen
}
But NotificationCenter is better approach for this to rely on, here is basic ThemeManager example
class ThemeManager {
static let shared = ThemeManager()
static let NotificationName = NSNotification.Name("Notifacation.ThemeManager")
var theme: Theme!
func switchTheme(_ theme: Theme) {
self.theme = theme
NotificationCenter.default.post(name: ThemeManager.NotificationName, object: self.theme)
}
}
Usage:
class ViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(themeDidUpdate(_:)), name: ThemeManager.NotificationName, object: nil)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
NotificationCenter.default.removeObserver(self)
}
#objc func themeDidUpdate(_ notification: Notification) {
guard let theme = notification.object as? Theme else { return }
// here I will get theme changes and update my screen
}
}
Update-2 Example NotificationCenter with a closure
NotificationCenter.default.addObserver(forName: ThemeManager.NotificationName, object: nil, queue: OperationQueue.main) { [weak self] (notification) in
guard let strongSelf = self, let theme = notification.object as? Theme else { return }
// here I will get theme changes and update my screen
}
You can wrap the closures with a struct that has also has a property you can check for equality and return the value of that property when a closure is added. The view controller can then pass this id if it wants to remove the closure.You can hide the wrapper from the rest of the code. You can also use UUID if you don't want to keep track of some counter. You can also use a dictionary to store the closure with the id as the key.
class ThemeManager {
private var counter = 0
private var closures: [ClosureWrapper] = []
private struct ClosureWrapper {
let id: Int
let closure: () -> Void
}
func bind(closure: #escaping () -> Void) -> Int {
counter += 1
let wrapper = ClosureWrapper(id: counter, closure: closure)
closures.append(wrapper)
return wrapper.id
}
func removeClosure(with id: Int) {
guard let index = closures.firstIndex(where: { $0.id == id }) else {
return
}
closures.remove(at: index)
}
}
Here's version where you don't need to keep track of an id for the closure. It uses NSMapTable with weak keys to store the closures. You can pass the view controller as the key and when it is deallocated the passed closure will be automatically removed from the map table.
class ThemeManager {
private let closureTable = NSMapTable<NSObject, ClosureWrapper>(keyOptions: .weakMemory, valueOptions: .strongMemory)
private class ClosureWrapper {
let closure: () -> Void
init(closure: #escaping () -> Void) {
self.closure = closure
}
}
func bind(from source: NSObject, closure: #escaping () -> Void) {
let wrapper = ClosureWrapper(closure: closure)
closureTable.setObject(wrapper, forKey: source)
}
func callClosures() {
for key in closureTable.keyEnumerator().allObjects {
let wrapper = closureTable.object(forKey: key as? NSObject)
wrapper?.closure()
}
}
}

Use closure instead selector argument for UIBarButtonItem BUT without using weak self

In order to use closure in argument of UIBarButtonItem I am using a subclass:
class ActionBarButtonItem: UIBarButtonItem {
private var actionHandler: (() -> Void)?
convenience init(title: String?, style: UIBarButtonItemStyle, actionHandler: (() -> Void)?) {
self.init(title: title, style: style, target: nil, action: #selector(barButtonItemPressed))
self.target = self
self.actionHandler = actionHandler
}
convenience init(image: UIImage?, style: UIBarButtonItemStyle, actionHandler: (() -> Void)?) {
self.init(image: image, style: style, target: nil, action: #selector(barButtonItemPressed))
self.target = self
self.actionHandler = actionHandler
}
#objc func barButtonItemPressed(sender: UIBarButtonItem) {
actionHandler?()
}
}
but now I need to weak [weak self] :
self.add(barButton: .menu, position: .left) { [weak self] in
guard let strongSelf = self else {return}
strongSelf.openMenu()
}
is there a way to still use closure as selector but not save the closure to avoid using weak self everywhere and you may forget it somewhere ?
In a word, no.
You have to save the closure if you're going to call it later. If you're saving a closure, and that closure refers to self, you should make self part of a capture list to avoid a retain cycle. That's what capture lists are for, and is the correct coding pattern for this situation.
Any time you refer to self in a closure you need to stop and think about retain cycles.

Pass on Argument in #selector Swift 2.2

I have a method:
func followUnfollow(followIcon: UIImageView, channelId: String) {
let followUnfollow = followIcon
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.followIconTapped(_:)))
followUnfollow.userInteractionEnabled = true
followUnfollow.addGestureRecognizer(tapGestureRecognizer)
}
And also I have a method:
func followIconTapped(sender: UITapGestureRecognizer) {
...
}
And it's work all right. But I need to pass on channelId to followIconTapped() method.
I try this:
func followUnfollow(followIcon: UIImageView, channelId: String) {
...
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.followIconTapped(_:channelId)))
...
}
and then I try catch it:
func followIconTapped(sender: UITapGestureRecognizer, channelId: String) {
...
}
xCode say that channelId will never used. Why?
When I build project I haven't any issue. But app is crash if I tap on followIcon.
Please, can you give me advice how to pass on channelId to followIconTapped()
creating a generic UITapGestureRecognizer instead use this:
class CustomTapGestureRecognizer: UITapGestureRecognizer {
var channelId: String?
}
also use this:
override func viewDidLoad() {
super.viewDidLoad()
let gestureRecognizer = CustomTapGestureRecognizer(target: self, action: #selector(tapped(_:))
gestureRecognizer.channelId = "Your string"
view1.addGestureRecognizer(gestureRecognizer)
}
func tapped(gestureRecognizer: CustomTapGestureRecognizer) {
if let channelId = gestureRecognizer.channelId {
//print
}
}
Try to set a tag for tapGestureRecognizer and define channelId depend on tag.
You cannot pass extra arguments to your method when you are using Selector. UITapGestureRecognizer can handle methods with signatures:
private dynamic func handleTap() { ... }
private dynamic func handleTapOn(gestureRecognizer: UIGestureRecognizer) { ... }
Selectors in swift aren't validate in other way then check syntactics. Thats why there is no error connected with method signature you pass to action method.
If you like use channelId in your handleTap method you have to keep your variable in some place. You can create property in your class and store there your variable to use in handleTap method.
class YourClass: BaseClass {
// MARK: - Properties
private var channellId: String? = nil
// MARK: - API
func followUnfollow(followIcon: UIImageView, channelId: String) {
self.channelId = channelId
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapOn(_:)))
}
// MARK: - Callbacks
private dynamic func handleTapOn(recognizer: UIGestureRecognizer) {
if let channelId = self.channelId {
// Do sth
}
}
}

Adding a closure as target to a UIButton

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")
}

UIGestureRecognizer with closure

I have as view, in which I want to perform a swipe right gesture. Unfortunately I receive the error
EXC_BAD_ACCESS. Does anybody know what's wrong here ? Please a look at the code below.
extension UIView {
func addGestureRecognizerWithAction(nizer: UIGestureRecognizer, action:() -> ()) {
class Invoker {
var action:() -> ()
init(action:() -> ()) {
self.action = action
}
func invokeTarget(nizer: UIGestureRecognizer) {
self.action()
println("Hi from invoker")
}
}
addGestureRecognizer(nizer)
nizer.addTarget(Invoker(action), action: "invokeTarget:")
}
}
class BugView: UIView {
override func awakeFromNib() {
super.awakeFromNib()
var swipeRight = UISwipeGestureRecognizer()
swipeRight.direction = UISwipeGestureRecognizerDirection.Right
self.addGestureRecognizerWithAction(swipeRight) {
println("Hi from the gesture closure")
}
}
}
For those who does not like associated objects and other tough stuff I have a small gesture recogniser that does not need any hustle. And it doesn't need to be stored somewhere. Works like a regular GR. Though it has some limitations - it's inherited from UITapGestureRecognizer and he knows only how to handle taps. But do we often need all the types there are? Personally me not.
final class BindableGestureRecognizer: UITapGestureRecognizer {
private var action: () -> Void
init(action: #escaping () -> Void) {
self.action = action
super.init(target: nil, action: nil)
self.addTarget(self, action: #selector(execute))
}
#objc private func execute() {
action()
}
}
Usage example:
let gestureRecognizer = BindableGestureRecognizer {
print("It's alive!")
}
self.view.addGestureRecognizer(gestureRecognizer)
In case some more meaningful lines are presented, don't forget about [weak self]. We don't want to create those undead buddies.
let colorGestureRecognizer = BindableGestureRecognizer { [weak self] in
self?.view.backgroundColor = .red
}
self.view.addGestureRecognizer(colorGestureRecognizer)
Seems handy for me to purify our view controller from those #objc one liners and to become a bit more.. reactive?
Here is imho the simpelst solution which doen't need subclassing, so it's quite generic.
But it needs associated objects to store the action:
extension UIGestureRecognizer {
typealias Action = ((UIGestureRecognizer) -> ())
private struct Keys {
static var actionKey = "ActionKey"
}
private var block: Action? {
set {
if let newValue = newValue {
// Computed properties get stored as associated objects
objc_setAssociatedObject(self, &Keys.actionKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
get {
let action = objc_getAssociatedObject(self, &Keys.actionKey) as? Action
return action
}
}
#objc func handleAction(recognizer: UIGestureRecognizer) {
block?(recognizer)
}
convenience public init(block: #escaping ((UIGestureRecognizer) -> ())) {
self.init()
self.block = block
self.addTarget(self, action: #selector(handleAction(recognizer:)))
}
}
This allows you to use following even with your own subclasses. Usage:
let tapRecognizer = UITapGestureRecognizer { recognizer in
print("Hallo")
}
myView.addGestureRecognizer(tapRecognizer)
Finally return true was right. In his answer above he pointed me in the right direction, by suggesting to make Invoker a subclass of NSObject.
But this was not the only mistake I had in my code. I figured out that when the swipe occurred, the Invoker object, which was meant to handle the event, had already disappeared from memory. I think the reference to it was not captured by the closure as it should have been. Now I figured out another way of implementing this feature which I would like to share with you:
class UIGestureRecognizerWithClosure: NSObject { // must subclass NSObject otherwise error: "class does not implement methodSignatureForSelector: -- " !!
var closure:() -> ()
init(view:UIView, nizer: UIGestureRecognizer, closure:() -> ()) {
self.closure = closure
super.init()
view.addGestureRecognizer(nizer)
nizer.addTarget(self, action: "invokeTarget:")
}
func invokeTarget(nizer: UIGestureRecognizer) {
self.closure()
}
}
And this snippet shows how you can use the code:
var swipeRight = UISwipeGestureRecognizer()
swipeRight.direction = UISwipeGestureRecognizerDirection.Right
// swipeWrapper has to be defined as a property: var swipeWrapper:UIGestureRecognizerWithClosure?
// -> this is needed in order to keep the object alive till the swipe event occurs
swipeWrapper = UIGestureRecognizerWithClosure(view: self, nizer:swipeRight) {
println("Hi from the gesture closure")
}
Here is a Swift 4.2 method, based on #user1755189 's answer but which I think is a bit more simple to call. First add the following classes:
class UITapGestureRecognizerWithClosure: UITapGestureRecognizer {
private var invokeTarget:UIGestureRecognizerInvokeTarget
init(closure:#escaping (UIGestureRecognizer) -> ()) {
// we need to make a separate class instance to pass
// to super.init because self is not available yet
self.invokeTarget = UIGestureRecognizerInvokeTarget(closure: closure)
super.init(target: invokeTarget, action: #selector(invokeTarget.invoke(fromTarget:)))
}
}
// this class defines an object with a known selector
// that we can use to wrap our closure
class UIGestureRecognizerInvokeTarget: NSObject {
private var closure:(UIGestureRecognizer) -> ()
init(closure:#escaping (UIGestureRecognizer) -> ()) {
self.closure = closure
super.init()
}
#objc public func invoke(fromTarget gestureRecognizer: UIGestureRecognizer) {
self.closure(gestureRecognizer)
}
}
Then you can add a UITapGestureRecognizer like this:
addGestureRecognizer(UITapGestureRecognizerWithClosure() {
// do something here
// use $0 to reference the target UIGestureRecognizer
})
Dude this is my ClosureActionsHandler for UIGestures.
I have extended this for UIView, so you can implement on all UIComponents.
And this is the Git-link.
Happy Coding awesome geeks.
import Foundation
import UIKit
private var AssociatedObjectHandle: UInt8 = 25
public enum closureActions : Int{
case none = 0
case tap = 1
case swipe_left = 2
case swipe_right = 3
case swipe_down = 4
case swipe_up = 5
}
public struct closure {
typealias emptyCallback = ()->()
static var actionDict = [Int:[closureActions : emptyCallback]]()
static var btnActionDict = [Int:[String: emptyCallback]]()
}
public extension UIView{
var closureId:Int{
get {
let value = objc_getAssociatedObject(self, &AssociatedObjectHandle) as? Int ?? Int()
return value
}
set {
objc_setAssociatedObject(self, &AssociatedObjectHandle, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
public func actionHandleBlocks(_ type : closureActions = .none,action:(() -> Void)? = nil) {
if type == .none{
return
}
var actionDict : [closureActions : closure.emptyCallback]
if self.closureId == Int(){
self.closureId = closure.actionDict.count + 1
closure.actionDict[self.closureId] = [:]
}
if action != nil {
actionDict = closure.actionDict[self.closureId]!
actionDict[type] = action
closure.actionDict[self.closureId] = actionDict
} else {
let valueForId = closure.actionDict[self.closureId]
if let exe = valueForId![type]{
exe()
}
}
}
#objc public func triggerTapActionHandleBlocks() {
self.actionHandleBlocks(.tap)
}
#objc public func triggerSwipeLeftActionHandleBlocks() {
self.actionHandleBlocks(.swipe_left)
}
#objc public func triggerSwipeRightActionHandleBlocks() {
self.actionHandleBlocks(.swipe_right)
}
#objc public func triggerSwipeUpActionHandleBlocks() {
self.actionHandleBlocks(.swipe_up)
}
#objc public func triggerSwipeDownActionHandleBlocks() {
self.actionHandleBlocks(.swipe_down)
}
public func addTap(Action action:#escaping() -> Void){
self.actionHandleBlocks(.tap,action:action)
let gesture = UITapGestureRecognizer()
gesture.addTarget(self, action: #selector(triggerTapActionHandleBlocks))
self.isUserInteractionEnabled = true
self.addGestureRecognizer(gesture)
}
public func addAction(for type: closureActions ,Action action:#escaping() -> Void){
self.isUserInteractionEnabled = true
self.actionHandleBlocks(type,action:action)
switch type{
case .none:
return
case .tap:
let gesture = UITapGestureRecognizer()
gesture.addTarget(self, action: #selector(triggerTapActionHandleBlocks))
self.isUserInteractionEnabled = true
self.addGestureRecognizer(gesture)
case .swipe_left:
let gesture = UISwipeGestureRecognizer()
gesture.direction = UISwipeGestureRecognizerDirection.left
gesture.addTarget(self, action: #selector(triggerSwipeLeftActionHandleBlocks))
self.isUserInteractionEnabled = true
self.addGestureRecognizer(gesture)
case .swipe_right:
let gesture = UISwipeGestureRecognizer()
gesture.direction = UISwipeGestureRecognizerDirection.right
gesture.addTarget(self, action: #selector(triggerSwipeRightActionHandleBlocks))
self.isUserInteractionEnabled = true
self.addGestureRecognizer(gesture)
case .swipe_up:
let gesture = UISwipeGestureRecognizer()
gesture.direction = UISwipeGestureRecognizerDirection.up
gesture.addTarget(self, action: #selector(triggerSwipeUpActionHandleBlocks))
self.isUserInteractionEnabled = true
self.addGestureRecognizer(gesture)
case .swipe_down:
let gesture = UISwipeGestureRecognizer()
gesture.direction = UISwipeGestureRecognizerDirection.down
gesture.addTarget(self, action: #selector(triggerSwipeDownActionHandleBlocks))
self.isUserInteractionEnabled = true
self.addGestureRecognizer(gesture)
}
}
}

Resources