How to remove passed closure from UIViewController - ios

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

Related

My custom RxDelegateProxy immediately disposing

I have some third party library that has delegate methods. But I like using RX so I should create RxDelegateProxy to receive delegate's callbacks via RX.
Here my custom DelegateProxy class:
extension Reactive where Base:GalleryController {
var selectedImages:Observable<(GalleryController, [Image])> {
let proxy = RxGalleryDelegateProxy.createProxy(for: self.base)
return proxy.imageSubject.asObservable()
}
}
private class RxGalleryDelegateProxy: DelegateProxy<GalleryController, GalleryControllerDelegate>, DelegateProxyType, GalleryControllerDelegate {
private var _imageSubject: PublishSubject<(GalleryController, [Image])>?
public weak fileprivate(set) var galleryController: GalleryController?
internal var imageSubject: PublishSubject<(GalleryController, [Image])> {
if let subject = _imageSubject {
return subject
}
let subject = PublishSubject<(GalleryController, [Image])>()
_imageSubject = subject
return subject
}
static func currentDelegate(for object: GalleryController) -> GalleryControllerDelegate? {
return object.delegate
}
static func setCurrentDelegate(_ delegate: GalleryControllerDelegate?, to object: GalleryController) {
object.delegate = delegate
}
static func registerKnownImplementations() {
self.register { RxGalleryDelegateProxy(parentObject: $0) }
}
private init(parentObject: GalleryController) {
self.galleryController = castOrFatalError(parentObject)
super.init(parentObject: parentObject, delegateProxy: RxGalleryDelegateProxy.self)
}
func galleryController(_ controller: GalleryController, didSelectImages images: [Image]) {
if let subject = _imageSubject {
subject.on(.next((controller, images)))
}
self._setForwardToDelegate(galleryController(controller, didSelectImages: images), retainDelegate: true)
}
deinit {
_imageSubject?.on(.completed)
}
}
In my UIViewController I subscribe with:
final class PhotoLibraryViewController: UIViewController {
private let _bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let gallery = GalleryController()
present(gallery, animated: true, completion: nil)
gallery.rx.selectedImages
.debug("--------")
.observeOn(MainScheduler.instance)
.subscribe(onNext: { (controller, images) in
print("\(images)")
}, onError: { (error) in
DDLogError("Error:\n\(error)")
})
.disposed(by: _bag)
}
}
But all I get in console output via .debug("--------") is:
2018-01-09 20:05:14.814: -------- -> subscribed
2018-01-09 20:05:14.817: -------- -> Event completed
2018-01-09 20:05:14.817: -------- -> isDisposed
So my object is immediately disposing just after creation. What did I do wrong?
Your gallery object is created inside viewDidLoad(), so once viewDidLoad() is completed, the deinit method from gallery is called and it is disposed.
Move your gallery object out of the viewDidLoad():
final class PhotoLibraryViewController: UIViewController {
private let _bag = DisposeBag()
let gallery = GalleryController()
override func viewDidLoad() {
super.viewDidLoad()
present(gallery, animated: true, completion: nil)
gallery.rx.selectedImages
.debug("--------")
.observeOn(MainScheduler.instance)
.subscribe(onNext: { (controller, images) in
print("\(images)")
}, onError: { (error) in
DDLogError("Error:\n\(error)")
})
.disposed(by: _bag)
}
}

Calling functions in the serial queue in Swift

I've got a function which is called by observing the NotificationCenter:
NotificationCenter.default.addObserver(self, selector: #selector(observedPosition(_: ), name: "calculatePosition", object: nil)
and then the function:
#objc func observedPosition(_ notification: NSNotification) {
if let data = notification.object as? Int {
self.sendPosition(from: data)
}
As this function can be called multiple times in very short time periods I would like to add it to the queue and call sendPosition() only once the previous sendPosition() has finished.
I tried something like this but dunno if it's a correct approach:
#objc func observedPosition(_ notification: NSNotification) {
let queue = DispatchQueue(label: queueLabel, attributes: [], targer: nil)
queue.sync {
if let data = notification.object as? Int {
self.sendPosition(from: data)
}
}
}
Details
Xcode Version 10.3 (10G8), Swift 5
Key features
Implemented own queue which will execute functions one by one
All operations (closures) stored in array
Thread safety
Solution
// MARK: - StackableOperationsQueue performs functions from the stack one by one (serial performing)
class StackableOperationsQueue {
private let semaphore = DispatchSemaphore(value: 1)
private lazy var operations = [QueueOperation]()
private lazy var isExecuting = false
fileprivate func _append(operation: QueueOperation) {
semaphore.wait()
operations.append(operation)
semaphore.signal()
execute()
}
func append(operation: QueueOperation) { _append(operation: operation) }
private func execute() {
semaphore.wait()
guard !operations.isEmpty, !isExecuting else { semaphore.signal(); return }
let operation = operations.removeFirst()
isExecuting = true
semaphore.signal()
operation.perform()
semaphore.wait()
isExecuting = false
semaphore.signal()
execute()
}
}
// MARK: - StackableOperationsCuncurentQueue performs functions from the stack one by one (serial performing) but in cuncurent queue
class StackableOperationsCuncurentQueue: StackableOperationsQueue {
private var queue: DispatchQueue
init(queue: DispatchQueue) { self.queue = queue }
override func append(operation: QueueOperation) {
queue.async { [weak self] in self?._append(operation: operation) }
}
}
// MARK: QueueOperation interface
protocol QueueOperation: class {
var сlosure: (() -> Void)? { get }
var actualityCheckingClosure: (() -> Bool)? { get }
init (actualityCheckingClosure: (() -> Bool)?, serialClosure: (() -> Void)?)
func perform()
}
extension QueueOperation {
// MARK: - Can queue perform the operation `сlosure: (() -> Void)?` or not
var isActual: Bool {
guard let actualityCheckingClosure = self.actualityCheckingClosure,
self.сlosure != nil else { return false }
return actualityCheckingClosure()
}
func perform() { if isActual { сlosure?() } }
init (actualIifNotNill object: AnyObject?, serialClosure: (() -> Void)?) {
self.init(actualityCheckingClosure: { return object != nil }, serialClosure: serialClosure)
}
}
class SerialQueueOperation: QueueOperation {
let сlosure: (() -> Void)?
let actualityCheckingClosure: (() -> Bool)?
required init (actualityCheckingClosure: (() -> Bool)?, serialClosure: (() -> Void)?) {
self.actualityCheckingClosure = actualityCheckingClosure
self.сlosure = serialClosure
}
}
Usage example
class TEST {
private lazy var stackableOperationsQueue: StackableOperationsCuncurentQueue = {
let queue = DispatchQueue(label: "custom_queue", qos: .background,
attributes: [.concurrent], autoreleaseFrequency: .workItem, target: nil)
return StackableOperationsCuncurentQueue(queue: queue)
}()
private func addOperationToQueue(closure: (() -> Void)?) {
let operation = SerialQueueOperation(actualIifNotNill: self) { closure?() }
stackableOperationsQueue.append(operation: operation)
print("!!!! Function added ")
}
private func simpleFunc(index: Int) {
print("Func \(index) started")
sleep(UInt32(index+1));
print("Func \(index) ended")
}
func run() {
(0...3).forEach { index in
addOperationToQueue { [weak self] in self?.simpleFunc(index: index) }
}
}
}
let test = TEST()
test.run()
Usage example results
// qos: .background
!!!! Function added
!!!! Function added
!!!! Function added
!!!! Function added
Func 0 started
Func 0 ended
Func 1 started
Func 1 ended
Func 2 started
Func 2 ended
Func 3 started
Func 3 ended
// qos: .userInitiated
!!!! Function added
Func 0 started
!!!! Function added
!!!! Function added
!!!! Function added
Func 0 ended
Func 1 started
Func 1 ended
Func 2 started
Func 2 ended
Func 3 started
Func 3 ended
That is correct, so long as you ensure the same queue is being used to schedule all sendPosition method calls. For example, if this queue were a local variable, it would be of no use at all.

Stripe - retrieveCustomer callback infinite loading

I had implement the Stripe to my project.I'm using an extension of default STPPaymentMethodsViewController like this:
class PaymentMethodVC: STPPaymentMethodsViewController {
convenience init()
{
let theme = STPTheme()
theme.primaryBackgroundColor = UIColor.pintHubDarkBrown
theme.secondaryBackgroundColor = UIColor.pintHubHeaderColor
theme.accentColor = UIColor.white
theme.primaryForegroundColor = UIColor.pintHubOrange
theme.secondaryForegroundColor = UIColor.pintHubOrange
theme.font = UIFont.mainRegular()
let paymentdelegate = PaymentMethodVCDelegate()
let paymentConfig = STPPaymentConfiguration.shared()
paymentConfig.publishableKey = "stripePublickToken"
let apiAdapter = PaymentApiAdapter()
self.init(configuration: paymentConfig, theme: theme, apiAdapter: apiAdapter, delegate: paymentdelegate)
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
}
PaymentMethodVCDelegate is an object that implements STPPaymentMethodsViewControllerDelegate that methods are never called and
PaymentApiAdapter is other object that implements STPBackendAPIAdapter protocol which methods are:
public func retrieveCustomer(_ completion: #escaping Stripe.STPCustomerCompletionBlock)
public func attachSource(toCustomer source: STPSource, completion: #escaping Stripe.STPErrorBlock)
public func selectDefaultCustomerSource(_ source: STPSource, completion: #escaping Stripe.STPErrorBlock)
everything works fine expect when i want to return an error to the callback method func retrieveCustomer(_ completion: #escaping Stripe.STPCustomerCompletionBlock) that is a method of the STPBackendAPIAdapter protocol more details here.
this is my code:
func retrieveCustomer(_ completion: #escaping (STPCustomer?, Error?) -> Swift.Void)
{
stripeEndpoint.getStripeCustomer(for: "myStrypeCustomerId") { (status, JSON) in
if !status.success()
{
let userInfo = [NSLocalizedDescriptionKey:status.error,
NSLocalizedFailureReasonErrorKey: status.code,
NSLocalizedRecoverySuggestionErrorKey: ""
] as [String : Any]
let error = NSError(domain: "MyDomain", code: Int(status.error) ?? 0, userInfo: userInfo)
completion(nil, error)
}
else
{
var customer:STPCustomer? = nil
if let jsonData = JSON
{
let deserializer = STPCustomerDeserializer(jsonResponse: jsonData)
customer = deserializer.customer!
}
completion(customer, nil)
}
}
and when i receive an error the screen displays and infinite loading indicator.
and if i call completion(nil, nil) the loading disappear but i when i press cancel the ViewController don't pop from stack

Swift closure in protocol extension

I want to Decorate UIViewController with the ability to adjust it's interface when setInteractionEnabled method is called from another class (ex. Network State Manager). All changes (if any) should be provided in the concrete controller by overriding onInteractionChanged. Here is my code:
import Foundation
typealias InteractionClosure = ((enabled: Bool) -> Void)
protocol Interaction: class {
var onInteractionChanged: InteractionClosure? { get set }
func setInteractionEnabled(enabled: Bool)
}
extension Interaction where Self: UIViewController {
// Default: Do nothing
// Throws: - Extensions may not contain stored properties
var onInteractionChanged: InteractionClosure? = nil
func setInteractionEnabled(enabled: Bool) {
onInteractionChanged?(enabled: enabled)
}
}
extension UIViewController : Interaction {}
How to add default implementation for onInteractionChanged?
Answering my own question is something usually I don't do, but here is my solution:
typealias InteractionClosure = (enabled: Bool) -> Void
protocol Interaction: class {
func addOnInteractionChanged(closure: InteractionClosure)
func setInteractionEnabled(enabled: Bool)
}
extension Interaction where Self: UIViewController {
func addOnInteractionChanged(closure: InteractionClosure) {
onInteractionChanged = closure
}
func setInteractionEnabled(enabled: Bool) {
onInteractionChanged?(enabled: enabled)
}
// MARK: - Private
private var onInteractionChanged: InteractionClosure? {
get {
let wrapper =
objc_getAssociatedObject(self, &icAssociationKey) as? ClosureWrapper
return wrapper?.closure
}
set(newValue) {
objc_setAssociatedObject(self,
&icAssociationKey,
ClosureWrapper(newValue),
.OBJC_ASSOCIATION_RETAIN)
}
}
}
extension UIViewController : Interaction {}
// Helpers
private var icAssociationKey: UInt8 = 0
private class ClosureWrapper {
var closure: InteractionClosure?
init(_ closure: InteractionClosure?) {
self.closure = closure
}
}
Client class:
class LoginViewController: UIViewController {
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
self.setup()
}
// MARK: - Private
private func setup() {
// ...
addOnInteractionChanged { [unowned self] (enabled) in
self.signInButton.enabled = enabled
self.activityIndicatorView.hidden = !enabled
}
}
}
In manager class:
visibleViewController?.setInteractionEnabled(true)
If you would like property to have only { get } ability, you can use:
protocol TestProtocol {
var testClosure: ((_ parameter: Bool) -> Void)? { get }
}
extension TestProtocol {
var testClosure: ((_ parameter: Bool) -> Void)? {
return { parameter in
print(parameter)
}
}
}

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

Resources