Objective-C: propriété / variable d'instance dans la catégorie

122

Comme je ne peux pas créer une propriété synthétisée dans une catégorie en Objective-C, je ne sais pas comment optimiser le code suivant:

@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

La méthode de test est appelée plusieurs fois à l'exécution et je fais beaucoup de choses pour calculer le résultat. Normalement, en utilisant une propriété synthétisée, je stocke la valeur dans un IVar _test la première fois que la méthode est appelée, et je renvoie simplement cet IVar la prochaine fois. Comment puis-je optimiser le code ci-dessus?

dhrm
la source
2
Pourquoi ne pas faire ce que vous faites normalement, uniquement au lieu d'une catégorie, ajouter la propriété à une classe de base MyClass? Et pour aller plus loin, effectuez vos tâches lourdes en arrière-plan et demandez au processus de déclencher une notification ou d'appeler un gestionnaire pour MyClass lorsque le processus est terminé.
Jeremy
4
MyClass est une classe générée à partir de Core Data. Si je mais mon code objet personnalisé dans la classe générée, il disparaîtrait si je régénère la classe à partir de mes données de base. Pour cette raison, j'utilise une catégorie.
dhrm le
1
Acceptez peut-être la question qui s'applique le mieux au titre? ("Propriété dans la catégorie")
hfossli
Pourquoi ne pas simplement créer une sous-classe?
Scott Zhu

Réponses:

124

La méthode de @ lorean fonctionnera (note: la réponse est maintenant supprimée) , mais vous n'auriez qu'un seul emplacement de stockage. Donc, si vous vouliez l'utiliser sur plusieurs instances et que chaque instance calcule une valeur distincte, cela ne fonctionnerait pas.

Heureusement, le runtime Objective-C a cette chose appelée Objets associés qui peut faire exactement ce que vous voulez:

#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
la source
lien fixe pour les objets associés: developer.apple.com/library/ios/#documentation/cocoa/Conceptual/…
MechEthan
5
@DaveDeLong Merci pour cette solution! Pas très beau mais ça marche :)
dhrm
42
"pas très beau"? C'est la beauté d'Objective-C! ;)
Dave DeLong
6
Très bonne réponse! Vous pouvez même vous débarrasser de la variable statique en utilisant @selector(test)comme clé, comme expliqué ici: stackoverflow.com/questions/16020918/...
Gabriele Petronella
1
@HotFudgeSunday - je pense que cela fonctionne dans Swift: stackoverflow.com/questions/24133058/…
Scott Corscadden
174

fichier .h

@interface NSObject (LaserUnicorn)

@property (nonatomic, strong) LaserUnicorn *laserUnicorn;

@end

fichier .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

Tout comme une propriété normale - accessible avec la notation par points

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

Syntaxe plus simple

Vous pouvez également utiliser @selector(nameOfGetter)au lieu de créer une clé de pointeur statique comme ceci:

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

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

Pour plus de détails, voir https://stackoverflow.com/a/16020927/202451

hfossli
la source
4
Bon article. Une chose à noter est une mise à jour dans l'article. Mise à jour du 22 décembre 2011: Il est important de noter que la clé de l'association est une clé void * de pointeur vide, pas une chaîne. Cela signifie que lors de la récupération d'une référence associée, vous devez transmettre exactement le même pointeur au runtime. Cela ne fonctionnerait pas comme prévu si vous utilisiez une chaîne C comme clé, puis copiiez la chaîne dans un autre emplacement de la mémoire et tentiez d'accéder à la référence associée en passant le pointeur vers la chaîne copiée comme clé.
Mr Rogers
4
Vous n'avez vraiment pas besoin du @dynamic objectTag;. @dynamicsignifie que le setter & getter seront générés ailleurs, mais dans ce cas ils sont implémentés ici.
IluTov
@NSAddict vrai! Fixé!
hfossli
1
Que diriez-vous du dealloc de laserUnicorns pour la gestion manuelle de la mémoire? Est-ce une fuite de mémoire?
Manny
Est publié automatiquement stackoverflow.com/questions/6039309/…
hfossli
32

La réponse donnée fonctionne très bien et ma proposition n'est qu'une extension de celle-ci qui évite d'écrire trop de code standard.

Afin d'éviter d'écrire à plusieurs reprises des méthodes getter et setter pour les propriétés de catégorie, cette réponse introduit des macros. De plus, ces macros facilitent l'utilisation de propriétés de type primitif telles que intou BOOL.

Approche traditionnelle sans macros

Traditionnellement, vous définissez une propriété de catégorie comme

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

Ensuite, vous devez implémenter une méthode getter et setter en utilisant un objet associé et le sélecteur get comme clé ( voir la réponse d'origine ):

#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

Mon approche suggérée

Maintenant, en utilisant une macro, vous allez écrire à la place:

@implementation MyClass (Category)

CATEGORY_PROPERTY_GET_SET(NSString*, text, setText:)

@end

Les macros sont définies comme suit:

#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_SETajoute un getter et un setter pour la propriété donnée. Les propriétés en lecture seule ou en écriture seule utiliseront respectivement la macro CATEGORY_PROPERTY_GETet CATEGORY_PROPERTY_SET.

Les types primitifs ont besoin d'un peu plus d'attention

Comme les types primitifs ne sont pas des objets, les macros ci-dessus contiennent un exemple à utiliser unsigned intcomme type de propriété. Il le fait en enveloppant la valeur entière dans un NSNumberobjet. Son utilisation est donc analogue à l'exemple précédent:

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

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

Suivant ce modèle, vous pouvez simplement ajouter des macros pour soutenir aussi signed int, BOOL, etc ...

Limites

  1. Toutes les macros utilisent OBJC_ASSOCIATION_RETAIN_NONATOMICpar défaut.

  2. Les IDE comme App Code ne reconnaissent actuellement pas le nom du setter lors de la refactorisation du nom de la propriété. Vous devrez le renommer vous-même.

Lars Blumberg
la source
1
n'oubliez pas de #import <objc/runtime.h>dans le fichier de catégorie .m sinon. erreur de compilation: la déclaration implicite de la fonction 'objc_getAssociatedObject' n'est pas valide en C99 . stackoverflow.com/questions/9408934/…
Sathe_Nagaraja
7

Utilisez simplement la bibliothèque libextobjc :

fichier h:

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

m-fichier:

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

@synthesizeAssociation (MyClass, test);

@end

En savoir plus sur @synthesizeAssociation

Mansurov Ruslan
la source
Quelle est la bonne façon de remplacer un accesseur avec ceci (par exemple, charger une propriété paresseusement)
David James
3

Testé uniquement avec iOS 9 Exemple: Ajout d'une propriété UIView à UINavigationBar (Category)

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
la source
-2

Une autre solution possible, peut-être plus simple, qui n'utilise pas Associated Objectsest de déclarer une variable dans le fichier d'implémentation de catégorie comme suit:

@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

L'inconvénient de ce type d'implémentation est que l'objet ne fonctionne pas comme une variable d'instance, mais plutôt comme une variable de classe. De plus, les attributs de propriété ne peuvent pas être attribués (comme ceux utilisés dans les objets associés comme OBJC_ASSOCIATION_RETAIN_NONATOMIC)

kernix
la source
La question porte sur la variable d'instance. Votre solution est comme Lorean
hfossli
1
Vraiment?? Saviez-vous ce que vous faites? Vous avez créé une paire de méthodes getter / setter pour accéder à une variable interne qui est indépendante pour un fichier MAIS un objet de classe, c'est-à-dire que si vous allouez deux objets de classe UIAlertView, leurs valeurs d'objet sont les mêmes!
Itachi