UIAppearance proxy for custom objects UIAppearance proxy for custom objects ios ios

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;}