前言
在一些app中会涉及到更改外观设置的功能,最普遍的就是夜间模式和白天模式的切换,而对于外观的更改必定是一个全局的东西。在iOS5以前,想要实现这样的效果是比较困难的,而再iOS5的时候Apple推出了UIAppearance
,使得外观的自定义更加容易实现。
通常某个app都有自己的主题外观,而在自定义导航栏的时候或许是使用到如下面的代码:
[UINavigationBar appearance].barTintColor = [UIColor redColor];
或者
[[UIBarButtonItem appearance] setTintColor:[UIColor redColor]];
这样使用appearance
的好处就显而易见了,因为这个设置是一个全局的效果,一处设置之后在其他地方都无需再设置。实际上,appearance
的作用就是统一外观设置。
那是否是所有的控件或者属性都可以这样设置尼?
实际上能使用appearance的地方是在方法或者属性后面有UI_APPEARANCE_SELECTOR
宏的地方
@property(nonatomic,assign) UIBarStyle barStyle UI_APPEARANCE_SELECTOR
- (void)setTitleTextAttributes:(nullable NSDictionary *)attributes forState:(UIControlState)state NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR;
简单使用
如果我们自定义的视图也想要一个全局的外观设置,那么使用UIAppearancel来实现非常的方便,接下来就以一个小demo实现。
自定义一个继承自UIView
的CardView,CardView
中添加两个SubView
:leftView
和rightView
,高度和CardView一样,宽度分别占据一半。
然后在.h文件中提供修改两个子视图颜色的API,并添加UI_APPEARANCE_SELECTOR宏
@property (nonatomic, strong)UIColor * leftColor UI_APPEARANCE_SELECTOR;
@property (nonatomic, strong)UIColor * rightColor UI_APPEARANCE_SELECTOR;
在.m文件中重写他们的setter方法设置两个子视图的颜色
- (void)setLeftColor:(UIColor *)leftColor {
_leftColor = leftColor;
self.leftView.backgroundColor = _leftColor;
}
- (void)setRightColor:(UIColor *)rightColor {
_rightColor = rightColor;
self.rightView.backgroundColor = _rightColor;
}
提供两个VC,在第一个VC的viewDidLoad方法中进行全局的颜色设置
- (void)viewDidLoad {
[super viewDidLoad];
[CardView appearance].leftColor = [UIColor redColor];
[CardView appearance].rightColor = [UIColor yellowColor];
}
分别在两个VC的touchesBegan
方法中初始化和添加CardView视图
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
CardView * cardView = [[CardView alloc]initWithFrame:CGRectMake(20, 100, 200, 100)];
[self.view addSubview:cardView];
}
然后运行之后发现两个VC中的CardView的颜色效果是相同的。
UIAppearance修改某一类型控件的全部实例和部分实例
当然UIAppearance不仅可以修改某一类型控件的全部实例,也可以修改部分实例,开发者只需要使用正确的 API 即可。
比如之前我们在demo中的第一个界面改变CardView
的leftColor
的全部实例的时候是这样做的
[CardView appearance].leftColor = [UIColor redColor];
这是使用了这个API
+ (instancetype)appearance;
如果我只想在修改部分实例需要使用另外的API
+ (instancetype)appearanceWhenContainedInInstancesOfClasses:(NSArray> *)containerTypes NS_AVAILABLE_IOS(9_0);
比如如果第二个VC是以presentViewController
的方式跳转的,只想修改第一个界面上的CardView
的leftColor
可以在上述代码后面增加如下代码:
[CardView appearanceWhenContainedInInstancesOfClasses:@[[UINavigationController class]]].leftColor = [UIColor greenColor];
运行之后第一个界面的效果为:
第二个界面不受影响。
深入剖析UIAppearance
会使用某个东西来达到效果只是一个初步的学习,接下来去看看UIAppearance究竟是一个什么东西。
查看API发现iOS5.0之后提供的不仅是UIAppearance
,还有另外一个叫做UIAppearanceContainer
的类,实际上他们都是protocol
@protocol UIAppearanceContainer @end
@protocol UIAppearance
...
...
@end
显然苹果的思路是:让 UIAppearance 成为一个可以返回代理的协议,通过它可以把任何配置转发给特定类的实例。
这样做的好处是:UIAppearance 可以处理所有类型的UI控件,无论它是 UIView 的子类,还是包含了视图实例的非 UIView 控件。
UIAppearance和UIAppearanceContainer的API
使用UIApearance 协议(Protocol)需实现这几个方法:
// 返回接受外观设置的代理
+ (instancetype)appearance;
// 当出现在某个类的出现时候才会改变
+ (instancetype)appearanceWhenContainedInInstancesOfClasses:(NSArray> *)containerTypes NS_AVAILABLE_IOS(9_0);
// 针对不同 trait 下的应用的 apperance 进行很简单的设定
+ (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait NS_AVAILABLE_IOS(8_0);
+ (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait whenContainedInInstancesOfClasses:(NSArray> *)containerTypes NS_AVAILABLE_IOS(9_0);
// 已经废弃的方法
+ (instancetype)appearanceWhenContainedIn:(nullable Class )ContainerClass, ... NS_REQUIRES_NIL_TERMINATION NS_DEPRECATED_IOS(5_0, 9_0, "Use +appearanceWhenContainedInInstancesOfClasses: instead") __TVOS_PROHIBITED;
+ (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait whenContainedIn:(nullable Class )ContainerClass, ... NS_REQUIRES_NIL_TERMINATION NS_DEPRECATED_IOS(8_0, 9_0, "Use +appearanceForTraitCollection:whenContainedInInstancesOfClasses: instead") __TVOS_PROHIBITED;
对于后面两个appearanceForTraitCollection
方法是用于解决 Size Classes 的问题而诞生的,通过这两个API,我们可以控制在不同屏幕尺寸下的样式。
而没有内容的UIAppearanceContainer
Protocol是什么尼?
UIAppearanceContainer
协议并没有任何约定方法。因为它只是作为一个容器。
比如 UIView 实现了 UIAppearance的协议,既可以获取外观代理,也可以作为外观容器。而 UIViewController 则是仅实现了 UIAppearanceContainer 协议,很简单,它本身是控制器而不是 view,作为容器,为UIView等服务。
事实上 所有的视图类都继承自 UIView,UIView 的容器也基本上是 UIView 或 UIViewController,基本不需要自己去实现这两个协议。对于需要支持使用 appearance 来设置的属性,在属性后增加 UI_APPEARANCE_SELECTOR 宏声明即可。
UIAppearance深入挖掘
接下来去看看UIAppearance的调用过程。
继续使用之前的demo,在两个setter方法上加上断点
运行的时候会发现viewDidLoad方法里面的这两句代码并没有调用setter方法
[CardView appearance].leftColor = [UIColor redColor];
[CardView appearance].rightColor = [UIColor yellowColor];
而当CardView视图被加到主视图(容器)的时候才走了setter方法,这说明:
在通过appearance设置属性的时候,并不会生成实例,立即赋值,而需要视图被加到视图tree中的时候才会生产实例。
所以使用 UIAppearance 只有在视图添加到 window 时才会生效,对于已经在 window 中的视图并不会生效。因此,对于已经在 window 里的视图,可以采用从视图里移除并再次添加回去的方法使得 UIAppearance 的设置生效。
方法的调用栈如下:
不难看出appearance 设置的属性,都以 Invocation 的形式存储到 _UIApperance 类中,等到视图树 performUpdates 的时候,会去检查有没有相关的属性设置,有则 invoke。所以使用 UIAppearance 只有在视图添加到 window 时才会生效。
总结如下:
每一个实现 UIAppearance 协议的类,都会有一个 _UIApperance 实例,保存着这个类通过 appearance 设置属性的 invocations,在该类被添加或应用到视图树上的时候,它会检查并调用这些属性设置。这样就实现了让所有该类的实例都自动统一属性。appearance 只是起到一个代理作用,在特定的时机,让代理替所有实例做同样的事。
虚无缥缈的UI_APPEARANCE_SELECTOR
前面说到使用的时候需要在属性后增加 UI_APPEARANCE_SELECTOR 宏声明支持使用 UIAppearance 来设置的属性。但是会发现它其实什么也没干:
#define UI_APPEARANCE_SELECTOR __attribute__((annotate("ui_appearance_selector")))
既然它什么多没做,那么我们在demo代码中将UI_APPEARANCE_SELECTOR
去掉试试。结果会发现效果是一样的。但是苹果官方说了这个是must be:
To support appearance customization, a class must conform to the UIAppearanceContainer protocol and relevant accessor methods must be marked with UI_APPEARANCE_SELECTOR.
所以还是加上比较号,或许在未来的iOS版本中,这些没有被UI_APPEARANCE_SELECTOR所marked的属性就不能使用UIAppearance了尼。