Is there a way to set associated objects in Swift?
Asked Answered
D

9

92

Coming from Objective-C you can call function objc_setAssociatedObject between 2 objects to have them maintain a reference, which can be handy if at runtime you don't want an object to be destroyed until its reference is removed also. Does Swift have anything similar to this?

Donative answered 10/6, 2014 at 4:48 Comment(4)
You can use objc_setAssociatedObject from Swift: developer.apple.com/library/prerelease/ios/documentation/Swift/…Empyrean
On this ten yr old question pls note it is now very simple, see the recent answer. No need for libraries, includes, objc, voodoo etc!Franklin
@Franklin – I don't think your answer is simpler than the highest voted answer.Aesthetics
JCS - the highest voted answer is nowadays wrong (the include is wrong, and it's completely wrong to not allow for the nil). Hope it helpsFranklin
J
147

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 ?? ""
    }
Joelynn answered 21/8, 2014 at 14:3 Comment(21)
Have you been able to set a function that conforms to a typealias in this fashion?Chalk
A function doesn't work for me, and if I wrap the function in an object I get a "segmentation fault". It seems like a limitation currently.Chalk
I tried this with var property: Int but it always return nil. It said ` unexpectedly found nil while unwrapping an Optional value` . How can I fix this ? Thank youPneumococcus
@PhamHoan Int is a Swift native type. Try using NSNumber instead.Joelynn
that's what I did in object-C but it doesn't work in Swift. However, I asked this question and got a pretty nice answer. #27635617Pneumococcus
@PhamHoan I added a solution for getting an uninitialized object.Joelynn
This doesn't work with value types. String, Int, Double, etc. are automatically bridged, but this solution doesn't work in general with structsWhitted
Using the nil coalescing operator (??) is cleaner: objc_getAssociatedObject(self, &AssociatedObjectHandle) ?? ""Kristy
@ChristopherSwasey yes, you are right, but as an answer on SO I prefer to make it more readable for novice Swift programmers.Joelynn
Feel free to reverse my edit if you'd like. I think the verbosity actually confuses what's going on, imo.Kristy
Just to be sure I understand the purpose of associated objects. It allows you to set the value of a computed property in an extension as you wouldn't be able to do it with just a normal computed property?Chambless
Yes I read it and I didn't really understand their purpose at first. I got it now . Thanks for the link anyway, might help some peopleChambless
@Joelynn - What is the purpose of setting the AssociatedObjectHandle's type to UInt8? Since the address of the variable is being used, wouldn't a normal (inferred) Int work?Litch
@Litch I guess, it does make no difference. The UInt8 comes from the linked original thread.Joelynn
OBJC_ASSOCIATION_RETAIN_NONATOMIC should be OBJC_ASSOCIATION_COPY_NONATOMIC since operating String?Sophisticated
hey @Jacky - what makes you say it should be COPY .. please let us know!Franklin
I tried this with var property: UITextField but it always return nil. It said ` unexpectedly found nil while unwrapping an Optional value` . How can I fix this ? Thank youPetiolule
@Klass I tried this with var stringProperty:UITextField { get { return objc_getAssociatedObject(self, &AssociatedObjectHandle) as! UITextField } set { objc_setAssociatedObject(self, &AssociatedObjectHandle, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } }for associating but it always return nil. It said ` unexpectedly found nil while unwrapping an Optional value` . How can I fix this ? Thank youPetiolule
@SteveGear Are you sure, that you are a) setting a non-nil value before accessing it? b) the exception is thrown in the getter?Joelynn
@Klass yes I just used the answer you provided above and just using UITextField instead of String...but getting crash.Petiolule
@SteveGear Have you tried it with a String and it worked?Joelynn
W
31

The solution supports all the value types as well, and not only those that are automagically bridged, such as String, Int, Double, etc.

Wrappers

import ObjectiveC

final class Lifted<T> {
    let value: T
    init(_ x: T) {
        value = x
    }
}

private func lift<T>(x: T) -> Lifted<T>  {
    return Lifted(x)
}

func setAssociatedObject<T>(object: AnyObject, value: T, associativeKey: UnsafePointer<Void>, policy: objc_AssociationPolicy) {
    if let v: AnyObject = value as? AnyObject {
        objc_setAssociatedObject(object, associativeKey, v,  policy)
    }
    else {
        objc_setAssociatedObject(object, associativeKey, lift(value),  policy)
    }
}

func getAssociatedObject<T>(object: AnyObject, associativeKey: UnsafePointer<Void>) -> T? {
    if let v = objc_getAssociatedObject(object, associativeKey) as? T {
        return v
    }
    else if let v = objc_getAssociatedObject(object, associativeKey) as? Lifted<T> {
        return v.value
    }
    else {
        return nil
    }
}

A possible Class extension (Example of usage)

extension UIView {

    private struct AssociatedKey {
        static var viewExtension = "viewExtension"
    }

    var referenceTransform: CGAffineTransform? {
        get {
            return getAssociatedObject(self, associativeKey: &AssociatedKey.viewExtension)
        }

        set {
            if let value = newValue {
                setAssociatedObject(self, value: value, associativeKey: &AssociatedKey.viewExtension, policy: objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            }
        }
    }
}
Whitted answered 15/4, 2015 at 23:13 Comment(6)
Can you please explain me what does those association flags mean? I actually am trying to make nav bar scroll with scrollView so am storing scrollView in extension. Which flags should I use?Hypanthium
Also I'm trying to store an array of [AnyObject] using this method but the getter returns me nil always. Somehow the else if of get is not returning properly the value.Hypanthium
The kind of associativity policy you decide to use is up to you, it depends on your program. For further information, I suggest you to check this article, nshipster.com/associated-objects, out. I've tried to store arrays, it seems to be working just fine. Which Swift version are you using by the way?Whitted
This is brilliant. For Swift 2.3 I ended up moving the Generics from the Lift and using Any instead. I'll do a blog article about this, I thinkCrackerjack
This got a lot easier in Swift 3. But anyway, here's myarticle based on your answer here: yar2050.com/2016/11/associated-object-support-for-swift-23.htmlCrackerjack
@DanRosenstark Thanks! Yeah, agree, got a lot easier in Swift 3.Whitted
L
8

I wrote a modern wrapper available at https://github.com/b9swift/AssociatedObject

You may be surprised that it even supports Swift structures for free.

Swift struct association

Lina answered 4/9, 2020 at 9:25 Comment(0)
J
5

Obviously, this only works with Objective-C objects. After fiddling around with this a bit, here's how to make the calls in Swift:

import ObjectiveC

// Define a variable whose address we'll use as key.
// "let" doesn't work here.
var kSomeKey = "s"

…

func someFunc() {
    objc_setAssociatedObject(target, &kSomeKey, value, UInt(OBJC_ASSOCIATION_RETAIN))

    let value : AnyObject! = objc_getAssociatedObject(target, &kSomeKey)
}
Jaela answered 5/7, 2014 at 10:32 Comment(2)
I have tried this method but my value object appears to be immediately deallocated when there are no other references to it from swift code. My target object is an SKNode and my value object is a swift class that extends NSObject. Should this work?Ammadis
Actually deinit appears to be called during objc_setAssociatedObject even though the object has just been created and is used further down in the method.Ammadis
F
4

Update in Swift 3.0 For example this is a UITextField

import Foundation
import UIKit
import ObjectiveC

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

extension UITextField
{
    var nextTextField:UITextField {
    get {
        return objc_getAssociatedObject(self, &AssociatedObjectHandle) as! UITextField
    }
    set {
        objc_setAssociatedObject(self, &AssociatedObjectHandle, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
}
Fairchild answered 29/11, 2016 at 9:28 Comment(0)
C
2

Klaas answer just for Swift 2.1:

import ObjectiveC

let value = NSUUID().UUIDString
var associationKey: UInt8 = 0

objc_setAssociatedObject(parentObject, &associationKey, value, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)

let fetchedValue = objc_getAssociatedObject(parentObject, &associationKey) as! String
Cyanogen answered 4/2, 2016 at 8:43 Comment(0)
F
1

For 2022, now very simple:

//  Utils-tags.swift

// Just a "dumb Swift trick" to add a string tag to a view controller.
// For example, with UIDocumentPickerViewController you need to know
// "which button was clicked to launch a picker"

import UIKit
private var _docPicAssociationKey: UInt8 = 0
extension UIDocumentPickerViewController {
    public var tag: String {
        get {
            return objc_getAssociatedObject(self, &_docPicAssociationKey)
               as? String ?? ""
        }
        set(newValue) {
            objc_setAssociatedObject(self, &_docPicAssociationKey,
               newValue, .OBJC_ASSOCIATION_RETAIN)
        }
    }
}
Franklin answered 24/9, 2022 at 17:32 Comment(0)
P
0

Just add #import <objc/runtime.h> on your brindging header file to access objc_setAssociatedObject under swift code

Phototelegraphy answered 10/6, 2014 at 5:38 Comment(1)
or you can just import ObjectiveC in SwiftRawdan
M
0

The above friend has answered your question, but if it is related to closure properties, please note:

```

import UIKit
public extension UICollectionView {

typealias XYRearrangeNewDataBlock = (_ newData: [Any]) -> Void
typealias XYRearrangeOriginaDataBlock = () -> [Any]

// MARK:- associat key
private struct xy_associatedKeys {
    static var originalDataBlockKey = "xy_originalDataBlockKey"
    static var newDataBlockKey = "xy_newDataBlockKey"
}


private class BlockContainer {
    var rearrangeNewDataBlock: XYRearrangeNewDataBlock?
    var rearrangeOriginaDataBlock: XYRearrangeOriginaDataBlock?
}


private var newDataBlock: BlockContainer? {
    get {
        if let newDataBlock = objc_getAssociatedObject(self, &xy_associatedKeys.newDataBlockKey) as? BlockContainer {
            return newDataBlock
        }
        return nil
    }

    set(newValue) {
        objc_setAssociatedObject(self, xy_associatedKeys.newDataBlockKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
    }
}
convenience init(collectionVewFlowLayout : UICollectionViewFlowLayout, originalDataBlock: @escaping XYRearrangeOriginaDataBlock, newDataBlock:  @escaping XYRearrangeNewDataBlock) {
    self.init()


    let blockContainer: BlockContainer = BlockContainer()
    blockContainer.rearrangeNewDataBlock = newDataBlock
    blockContainer.rearrangeOriginaDataBlock = originalDataBlock
    self.newDataBlock = blockContainer
}

```

Munmro answered 10/11, 2016 at 8:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.