Reputation: 120324
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
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
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