前言

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. vue .sync的理解

    假如父组件传给子组件的值,子组件接受之后,想要改变父组件传过来的值,就可以使用sync .sync是vue中用于实现简单的"双向绑定"的语法糖,在平时的开发中是非常使用的. vue ...

  2. Gateway集成Netty服务

    目录 一.Netty简介 二.Netty入门案例 1.服务端启动 2.通道初始化 3.自定义处理器 4.测试请求 三.Gateway集成 1.依赖层级 2.自动化配置 四.配置加载 1.基础配置 2. ...

  3. Java语言输出菱形图型

    package fuxi;public class Diamond {    public static void main(String[] args) {        printHollowRh ...

  4. redis 集群配置(从0到1)

    1.关闭配置文件 appendonly yes改为no 2.到redis目录下拷贝redis.conf文件 cp ./redis.conf ./7001/ cp ./redis.conf ./7002 ...

  5. python笔记--在文件进行输出

    将print的内容输出到文件中 1 #将数据输出到文件中 2 fp=open('E:/text1.txt','a+') 3 print('hello word',file=fp) 4 fp.close ...

  6. #加IPV6路由

    #加IPV6路由route -A inet6 add 2409:802f:6005:4204::31:0/123 gw 2409:805b:6005:4218::41f:1#路由写入网卡配置文件vi ...

  7. cascader卡顿

    <el-cascader :options="categoryTree" :props="props" collapse-tags v-model=&qu ...

  8. 客户端发送信息给服务器以及服务器接收客户端发来的信息(socket)

    服务器端: using System; using System.Collections.Generic; using System.ComponentModel; using System.Data ...

  9. VUE学习-:class & :style

    :class & :style :class 键值对 <div id="app" v-bind:class="{ 'active': isActive }& ...

  10. 部署mall电商系统踩坑记录

    一. mysql docker run -p 3306:3306 --name mysql -v /mydata/mysql/log:/var/log/mysql -v /mydata/mysql/d ...