amleszk
amleszk

Reputation: 6290

Is there a way to set associated objects in Swift?

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?

Upvotes: 93

Views: 44218

Answers (9)

BB9z
BB9z

Reputation: 2720

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:

Swift struct association

var rangeAsso = AssociatedObject<Range<int>>()    AssociatedObject<Range<int>>
class A {
}
extension A {
    var rang: Range<Int>? {
        get { rangeAsso[self] }                  (3 times)
        set { rangeAsso[self] = newValue }       (2 times)
    }
}

var a1 = A()                                       A
var a2 = A()                                       A
var a3 = a1                                        A
a1.rang = Range(NSRange (location: 1, length: 1))  A
a2.rang = Range(NSRange (location: 2, length: 2))  A
a1.rang                                            {lowerBound 1, upperBound 2}
a2.rang                                            {lowerBound 2, upperBound 4}
a3.rang                                            {lowerBound 1, upperBound 2}

Upvotes: 9

Fattie
Fattie

Reputation: 12641

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)
        }
    }
}

Upvotes: 2

Klaas
Klaas

Reputation: 22763

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 ?? ""
    }

Upvotes: 149

flame3
flame3

Reputation: 2992

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)
        }
    }
}

Upvotes: 4

Alpface
Alpface

Reputation: 1

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
}

```

Upvotes: 0

RhodanV5500
RhodanV5500

Reputation: 1107

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

Upvotes: 2

HepaKKes
HepaKKes

Reputation: 1549

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)
            }
        }
    }
}

Upvotes: 31

DarkDust
DarkDust

Reputation: 92335

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)
}

Upvotes: 5

jaumard
jaumard

Reputation: 8292

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

Upvotes: 0

Related Questions