Associated Object and Swift Struct

Since in Swift, Objective-C’s Associated Object API needs the type of utilizing property is convertible to be type of AnyObject, we cannot add Swift struct properties to a class extension in the normal way. But due to the advantage of value type and immutability, we sometimes hope that we could do this.

In fact, that’s not totally impossible. Though I haven’t read any line of Objective-C's source code about Associated Object, but I can guess that it must utilized Objective-C’s runtime feature (remind by where you should use a C string to access the associated object) and the convert-ability offered by NSValue and its subclasses which made anything in Objective-C can be presented with an object type. But since such a convert-ability is not aware of Swift struct, we cannot add associated Swift struct directly.

Firstly, I came up with an idea of using NSValue to encode and decode a Swift struct, because that each Objective-C class is indeed a C struct and Swift’s struct has the same memory layout to C struct. But what can be expected straightforwardly is that: It will turn out to be a complicated solution when you encountered a Swift struct with any String member. Why? Because Swift String indeed only stores a reference to its content and makes use of the copy-on-time principle to deal with content changes. When you tried to snapshot a String-member-contained Swift struct with NSValue, you actually just snapshotted the reference to the String member’s content.

Such as code below, by running it in a playground, you can find that, though you can correctly deal with an Int member, you can never get the right value of the String member in a Swift struct in this way.

import Cocoa

struct TabBarItem {  
    let index: Int
    let title: String
}

private var tabBarItemKey = "tabBarItem"

extension NSViewController {  
    var tabBarItem: TabBarItem? {
        get {
            if let rawTabBarItem = objc_getAssociatedObject(
                self,
                &tabBarItemKey
                )
                as? NSValue
            {
                var tabBarItem = TabBarItem(index: 0, title: "")
                rawTabBarItem.getValue(&tabBarItem)
                return tabBarItem
            }
            return nil
        }
        set {
            var mutableNewValue = newValue
            let rawTabBarItem = NSValue(
                bytes: &mutableNewValue,
                objCType: "@"
            )
            objc_setAssociatedObject(self, 
                &tabBarItemKey,
                rawTabBarItem,
                .OBJC_ASSOCIATION_RETAIN)
        }
    }
}

let aViewController = NSViewController(nibName: nil, bundle: nil)

aViewController?.tabBarItem = TabBarItem(index: 9, title: "Hehe")

print(aViewController?.tabBarItem?.title)  
print(aViewController?.tabBarItem?.index)  

The output is always:

Optional("")

Optional(9)

To make the previous solution perfect and stupid needs a very complicated work. But, there is another easy way. Talk is cheap, show you the code.

public final class Associated<T>: NSObject, NSCopying {  
    public typealias Type = T
    public let value: Type

    public init(_ value: Type) { self.value = value }

    public func copyWithZone(zone: NSZone) -> AnyObject {
        return self.dynamicType.init(value)
    }
}

extension Associated where T: NSCopying {  
    public func copyWithZone(zone: NSZone) -> AnyObject {
        return self.dynamicType.init(value.copyWithZone(zone) as! Type)
    }
}

struct NavigationItem {  
    let index: Int
    let title: String
}

private var navigationItemKey = "navigationItem"

extension NSViewController {  
    var navigationItem: NavigationItem? {
        get {
            return (objc_getAssociatedObject(self, &navigationItemKey)
                as? Associated<NavigationItem>)
                .map {$0.value}
        }
        set {
            objc_setAssociatedObject(self,
                &navigationItemKey,
                newValue.map
                    { Associated<NavigationItem>($0) },
                .OBJC_ASSOCIATION_RETAIN)
        }
    }
}

let anotherViewController = NSViewController(nibName: nil, bundle: nil)

let navigationItem = NavigationItem(index: 9, title: "Hehe")

anotherViewController?.navigationItem = navigationItem

print(anotherViewController?.navigationItem?.title)  
print(anotherViewController?.navigationItem?.index)  

And here is the output:

Optional("Hehe")

Optional(9)

As the code shows, you can wrap a Swift struct in a generic container which inherited from NSObject. Such a generic container perfectly solved different memory management model handling issue, type casting issue, which—NSValue—subclass-should-I-use issue and writing-too-many-code issue.

WeZZard

Independent iOS developer, World of Warcraft add-on developer. Interested in Computer Graphics and Machine Learning.

People's Republic of China https://github.com/WeZZard