Objective-C: variável de propriedade / instância na categoria

122

Como não consigo criar uma propriedade sintetizada em uma categoria no Objective-C, não sei como otimizar o seguinte 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

O método de teste é chamado várias vezes em tempo de execução e estou fazendo muitas coisas para calcular o resultado. Normalmente, usando uma propriedade sintetizada, eu armazeno o valor em um IVar _test na primeira vez que o método é chamado, e apenas retornando esse IVar na próxima vez. Como posso otimizar o código acima?

dhrm
fonte
2
Por que não fazer o que você normalmente faz, apenas em vez de uma categoria, adicionar a propriedade a uma classe base MyClass? E, para levar adiante, execute suas tarefas pesadas em segundo plano e faça com que o processo inicie uma notificação ou chame algum manipulador para MyClass quando o processo estiver concluído.
Jeremy
4
MyClass é uma classe gerada a partir dos dados principais. Se eu mas meu código de objeto personalizado dentro da classe gerada, ele desapareceria se eu regenerar a classe dos meus Dados Principais. Por causa disso, estou usando uma categoria.
DHRM
1
Talvez aceite a pergunta que melhor se aplica ao título? ("Propriedade na categoria")
hfossli
Por que não apenas criar uma subclasse?
Scott Zhu

Respostas:

124

O método do @ coreano funcionará (nota: a resposta agora foi excluída) , mas você só teria um único slot de armazenamento. Portanto, se você quiser usá-lo em várias instâncias e fazer com que cada instância calcule um valor distinto, não funcionará.

Felizmente, o tempo de execução do Objective-C tem uma coisa chamada Objetos Associados que pode fazer exatamente o que você deseja:

#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
fonte
link fixo para objetos associados: developer.apple.com/library/ios/#documentation/cocoa/Conceptual/…
MechEthan 4/12
5
@DaveDeLong Obrigado por esta solução! Não é muito bonito, mas funciona :)
DHRM
42
"não é muito bonito"? Essa é a beleza do Objective-C! ;)
Dave DeLong
6
Ótima resposta! Você pode até conseguir livrar a variável estática usando @selector(test)como uma chave, como explicado aqui: stackoverflow.com/questions/16020918/...
Gabriele Petronella
1
@HotFudgeSunday - acho que funciona rapidamente: stackoverflow.com/questions/24133058/…
Scott Corscadden #
174

arquivo .h

@interface NSObject (LaserUnicorn)

@property (nonatomic, strong) LaserUnicorn *laserUnicorn;

@end

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

Assim como uma propriedade normal - acessível com notação de ponto

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

Sintaxe mais fácil

Como alternativa, você pode usar em @selector(nameOfGetter)vez de criar uma chave de ponteiro estática da seguinte maneira:

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

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

Para mais detalhes, consulte https://stackoverflow.com/a/16020927/202451

hfossli
fonte
4
Bom artigo. Uma coisa a observar é uma atualização no artigo. Atualização 22 de dezembro de 2011: É importante observar que a chave da associação é uma tecla void poid void *, não uma string. Isso significa que, ao recuperar uma referência associada, você deve passar exatamente o mesmo ponteiro para o tempo de execução. Não funcionaria como planejado se você usasse uma cadeia C como chave, depois a copiasse para outro local na memória e tentasse acessar a referência associada passando o ponteiro para a cadeia copiada como chave.
Rogers,
4
Você realmente não precisa do @dynamic objectTag;. @dynamicsignifica que o setter & getter serão gerados em outro lugar, mas, neste caso, eles serão implementados aqui.
IluTov
@NSAddict true! Fixo!
Hfossli
1
E o negócio de laserUnicorns para gerenciamento manual de memória? Isso é um vazamento de memória?
Manny
É lançado automaticamente stackoverflow.com/questions/6039309/…
hfossli
32

A resposta dada funciona muito bem e minha proposta é apenas uma extensão que evita escrever muito código clichê.

Para evitar escrever métodos getter e setter repetidamente para propriedades de categoria, esta resposta apresenta macros. Além disso, essas macros facilitam o uso de propriedades do tipo primitivo, como intou BOOL.

Abordagem tradicional sem macros

Tradicionalmente, você define uma propriedade de categoria como

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

Em seguida, você precisa implementar um método getter e setter usando um objeto associado e o seletor get como a chave ( consulte a resposta 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

Minha abordagem sugerida

Agora, usando uma macro, você escreverá:

@implementation MyClass (Category)

CATEGORY_PROPERTY_GET_SET(NSString*, text, setText:)

@end

As macros são definidas da seguinte maneira:

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

A macro CATEGORY_PROPERTY_GET_SETadiciona um getter e setter para a propriedade especificada. As propriedades somente leitura ou somente gravação usarão a macro CATEGORY_PROPERTY_GETe, CATEGORY_PROPERTY_SETrespectivamente.

Tipos primitivos precisam de um pouco mais de atenção

Como os tipos primitivos não são objetos, as macros acima contêm um exemplo para uso unsigned intcomo o tipo da propriedade. Isso é feito envolvendo o valor inteiro em um NSNumberobjeto. Portanto, seu uso é análogo ao exemplo anterior:

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

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

Seguindo esse padrão, você pode simplesmente adicionar mais macros para também apoiar signed int, BOOL, etc ...

Limitações

  1. Todas as macros estão sendo usadas OBJC_ASSOCIATION_RETAIN_NONATOMICpor padrão.

  2. IDEs como o App Code atualmente não reconhecem o nome do levantador ao refatorar o nome da propriedade. Você precisaria renomeá-lo sozinho.

Lars Blumberg
fonte
1
não se esqueça de #import <objc/runtime.h>na categoria .m arquivo caso contrário. erro em tempo de compilação: A declaração implícita da função 'objc_getAssociatedObject' é inválida no C99 . stackoverflow.com/questions/9408934/…
Sathe_Nagaraja 25/01
7

Basta usar a biblioteca libextobjc :

arquivo h:

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

arquivo m:

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

@synthesizeAssociation (MyClass, test);

@end

Mais sobre @synthesizeAssociation

Mansurov Ruslan
fonte
O que é a maneira correta de substituir um acessador com isso (por exemplo carga preguiçoso uma propriedade)
David James
3

Testado apenas com o iOS 9 Exemplo: Adicionando uma propriedade UIView ao UINavigationBar (Categoria)

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
fonte
-2

Outra solução possível, talvez mais fácil, que não usa Associated Objectsé declarar uma variável no arquivo de implementação da categoria da seguinte maneira:

@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

A desvantagem desse tipo de implementação é que o objeto não funciona como uma variável de instância, mas como uma variável de classe. Além disso, não é possível atribuir atributos de propriedade (como usado em objetos associados, como OBJC_ASSOCIATION_RETAIN_NONATOMIC)

kernix
fonte
A pergunta pergunta sobre a variável de instância. Sua solução é como Lorean
hfossli
1
Realmente?? Você sabia o que está fazendo? Você criou um par de método getter / setter para acessar uma variável interna que é independente para um arquivo, MAS um objeto de classe, ou seja, se você alocar dois objetos de classe UIAlertView, os valores dos objetos são os mesmos!
Itachi