¿Hay alguna forma de configurar objetos asociados en Swift?

82

Viniendo del objetivo C, puede llamar a la función objc_setAssociatedObjectentre 2 objetos para que mantengan una referencia, lo que puede ser útil si en tiempo de ejecución no desea que se destruya un objeto hasta que también se elimine su referencia. ¿Swift tiene algo similar a esto?

amleszk
fuente
3
Puede usar objc_setAssociatedObjectdesde Swift: developer.apple.com/library/prerelease/ios/documentation/Swift/…
jtbandes
EJEMPLO LARGO CON CÓDIGO COMPLETO para cortar y pegar: stackoverflow.com/documentation/swift/1085/associated-objects/…
Fattie

Respuestas:

130

Aquí hay un ejemplo simple pero completo derivado de la respuesta de jckarter .

Muestra cómo agregar una nueva propiedad a una clase existente. Lo hace definiendo una propiedad calculada en un bloque de extensión. La propiedad calculada se almacena como un objeto asociado:

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

EDITAR:

Si necesita admitir la obtención del valor de una propiedad no inicializada y para evitar el error unexpectedly found nil while unwrapping an Optional value, puede modificar el getter de la siguiente manera:

    get {
        return objc_getAssociatedObject(self, &AssociatedObjectHandle) as? String ?? ""
    }
Klaas
fuente
¿Ha podido configurar una función que se ajuste a un typealias de esta manera?
Mork desde el
1
@PhamHoan Int es un tipo nativo de Swift. Intente usar NSNumber en su lugar.
Klaas
1
@PhamHoan Agregué una solución para obtener un objeto no inicializado.
Klaas
1
@Jawwad Supongo que no hace ninguna diferencia. La UInt8proviene del hilo original vinculado.
Klaas
1
Hola @Jacky, ¿qué te hace decir que debería ser COPIA?
Fattie
31

La solución también admite todos los tipos de valores , y no solo aquellos que están puenteados automágicamente , como String, Int, Double, etc.

Envoltorios

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

Una posible extensión de clase (ejemplo de uso)

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)
            }
        }
    }
}
HepaKKes
fuente
¿Puede explicarme qué significan esas banderas de asociación? De hecho, estoy tratando de hacer que la barra de navegación se desplace con scrollView, así que estoy almacenando scrollView en la extensión. ¿Qué banderas debo utilizar?
meteoritos
1
También estoy tratando de almacenar una matriz de [AnyObject] usando este método pero el captador siempre me devuelve nil. De alguna manera, el else if de get no devuelve correctamente el valor.
meteoros
El tipo de política de asociatividad que decida utilizar depende de usted, depende de su programa. Para obtener más información, le sugiero que consulte este artículo, nshipster.com/associated-objects . He intentado almacenar matrices, parece que funciona bien. Por cierto, ¿qué versión de Swift estás usando?
HepaKKes
Esto es brillante. Para Swift 2.3 terminé moviendo los genéricos del ascensor y usé en su Anylugar. Creo que
escribiré
5
Esto se volvió mucho más fácil en Swift 3. Pero de todos modos, aquí está mi artículo basado en su respuesta aquí: yar2050.com/2016/11/associated-object-support-for-swift-23.html
Dan Rosenstark
5

Obviamente, esto solo funciona con objetos Objective-C. Después de jugar un poco con esto, aquí le mostramos cómo hacer las llamadas en 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)
}
DarkDust
fuente
Probé este método, pero mi objeto de valor parece ser desasignado inmediatamente cuando no hay otras referencias a él desde el código swift. Mi objeto de destino es un SKNode y mi objeto de valor es una clase rápida que extiende NSObject. ¿Debería funcionar esto?
nacross
En realidad, parece que se llama a deinit durante objc_setAssociatedObject aunque el objeto se acaba de crear y se utiliza más abajo en el método.
nacross
4

Actualización en Swift 3.0 Por ejemplo, este es un 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)
        }
    }
}
flame3
fuente
3

Escribí un ayudante moderno que requiere Swift 5.1.

/*
 AssociatedObject.swift

 Copyright © 2020 RFUI.
 https://github.com/BB9z/iOS-Project-Template

 The MIT License
 https://opensource.org/licenses/MIT
 */

import Foundation

/**
 Objective-C associated value wrapper.

 Usage

 ```
 private let fooAssociation = AssociatedObject<String>()
 extension SomeObject {
     var foo: String? {
         get { fooAssociation[self] }
         set { fooAssociation[self] = newValue }
     }
 }
 ```
 */
public final class AssociatedObject<T> {
    private let policy: objc_AssociationPolicy

    /// Creates an associated value wrapper.
    /// - Parameter policy: The policy for the association.
    public init(policy: objc_AssociationPolicy = .OBJC_ASSOCIATION_RETAIN_NONATOMIC) {
        self.policy = policy
    }

    /// Accesses the associated value.
    /// - Parameter index: The source object for the association.
    public subscript(index: AnyObject) -> T? {
        get { objc_getAssociatedObject(index, Unmanaged.passUnretained(self).toOpaque()) as? T }
        set { objc_setAssociatedObject(index, Unmanaged.passUnretained(self).toOpaque(), newValue, policy) }
    }
}

Puede que te sorprenda que incluso admita estructuras Swift de forma gratuita.

Asociación de estructura rápida

BB9z
fuente
2

Klaas responde solo para 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
RhodanV5500
fuente
0

Simplemente agregue #import <objc/runtime.h>su archivo de encabezado de puente para acceder a objc_setAssociatedObject bajo código rápido

jaumard
fuente
18
o simplemente import ObjectiveCen Swift
newacct
0

El amigo anterior respondió a su pregunta, pero si está relacionada con las propiedades de cierre, tenga en cuenta:

''

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
}

''

Alpface
fuente