t[i]ngshu[O]最舒适的阅读[S]hi间是(3)分钟?!

72th国庆

对不起,这个系列做不到,只为给感兴趣的你带来最充实最详细的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——展示表格类型内容(如电话簿)
UIScrollView
UITableView
  • UIViewController——管理View的容器
    • UITabBarController——管理多个ViewController切换,标签页切换方式
    • UINavigationController——管理多个ViewController切换,推入推出切换方式
UITabBarController
UINavigationController

对它们有一个粗略的认识后,来做一个小练习,下面两个动图中主要包含了哪些UIView和哪些UIViewController:

UITableView、UINavigationController...
UITableView、UITabBarController、UINavigationController...

如果你能很快地判断出来它们的基本构成,说明你已经对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步:

  1. 复用池里找 kCellId (自定义的Cell代号)对应的 cell ,如果不存在,再手动生成相应 cell ,并在复用池里注册其代号为 kCellId
  2. 设置 cell 数据
  3. 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种不同的页面切换方式,那么常用的页面切换方式有哪些呢?

  1. tabs:您知道是指谁吧~
  2. push / pop:您知道是指谁吧~
  3. present / dismiss:与第2种方式不同,它一般用于不同业务界面之间的切换,并且只能逐级返回。
  1. 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中的数据,并进行相应的操作,产生最终结果。