ios - UIAppearance proxy para objetos personalizados
objective-c (4)
Tengo un objeto personalizado, se hereda de NSObject. Este objeto hace "algunas cosas", uno de ellos está creando una UIView con algunos objetos UIKit (UILabel, UIButtons ecc ecc ...). Este objeto tiene algunas propiedades como: textColor, font, backgroundColor ... que se utilizan para personalizar la apariencia de los objetos UIKit contenidos.
Me gustaría personalizar estas propiedades "un disparo" para todas las instancias creadas de este objeto, y he examinado el protocolo UIAppearance.
Los objetos UIKit estándar ya se ajustan al protocolo de apariencia UIA, pero no quiero aplicar el estilo a TODOS los UILabels o UIButtons. Quiero aplicar estilos solo a los UILabels y UIButtons contenidos dentro de mis instancias de objetos. Además, no puedo (y no quiero) usar aparienciaWhenContainedIn: porque el desarrollador que usa mi objeto personalizado puede no saber qué tipo de objetos están "contenidos" dentro de él.
Por lo tanto, estaba analizando cómo hacer que mi objeto personalizado se ajuste al protocolo UIAppearance.
AFAIK debe implementar el
+ (id)appearance
método. Este método debe devolver un objeto proxy donde puede enviar todas sus personalizaciones. Pero, observando el método de apariencia de los objetos UIKit, veo que se devuelve un objeto privado. Un objeto de la clase _UIApearance.
Entonces, parece que Apple no me da un objeto proxy estándar para personalizar el mío, y tengo que crearlo desde cero. ¿Está bien o estoy perdiendo algo?
Gracias
Buena implementación, modifiqué ligeramente el código y creé la clase como una subclase de NSProxy
. Usándolo en un proyecto encontré una pérdida de memoria:
Por ejemplo: al usar el proxy para establecer la configuración / apariencia global, cada instancia de esa clase nunca alcanzará refCount 0, por dealloc
que nunca se llamará dealloc
.
Código de fuga:
-(void)forwardInvocation:(NSInvocation *)anInvocation;
{
[...]
// !! This will retain also the target
[anInvocation retainArguments];
[...]
}
Fijar:
-(void)forwardInvocation:(NSInvocation *)anInvocation
{
[anInvocation setTarget:nil];
[anInvocation retainArguments];
// add the invocation to the array
[self.invocations addObject:anInvocation];
}
-(void)startForwarding:(id)sender
{
for (NSInvocation *invocation in self.invocations) {
// Create a new copy of the stored invocation,
// otherwise setting the new target, this will never be released
// because the invocation in the array is still alive after the call
NSInvocation *targetInvocation = [invocation copy];
[targetInvocation setTarget:sender];
[targetInvocation invoke];
targetInvocation = nil;
}
}
Copiar categoría para NSInvocation
-(id)copy
{
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self methodSignature]];
NSUInteger numberOfArguments = [[self methodSignature] numberOfArguments];
[invocation setTarget:self.target];
[invocation setSelector:self.selector];
if (numberOfArguments > 2) {
for (int i = 0; i < (numberOfArguments - 2); i++) {
char buffer[sizeof(intmax_t)];
[self getArgument:(void *)&buffer atIndex:i + 2];
[invocation setArgument:(void *)&buffer atIndex:i + 2];
}
}
return invocation;
}
Después de algunas investigaciones, me "doy por vencido" por usar un objeto estándar de Apple. No existe, por ahora. He creado mi propio proxy, es bastante simple (ahora solo funciona con "apariencia:").
Vamos a explicarlo. Quiero establecer la apariencia de "textColor" en una subclase NSObject, llamémoslo "FLObject". Make FLObject se ajusta al protocolo UIAppearance y anula el método de apariencia. En este método, debe devolver una clase proxy (la que creé):
+ (id)appearance
{
return [FLAppearance appearanceForClass:[self class]];
}
¿Cómo funciona? FLAppearance crea una única instancia de sí mismo para cada clase pasada por el métodoearanceForClass:. Si lo llama dos veces para la misma clase, se devuelve la misma instancia.
Entonces, puedes hacer algo como esto:
[[FLObject appearance] setTextColor:[UIColor redColor]];
FLAppearance anula el método forwardInvocation:, por lo que acepta todos los métodos enviados. Luego, pone todas las invocaciones en una matriz. Cuando se inicializa FLObject, una simple llamada a
[(FLAppearance *)[FLAppearance appearanceForClass:[self class]] startForwarding:self];
Comenzará a enviar invocaciones y establecerá la apariencia. Claro, esto necesita un poco de ajuste y comprobación de errores, pero creo que es un buen comienzo.
@interface FLAppearance ()
@property (strong, nonatomic) Class mainClass;
@property (strong, nonatomic) NSMutableArray *invocations;
@end
static NSMutableDictionary *dictionaryOfClasses = nil;
@implementation FLAppearance
// this method return the same object instance for each different class
+ (id) appearanceForClass:(Class)thisClass
{
// create the dictionary if not exists
// use a dispatch to avoid problems in case of concurrent calls
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (!dictionaryOfClasses)
dictionaryOfClasses = [[NSMutableDictionary alloc]init];
});
if (![dictionaryOfClasses objectForKey:NSStringFromClass(thisClass)])
{
id thisAppearance = [[self alloc]initWithClass:thisClass];
[dictionaryOfClasses setObject:thisAppearance forKey:NSStringFromClass(thisClass)];
return thisAppearance;
}
else
return [dictionaryOfClasses objectForKey:NSStringFromClass(thisClass)];
}
- (id)initWithClass:(Class)thisClass
{
self = [self initPrivate];
if (self) {
self.mainClass = thisClass;
self.invocations = [NSMutableArray array];
}
return self;
}
- (id)init
{
[NSException exceptionWithName:@"InvalidOperation" reason:@"Cannot invoke init. Use appearanceForClass: method" userInfo:nil];
return nil;
}
- (id)initPrivate
{
if (self = [super init]) {
}
return self;
}
-(void)forwardInvocation:(NSInvocation *)anInvocation;
{
// tell the invocation to retain arguments
[anInvocation retainArguments];
// add the invocation to the array
[self.invocations addObject:anInvocation];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [self.mainClass instanceMethodSignatureForSelector:aSelector];
}
-(void)startForwarding:(id)sender
{
for (NSInvocation *invocation in self.invocations) {
[invocation setTarget:sender];
[invocation invoke];
}
}
Echa un vistazo a http://logicalthought.co/blog/2012/10/8/uiappearance-and-custom-views
Básicamente, solo necesita etiquetar sus propiedades con UI_APPEARANCE_SELECTOR
y todo funciona siempre y cuando su clase sea una subclase de UIView
que manejará la venta real de la clase privada _UIAppearance
.
Editar:
Probablemente sea mejor simplemente lanzar su propia solución con un singleton y algunos métodos de clase en lugar de intentar hacer algo que dé miedo con el tiempo de ejecución. No parece que UIAppearance
compatible con tu caso de uso.
Por otro lado, podría pegar cada objeto que venda en una subclase UIView
privada y luego vender instancias de esa subclase. Luego, puede reenviar los mensajes de apariencia enviados a su NSObject
a las instancias que venda y utilizar appearanceWhenContainedIn:<your private subclass>
. Sin embargo, eso podría causar problemas y podría ser confuso para los consumidores de su clase.
Para los fines de mi propio proyecto, recopilo todo y publiqué el proxy UIApperance personalizado como proyecto de código abierto MZApperance