对不起,这个系列做不到,只为给感兴趣的你带来最充实最详细的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中的数据,并进行相应的操作,产生最终结果。