Swift y Objective-C

Associated Objects de Objective-C: añadir propiedades a categorías

Las categorías no pueden añadir ivar, pero sí asociar valores mediante Associated Objects. Aquí se explica cómo se almacenan en una tabla lateral junto al objeto, cómo implementar propiedades de categoría y la política de memoria en la que solo hay que vigilar assign.

4 min de lectura
Imagen de portada de Associated Objects de Objective-C: añadir propiedades a categorías

En el artículo «Categorías vs. extension» resumimos que «una categoría no puede añadir propiedades almacenadas». La razón es que el diseño de memoria de la clase queda fijado en compilación y una categoría cargada después no tiene espacio para insertar ivar.

Sin embargo, en código real a veces encontramos propiedades plenamente funcionales en categorías. El secreto es Associated Objects: una función del runtime que asocia valores a objetos sin usar ivar.

Es la vía alternativa oficial para resolver el problema de las propiedades almacenadas en categorías y el almacenamiento auxiliar que el runtime de Objective-C mantiene oculto fuera del objeto.


Principio: una tabla lateral junto al objeto

Las dos funciones clave de Associated Objects son estas.

objc_setAssociatedObject(objeto destino, clave, valor y política de memoria);
objc_getAssociatedObject(objeto destino y clave);

El valor no se almacena en el área de ivar del objeto. El runtime lo registra en una tabla asociativa (tabla lateral) global con el formato «dirección del objeto → {clave: valor}».

El diseño de memoria del objeto no cambia ni un byte, así que no entra en conflicto con las restricciones fijadas durante la compilación.

Cuando se libera el objeto destino, el runtime busca la entrada correspondiente en la tabla asociativa y la limpia según la política. La gestión automática de la vida útil resulta práctica.

Diagrama de cómo objc_setAssociatedObject registra valores en la tabla asociativa global del runtime
El diseño del objeto permanece intacto; el valor vive en la tabla lateral

Implementación completa de una propiedad de categoría

Al declarar una propiedad en una categoría, el compilador solo genera las declaraciones de getter/setter; no crea almacenamiento. Associated Objects llena ese vacío.

#import <objc/runtime.h>

@interface UIView (BadgeCount)
@property (nonatomic, strong) NSNumber *badgeCount;
@end

@implementation UIView (BadgeCount)

- (NSNumber *)badgeCount {
    return objc_getAssociatedObject(self, @selector(badgeCount));
}

- (void)setBadgeCount:(NSNumber *)badgeCount {
    objc_setAssociatedObject(self, @selector(badgeCount),
                             badgeCount,
                             OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

La convención es usar @selector(badgeCount)como clave. Las claves se comparan por dirección del puntero, no por valor, así que basta con una dirección única en toda la aplicación.

Los selectores tienen unicidad garantizada por el runtime y no requieren declarar una variable static aparte, por lo que son la opción más limpia. Métodos clásicos como static void *kBadgeKey = &kBadgeKey;siguen siendo válidos.

Usar literales de cadena como claves es una trampa. Aunque el contenido coincida, la dirección puede variar entre unidades de compilación.

De ahí nace el misterio de «lo guardé claramente, pero obtengo nil».


Política de memoria: cuidado únicamente con assign

El cuarto argumento corresponde a los atributos de la propiedad.

Política Atributo correspondiente
OBJC_ASSOCIATION_RETAIN_NONATOMIC strong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC copy, nonatomic
OBJC_ASSOCIATION_RETAIN / COPY Igual que el anterior, pero atomic
OBJC_ASSOCIATION_ASSIGN assign — no es weak

Solo hay que vigilar ASSIGN. Aunque el nombre parece weak, es unsafe_unretained y no se convierte automáticamente en nil.

Si el objeto asociado se libera primero, queda un puntero colgante. El acceso provoca un crasheo.

Si necesitas semántica weak, debes asociar con RETAIN un objeto wrapper cuyo valor esté envuelto en una propiedad weak.

Para borrar un valor asociado concreto, haz set de nil. objc_removeAssociatedObjectselimina todos los valores asociados del objeto, por lo que prácticamente no debería usarse.


Usos reales y límites

Estas son combinaciones habituales en código de producción.

  • Extensiones de clases UIKit: añadir un número de insignia a UIView, un manejador de acciones basado en cierres a UIButton y un nombre de pantalla para analítica a UIViewController
  • Wrapper de conversión de delegado a bloque: asociar el delegado al objeto original para vincular sus ciclos de vida (era habitual en categorías de red anteriores a Alamofire)
  • Swizzling y compañía: se usa cuando la lógica insertada mediante swizzling necesita almacenar estado. Por eso estas dos técnicas del runtime suelen aparecer juntas en producción.

Aun así, hay que respetar los límites. Associated Objects es solo un canal para datos auxiliares.

Si el estado central del objeto se dispersa por la tabla asociativa, resulta difícil entender el diseño completo. Si puedes crear una subclase, ivar es la respuesta correcta.

Esta herramienta debe reservarse para añadir información adicional a clases que no posees.

También puedes usar las mismas funciones para sortear la restricción de stored property en Swift extension (solo en la familia NSObject). En Swift, suele ser más natural combinar un protocolo con un tipo de almacenamiento privado o resolverlo mediante composition.

Ilustración de Associated Objects como una bolsa sujeta a una maleta con un mosquetón
Es como colgar una bolsa en el exterior de la maleta

Resumen

  • Associated Objects almacena valores en la tabla lateral del runtime, no en el objeto, por eso también funciona en categorías.
  • Las claves se comparan por dirección de puntero. Reutilizar @selector es la convención; los literales de cadena son una trampa.
  • Entre las políticas de memoria, ASSIGN no es weak, sino unsafe_unretained. Cuidado con los crasheos por punteros colgantes.
  • Al liberar el objeto destino, los valores asociados también se limpian automáticamente.
  • Su uso se limita a añadir datos auxiliares a clases que no posees. Poner aquí el estado central es una señal de alarma de diseño.

El próximo artículo de la serie del runtime tratará sobre NSProxy. Analizaremos cómo otra clase raíz que no es NSObject, anunciada en el artículo sobre reenvío de mensajes, funciona como un «delegado puro».

Seguir leyendo