Objetivo-C: Propiedad / instancia variable en la categoría

122

Como no puedo crear una propiedad sintetizada en una Categoría en Objective-C, no sé cómo optimizar el siguiente código:

@interface MyClass (Variant)
@property (nonatomic, strong) NSString *test;
@end

@implementation MyClass (Variant)

@dynamic test;

- (NSString *)test {
    NSString *res;
    //do a lot of stuff
    return res;
}

@end

El método de prueba se llama varias veces en tiempo de ejecución y estoy haciendo muchas cosas para calcular el resultado. Normalmente, usando una propiedad sintetizada, almaceno el valor en un IVar _test la primera vez que se llama al método, y solo devuelvo este IVar la próxima vez. ¿Cómo puedo optimizar el código anterior?

dhrm
fuente
2
¿Por qué no hacer lo que normalmente hace, solo en lugar de una categoría, agregar la propiedad a una clase base MyClass? Y para llevarlo más lejos, realice sus tareas pesadas en segundo plano y haga que el proceso active una notificación o llame a algún controlador para MyClass cuando se complete el proceso.
Jeremy
44
MyClass es una clase generada a partir de Core Data. Si yo, pero mi código de objeto personalizado dentro de la clase generada, desaparecería si regenerara la clase a partir de mis Datos básicos. Debido a esto, estoy usando una categoría.
dhrm
1
¿Quizás acepte la pregunta que se aplica mejor al título? ("Propiedad en la categoría")
hfossli
¿Por qué no simplemente crear una subclase?
Scott Zhu

Respuestas:

124

El método de @lorean funcionará (nota: la respuesta ahora se elimina) , pero solo tendría una ranura de almacenamiento. Entonces, si quisiera usar esto en varias instancias y hacer que cada instancia calcule un valor distinto, no funcionaría.

Afortunadamente, el tiempo de ejecución de Objective-C tiene una cosa llamada objetos asociados que puede hacer exactamente lo que quieres:

#import <objc/runtime.h>

static void *MyClassResultKey;
@implementation MyClass

- (NSString *)test {
  NSString *result = objc_getAssociatedObject(self, &MyClassResultKey);
  if (result == nil) {
    // do a lot of stuff
    result = ...;
    objc_setAssociatedObject(self, &MyClassResultKey, result, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  }
  return result;
}

@end
Dave DeLong
fuente
enlace fijo para objetos asociados: developer.apple.com/library/ios/#documentation/cocoa/Conceptual/…
MechEthan
55
@DaveDeLong ¡Gracias por esta solución! No es muy bonito pero funciona :)
dhrm
42
"no muy hermosa"? Esta es la belleza de Objective-C! ;)
Dave DeLong
66
¡Gran respuesta! Incluso puede deshacerse de la variable estática utilizando @selector(test)como clave, como se explica aquí: stackoverflow.com/questions/16020918/…
Gabriele Petronella
1
@HotFudgeSunday: creo que funciona rápidamente: stackoverflow.com/questions/24133058/…
Scott Corscadden
174

archivo .h

@interface NSObject (LaserUnicorn)

@property (nonatomic, strong) LaserUnicorn *laserUnicorn;

@end

archivo .m

#import <objc/runtime.h>

static void * LaserUnicornPropertyKey = &LaserUnicornPropertyKey;

@implementation NSObject (LaserUnicorn)

- (LaserUnicorn *)laserUnicorn {
    return objc_getAssociatedObject(self, LaserUnicornPropertyKey);
}

- (void)setLaserUnicorn:(LaserUnicorn *)unicorn {
    objc_setAssociatedObject(self, LaserUnicornPropertyKey, unicorn, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 
}

@end

Al igual que una propiedad normal, accesible con notación de puntos

NSObject *myObject = [NSObject new];
myObject.laserUnicorn = [LaserUnicorn new];
NSLog(@"Laser unicorn: %@", myObject.laserUnicorn);

Sintaxis más fácil

Alternativamente, podría usar en @selector(nameOfGetter)lugar de crear una tecla de puntero estática como esta:

- (LaserUnicorn *)laserUnicorn {
    return objc_getAssociatedObject(self, @selector(laserUnicorn));
}

- (void)setLaserUnicorn:(LaserUnicorn *)unicorn {
    objc_setAssociatedObject(self, @selector(laserUnicorn), unicorn, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 
}

Para obtener más detalles, consulte https://stackoverflow.com/a/16020927/202451

revs hfossli
fuente
44
Buen artículo. Una cosa a tener en cuenta es una actualización en el artículo. Actualización 22 de diciembre de 2011: es importante tener en cuenta que la clave para la asociación es una clave void * puntero void, no una cadena. Eso significa que al recuperar una referencia asociada, debe pasar exactamente el mismo puntero al tiempo de ejecución. No funcionaría según lo previsto si usara una cadena C como clave, luego copiara la cadena a otro lugar en la memoria e intentara acceder a la referencia asociada pasando el puntero a la cadena copiada como clave.
Sr. Rogers
44
Realmente no necesitas el @dynamic objectTag;. @dynamicsignifica que el setter y getter se generarán en otro lugar, pero en este caso se implementan aquí.
IluTov
@NSAddict cierto! ¡Fijo!
hfossli
1
¿Qué tal el dealloc de laserUnicorns para la gestión manual de memoria? ¿Es esto una pérdida de memoria?
Manny
Se lanza automáticamente stackoverflow.com/questions/6039309/…
hfossli
32

La respuesta dada funciona muy bien y mi propuesta es solo una extensión que evita escribir demasiado código repetitivo.

Para evitar escribir repetidamente métodos getter y setter para propiedades de categoría, esta respuesta introduce macros. Además, estas macros facilitan el uso de propiedades de tipo primitivas como into BOOL.

Enfoque tradicional sin macros

Tradicionalmente, define una propiedad de categoría como

@interface MyClass (Category)
@property (strong, nonatomic) NSString *text;
@end

Luego debe implementar un método getter y setter utilizando un objeto asociado y el selector get como clave ( consulte la respuesta original ):

#import <objc/runtime.h>

@implementation MyClass (Category)
- (NSString *)text{
    return objc_getAssociatedObject(self, @selector(text));
}

- (void)setText:(NSString *)text{
    objc_setAssociatedObject(self, @selector(text), text, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end

Mi enfoque sugerido

Ahora, usando una macro, escribirás en su lugar:

@implementation MyClass (Category)

CATEGORY_PROPERTY_GET_SET(NSString*, text, setText:)

@end

Las macros se definen de la siguiente manera:

#import <objc/runtime.h>

#define CATEGORY_PROPERTY_GET(type, property) - (type) property { return objc_getAssociatedObject(self, @selector(property)); }
#define CATEGORY_PROPERTY_SET(type, property, setter) - (void) setter (type) property { objc_setAssociatedObject(self, @selector(property), property, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }
#define CATEGORY_PROPERTY_GET_SET(type, property, setter) CATEGORY_PROPERTY_GET(type, property) CATEGORY_PROPERTY_SET(type, property, setter)

#define CATEGORY_PROPERTY_GET_NSNUMBER_PRIMITIVE(type, property, valueSelector) - (type) property { return [objc_getAssociatedObject(self, @selector(property)) valueSelector]; }
#define CATEGORY_PROPERTY_SET_NSNUMBER_PRIMITIVE(type, property, setter, numberSelector) - (void) setter (type) property { objc_setAssociatedObject(self, @selector(property), [NSNumber numberSelector: property], OBJC_ASSOCIATION_RETAIN_NONATOMIC); }

#define CATEGORY_PROPERTY_GET_UINT(property) CATEGORY_PROPERTY_GET_NSNUMBER_PRIMITIVE(unsigned int, property, unsignedIntValue)
#define CATEGORY_PROPERTY_SET_UINT(property, setter) CATEGORY_PROPERTY_SET_NSNUMBER_PRIMITIVE(unsigned int, property, setter, numberWithUnsignedInt)
#define CATEGORY_PROPERTY_GET_SET_UINT(property, setter) CATEGORY_PROPERTY_GET_UINT(property) CATEGORY_PROPERTY_SET_UINT(property, setter)

La macro CATEGORY_PROPERTY_GET_SETagrega un getter y setter para la propiedad dada. Las propiedades de solo lectura o de solo escritura usarán la macro CATEGORY_PROPERTY_GETy CATEGORY_PROPERTY_SETrespectivamente.

Los tipos primitivos necesitan un poco más de atención.

Como los tipos primitivos no son objetos, las macros anteriores contienen un ejemplo para usar unsigned intcomo tipo de propiedad. Lo hace envolviendo el valor entero en un NSNumberobjeto. Entonces su uso es análogo al ejemplo anterior:

@interface ...
@property unsigned int value;
@end

@implementation ...
CATEGORY_PROPERTY_GET_SET_UINT(value, setValue:)
@end

Siguiendo este patrón, simplemente puede añadir más macros para apoyar también signed int, BOOL, etc ...

Limitaciones

  1. Todas las macros están usando OBJC_ASSOCIATION_RETAIN_NONATOMICpor defecto.

  2. Los IDEs como App Code actualmente no reconocen el nombre del configurador cuando refactorizan el nombre de la propiedad. Tendría que cambiarle el nombre usted mismo.

Lars Blumberg
fuente
1
No olvide #import <objc/runtime.h>en la categoría .m archivo de lo contrario. error de tiempo de compilación: la declaración implícita de la función 'objc_getAssociatedObject' no es válida en C99 aparece. stackoverflow.com/questions/9408934/…
Sathe_Nagaraja
7

Simplemente use la biblioteca libextobjc :

archivo h:

@interface MyClass (Variant)
@property (nonatomic, strong) NSString *test;
@end

archivo-m:

#import <extobjc.h>
@implementation MyClass (Variant)

@synthesizeAssociation (MyClass, test);

@end

Más acerca de @synthesizeAssociation

Mansurov Ruslan
fuente
¿Cuál es la forma correcta de anular un accesorio con esto (por ejemplo, carga diferida de una propiedad)
David James
3

Probado solo con iOS 9 Ejemplo: Agregar una propiedad UIView a UINavigationBar (Categoría)

UINavigationBar + Helper.h

#import <UIKit/UIKit.h>

@interface UINavigationBar (Helper)
@property (nonatomic, strong) UIView *tkLogoView;
@end

UINavigationBar + Helper.m

#import "UINavigationBar+Helper.h"
#import <objc/runtime.h>

#define kTKLogoViewKey @"tkLogoView"

@implementation UINavigationBar (Helper)

- (void)setTkLogoView:(UIView *)tkLogoView {
    objc_setAssociatedObject(self, kTKLogoViewKey, tkLogoView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (UIView *)tkLogoView {
    return objc_getAssociatedObject(self, kTKLogoViewKey);
}

@end
Rikco
fuente
-2

Otra posible solución, quizás más fácil, que no se utiliza Associated Objectses declarar una variable en el archivo de implementación de categoría de la siguiente manera:

@interface UIAlertView (UIAlertViewAdditions)

- (void)setObject:(id)anObject;
- (id)object;

@end


@implementation UIAlertView (UIAlertViewAdditions)

id _object = nil;

- (id)object
{
    return _object;
}

- (void)setObject:(id)anObject
{
    _object = anObject;
}
@end

La desventaja de este tipo de implementación es que el objeto no funciona como una variable de instancia, sino como una variable de clase. Además, no se pueden asignar atributos de propiedad (como los utilizados en Objetos asociados como OBJC_ASSOCIATION_RETAIN_NONATOMIC)

Kernix
fuente
La pregunta se refiere a la variable de instancia. Su solución es como Lorean
hfossli
1
¿¿De Verdad?? ¿Sabías lo que estás haciendo? ¡Creó un par de métodos getter / setter para acceder a una variable interna que es independiente para un archivo PERO un objeto de clase, es decir, si asigna dos objetos de clase UIAlertView, los valores de objeto de ellos son los mismos!
Itachi