对不起,这个系列做不到,只为给感兴趣的你带来最充实最详细的 iOS 学习体验,阅读全文可能需要 3 小时,可能 30 分钟,也可能 3 分钟,还可能 3 秒,之后我会分析分析有多少读者只坚持了 3 秒,继续改进。
欢迎回到从《虾票票》带你入门 iOS 系列(3)—— 常用 UI 组件。这一篇主要聊聊iOS 的系统架构、UIKit 组件的重要成员以及它们的基本使用和特性、《虾票票》UI 解剖。
iOS 系统架构#
iOS 的系统架构主要分为 4 层,从下到上分别是核心系统层(Core OS)、核心服务层(Core Services)、媒体层(Media)以及触摸层(Cocoa Touch),再往上就构成了一个应用(Application)。
从更细的粒度再看一下这 4 层的主要组成框架:
对于入门,其实主要关注:触摸层的「UIKit 框架」➕核心服务层的「Foundation 框架」即可,下面的介绍来自苹果开发者文档:
- UIKit:为您的 iOS 或 tvOS 应用程序构建和管理图形化、事件驱动的用户界面;
- Foundation:访问基本数据类型、集合和操作系统服务,以定义应用程序的基础功能层。
它们的框架组成可查看附录 1 和附录 2~
关于 Foundation,主要先关注基本数据类型的封装,可参阅Objective_C 基础框架—— 易百教程;关于UIKit,它是我们今天的主角❗️
UIKit#
下面是 UIKit 重要成员的继承关系及各自的作用:
- UIView—— 展示内容,提供交互
- UIImageView—— 展示图片
- UILabel—— 展示文字
- UIScrollView—— 展示大于屏幕的内容(如地图)
- UITableView—— 展示表格类型内容(如电话簿)
- UIViewController—— 管理 View 的容器
- UITabBarController—— 管理多个 ViewController 切换,标签页切换方式
- UINavigationController—— 管理多个 ViewController 切换,推入推出切换方式
对它们有一个粗略的认识后,来做一个小练习,下面两个动图中主要包含了哪些 UIView 和哪些 UIViewController:
如果你能很快地判断出来它们的基本构成,说明你已经对 UIKit 基础组件的作用有一个基本的认识了~
那么这些基础的组件该怎么使用,又分别有哪些特性呢?
下面我们先来聊聊其中两位重量级成员:UIView 与 UIViewController。
UIView#
展示内容,提供交互
基本使用#
// 1)alloc:根据UIView申请空间;init:初始化对象
UIView *view = [[UIView alloc] init];
// 2)左上角点坐标(100, 100),宽高100 * 100
view.frame = CGRectMake(100, 100, 100, 100);
// +设置背景色
view.backgroundColor = [UIColor redColor]; // 等同于UIColor.redColor
// 3)将新定义的view添加到父view中
[self.view addSubview:view];
下面这 3 步是必须的。(❗️:继承自 UIView 类型的组件大都离不开这 3 步)
1)初始化:首先通过调用 UIView 的类方法 alloc 申请空间,通过返回的实例调用 init 方法初始化对象;
2)设置 frame:即它的左上角坐标以及宽高;
3)添加:将定义好的 UIView 对象添加到一个父 UIView 对象上。
特性#
1)栈结构管理子视图:其可以添加多个子视图,后添加的视图展示在先添加的上面,父视图可以管理子视图的视图层级。
从上面这张图很容易看出两个方块添加的顺序,先红后绿。
⚠️:
- 这样的特性很可能是交互失效的原因之一,因为对于有重合的 View,上层的交互会让下层的交互失效。
- 细心的朋友可能会疑惑代码中的self是个怎样的存在,它的 view 属性,这就是下面要提到的第 2 个重量级成员。
OC 作为一门面向对象编程语言,self 指代的其实就是当前调用该方法的对象,而上面 self 的类型就是 UIViewController。
UIViewController#
管理 View 的容器
如果了解 MVC 模式(见文末附录 3),其与 UIView 的关系就很容易理解了:UIView 是 MVC 中的 V--View,UIViewController 是 MVC 中的 C--Controller。
基本使用#
// 1)alloc:根据UIViewController申请空间;init:初始化对象
UIViewController *viewController = [[UIViewController alloc] init];
// 2)将某一个view附到viewController的view中
[viewController.view addSubview:someView];
1)初始化
2)添加要管理的 UIView 对象
特性#
1)相当于一个容器,自身包含一个默认的 view 属性,类型为 UIView,与上文呼应了❗️
RootView 就是 UIViewController 对象自带的一个 UIView 对象。
2)下面是它的生命周期,开发者可以选择在合适的时机重写并添加一些自定义操作。
- init ->loadView->
- viewDidLoad->viewWillAppear->viewDidAppear->
- viewWillDisappear->viewDidDisappear->
- dealloc
- 这张图引自 quanqingyang 的 CSDN 博客,已经很好地说明了 UIViewController 对象从初始化👉加载视图👉展示视图👉视图消失👉卸载视图👉销毁的过程。
- 初学者一般关注 init 和 viewDidLoad,在 init 时添加一些自定义对象的初始化,在 viewDidLoad 时给 self.view 添加一些自定义的子视图。
PS:
- 新建一个工程时,会自动生成一个 ViewController(.h 头文件和.m 源文件),其默认作为整个工程的根 ViewController。
- 一般地,我们会封装一些继承自 UIViewController 的自定义 ViewController,在里面添加特定的视图和交互逻辑。
聊完了 2 位重量级成员,可以稍微休息一会儿~接下来看看它们后代们的基本使用和特点。
UIView 的子孙们#
UIImageView#
展示图片
基本使用#
// 1)alloc:根据UIImageView申请空间;init:初始化对象
UIImageView *imageView = [[UIImageView alloc] init];
// 2)左上角点坐标(100, 100),宽高100 * 100
imageView.frame = CGRectMake(100, 100, 100, 100);
// 3)设置图片的填充方式
imageView.contentMode = UIViewContentModeScaleAspectFill;
// 4)将指定图片定义成UIImage对象,赋给imageView的image属性
#define kImageName @"./ford.jpg"
imageView.image = [UIImage imageNamed:kImageName];
1)初始化
2)设置 frame
3)设置图片的填充方式 contentMode
主要有以下这些:
上面代码里设置的 contentMode 为 UIViewContentModeScaleAspectFill,对应上图第二个示例,这是比较常用的方式。
4)给 image 属性赋值
所有的图片都要先封装成 UIImage 对象,再通过 UIImageView 展示。
特性#
1)展示动态图片:给 UIImageView 的 animationImages 属性传 UIImage 类型的数组;
2)相关第三方库:SDWebImage,它可以优化 UIImage 的生成过程,一般用于拉取网络图片。
PS:SDWebImage:https://github.com/SDWebImage/SDWebImage
UILabel#
展示文字
基本使用#
// 1)初始化
UILabel *summaryTabLabel = [[UILabel alloc] init];
// 2)左上角点坐标(100, 100),宽高100 * 100
imageView.frame = CGRectMake(100, 100, 100, 100);
// 3)设置文本
summaryTabLabel.font = [UIFont systemFontOfSize:14];
summaryTabLabel.textAlignment = NSTextAlignmentCenter;
summaryTabLabel.textColor = [UIColor orangeColor];
[summaryTabLabel setText:@“电影简介"];
// PS:设置手势
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(summaryTabTapAction)];
[summaryTabLabel addGestureRecognizer:tapGesture];
summaryTabLabel.userInteractionEnabled = YES;
1)初始化
2)设置 frame
3)设置文本:字体、对齐方式、文本颜色、文本内容
PS)设置手势
《虾票票》详情页面里的标签栏(电影简介、演员信息、更多信息)是基于 UILabel 实现的,给它们添加点击的手势,便可以实现像按钮一样的点击效果。
⚠️:UILabel 对象的交互默认是关闭的,需要设置 userInteractionEnabled 属性为 YES 来开启。
特性#
1)多行显示(如上面动图中每个标签栏里的内容)
summaryTabLabel.numberOfLines = 0; // 默认为1
[summaryTabLabel sizeToFit];
2)文本截断方式:lineBreakMode 属性,如按字符换行,省略中间部分显示等等
3)展示复杂的文本:使用 NSAttributeString 类型、YYText(第三方库)
UIScrollView#
展示大于屏幕的内容
基本使用#
// 1)初始化并设置可视范围——frame
UIScrollView *detailScrollView = [[UIScrollView alloc] initWithFrame:self.view.frame];
// 2)设定滚动范围——contentSize
#define kScreenWidth UIScreen.mainScreen.bounds.size.width
#define kScreenHeight UIScreen.mainScreen.bounds.size.height
detailScrollView.contentSize = CGSizeMake(kScreenWidth * 3, kScreenHeight);
// PS:是否分页滚动
detailScrollView.pagingEnabled = YES;
1)初始化并设置 frame
frame 是该视图的可视范围,在初始化的时候就可以设置,使用 initWithFrame 而不是 init 了。
上面代码中赋值的 self.view.frame 就是 self.view 的 frame,self.view 你应该知道是什么了吧,一开始提到过的。
2)设定 contentSize
contentSize 是该视图的滚动范围,换句话说是它的全部范围。
PS)设置滚动是否以页为单位:pagingEnabled 属性
再看看《虾票票》这张动图,它的 contentSize 是三倍屏宽,分页滚动。
特性#
1)frame 和 contentSize
前者是可视范围,后者是滚动范围。
它俩是 UIScrollView 初始化时最重要的两个属性,设置不对可能会滚不起来哦~
2)setContentOffset 方法
[detailScrollView setContentOffset:CGPointMake(detailScrollView.bounds.size.width * 2, 0) animated:YES];
在上一小节里,点击标签栏触发 UIScrollView 的滚动,即 UILabel 点击手势对应的事件 summaryTabTapAction 里,就需要用到 setContentOffset 方法。
下面是 UIView 的子孙里辈分最小的一位,它是继承自 UIScrollView 的,同时也是这里最难理解的。
UITableView : UIScrollView#
展示表格类型
基本使用#
// 1)初始化并设置可视范围——frame
UITableView *homeTableView = [[UITableView alloc] initWithFrame:self.view.bounds];
// 2)指定dataSource、delegate代理方(记得在类声明时添加两个协议)
homeTableView.dataSource = self;
homeTableView.delegate = self;
// 3)接下来实现协议里的方法(@required必须项、@optional可选项)
1)初始化并设置 frame
frame 指可视范围,类同 UIScrollView。
2)指定 dataSource 和 delegate 的代理方
dataSource 和 delegate 是 UITableView 的 2 个协议,「指定代理方」的意思是让「他人」来遵守这个协议,帮忙做协议里的一些事情,这些事情有些是必须做的,有些是可选的;可以类比我们生活中的劳动协议、租赁合同等等。
i. dataSource 负责表格的数据源、单元内容等;
ii. delegate 负责单元的交互、配置等。
这里还需要明确一下,上面代码是写在Controller里的,self 指的是 UIViewController 类型的对象。也就是说,self 遵守了这两个协议,并需要去实现协议里的方法。
3)实现协议里的方法
i. dataSource
#pragma mark - UITableViewDataSource
// @required
// 1)设置每个Section缓冲的Cell数量
(NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section {
return 3;
}
// 2)渲染每个Cell的内容
(UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// 1.从复用池里找kCellId对应的cell(#define kCellId@“id1”)
// [tableView dequeueReusableCellWithIdentifier:kCellId];
// 1-2.如果不存在,再手动生成相应cell并注册为kCellId
// [… reuseIdentifier:kCellId];
// 2.设置cell数据
// 3.return cell
}
// @optional…
dataSource 协议里有 2 个 @required
(必须实现)和若干个 @optional
(可选实现)的方法。
第 1 个 @required
的方法,用来设置每个 Section(分区)缓冲的 Cell(单元)数量, return 3
代表只渲染 3 条数据。实际使用时,一般会根据网络请求返回的数据个数来决定返回。
第 2 个 @required
的方法,用来设置每个单元渲染的内容,参考代码注释,一般分为 3 步:
- 从复用池里找
kCellId
(自定义的 Cell 代号)对应的cell
,如果不存在,再手动生成相应cell
,并在复用池里注册其代号为kCellId
- 设置
cell
数据 return cell
PS:关于复用池一会儿会再次提到,我们先和它混个面熟。UITableViewCell 类型继承自 UIView,其基本使用和特性大同小异。
ii. delegate
#pragma mark - UITableViewDelegate
// @optional
// 1)设置每个Cell的行高
(CGFloat)tableView:(UITableView *)tableView
heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 100;
// 2)设置点击Cell后的事件
(void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// do Something after cell selected
}
// …
delegate 协议里有若干个 @optional
(可选实现)的方法,上面列出了 2 个常用的待实现方法。
第 1 个 @optional
的方法,用来设置每个 Cell 的行高。
第 2 个 @optional
的方法, 用来设置点击 Cell 后的事件,比如推入另一个页面。
根据《虾票票》的首页再感受一下上述 4 个待实现方法的作用:
这份电影表格里的电影数量是根据网络请求的数据决定的;每个 Cell 的格式是可以复用的;行高固定;点击某一部电影后的画面请自行脑补。
特性#
1)UITableView 只负责展示,数据、Cell 及交互需要开发者提供
i. dataSource:数据源、Cell 内容等
ii. delegate:Cell 交互、配置等
2)复用池
因为生成和销毁 Cell 很耗性能,所以有了复用池机制,其基本原理如下:
复用池的底层实现是基于双端队列(dequeue),向上滑动屏幕时,上面的 Cell 消失,被系统自动回收,放入复用池,下面的 Cell 需要加载,先根据其唯一标识 id 去复用池里检索,如果找到了,就可以成功复用,否则,再手动生成相应 Cell,并将其注册到复用池中,绑定 id。
从下图可以体会复用池的精髓:
往上滑动时,新加载的 Cell 不仅复用了 Cell 的样式,还把右边绿色的勾选✅也复用了。
实际使用中,这样做是有些滑稽的,所以我们一般会在复用 Cell 后,设置其数据,上面基本使用的第 3 步有提到;或者,重写 prepareForReuse
方法,在每次复用 Cell 前,清理不需要的数据。(注:prepareForReuse
是 UITableViewCell 的方法)
到这里,我们再对 UITableView 的完整组成进行了解:
刚刚我们展示的《虾票票》Demo 仅仅是一个分区,也没有设置 Header 和 Footer。
实际上,一个完整的 UITableView 可参考上图,用公式表示如下:
UITableView = tableHeaderView + n ✖️ Section + tableFooterView
其中,Section = sectionHeader + n ✖️ UITableViewCell + sectionFooter
终于终于,你是否坚持到这里了呢?「UIView 的子孙们」打卡点,💳滴~
下面是 UIViewController 的 2 个争气的孩子。
UIViewController的孩子们#
这 2 位小孩的相同点是:都可以管理多个 UIViewController 对象的切换,听起来像是儿子也可以管理多个爸爸~😛
不同点可以从下面介绍中对照着体会:
UITabBarController#
基本使用#
// 1)初始化
UITabBarController *tabbarController = [[UITabBarController alloc] init];
// 2)设置要管理的多个Controller(属于或继承自UIViewController)
[tabbarController setViewControllers:@[controller1, controller2, controller3]];
1)初始化
2)设置要管理的多个 Controller
在一开始就设置好了,每个 Controller 的类型属于或继承自 UIViewController。
再看看它的 Demo:
该 UITabBarController 对象管理了 3 个 Controller。
特性#
1)UITabBar,即页面下方的切换栏:
被管理的 UIViewController 对象自己去修改UITabBar 上对应 Tab 的内容:
controller1.tabBarItem.title = @“新闻";
controller1.tabBarItem.image = [UIImage imageNamed:@"tab1.png"];
如图标、标题等。
UINavigationController#
基本使用#
// 1)初始化,并设置根Controller
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:controller1];
// 2)在合适的时候push另一个Controller
[navigationController pushViewController:controller4 animated:YES];
1)初始化,并设置根 Controller
只需要设置 1 个。
2)之后在合适的时候推入另一个 Controller
如在《虾票票》中,点击首页的某一部电影,推入对应的详情界面 Controller。
再看看它的 Demo:
在点击 Show 按钮的时候,推入了另一个 Controller。
特性#
1)UINavigationBar,即页面上方的导航栏:
同样是 UIViewController 对象自己去修改其页面 UINavigationBar 的内容:
controller4.navigationItem.title = @“内容";
如标题等,同时可以自定义两侧的按钮。
2)栈管理:可以返回上一级,也可以返回到根视图或指定视图。
通过上面的讲述,你是否领会 UITabBarController 和 UINavigationController 的区别了呢?
下面还延伸 2 个知识点,好学生最喜欢的环节~其实也能加深对上面内容的理解。
延伸 1:常用的页面切换方式#
从刚才的 2 种 Controller 其实可以感受到 2 种不同的页面切换方式,那么常用的页面切换方式有哪些呢?
- tabs:您知道是指谁吧~
- push /pop:您知道是指谁吧~
- present /dismiss:与第 2 种方式不同,它一般用于不同业务界面之间的切换,并且只能逐级返回。
- show(iOS 8.0+)
该方式会根据 ViewController 的类型自动选择切换方式,比如 splitViewController 的切换方式如下图:
一般见于 iPad 设备,如淘宝 App。
延伸 2:两种嵌套方式#
在实际使用中,UITabBarController 和 UINavigationController 经常是结合起来使用的,与其说是结合方式,不如说是嵌套方式。
方式一:
方式二:
这两种方式最明显的区别在于页面切换时,TabBar 是否会消失。
但是为什么 Apple 官方推荐第一种方式呢?归纳起来大概有 2 点:
1)自由度更高,每个 NavigationController 是独立的,并且可以选择不嵌套 NavigationController;
2)可以实现与方式二相同的页面切换效果,手动隐藏 Tab 栏即可。
注:UI 绘制都在主线程#
今天大费篇幅介绍了 UIKit 的重要组件们,那么在使用它们的过程中一定不要忘了最后这一点提示:UI 绘制都是在主线程❗️
至于为什么?这里摘抄了为什么必须在主线程操作 UI—— 掘金这篇文章里的内容。
1)UIKit 是一个线程不安全的类。UI 操作涉及到访问各种 View 对象的属性,如果异步操作,会存在读写问题;如果为其加锁,又会耗费大量资源并拖慢运行速度;
2)只能在主线程上才能对事件进行响应。整个程序的起点 UIApplication 是在主线程进行初始化,所有的用户事件都是在主线程上进行传递(如点击、拖动),所以 UI 只能在主线程上才能对事件进行响应;
3)确保实现图像的同步更新。在渲染方面,由于图像的渲染需要以 60 帧的刷新率在屏幕上同时更新,而在非主线程异步化的情况下,无法确定这个处理过程能否实现同步更新。
所以,在进行 UI 绘制时,一定要关注当前线程是否为主线程!
《虾票票》UI 解剖#
下面趁热打铁,来看看《虾票票》是由哪些 UIKit 组件组成的,直接上图!
👏今天是否收获满满呢?如果你可以复现上图,那你对 UIKit 的掌握一定很不错啦!欢迎留言说说你的看法,或者对哪里有疑问。
下周再见#
我们会聊到 Xcode 里的调试大法,敬请期待。
附录#
1)Foundation
2)UIKit
通过观察组件的父类,可以很方便地定位组件的功能。
3)MVC 模式
MVC 模式是一种软件架构模式,MVC 是三个单词的首字母缩写,它们分别是 Model(模型)、View(视图)和 Controller(控制):
1)Model,是程序需要操作的数据或信息。
2)View,是提供给用户的操作界面,是程序的外壳。
3)Controller,在上面两者中间,根据用户在 View 中输入的指令,选取 Model 中的数据,并进行相应的操作,产生最终结果。