前言
在iOS开发中有时候需要全局设置某个控件的属性
1 | [UINavigationBar appearance].barTintColor = xxx; |
那么它是如何实现的呢?
定义
UIApperance
实际上是一个协议(Protocol),我们可以用它来获取一个类的外观代理(Apperance Proxy).
1 | + (instancetype)appearance; |
另外一个与之对应的协议是UIApperanceContainer
,该协议并没有任何约定方法。因为它只是一个容器。
UIView实现了这两种协议,既可以获取外观代理,也可以作为外观容器。
UIViewController则是仅实现了UIApperanceContainer
协议。
对于我们继承与UIView的自定义控件,如果需要支持使用apperance来设置的属性,需要在属性后增加UI_APPERANCE_SELECTOR
(并没有干什么事,如文档所说,只是 tag 一下)宏声明即可。
1 | To participate in the appearance proxy API, tag your appearance property selectors in your header with UI_APPEARANCE_SELECTOR. |
实践
写一个简单的小 Demo,自定义 CardView,有两个 subview: headerView 和 footerView,声明 2 个属性:
1 | @property (nonatomic, strong) UIColor *headerColor UI_APPEARANCE_SELECTOR; |
Setter 方法都加断点调试:
1 | - (void)setHeaderColor:(UIColor *)headerColor |
在 ViewController 的 view 中加一个按钮,点击则创建并添加 CardView,每行代码均加断点:
1 | - (IBAction)createButtonTouched:(id)sender |
另外,在较早的时候,添加 appearance 设置:
1 | [CardView appearance].headerColor = [UIColor redColor]; |
运行发现,在通过 appearance 设置属性的时候,并没有调用 setter 方法,由此可知 appearance 并不会生成实例,立即赋值。当 cardView 被添加到主视图(即视图树)中去的时候,才依次调用两个 setter 方法,调用栈如下
从 15 至 11 可以看出确实是加入到视图树中才触发的,从 7 至 2 可以基本猜测出,appearance 设置的属性,都以 Invocation
的形式存储到 UIApperance
类中(事实上 UIApperance 类中就有一个 _appearanceInvocations
数组),等到视图树 performUpdates 的时候,会去检查有没有相关的属性设置,有则 invoke。(这里可以看看 NSInvocation
)
紧接着,它进入了 bodyColor 的 setter
然后,当手动设置属性的时候,它是直接进入 setter 的。
总结:
- 每一个实现 UIAppearance 协议的类,都会有一个 _UIApperance 实例,保存着这个类通过 appearance 设置属性的 invocations,在该类被添加或应用到视图树上的时候,它会检查并调用这些属性设置。这样就实现了让所有该类的实例都自动统一属性。
当然,如果后面又手动设置了属性,肯定会覆盖了。
2.去掉 UI_APPEARANCE_SELECTOR 宏声明,然后通过 appearance 设置属性,会发现结果是一样的。也就是说 UI_APPEARANCE_SELECTOR 并没有干什么事,正如文档所说,只是 tag 一下。看 UI_APPEARANCE_SELECTOR 宏定义如下
1 |