Bo2SS

Bo2SS

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

72th 国庆

对不起,这个系列做不到,只为给感兴趣的你带来最充实最详细的 iOS 学习体验,阅读全文可能需要 3 小时,可能 30 分钟,也可能 3 分钟,还可能 3 秒,之后我会分析分析有多少读者只坚持了 3 秒,继续改进。

image

欢迎回到从《虾票票》带你入门 iOS 系列(3)—— 常用 UI 组件。这一篇主要聊聊iOS 的系统架构、UIKit 组件的重要成员以及它们的基本使用和特性、《虾票票》UI 解剖

iOS 系统架构#

iOS 的系统架构主要分为 4 层,从下到上分别是核心系统层(Core OS)、核心服务层(Core Services)、媒体层(Media)以及触摸层(Cocoa Touch),再往上就构成了一个应用(Application)。

image

从更细的粒度再看一下这 4 层的主要组成框架:

image

对于入门,其实主要关注:触摸层的「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)栈结构管理子视图:其可以添加多个子视图,后添加的视图展示在先添加的上面,父视图可以管理子视图的视图层级。

image

从上面这张图很容易看出两个方块添加的顺序,先红后绿。

⚠️:

  • 这样的特性很可能是交互失效的原因之一,因为对于有重合的 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,与上文呼应了❗️

image

RootView 就是 UIViewController 对象自带的一个 UIView 对象。

2)下面是它的生命周期,开发者可以选择在合适的时机重写并添加一些自定义操作。

  • init ->loadView->
  • viewDidLoad->viewWillAppear->viewDidAppear->
  • viewWillDisappear->viewDidDisappear->
  • dealloc

image

  • 这张图引自 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

主要有以下这些:

image

上面代码里设置的 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)设置手势

image

《虾票票》详情页面里的标签栏(电影简介、演员信息、更多信息)是基于 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 属性

image

再看看《虾票票》这张动图,它的 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 个待实现方法的作用:

image

这份电影表格里的电影数量是根据网络请求的数据决定的;每个 Cell 的格式是可以复用的;行高固定;点击某一部电影后的画面请自行脑补。

特性#

1)UITableView 只负责展示,数据、Cell 及交互需要开发者提供

i. dataSource:数据源、Cell 内容等

ii. delegate:Cell 交互、配置等

2)复用池

因为生成和销毁 Cell 很耗性能,所以有了复用池机制,其基本原理如下:

image

复用池的底层实现是基于双端队列(dequeue),向上滑动屏幕时,上面的 Cell 消失,被系统自动回收,放入复用池,下面的 Cell 需要加载,先根据其唯一标识 id 去复用池里检索,如果找到了,就可以成功复用,否则,再手动生成相应 Cell,并将其注册到复用池中,绑定 id。

从下图可以体会复用池的精髓:

image

往上滑动时,新加载的 Cell 不仅复用了 Cell 的样式,还把右边绿色的勾选✅也复用了。

实际使用中,这样做是有些滑稽的,所以我们一般会在复用 Cell 后,设置其数据,上面基本使用的第 3 步有提到;或者,重写 prepareForReuse 方法,在每次复用 Cell 前,清理不需要的数据。(注:prepareForReuse 是 UITableViewCell 的方法)

到这里,我们再对 UITableView 的完整组成进行了解:

image

刚刚我们展示的《虾票票》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:

image

该 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:

image

在点击 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 种方式不同,它一般用于不同业务界面之间的切换,并且只能逐级返回。

image

  1. show(iOS 8.0+)

该方式会根据 ViewController 的类型自动选择切换方式,比如 splitViewController 的切换方式如下图:

image

一般见于 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 组件组成的,直接上图!

image

👏今天是否收获满满呢?如果你可以复现上图,那你对 UIKit 的掌握一定很不错啦!欢迎留言说说你的看法,或者对哪里有疑问。

下周再见#

我们会聊到 Xcode 里的调试大法,敬请期待。

附录#

1)Foundation

image

2)UIKit

image

通过观察组件的父类,可以很方便地定位组件的功能。

3)MVC 模式

image

MVC 模式是一种软件架构模式,MVC 是三个单词的首字母缩写,它们分别是 Model(模型)、View(视图)和 Controller(控制):

1)Model,是程序需要操作的数据或信息。

2)View,是提供给用户的操作界面,是程序的外壳。

3)Controller,在上面两者中间,根据用户在 View 中输入的指令,选取 Model 中的数据,并进行相应的操作,产生最终结果。

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.