自定义对象的UIAppearance代理

17
我有一个自定义对象,它继承自NSObject。这个对象会执行“一些操作”,其中之一是创建一个UIView并使用一些UIKit对象(如UILabel、UIButtons等)来装饰它。该对象有一些属性,比如textColor、font、backgroundColor等,用于自定义包含的UIKit对象的外观。
我想为所有创建的该对象实例“单次”自定义这些属性,并且已经查看了UIAppearance协议。
标准的UIKit对象已经符合UIAppearance协议,但我不想将样式应用于所有UILabel或UIButtons。我只想将样式应用于我的对象实例中包含的UILabel和UIButtons。 此外,我不能(也不想)使用appearanceWhenContainedIn:因为使用我的自定义对象的开发人员可能不知道它所包含的对象类型。
因此,我正在寻找如何使我的自定义对象符合UIAppearance协议。
据我所知,它必须实现
+ (id)appearance

方法。这个方法应该返回一个代理对象,你可以向它发送所有的自定义内容。 但是,看着UIKit对象的appearance方法,我发现返回了一个私有对象。 一个_UIAppearance类的对象。

所以,似乎苹果并没有为我提供一个标准的代理对象来自定义我的东西,我必须从头开始创建。 是这样的吗?还是我遗漏了什么?

谢谢

4个回答

16

经过一番研究,我放弃使用标准的苹果对象。目前它不存在。我创建了自己的代理,它很简单(目前仅适用于“外观:”)。

让我们解释一下。 我想在NSObject子类上设置“textColor”的外观,让我们称之为“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];
    }
}

代码运行良好,我对其进行了扩展,使其可以沿着类层次结构向上移动,因此它还可以设置父类的选项:https://gist.github.com/vkodocha/5500276(代码太长无法内联添加)。 - Chris
干得好!作为改进,您可以使用instancetype而不是id作为返回类型,这样您甚至可以跳过对FLAppearance的显式转换。 - Gabriele Petronella

4

出于自己项目的需要,我将所有内容汇集起来,并发布了自定义UIApperance代理的开源项目MZApperance


2

实现不错,我稍微修改了代码,并将类创建为NSProxy的子类。在项目中使用它时,我发现了一个内存泄漏问题:

例如:使用代理设置全局设置/外观,每个该类的实例永远不会达到refCount 0,因此dealloc永远不会被调用。

泄漏代码:

-(void)forwardInvocation:(NSInvocation *)anInvocation;
{
    [...]

    // !! This will retain also the target

    [anInvocation retainArguments];

    [...]
}

修复:

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

1
答案在 startForwarding 上有小错误,应该是: NSInvocation *targetInvocation = [invocation copy]; [targetInvocation setTarget:sender]; [targetInvocation invoke]; targetInvocation = nil; - Michal Zaborowski

0

请查看http://logicalthought.co/blog/2012/10/8/uiappearance-and-custom-views

基本上,您只需要使用UI_APPEARANCE_SELECTOR标记您的属性,只要您的类是UIView的子类,它将处理私有_UIAppearance类的实际出售。


编辑:

你最好使用单例和一些类方法来自己编写解决方案,而不是尝试在运行时做一些可怕的事情。看起来UIAppearance不支持你的用例。

另一方面,你可以将每个对象放入一个私有的UIView子类中,然后提供该子类的实例。然后,你可以将发送给你的NSObject的外观消息转发到你提供的实例,并使用appearanceWhenContainedIn:<your private subclass>。但这可能会变得混乱,并且可能会让你的类的消费者感到困惑。


谢谢,我知道如何在UIView子类中使用它。正如我所指定的,我想在NSObject子类中实现UIAppearance。 - LombaX
我选择创建自己的代理以兼容UIAppearance协议,这比看起来要容易 :-) 我已经在答案中添加了代码。 - LombaX

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接