前言

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 只是起到一个代理作用,在特定的时机让代理替所有实例做同样的事

UI基础 - UIAppearance协议的更多相关文章

  1. 转发-UI基础教程 – 原生App切图的那些事儿

    UI基础教程 – 原生App切图的那些事儿 转发:http://www.shejidaren.com/app-ui-cut-and-slice.html 移动APP切图是UI设计必须学会的一项技能,切 ...

  2. Android UI基础之五大布局

    Android  UI基础之五大布局 Android的界面是有布局和组件协同完成的,布局好比是建筑里的框架,而组件则相当于建筑里的砖瓦.组件按照布局的要求依次排列,就组成了用户所看见的界面.Andro ...

  3. iOS开发UI基础—手写控件,frame,center和bounds属性

    iOS开发UI基础—手写控件,frame,center和bounds属性 一.手写控件 1.手写控件的步骤 (1)使用相应的控件类创建控件对象 (2)设置该控件的各种属性 (3)添加控件到视图中 (4 ...

  4. Android UI基础教程 目录

    从csdn下载了这本英文版的书之后,又去京东搞了一个中文目录下来.对照着看. 话说,这本书绝对超值.有money的童鞋看完英文版记得去买中文版的~~ Android UI基础教程完整英文版 pdf+源 ...

  5. UI基础UIButton

    UI基础UIButton 前面写了UIWindow.UIViewController,那些都是一些框架,框架需要填充上具体的view才能组成我们的应用,移动应用开发中UI占了很大一部分,最基础的UI实 ...

  6. UI基础UIWindow、UIView

    UI基础UIWindow.UIView 在PC中,应用程序多是使用视窗的形式显示内容,手机应用也不例外,手机应用中要在屏幕上显示内容首先要创建一个窗口承载内容,iOS应用中使用UIWindow.UIV ...

  7. #WEB安全基础 : HTTP协议 | 文章索引

    本系列讲解WEB安全所需要的HTTP协议 #WEB安全基础 : HTTP协议 | 0x0 TCP/IP四层结构 #WEB安全基础 : HTTP协议 | 0x1 TCP/IP通信 #WEB安全基础 : ...

  8. IOS开发UI基础--数据刷新

    IOS开发UI基础--数据刷新 cell的数据刷新包括下面几个方面 加入数据 删除数据 更改数据 全局刷新方法(最经常使用) [self.tableView reloadData]; // 屏幕上的全 ...

  9. Android 的UI基础布局的学习

    一. 今天学习了Android 的UI基础布局的部分,绝大多数的布局都在Androidstudio的这个界面里,如下: 在左边的框里的palette的内部,包含了的大多数的布局所要用的button按钮 ...

  10. IOS开发UI基础UIView

    主要介绍下UIView得基本概念和一些属性的介绍至于属性的用户后面会由详细的介绍 -.UIView基本概念 1.什么是控件? 屏幕上所有的UI元素都叫做控件 (也有很多书中叫做视图 组件) 比如 按钮 ...

随机推荐

  1. 免杀之:Python加载shellcode免杀

    免杀之:Python加载shellcode免杀 目录 免杀之:Python加载shellcode免杀 1 Python 加载Shellcode免杀 使用Python可以做一些加密.混淆,但使用Pyth ...

  2. Mybatis 实体类驼峰命名与数据库字段之间映射

    数据库的命名规则和 Java 的命名规则不一致,导致实体类与数据库字段不能完美映射. 一.可以在 mapper.xml 中通过 resultMap 来解决: <resultMap id=&quo ...

  3. 火山引擎 DataLeap:揭秘字节跳动数据血缘架构演进之路

    更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 DataLeap 是火山引擎数智平台 VeDI 旗下的大数据研发治理套件产品,帮助用户快速完成数据集成.开发.运维 ...

  4. LeetCode-398 随机数索引

    来源:力扣(LeetCode)链接:https://leetcode-cn.com/problems/random-pick-index 题目描述 给定一个可能含有重复元素的整数数组,要求随机输出给定 ...

  5. LeetCode-540 有序数组中单一元素

    来源:力扣(LeetCode)链接:https://leetcode-cn.com/problems/single-element-in-a-sorted-array 题目描述 给你一个仅由整数组成的 ...

  6. 副三角形行列式转成上(下)三角形行列式为什么依次对换而不用第n行直接对换首行,第n-1行直接对换次行

    副三角形行列式转成上(下)三角形行列式为什么依次对换而不用第n行直接对换首行,第n-1行直接对换次行 前言:重在记录,可能出错. 1. 简而言之,可以用第n行直接对换首行,第n-1行直接对换次行,直到 ...

  7. 解决html2canvas.js和pdf.js导出页面问题

    最近在做项目时有这么一个需求,需要将当前html页面导出pdf到本地.由于之前是做过类似的功能的借助了两个插件分别是html2canvas.js和pdf.js,本以为是非常顺利就能完成的,实际在使用过 ...

  8. 01.JavaSE学习

    一.java入门 java三大版本(write once,run anywhere) JavaSE:标准版(用于桌面开发,控制台开发) javaME:嵌入式开发(手机,小家电) javaEE:以jav ...

  9. xml简单操作

    1.创建简单的XML 1 XmlDocument XmlDoc = new XmlDocument(); 2 //XML声明 3 var xmlDeclaration = XmlDoc.CreateX ...

  10. Matplotlib 绘图线

    绘图过程如果我们自定义线的样式,包括线的类型.颜色和大小等. 线的类型 线的类型可以使用 linestyle 参数来定义,简写为 ls. 类型 简写 说明 'solid' (默认) '-' 实线 'd ...