How to Set Associated Objects in Swift

Is there a way to set associated objects in Swift?

Here is a simple but complete example derived from jckarter's answer.

It shows how to add a new property to an existing class. It does it by defining a computed property in an extension block. The computed property is stored as an associated object:

import ObjectiveC

// Declare a global var to produce a unique address as the assoc object handle
private var AssociatedObjectHandle: UInt8 = 0

extension MyClass {
var stringProperty:String {
get {
return objc_getAssociatedObject(self, &AssociatedObjectHandle) as! String
}
set {
objc_setAssociatedObject(self, &AssociatedObjectHandle, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}

EDIT:

If you need to support getting the value of an uninitialized property and to avoid getting the error unexpectedly found nil while unwrapping an Optional value, you can modify the getter like this:

    get {
return objc_getAssociatedObject(self, &AssociatedObjectHandle) as? String ?? ""
}

Associated objects and Swift arrays

The NSMutableArray here is a reference type. The consequence of this is that the moment you hand somebody a reference to your view object's queue, your view object loses all control of it. The recipient of the reference can rearrange items, delete all items, etc., and your view wouldn't know about it until next time it tried reading it.

Generally speaking, this sort of implicit data sharing has been found to be a bad idea, because it breaks encapsulation, and complicates systems, because you can no longer reason locally about code, because there's always the threat that another thread has a reference to your aliased object, and is changing it under your feet.

This model isn't compatible with Swift's value-types, such as Array. If queue were an Array<T>, every person who accessed it would get back their own value. Semantically, these copies are all completely isolated from each other, and there's no way in which a mutation done through one reference can cause an effect that's observable via the other reference.

If you wanted to faithfully preserve the reference semantics of your current code, you would need a mechanism to listen for changes to the NSMutableArray, and update every individual Array<T> that was derived from it. This isn't really practical, nor is it a good idea.

Here's what I would do:

  1. Make the interface more explicitly transactional. More than likely, you can hide the queue entirely. Make it private, and have your view expose public methods like push and pop.

    import Foundation

    class C: NSObject {
    private enum AssociatedObjectKeys {
    static var queue: Int8 = 0
    }

    private var queue: Array<Int> {
    get {
    guard let existingValue = objc_getAssociatedObject(self, &AssociatedObjectKeys.queue) else {
    self.queue = []
    return []
    }
    guard let existingArray = existingValue as? Array<Int> else {
    fatalError("Found an associated object that had the wrong type!")
    }
    return existingArray
    }
    set {
    objc_setAssociatedObject(self, &AssociatedObjectKeys.queue, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    }
    }

    public func printQueueForDebugging() {
    print(self.queue)
    }

    public func push(_ newValue: Int) {
    self.queue.append(newValue)
    }

    public func pushAll<S: Sequence>(_ newValues: S) where S.Element == Int {
    self.queue.append(contentsOf: newValues)
    }

    public func pop() -> Int? {
    if self.queue.isEmpty { return nil }
    return self.queue.removeFirst()
    }
    }

    let c = C()
    c.printQueueForDebugging()
    c.pushAll(1...3)
    c.printQueueForDebugging()
    c.push(4)
    c.printQueueForDebugging()
    print(c.pop() as Any)
    print(c.pop() as Any)
    print(c.pop() as Any)
    print(c.pop() as Any)
    print(c.pop() as Any)
  2. I would use a type-safe Swift wrapper to tuck away associated object sets/gets, which can do type checking and casting automatically behind the scenes. The new property wrapper feature is perfect for this.

  3. Extract out a separate Queue<T> data structure. Array<T> by itself doesn't make for a good queue, because removing at the start has O(n) time complexity. There are lots of data structure libraries out there, I would use one of their queues instead.

Associated objects in Swift, does global key actually produce specific instances?

Objective-C's concept of "associated objects" lets you connect one "target" object instance with one "source" object instance. It's a unidirectional association where only the source knows the target:

Associated Objects with source and target instances

objc_setAssociatedObject(source, key, target, ...)

The association uses a key to be able to connect and distinguish any number of associated objects with one source. The keys must obviously be different—but just for one source instance.

Because you have to provide both the key and the source instance to retrieve the associated object, it's not necessary to use really unique keys. The implementation of objc_***AssociatedObject can form a process-unique key by combining the instance pointer and the key.

So for your example, yes, both aoAA and aoBB will return individual values per each UIViewController instance, even though keyA and keyB are global.

To be absolutely clear, you do need different keys for each associated object in a given extension. So, aoAA and aoBB each need their own key, as shown in the example code (to wit, the pointers to keyA and keyB). But as is asked in the question it's correct have only one key for each associated object, no matter how many conforming classes the extension is used in.

Can I add associated object to Swift Struct?

Based on @Hamish's comment, I created the following solution to workaround the issue.
Preconditions:

  • Have a framework which proposes prefilled objects, the app works on these objects and the framework should know which of the properties are modified during the processing of this object later.

  • Not using looooong initializers to setup all properties of MyObject is a design decision.

In my example, the usage of the myObject is a dummy and shows what happens in the framework and what happens in the app.

// protocol is used, as we could handle more modifiable structs/classes in a common way
protocol PropertyState {
var isModified: Bool {get set}
}

internal struct ModifiableString : PropertyState {
var string: String
var isModified: Bool
}

class MyObject: PropertyState {
internal var _name = ModifiableString(string: "", isModified: false)
public var name: String {
get {
return _name.string
}
set(value) {
_name.string = value
_name.isModified = true
}
}

// + N similar properties (they can be other types than String, by implementing other structs, like ModifiableBool)

var isModified: Bool {
get {
return _name.isModified // || _myAnotherProperty.isModified
}
set(value) {
_name.isModified = value
// _myAnotherProperty.isModified = value
}
}
}

// internal filling of the object inside of the framework
let myObject = MyObject()
myObject.name = "originalValue"
print(myObject.isModified) // true
// filling the object with values ended, so we can set the state
myObject.isModified = false
print(myObject.isModified) // false

// the app can work with the object
// let myObject = Framework.getObject()
myObject.name = "modifiedValue"

// now the framework should now which properties are modified
print(myObject._name.isModified) // true

Swift objc_getAssociatedObject always nil

You cannot add associated objects to a Swift Array (or any Swift value type).
objc_setAssociatedObject() and objc_getAssociatedObject() are from the
Objective-C runtime, they expect an instance of NSObject as first
argument.

Your code compiles and runs only because any Swift value is automatically
bridged to an object if necessary.

  • When you call objc_setAssociatedObject(self, ...) then self
    is bridged to a (temporary) instance of NSArray, and the association
    is made on that object.

  • Later, when objc_getAssociatedObject(self, ...) is called,
    another (temporary) instance of NSArray is created, and that
    has no associated object.

That's why you get nil as the result.

set associated objects for literal value in Swift

NSData is part of a class cluster, so your custom init method is not necessarily called,
e.g.

let d = NSMutableData()

does not use your init method. The next problem is that your init method calls
itself recursively, therefore

let d = NSData()

crashes with a stack overflow. Note also that the Objective-C code relies on
undefined behaviour, because it replaces a method in a class extension.

So better remove your custom initialization, and change the getter to
return a default value if the associated object has not been set.
This can easily be achieved with an optional cast (as? Int) and the
nil-coalescing operator (??):

extension NSData {

var position: Int {
get {
return objc_getAssociatedObject(self, &xoAssociationKey) as? Int ?? 0
}
set {
objc_setAssociatedObject(self, &xoAssociationKey, newValue, objc_AssociationPolicy(OBJC_ASSOCIATION_RETAIN_NONATOMIC))
}
}
}

cant set stored property with associated object in extention

it only works right for classes (not structs) and on top only for those that are objc compatible.

for a workaround, see also: https://wezzard.com/2015/10/09/associated-object-and-swift-struct/

How to use associated objects with enums?

Change your code to this

extension UIViewController {

private struct AssociatedKeys {
static var handle = "handle"
static var enumContext = "enumContext"
}

enum CustomStringEnum: String {
case One = "One"
case Two = "Two"
case Three = "Three"
}

var customEnum: CustomStringEnum {
get {
let rawvalue = objc_getAssociatedObject(self, &AssociatedKeys.enumContext)
if rawvalue == nil{
return .One
}else{
return CustomStringEnum(rawValue: rawvalue as! String)!;
}
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.enumContext, newValue.rawValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}

var descriptiveName: String {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.handle) as! String
}

set {
objc_setAssociatedObject(
self,
&AssociatedKeys.handle,
newValue as NSString?,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
}
}
}

Then it will work

Binding Protocols with Associated Objects - Protocol Oriented Programming with Swift

I think in this case you need to use a concrete type to alias FactoryBuilder instead of BindedBuilder, as protocols do not conform to themselves.

This code effectively compiles, would something like that match your requirements?

class ConcreteFactory: BindedFactory {
typealias FactoryBuilder = ConcreteObject

var parameters: ConcreteParameters {
return ConcreteParameters(string: "Hello ")
}
}

Otherwise you can also try type erasing BindedBuilder and create AnyBindedBuilder, as suggested in the same link.



Related Topics



Leave a reply



Submit