学计算机的那个

不是我觉到、悟到,你给不了我,给了也拿不住;只有我觉到、悟到,才有可能做到,能做到的才是我的.

0%

UIApperance协议

前言

在iOS开发中有时候需要全局设置某个控件的属性

1
[UINavigationBar appearance].barTintColor = xxx;

那么它是如何实现的呢?

定义

UIApperance实际上是一个协议(Protocol),我们可以用它来获取一个类的外观代理(Apperance Proxy).

1
2
3
+ (instancetype)appearance;
+ (instancetype)appearanceWhenContainedInInstancesOfClasses:(NSArray<Class <UIAppearanceContainer>> *)containerTypes NS_AVAILABLE_IOS(9_0);
// 详细方法见 UIKit/UIAppearance.h

另外一个与之对应的协议是UIApperanceContainer,该协议并没有任何约定方法。因为它只是一个容器。

UIView实现了这两种协议,既可以获取外观代理,也可以作为外观容器。
UIViewController则是仅实现了UIApperanceContainer协议。
对于我们继承与UIView的自定义控件,如果需要支持使用apperance来设置的属性,需要在属性后增加UI_APPERANCE_SELECTOR(并没有干什么事,如文档所说,只是 tag 一下)宏声明即可。

1
2
3
4
5
6
To participate in the appearance proxy API, tag your appearance property selectors in your header with UI_APPEARANCE_SELECTOR.

Appearance property selectors must be of the form:
- (void)setProperty:(PropertyType)property forAxis1:(IntegerType)axis1 axis2:(IntegerType)axis2 axisN:(IntegerType)axisN;
- (PropertyType)propertyForAxis1:(IntegerType)axis1 axis2:(IntegerType)axis2 axisN:(IntegerType)axisN;
You may have no axes or as many as you like for any property. PropertyType may be any standard iOS type: id, NSInteger, NSUInteger, CGFloat, CGPoint, CGSize, CGRect, UIEdgeInsets or UIOffset. IntegerType must be either NSInteger or NSUInteger; we will throw an exception if other types are used in the axes.

实践

写一个简单的小 Demo,自定义 CardView,有两个 subview: headerView 和 footerView,声明 2 个属性:

1
2
@property (nonatomic, strong) UIColor *headerColor UI_APPEARANCE_SELECTOR;
@property (nonatomic, strong) UIColor *bodyColor UI_APPEARANCE_SELECTOR;

Setter 方法都加断点调试:

1
2
3
4
5
6
7
8
9
10
11
- (void)setHeaderColor:(UIColor *)headerColor
{
_headerColor = headerColor;
self.headerView.backgroundColor = _headerColor;
}

- (void)setBodyColor:(UIColor *)bodyColor
{
_bodyColor = bodyColor;
self.bodyView.backgroundColor = _bodyColor;
}

在 ViewController 的 view 中加一个按钮,点击则创建并添加 CardView,每行代码均加断点:

1
2
3
4
5
- (IBAction)createButtonTouched:(id)sender
CardView *cardView = [[CardView alloc] initWithFrame:CGRectMake(20, 100, 80, 120)];
[self.view addSubview:cardView];
cardView.headerColor = [UIColor greenColor];
}

另外,在较早的时候,添加 appearance 设置:

1
2
[CardView appearance].headerColor = [UIColor redColor];
[CardView appearance].bodyColor = [UIColor orangeColor];

运行发现,在通过 appearance 设置属性的时候,并没有调用 setter 方法,由此可知 appearance 并不会生成实例,立即赋值。当 cardView 被添加到主视图(即视图树)中去的时候,才依次调用两个 setter 方法,调用栈如下

从 15 至 11 可以看出确实是加入到视图树中才触发的,从 7 至 2 可以基本猜测出,appearance 设置的属性,都以 Invocation 的形式存储到 UIApperance 类中(事实上 UIApperance 类中就有一个 _appearanceInvocations 数组),等到视图树 performUpdates 的时候,会去检查有没有相关的属性设置,有则 invoke。(这里可以看看 NSInvocation)

紧接着,它进入了 bodyColor 的 setter

然后,当手动设置属性的时候,它是直接进入 setter 的。

总结:

  1. 每一个实现 UIAppearance 协议的类,都会有一个 _UIApperance 实例,保存着这个类通过 appearance 设置属性的 invocations,在该类被添加或应用到视图树上的时候,它会检查并调用这些属性设置。这样就实现了让所有该类的实例都自动统一属性。

当然,如果后面又手动设置了属性,肯定会覆盖了。

2.去掉 UI_APPEARANCE_SELECTOR 宏声明,然后通过 appearance 设置属性,会发现结果是一样的。也就是说 UI_APPEARANCE_SELECTOR 并没有干什么事,正如文档所说,只是 tag 一下。看 UI_APPEARANCE_SELECTOR 宏定义如下

1
#define UI_APPEARANCE_SELECTOR __attribute__((annotate("ui_appearance_selector")))

参考

1.iOS UIAppearance 探秘