前言

1 - 在一些 app 中会涉及到更改外观设置的功能,最普遍的就是夜间模式和白天模式的切换,而对于外观的更改必定是一个全局的东西。这在 iOS5 以前想要实现这样的效果是比较困难的,但是 iOS5 时 Apple 推出了 UIAppearance,使得外观的自定义更加容易实现

2 - 通常某个 app 都有自己的主题外观,而在自定义导航栏的时候或许是使用到如下面的代码

[UINavigationBar appearance].barTintColor = [UIColor  redColor];

appearance 是一个全局的效果,实际上它的作用就是统一外观设置

3 - 是否是所有的控件或者属性都可以这样设置?实际上能使用 appearance 的地方是在方法或者属性后面都带有一个 UI_APPEARANCE_SELECTOR 的宏,如下

1 // 属性
2 @property(nonatomic,assign) UIBarStyle barStyle UI_APPEARANCE_SELECTOR
3 // 方法
4 - (void)setTitleTextAttributes:(nullable NSDictionary<NSString *,id> *)attributes forState:(UIControlState)state NS_AVAILABLE_IOS(5_0) UI_APPEARANCE_SELECTOR;

如何使用

1 - 如果我们自定义的视图也想要一个全局的外观设置,那么使用 UIAppearancel 来实现非常方便

2 - 代码示例

① 分别在 ViewController 和 SecondViewController 中的 touchesBegan 方法中初始化已经配置好的视图 DemoView

// - DemoView.h

#import <UIKit/UIKit.h>
@interface DemoView : UIView // 配置两个子视图:高同父视图;宽各占父视图的一半
@property (nonatomic, strong)UIView *leftView;
@property (nonatomic, strong)UIView *rightView; // 视图背景颜色
// 修改两个子视图颜色:添加 UI_APPEARANCE_SELECTOR 宏
@property (nonatomic, strong)UIColor *leftColor UI_APPEARANCE_SELECTOR;
@property (nonatomic, strong)UIColor *rightColor UI_APPEARANCE_SELECTOR; @end

// - DemoView.m

 1 #import "DemoView.h"
2 @implementation DemoView
3
4 - (instancetype)initWithFrame:(CGRect)frame{
5 self = [super initWithFrame:frame];
6 if (self) {
7
8 // 创建视图
9 self.leftView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, frame.size.width/2.0, frame.size.height)];
10 [self addSubview:self.leftView];
11
12 self.rightView = [[UIView alloc] initWithFrame:CGRectMake(frame.size.width/2.0, 0,frame.size.width/2.0, frame.size.height)];
13 [self addSubview:self.rightView];
14 }
15 return self;
16 }
17
18 // 在 setter 方法中配置两个子视图的颜色
19 - (void)setLeftColor:(UIColor *)leftColor {
20 _leftColor = leftColor;
21 self.leftView.backgroundColor = _leftColor;
22 }
23
24 - (void)setRightColor:(UIColor *)rightColor {
25 _rightColor = rightColor;
26 self.rightView.backgroundColor = _rightColor;
27 }
28
29 @end

// - ViewController.m

 1 #import "ViewController.h"
2 #import "SecondViewController.h"
3 #import "DemoView.h"
4 @interface ViewController()
5
6 @end
7
8 @implementation ViewController
9
10 - (void)viewDidLoad {
11 [super viewDidLoad];
12 self.view.backgroundColor = [UIColor cyanColor];
13
14 // 全局配置
15 // 修改某一类型控件的全部实例 + (instancetype)appearance;
16 [DemoView appearance].leftColor = [UIColor redColor];
17 [DemoView appearance].rightColor = [UIColor blueColor];
18
19 // 下一页
20 [self createNextPageBtn];
21
22 }
23 -(void)createNextPageBtn{
24
25 UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
26 [btn setTitle:@"下一页" forState:UIControlStateNormal];
27 btn.backgroundColor = [UIColor darkGrayColor];
28 btn.frame = CGRectMake(40, 50, SCREEN_WIDTH-80, 50);
29 [btn addTarget:self action:@selector(nextPage) forControlEvents:UIControlEventTouchUpInside];
30 [self.view addSubview:btn];
31 }
32
33 -(void)nextPage{
34
35 SecondViewController *secVC = [[SecondViewController alloc] init];
36 secVC.view.backgroundColor = [UIColor brownColor];
37 [self.navigationController pushViewController:secVC animated:YES];
38
39 }
40
41 // 在 touchesBegan 创建视图,注意查看效果(全局配置)
42 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
43
44 DemoView *cardView = [[DemoView alloc] initWithFrame:CGRectMake(40, 120, 200, 100)];
45 [self.view addSubview:cardView];
46 }
47
48 @end

// - SecondViewController.m

 1 #import "SecondViewController.h"
2 #import "DemoView.h"
3 @implementation SecondViewController
4
5 - (void)viewDidLoad {
6 [super viewDidLoad];
7
8 }
9 // 在 touchesBegan 创建视图,注意查看效果(全局配置)
10 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
11 DemoView * cardView = [[DemoView alloc] initWithFrame:CGRectMake(40, 120, 200, 100)];
12 [self.view addSubview:cardView];
13 }
14
15 @end

运行效果:两个视图控制器中 DemoView 的背景颜色均在全局配置实现

     

② 当然 UIAppearance 不仅可以修改某一类型控件的全部实例,也可以修改部分实例,开发者只需要使用正确的 API 即可,比如我们修改 ViewController 中的方法

 1 // 在 touchesBegan 创建视图,注意查看效果(全局配置)
2 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
3
4 DemoView *cardView = [[DemoView alloc] initWithFrame:CGRectMake(40, 120, 200, 100)];
5 [self.view addSubview:cardView];
6
7 // 添加的代码:修改某一控件的部分实例
8 // 添加以下代码后:第一个界面的 DemoView背景颜色由原来的 左红右蓝 变成 左黄右蓝;而第二个界面保持原样
9 // + (instancetype)appearanceWhenContainedInInstancesOfClasses:(NSArray<Class <UIAppearanceContainer>> *)containerTypes NS_AVAILABLE_IOS(9_0);
10 [DemoView appearanceWhenContainedInInstancesOfClasses:@[[self class]]].leftColor = [UIColor yellowColor];
11 }

剖析 UIAppearance

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

 1 // UIAppearanceContainer 协议并没有任何约定方法,因为它只是作为一个容器
2 // 比如 UIView 实现了 UIAppearance 中的协议,既可以获取外观代理,也可以作为外观容器
3 // 而 UIViewController 则是仅实现了 UIAppearanceContainer 协议,它本身是控制器,并不是 view。但是它作为容器,就可以为 UIView 等服务
4
5 // 事实 UIView 的容器也基本上是 UIView 或 UIViewController,基本不需要自己去实现这两个协议
6 // 对于需要支持使用 appearance 来设置的属性,在属性后增加 UI_APPEARANCE_SELECTOR 宏声明即可
7 @protocol UIAppearanceContainer <NSObject> @end
8
9
10 @protocol UIAppearance <NSObject>
11 // 返回接受外观设置的代理
12 + (instancetype)appearance;
13
14 // 当出现在某个类的出现时候才会改变
15 + (instancetype)appearanceWhenContainedIn:(nullable Class <UIAppearanceContainer>)ContainerClass, ... NS_REQUIRES_NIL_TERMINATION API_DEPRECATED_WITH_REPLACEMENT("appearanceWhenContainedInInstancesOfClasses:", ios(5.0, 9.0)) API_UNAVAILABLE(tvos);
16
17 // 针对不同 trait 下的应用的 apperance 进行简单设定
18 // 这两个 appearanceForTraitCollection方法是用于解决 Size Classes 的问题而诞生的,通过这两个 API 我们可以控制在不同屏幕尺寸下的样式
19 + (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait API_AVAILABLE(ios(8.0));
20 + (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait whenContainedInInstancesOfClasses:(NSArray<Class <UIAppearanceContainer>> *)containerTypes API_AVAILABLE(ios(9.0));
21 // 已经废弃的方法
22 + (instancetype)appearanceWhenContainedInInstancesOfClasses:(NSArray<Class <UIAppearanceContainer>> *)containerTypes API_AVAILABLE(ios(9.0));
23 + (instancetype)appearanceForTraitCollection:(UITraitCollection *)trait whenContainedIn:(nullable Class <UIAppearanceContainer>)ContainerClass, ... NS_REQUIRES_NIL_TERMINATION API_DEPRECATED_WITH_REPLACEMENT("appearanceForTraitCollection:whenContainedInInstancesOfClasses:", ios(8.0, 9.0)) API_UNAVAILABLE(tvos);
24
25 @end

显然苹果的思路是让 UIAppearance 成为一个可以返回代理的协议,通过它可以把任何配置转发给特定类的实例。这样做的好处就是 UIAppearance 可以处理所有类型的UI控件:无论它是 UIView 的子类、还是包含了视图实例的非 UIView 控件

2 - UI_APPEARANCE_SELECTOR

① 前面说到使用的时候需要在属性后增加 UI_APPEARANCE_SELECTOR 宏声明支持使用 UIAppearance 来配置属性,但是会发现它其实什么也没干

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

② 既然它什么多没做,将 DemoView 中的代码将属性后的 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.

3 - UIAppearance 工作原理

① DemoView 中在两个 setter 上打上断点

② 运行程序,发现 viewDidLoad 方法里面的这两句代码并没有调用 setter 方法,而当 DemoView 被加到主视图(容器)的时候才走了 setter 方法

③ 在通过 appearance 设置属性的时并不会生成实例,也不会不进行赋值操作,而需要视图被加到视图树中的时才会创建实例

不难看出 appearance 设置的属性,都以 Invocation 的形式存储到 _UIApperance 类中,等到视图树 performUpdates 时会去检查有没有相关的属性设置,有则 invoke。就是说使用 UIAppearance 时只有在视图添加到 window 时才会生效

小结

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

2 - appearance 只是起到一个代理作用,在特定的时机让代理替所有实例做同样的事

最新文章

  1. 如何正确的使用jquery-ajax
  2. LVM &#39;Can’t open /dev/sdb1 exclusively. Mounted filesystem?&#39; Problem
  3. 简单的聊天室代码php+swoole
  4. C#中的new修饰符
  5. jquery easy ui 1.3.4 快速入门(1)
  6. 合金装备V 幻痛 制作技术特辑
  7. Git入门及上传项目到github中
  8. TFS使用指南
  9. 分类算法之贝叶斯(Bayes)分类器
  10. Android实例-使用自定义字体文件(XE8+小米2)
  11. keystone 手动建立租户,用户,角色,服务,端口
  12. Kent Beck揭秘Facebook开发部署流程
  13. [转] linux中cat more less head tail 命令
  14. 监听&lt;input/&gt;标签行为的方法总结
  15. UVA 10537 - The Toll! Revisited(dijstra扩张)
  16. Linux下一个C基本的编程----写进Blog在那之前
  17. C#用 excel 作为模板打印
  18. 【JAVASCRIPT】React + Redux
  19. elk5.4小白踩坑记录
  20. ddt运行测试方法时报错AttributeError: type object &#39;TestHttpRq&#39; has no attribute &#39;test_http_rq_login&#39;

热门文章

  1. 都在用 AI 生成美少女,而我却。。。
  2. Net Core 网关 Ocelot 简单案例
  3. autodesk2023全家桶 Autodesk 2023 所有产品下载地址
  4. mfc拷贝到我的电脑出现的问题
  5. UVM——通过一个简单的testbench来了解UVM组件的phase执行顺序
  6. Unity 使整个UnityUI变成彩色的BUG
  7. 去除Bam文件中的PCR 重复
  8. jquery 中上传的图片file文件,如果在html img元素中通过src展示 ,可以img的图片src属性直接去等于file文件吗,如果不可以的话,那么src怎么去展示file文件呢
  9. cookie是什么?有什么用?
  10. c语言中计算逻辑表达式