Associated values by keypath

Objective-C's associated values can be useful at times. One of the cases where I tend to reach for them is when encountering the limitation that I cannot declare stored properties on extensions, a capability that is particularly useful for accessing private cached information to avoid re-computation in computed properties.

Though obtaining a useful associated value key in this context is a challenge. What would be fantastic is if we were able to use key paths as associated value keys.

There isn't too much direct documentation about what constitutes a good associated value key or conversely, how key paths or metatypes can be represented reliably as pointer types. I'd love to put this experiment out here and source some feedback.

In particular, I am not a fan of the keyPath.hashValue, nor am I sure about whether it's sane/safe to bit-cast a metatype or perform the + operation on the result. (Probably not, TBH) Looking forward to any of you who have perhaps a more safe/reliable suggestion.

public struct AssociatedValue {
    public static func resolve<H: AnyObject, V>(of host: H, for keyPath: KeyPath<H, V>, init: () -> V) -> V {
        let key = unsafeBitCast(H.self, to: UnsafeRawPointer.self) + keyPath.hashValue
        if let value = objc_getAssociatedObject(host, key) as? V {
            return value
        }

        let value = `init`()
        objc_setAssociatedObject(host, key, value, .OBJC_ASSOCIATION_RETAIN)
        return value
    }
}

extension NSString {
    var assoc: String {
        AssociatedValue.resolve(of: self, for: \.assoc) {
            return (self as String) + "::quux::\(UUID())"
        }
    }
}

EDIT: I just remembered I have a newer version that is considerably simpler.

I use the following code to make it easier: (I have removed the comments and condensed the spacing to better fit in a post.)

public protocol AssociatedObject: AnyObject {
    static var key: UnsafeRawPointer { get }
    init()
}

public extension AssociatedObject {
    static var key: UnsafeRawPointer { UnsafeRawPointer(bitPattern: UInt(bitPattern: ObjectIdentifier(Self.self)))! }
}

public extension NSObject {
    func get<Object: AssociatedObject>(_ objectClass: Object.Type ) -> Object {
        var object = objc_getAssociatedObject(self, Object.key) as? Object
        if object == nil {
            object = objectClass.init()
            objc_setAssociatedObject(self, Object.key, object, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
        return object!
    }
}

Here is an example of how to use it:

// Here is how add a new property to this class using the above:
@objc class OriginalObject: NSObject { func doSomething() { print("Did something.") } }

// To support this protocol:
protocol SomeProtocol { var someState: Int {get set} }

extension OriginalObject: SomeProtocol {
    
    // Declare the associated object class:
    final class SomeProtocolStorage: AssociatedObject {
        var someState: Int = 0
    }
    
    // and satisfy the protocol requirements:
    public var someState: Int {
        get { get(SomeProtocolStorage.self).someState }
        set { get(SomeProtocolStorage.self).someState = newValue }
    }
}

// Demo:
let objects = [ OriginalObject(), OriginalObject(), OriginalObject() ]
for i in objects.indices { objects[i].someState = i }
for o in objects { print(o.someState) }

That's interesting!

Another somewhat similar approach with an internal type:

extension X {
    enum Keys: Int {
        var key: UnsafeRawPointer { .init(bitPattern: self.rawValue)! }
        case property = 1
    }

    var property: String? {
        get { objc_getAssociatedObject(self, Keys.property.key) as? String }
        set { objc_setAssociatedObject(self, Keys.property.key, newValue, .OBJC_ASSOCIATION_RETAIN) }
    }
}

Though I was hoping to avoid creating what is essentially a property duplicate purely for the sake of having a stable key pointer.

In this case, you can't be sure that the key is unique.

Also, if you want to associate a single object of a given type (not multiple properties of potentially value type) you can use the ObjectIdentifier trick to get a stable unique key for it and skip the rest. For example, if your associated object type is MyAttachedObject then:

func key(of objecdtType: AnyObject.Type) -> UnsafeRawPointer { UnsafeRawPointer(bitPattern: UInt(bitPattern: ObjectIdentifier(objecdtType)))! }

let key = key(of: MyAttachedObject.self)

// Use the above `key` to associate any `MyAttachedObject` object with any other objects.
1 Like

My expectation had been that the key passed to objc_getAssociatedObject is scoped to the metatype of the object passed. But I suppose the documentation doesn't really make that clear.