0

I have such code a little modified from code of Eric Armstrong

Adding a closure as target to a UIButton

But there is the problem with both codes. Those from Eric does remove all target-actions on

func removeTarget(for controlEvent: UIControl.Event = .touchUpInside)

And modified code on the other hand do not remove target-actions at all. Of course it is caused by if condition, but it also means that there are no targets stored properly in Storable property.

extension UIControl: ExtensionPropertyStorable {

    class Property: PropertyProvider {
        static var property = NSMutableDictionary()

        static func makeProperty() -> NSMutableDictionary? {
            return NSMutableDictionary()
        }
    }

    func addTarget(for controlEvent: UIControl.Event = .touchUpInside, target: @escaping (_ sender: Any) ->()) {
        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)
        if let target = property[key] as? Target {
            removeTarget(target, action: target.action, for: controlEvent)
            property[key] = nil
        }

    }
}


// Wrapper class for the selector
class Target {

    private let t: (_ sender: Any) -> ()
    init(target t: @escaping (_ sender: Any) -> ()) { self.t = t }
    @objc private func s(_ sender: Any) { t(sender) }

    public var action: Selector {
        return #selector(s(_:))
    }
}

// Protocols with associatedtypes so we can hide the objc_ code
protocol PropertyProvider {
    associatedtype PropertyType: Any

    static var property: PropertyType { get set }
    static func makeProperty() -> PropertyType?
}

extension PropertyProvider {
    static func makeProperty() -> PropertyType? {
        return nil
    }
}

protocol ExtensionPropertyStorable: class {
    associatedtype Property: PropertyProvider
}

// Extension to make the property default and available
extension ExtensionPropertyStorable {

    typealias Storable = Property.PropertyType

    var property: Storable {
        get {
            let key = String(describing: type(of: Storable.self))

            guard let obj = objc_getAssociatedObject(self, key) as? Storable else {

                if let property = Property.makeProperty() {
                    objc_setAssociatedObject(self, key, property, .OBJC_ASSOCIATION_RETAIN)
                }

                return objc_getAssociatedObject(self, key) as? Storable ?? Property.property
            }

            return obj
        }
        set {
            let key = String(describing: type(of: Storable.self))
            return objc_setAssociatedObject(self, key, newValue, .OBJC_ASSOCIATION_RETAIN) }
    }
}

My aim is to precisely register target-actions with closures and remove them without removing all other target-actions added to given UITextField via #selector. Now I can have removed ALL or NONE of target-actions while using this approach for closure-style target actions.

UPDATE

Based on Eric Armstrong answer i have implemented my version. But what I have experienced in version proposed by Eric was that when adding target actions to TextField on TableView list while cells appear and then removing this target actions from Text Fields while cells diseappear the previous code seems to remove all target actions on removeTarget(for:) exection. So when in other place in code like UITableViewCell I have added additional target action on totaly different target (UITableViewCell object, not this custom Target() objects) while cells was disappearing and then again appearing on screen and removeTarget(for) was executed then this other (external as I call them target actions) also was removed and never called again.

I consider that some problem was usage of [String: Target] dictionary which is value type and it was used in case of property getter in objc_getAssociatedObject where there was

objc_getAssociatedObject(self, key) as? Storable ?? Property.property

So as I understand it then there wasn't objc object for given key and Storable was nil and nil-coalescing operator was called and static value type Property.property return aka [String : Dictionary]

So it was returned by copy and Target object was stored in this copied object which wasn't permanently stored and accessed in removeTarget(for:) always as nil. So nil was passed to UIControl.removetTarget() and all target actions was always cleared!.

I have tried simple replacing [String: Target] Swift dictionary with NSMutableDictionary which is a reference type so I assume it can be stored. But this simple replacement for static variable and just returning it via nil-coalesing operator caused as I assume that there as only one such storage for Target objects and then while scrolling Table View each removeForTarget() has somehow remove all target actions from all UITextFields not only from current.

I also consider usage of String(describing: type(of: Storable.self)) as being wrong as it will be always the same for given Storable type.

Michał Ziobro
  • 10,759
  • 11
  • 88
  • 143

1 Answers1

1

Ok, I think I finally solved this issue

The main problem was usage of AssociatedKey! it needs to be done like below

https://stackoverflow.com/a/48731142/4415642

So I ended up with such code:

import UIKit

/**
 * Swift 4.2 for UIControl and UIGestureRecognizer,
 * and and remove targets through swift extension
 * stored property paradigm.
 * https://stackoverflow.com/a/52796515/4415642
 **/

extension UIControl: ExtensionPropertyStorable {

    class Property: PropertyProvider {
        static var property = NSMutableDictionary()

        static func makeProperty() -> NSMutableDictionary? {
            return NSMutableDictionary()
        }
    }

    func addTarget(for controlEvent: UIControl.Event = .touchUpInside, target: @escaping (_ sender: Any) ->()) {
        let key = String(describing: controlEvent)
        let target = Target(target: target)

        addTarget(target, action: target.action, for: controlEvent)
        property[key] = target

        print("ADDED \(ObjectIdentifier(target)), \(target.action)")
    }

    func removeTarget(for controlEvent: UIControl.Event = .touchUpInside) {
        let key = String(describing: controlEvent)

        if let target = property[key] as? Target {
            print("REMOVE \(ObjectIdentifier(target)), \(target.action)")
            removeTarget(target, action: target.action, for: controlEvent)
            property[key] = nil

        }

    }
}

extension UIGestureRecognizer: ExtensionPropertyStorable {

    class Property: PropertyProvider {
        static var property: Target?
    }

    func addTarget(target: @escaping (Any) -> ()) {
        let target = Target(target: target)
        addTarget(target, action: target.action)
        property = target
    }

    func removeTarget() {
        let target = property
        removeTarget(target, action: target?.action)
        property = nil
    }
}

// Wrapper class for the selector
class Target {

    private let t: (_ sender: Any) -> ()
    init(target t: @escaping (_ sender: Any) -> ()) { self.t = t }
    @objc private func s(_ sender: Any) { t(sender) }

    public var action: Selector {
        return #selector(s(_:))
    }

    deinit {
        print("Deinit target: \(ObjectIdentifier(self))")
    }
}

// Protocols with associatedtypes so we can hide the objc_ code
protocol PropertyProvider {
    associatedtype PropertyType: Any

    static var property: PropertyType { get set }
    static func makeProperty() -> PropertyType?
}

extension PropertyProvider {
    static func makeProperty() -> PropertyType? {
        return nil
    }
}

protocol ExtensionPropertyStorable: class {
    associatedtype Property: PropertyProvider
}

// Extension to make the property default and available
extension ExtensionPropertyStorable {

    typealias Storable = Property.PropertyType

    var property: Storable {
        get {
            guard let obj = objc_getAssociatedObject(self, &AssociatedKeys.property) as? Storable else {

                if let property = Property.makeProperty() {
                    objc_setAssociatedObject(self, &AssociatedKeys.property, property, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
                }

                return objc_getAssociatedObject(self, &AssociatedKeys.property) as? Storable ?? Property.property
            }

            return obj
        }
        set {
            return objc_setAssociatedObject(self, &AssociatedKeys.property, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
    }
}

private struct AssociatedKeys {
    static var property = "AssociatedKeys.property"
}
Michał Ziobro
  • 10,759
  • 11
  • 88
  • 143
  • Important part is this &AssociatedKeys.property. It can even be used as in the code of Eric with [String: Target] Storable and just simple nil-coalescing operator after objc_getAssociated... – Michał Ziobro Oct 10 '19 at 19:53