hpique
hpique

Reputation: 120324

Associating Swift things with NSObject instances

I'd like to associate Swift things (generics, structs, tuples, anything Objective-C doesn't like) with NSObject instances via extensions.

How can we do that today? objc_setassociatedobject is of no use to deal with Swift features.

My first approach was to use a global dictionary with weak keys to store the associations. Something like:

struct WeakKey<T where T: NSObject> : Hashable {
    weak var object : T!

    init (object: T) {
        self.object = object
    }

    var hashValue: Int { return self.object.hashValue  }
}

func ==<T where T: NSObject>(lhs: WeakKey<T>, rhs: WeakKey<T>) -> Bool {
    return lhs.object == rhs.object
}

typealias ThingObjcDoesntLike = (Int, String)

var _associations : [WeakKey<NSObject>: ThingObjcDoesntLike] = [:]

extension NSObject {

    var associatedThing : ThingObjcDoesntLike! {
        get {
            let key = WeakKey(object: self)
            return _associations[key]
        }
        set(thing) {
            let key = WeakKey(object: self)
            _associations[key] = thing
        }
    }

}

let o = NSObject()
let t = (1, "Hello World")
o.associatedThing = t

Unfortunately this crashes with EXC_BAD_ACCESS in the _associations declaration. Removing the weak solves the crash, but would cause NSObject instances to be retained.

In order for this to be viable we should also set the association to nil when the NSObject is dealloc-ed. We can use an associated witness object for that, and override its dealloc to do the work. For simplicity's sake, I'm leaving that out from the code above.

Upvotes: 3

Views: 1388

Answers (2)

hpique
hpique

Reputation: 120324

@NickLockwood's answer should be the right answer. However, when using his approach I started to get strange memory exceptions on run time. It might have been another thing, but the problem disappeared when I tried another approach. Have to dig further.

Here's what I did in Playground form. I'm posting it because it appears to work and I find the code interesting, even if it ends up not being the best solution.

I'm using a global Swift Dictionary with pointers as weak keys. To clear the dictionary, I use a witness associated object that calls back the object with the association on deinit.

import Foundation
import ObjectiveC

var _associations : [COpaquePointer: Any] = [:]

@objc protocol HasAssociatedSwift : class {

    func clearSwiftAssociations()
}

var _DeinitWitnessKey: UInt8 = 0

class DeinitWitness : NSObject {

    weak var object : HasAssociatedSwift!

    init (object: HasAssociatedSwift) {
        self.object = object
    }

    deinit {
        object.clearSwiftAssociations()
    }

    class func addToObject(object : NSObject) {
        var witness = objc_getAssociatedObject(object, &_DeinitWitnessKey) as DeinitWitness?
        if (witness == nil) {
            witness = DeinitWitness(object: object)
            objc_setAssociatedObject(object, &_DeinitWitnessKey, witness, UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC))
        }
    }
}

extension NSObject : HasAssociatedSwift {

    var associatedThing : Any! {
        get {
            return _associations[self.opaquePointer]
        }
        set(thing) {
            DeinitWitness.addToObject(self)
            _associations[self.opaquePointer] = thing
        }
    }

    var opaquePointer : COpaquePointer {
        return Unmanaged<AnyObject>.passUnretained(self).toOpaque()
    }

    func clearSwiftAssociations() {
        _associations[self.opaquePointer] = nil
    }
}

let o = NSObject()
o.associatedThing = (1, "Hello")

Upvotes: 1

Nick Lockwood
Nick Lockwood

Reputation: 40995

How about something like this:

class ObjectWrapper : NSObject {
    let value: ThingObjcDoesntLike

    init(value: ThingObjcDoesntLike) {
       self.value = value
    }
}

extension NSObject {

    var associatedThing : ThingObjcDoesntLike! {
        get {
            let wrapper = objc_getAssociatedObject(self, someKey) as ObjectWrapper?
            return wrapper?.value
        }
        set(value) {
            let wrapper = ObjectWrapper(value: value)
            objc_setAssociatedObject(self, someKey, wrapper, UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC))
        }
    }    
}

Upvotes: 1

Related Questions