UIAppearance proxy for custom objects
After some reserach I "give up" about using a standard Apple object. It doesn't exists, for now. I've created my own proxy, it's quite simple (works only with "appearance:" by now).
Let's explain it.I want to set the appearance of "textColor" on a NSObject subclass, let's call it "FLObject".Make FLObject conforms to UIAppearance protocol and override the appearance method.In this method, you should return a proxy class (the one I created):
+ (id)appearance{ return [FLAppearance appearanceForClass:[self class]];}
How it works?FLAppearance creates a single instance of itself for each class passed by the appearanceForClass: method.If you call it two times for the same class, the same instance is returned.
Then, you can do something like this:
[[FLObject appearance] setTextColor:[UIColor redColor]];
FLAppearance overrides the forwardInvocation: method, so it accepts all methods sent.Then, it puts all invocations in an array. When FLObject is initialized, a simple call to
[(FLAppearance *)[FLAppearance appearanceForClass:[self class]] startForwarding:self];
will start to send invocations and set the appearance.Sure, this needs some tuning and error checking, but I think it's a good start.
@interface FLAppearance ()@property (strong, nonatomic) Class mainClass;@property (strong, nonatomic) NSMutableArray *invocations;@endstatic 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]; }}
For the purposes of my own project, I collect everything together and released custom UIApperance proxy as open source project MZApperance
Nice implementation, I slightly modified the code and created the class as a subclass of NSProxy
. Using it in a project I found a memory leak:
For example: using the proxy to set global settings/appearance, each instance of that class will never reach refCount 0, so dealloc
will never be called.
Leak code:
-(void)forwardInvocation:(NSInvocation *)anInvocation;{ [...] // !! This will retain also the target [anInvocation retainArguments]; [...]}
Fix:
-(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; }}
Copy category for 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;}