UIAppearance漫谈

前言

在一些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<NSString *,id> *)attributes forState:(UIControlState)state NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR;

简单使用

如果我们自定义的视图也想要一个全局的外观设置,那么使用UIAppearancel来实现非常的方便,接下来就以一个小demo实现。

自定义一个继承自UIView的CardView,CardView中添加两个SubViewleftViewrightView,高度和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<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    CardView * cardView = [[CardView alloc]initWithFrame:CGRectMake(20, 100, 200, 100)];
    [self.view addSubview:cardView];
}

然后运行之后发现两个VC中的CardView的颜色效果是相同的。


image

image
UIAppearance修改某一类型控件的全部实例和部分实例

当然UIAppearance不仅可以修改某一类型控件的全部实例,也可以修改部分实例,开发者只需要使用正确的 API 即可

比如之前我们在demo中的第一个界面改变CardViewleftColor的全部实例的时候是这样做的

 [CardView appearance].leftColor = [UIColor redColor];

这是使用了这个API

+ (instancetype)appearance;

如果我只想在修改部分实例需要使用另外的API

+ (instancetype)appearanceWhenContainedInInstancesOfClasses:(NSArray<Class <UIAppearanceContainer>> *)containerTypes NS_AVAILABLE_IOS(9_0);

比如如果第二个VC是以presentViewController的方式跳转的,只想修改第一个界面上的CardViewleftColor可以在上述代码后面增加如下代码:

[CardView appearanceWhenContainedInInstancesOfClasses:@[[UINavigationController class]]].leftColor = [UIColor greenColor];

运行之后第一个界面的效果为:

image

第二个界面不受影响。


深入剖析UIAppearance

会使用某个东西来达到效果只是一个初步的学习,接下来去看看UIAppearance究竟是一个什么东西。

查看API发现iOS5.0之后提供的不仅是UIAppearance,还有另外一个叫做UIAppearanceContainer的类,实际上他们都是protocol

@protocol UIAppearanceContainer <NSObject> @end

@protocol UIAppearance <NSObject>
    ...
    ...
@end

显然苹果的思路是:让 UIAppearance 成为一个可以返回代理的协议,通过它可以把任何配置转发给特定类的实例。

这样做的好处是:UIAppearance 可以处理所有类型的UI控件,无论它是 UIView 的子类,还是包含了视图实例的非 UIView 控件。

UIAppearance和UIAppearanceContainer的API

使用UIApearance 协议(Protocol)需实现这几个方法:

// 返回接受外观设置的代理
+ (instancetype)appearance;

// 当出现在某个类的出现时候才会改变
+ (instancetype)appearanceWhenContainedInInstancesOfClasses:(NSArray<Class <UIAppearanceContainer>> *)containerTypes NS_AVAILABLE_IOS(9_0);

// 针对不同 trait 下的应用的 apperance 进行很简单的设定
+ (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait NS_AVAILABLE_IOS(8_0);

+ (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait whenContainedInInstancesOfClasses:(NSArray<Class <UIAppearanceContainer>> *)containerTypes  NS_AVAILABLE_IOS(9_0);

// 已经废弃的方法
+ (instancetype)appearanceWhenContainedIn:(nullable Class <UIAppearanceContainer>)ContainerClass, ... NS_REQUIRES_NIL_TERMINATION NS_DEPRECATED_IOS(5_0, 9_0, "Use +appearanceWhenContainedInInstancesOfClasses: instead") __TVOS_PROHIBITED;

+ (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait whenContainedIn:(nullable Class <UIAppearanceContainer>)ContainerClass, ... NS_REQUIRES_NIL_TERMINATION NS_DEPRECATED_IOS(8_0, 9_0, "Use +appearanceForTraitCollection:whenContainedInInstancesOfClasses: instead") __TVOS_PROHIBITED;

对于后面两个appearanceForTraitCollection方法是用于解决 Size Classes 的问题而诞生的,通过这两个API,我们可以控制在不同屏幕尺寸下的样式。

而没有内容的UIAppearanceContainerProtocol是什么尼?

UIAppearanceContainer协议并没有任何约定方法。因为它只是作为一个容器。
比如 UIView 实现了 UIAppearance的协议,既可以获取外观代理,也可以作为外观容器。而 UIViewController 则是仅实现了 UIAppearanceContainer 协议,很简单,它本身是控制器而不是 view,作为容器,为UIView等服务。

事实上 所有的视图类都继承自 UIView,UIView 的容器也基本上是 UIView 或 UIViewController,基本不需要自己去实现这两个协议。对于需要支持使用 appearance 来设置的属性,在属性后增加 UI_APPEARANCE_SELECTOR 宏声明即可。

UIAppearance深入挖掘

接下来去看看UIAppearance的调用过程。
继续使用之前的demo,在两个setter方法上加上断点


image

运行的时候会发现viewDidLoad方法里面的这两句代码并没有调用setter方法

    [CardView appearance].leftColor = [UIColor redColor];
    [CardView appearance].rightColor = [UIColor yellowColor];

而当CardView视图被加到主视图(容器)的时候才走了setter方法,这说明:
在通过appearance设置属性的时候,并不会生成实例,立即赋值,而需要视图被加到视图tree中的时候才会生产实例

所以使用 UIAppearance 只有在视图添加到 window 时才会生效,对于已经在 window 中的视图并不会生效。因此,对于已经在 window 里的视图,可以采用从视图里移除并再次添加回去的方法使得 UIAppearance 的设置生效。

方法的调用栈如下:

image

不难看出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了尼。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容