Прокси-сервер UIAppearance для пользовательских объектов

У меня есть пользовательский объект, он наследуется от NSObject. Этот объект выполняет "некоторые вещи", один из которых создает UIView с некоторыми объектами UIKit (UILabel, UIButtons ecc ecc...). Этот объект имеет некоторые свойства, такие как textColor, font, backgroundColor..., которые используются для настройки внешнего вида содержащихся объектов UIKit.

Я хотел бы настроить эти свойства "один снимок" для всех созданных экземпляров этого объекта, и я просмотрел протокол UIAppearance.

Стандартные объекты UIKit уже соответствуют протоколу UIAppearance, но я не хочу применять стиль для ВСЕХ UILabels или UIButtons. Я хочу применять стили только к UILabels и UIButtons, содержащимся внутри экземпляров объектов. Более того, я не могу (и не хочу) использовать внешний видWhenContainedIn: потому что разработчик, использующий мой пользовательский объект, может не знать, какие объекты "содержатся" внутри него.

Итак, я смотрел, как сделать свой пользовательский объект совместимым с протоколом UIAppearance.

AFAIK он должен реализовать

+ (id)appearance

метод. Этот метод должен возвращать прокси-объект, в котором вы можете отправить все свои настройки. Но, глядя на метод внешнего вида объектов UIKit, я вижу, что возвращается частный объект. Объект класса _UIAppearance.

Итак, похоже, что Apple не дает мне стандартный прокси-объект для настройки моего собственного, и я должен создать, если с нуля. Правильно или я что-то теряю?

Спасибо

Ответ 1

После некоторого повторного запуска я "откажусь" от использования стандартного объекта Apple. На данный момент этого не существует. Я создал свой собственный прокси-сервер, он довольно прост (работает только с "внешним видом:" ).

Объясним это. Я хочу установить внешний вид "textColor" в подклассе NSObject, назовем его "FLObject". Сделать FLObject совместимым с протоколом UIAppearance и переопределить метод внешнего вида. В этом методе вы должны вернуть прокси-класс (тот, который я создал):

+ (id)appearance
{
    return [FLAppearance appearanceForClass:[self class]];
}

Как это работает? FLAppearance создает отдельный экземпляр для каждого класса, переданного методом appearanceForClass:. Если вы вызываете это два раза для одного и того же класса, возвращается тот же экземпляр.

Затем вы можете сделать что-то вроде этого:

[[FLObject appearance] setTextColor:[UIColor redColor]]; 

FLAppearance переопределяет метод forwardInvocation: поэтому он принимает все отправленные методы. Затем он помещает все вызовы в массив. Когда FLObject инициализируется, простой вызов

[(FLAppearance *)[FLAppearance appearanceForClass:[self class]] startForwarding:self];

начнет отправлять вызовы и устанавливать внешний вид. Конечно, это требует некоторой настройки и проверки ошибок, но я думаю, что это хороший старт.

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

Ответ 2

Для моего собственного проекта я собираю все вместе и выпускаю пользовательский прокси UIApperance как проект с открытым исходным кодом MZApperance

Ответ 3

Хорошая реализация, я слегка изменил код и создал класс как подкласс NSProxy. Используя его в проекте, я обнаружил утечку памяти:

Например: используя прокси для установки глобальных настроек/внешнего вида, каждый экземпляр этого класса никогда не достигнет refCount 0, поэтому dealloc никогда не будет вызываться.

Код утечки:

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

Категория копирования для 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;
}

Ответ 4

Отъезд http://logicalthought.co/blog/2012/10/8/uiappearance-and-custom-views

В принципе вам просто нужно пометить свои свойства с помощью UI_APPEARANCE_SELECTOR, и все будет работать до тех пор, пока ваш класс является подклассом UIView, который будет обрабатывать фактический вендинг частного класса _UIAppearance.


Edit:

Вам, вероятно, лучше просто сворачивать собственное решение, используя однотонные и некоторые методы класса, а не пытаться сделать что-то страшное со временем выполнения. Это не похоже на то, что UIAppearance поддерживает ваш прецедент.

С другой стороны, вы можете привязать каждый объект, который вы продаете, в приватном подклассе UIView, а затем вместо него - экземпляры поставщика этого подкласса. Затем вы можете отправлять сообщения о внешнем виде, отправленные на ваш NSObject, в экземпляры, которые вы продаете, и использовать appearanceWhenContainedIn:<your private subclass>. Это может быть запутанным, хотя и может быть запутанным для потребителей вашего класса.