第3章 表视图
学习目标
- 掌握表视图的组成,明确不同类型表视图的内部结构。
- 掌握表视图的创建、修改,会为表视图添加索引。
- 掌握表视图的UI设计模式,掌握分页、下拉刷新模式的使用。
在iOS应用中,经常需要展示一些数据列表,例如,iOS系统自带的Setting(设置)、通信录等,这些数据列表不仅可以有规律地展示数据,而且还可以多层次嵌套数据。通常来讲,我们将这种用于显示数据列表的视图对象称为表视图,它普遍运用于iOS的应用程序中,是开发中最常用的视图之一,本章将针对表视图进行详细讲解。
3.1 表视图基础
3.1.1 表视图的组成
在众多App中,到处可以看到各种各样的表格数据,通常情况下,这些表格数据都是通过表视图展示的。表视图不仅可以显示文本数据,还可以显示图片,为了帮助大家更好地掌握表视图的显示方式,接下来,通过一张图来分析表视图的组成,如图3-1所示。
图3-1 表视图的组成部分
图3-1所示的表视图包括很多组成部分,这些组成部分所代表的含义具体如下。
- 表头视图(tableHeaderView):表视图最上面的视图,用来展示表视图的信息。
- 表脚视图(tableFooterView):表视图最下面的视图,用来展示表视图的信息。
- 单元格:(cell):组成表视图每一行的单位视图。
- 分区:(section):具有相同特征的多个单元格组成。
- 分区头:(sectionHeader):用来描述每一节的信息。
- 分区脚:(sectionFooter):用来描述节的信息和声明。
在iOS中,表视图使用UITableView表示,它继承自UIScrollView,并且拥有两个非常重要的协议,分别是UITableViewDelegate委托协议和UITableViewDataSource数据源协议。由于UITableView并不负责存储表中的数据,因此,它需要从遵守这两个协议的对象中获取配置的数据。关于表视图委托协议和数据源协议的具体讲解,将在后面的小节中进行详细介绍。
3.1.2 表视图样式设置
在移动应用中,不同应用所包含的表视图的风格也不尽相同。iOS中的表视图分为普通表视图和分组表视图,接下来,通过一张图来描述这两种表视图的区别,如图3-2所示。
图3-2 表视图的两种样式
在图3-2中,左边样式的表视图是普通表视图,右边样式的表视图是分组表视图,它们在视觉上的差异在于分组表视图是将数据按组进行区分。
其实,除了视觉方面的设计,表视图的样式还可以通过设置属性来体现。表视图属性的设置分为两种方式,这两种设置方式具体如下。
1.在属性检查器中设置表视图样式
进入Storyboard界面,从对象库中将Table View控件拖曳到界面中,在属性检查器面板中有一个Style属性,该属性所支持的选项如图3-3所示。
图3-3 表视图的属性检查器面板
从图3-3中可以看出,表视图的Style样式支持两个选项,分别是Plain和Grouped,其中,Plain用于指定普通表视图,Grouped用于指定分组表视图。
2.通过代码设置表视图样式
当通过代码创建表视图时,可以调用initWithFrame方法来实现,该方法的语法格式如下所示:
- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style;
在上述方法中,参数style用于指定表视图的样式,它是一个枚举类型,包含两个值,具体语法格式如下所示:
typedef NS_ENUM(NSInteger, UITableViewStyle) {
UITableViewStylePlain,
UITableViewStyleGrouped
};
在上述枚举类型中,UITableViewStylePlain用于指定普通表视图,UITableViewStyleGrouped用于指定分组表视图,它们的功能和在属性检查器面板中设置Style属性相同。
3.1.3 数据源协议
设置好表视图的样式后,需要给表视图设置数据。在iOS中表视图显示的数据都是从遵守数据源协议(UITableViewDataSource)的对象中获取的,在配置表视图的时候,表视图会向数据源查询一共有多少行数据,以及每一行显示什么数据等。为此,UITableViewDataSource提供了相关的方法,其中最重要的3个方法见表3-1。
表3-1 UITableViewDataSource的主要方法
方法名 |
功能描述 |
---|---|
- (NSInteger)numberOfSectionsInTableView:(UITableView*) tableView; |
返回表视图将划分为多少个分区 |
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section; |
返回给定分区包含多少行,分区编号从0开始 |
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath; |
返回一个单元格对象,用于显示在表视图指定的位置 |
表3-1列举的3个方法中,tableView:numberOfRowsInSection: 和tableView:cellForRowAtIndexPath:这两个方法必须实现,否则程序会发生异常;而当表视图有多个分组的时候,numberOfSectionsInTableView这个方法用于指定表视图分组的个数,因此,该方法也必须实现。
3.1.4 委托协议
除数据源协议外,与表视图相关的还有一个委托协议(UITableViewDelegate)。表视图的委托协议包含多个对用户在表视图中执行的操作进行响应的方法,例如,选中某个单元格、设置单元格高度等。表3-2列举了表视图代理协议(UITableViewDelegate)提供的一些方法。
表3-2 UITableViewDataDelegate的主要方法
方法名 |
功能描述 |
---|---|
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath; |
响应选择表视图单元格时调用的方法 |
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath; |
设置表视图中单元格的高度 |
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section; |
设置指定分区头部的高度,其中参数section用于指定某个分区 |
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(Integer)section; |
设置指定分区头部要显示的视图,其中参数section用于指定某个分区 |
- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(Integer)section; |
设置指定分区尾部显示的视图,其中参数section用于指定某个分区 |
- (Integer)tableView:(UITableView *)tableView indentationLevelForRowAtIndexPath:(NSIndexPath *)indexPath; |
设置表视图中单元格的等级缩进(数字越小等级越高) |
表3-2列举了委托协议中的一些常用方法,其中didSelectRowAtIndexPath方法是响应选择单元格时调用的,从选择单元格到触摸结束,再到编辑单元格,我们只需要向该方法传递一个NSIndexPath对象,指出触摸的位置,就可以对触摸所属的分区和行做出响应。
3.1.5 单元格的组成和样式
单元格作为构成表视图的最主要元素,掌握它的组成结构是非常重要的。默认情况下,单元格由图标(imageView)、标题(textLabel)、详细内容(detailTextLabel)等组成,这些组成在单元格中的排列方式如图3-4所示。
图3-4 默认单元格(cell)的组成
图3-4所示的是一个单元格,它包含单元格内容和扩展视图两部分,其中,单元格内容视图中的图标、标题、详细内容都可以根据需要进行选择性设置。当然,单元格本身也有很多显示的样式,通常情况下,我们会在调用initWithStyle方法初始化单元格的时候设置样式,initWithStyle方法的语法格式如下所示。
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
上述方法中,参数reuseIdentifier是用来表示重用的标识符,参数style用于指定单元格的样式,它所属的类型UITableViewCellStyle是一个枚举类型,UITableViewCellStyle的具体语法格式如下所示。
typedef NS_ENUM(NSInteger, UITableViewCellStyle) {
UITableViewCellStyleDefault, // 默认的单元格样式
UITableViewCellStyleValue1, // 有图标带有主标题的单元格样式
UITableViewCellStyleValue2, // 无图标带有详细内容的单元格样式
UITableViewCellStyleSubtitle // 带有详细内容的单元格样式
};
从上述语法可以看出,UITableViewCellStyle的值有4个,说明单元格可以设置4种样式。为了大家更好地区分这4种样式,接下来,将相同的数据按照不同的样式进行展示,从而体现出4种样式不同的效果,具体如下。
- UITableViewCellStyleDefault:默认样式,只有图标和标题,效果如图3-5所示。
图3-5 默认样式
- UITableViewCellStyleValue1:带图标、标题和详细内容的样式,详细内容位于最右侧,效果如图3-6所示。
图3-6 详细内容位于右侧的样式
- UITableViewCellStyleValue2:无图标带详细内容的样式,效果如图3-7所示。
图3-7 无图标带详细内容的样式
- UITableViewCellStyleSubtitle:带图标、标题和详细内容的样式,详细内容位于标题下方,效果如图3-8所示。
图3-8 详细内容位于标题下方的样式
同理,扩展视图的样式也有很多种,它可以使用苹果公司提供的固有样式,也可以自定义。扩展视图是在枚举类型UITableViewCellAccessoryType中定义的,其定义的语法格式如下所示。
typedef NS_ENUM(Integer, UITableViewCellAccessoryType) {
UITableViewCellAccessoryNone,
UITableViewCellAccessoryDisclosureIndicator,
UITableViewCellAccessoryDetailDisclosureButton,
UITableViewCellAccessoryCheckmark,
UITableViewCellAccessoryDetailButton
};
从上述语法可以看出,UITableViewCellAccessoryType类包含5个常量,其中第一个常量表示没有扩展图标,而其他4个常量所表示的样式如下所示。
- UITableViewCellAccessoryDisclosureIndicator:扩展样式,图标样式为,效果如图3-9所示。
图3-9 扩展样式
- UITableViewCellAccessoryDetailDisclosureButton:细节样式,图标,效果如图3-10所示。
图3-10 细节样式
- UITableViewCellAccessoryCheckmark:选中样式,图标为,表示该行被选中,效果如图3-11所示。
图3-11 选中样式
- UITableViewCellAccessoryDetailButton:细节展示样式,图标为,用于展示单元格的具体细节,效果如图3-12所示。
图3-12 细节展示样式
3.2 实战演练——汽车品牌
通过3.1节的学习,我们已经了解了表视图的基础知识,但是,表视图的形式灵活多变,本着由浅入深的原则,本节将通过一个展示汽车品牌的案例来讲解表视图的其他知识,包括简单表视图的创建、为表视图添加搜索栏以及添加表视图的索引等。
3.2.1 实战演练——创建简单表视图
表视图可以通过storyboard和代码两种方式创建,但无论采用哪种方式,都需要通过遵守数据源协议UITableViewDataSource和委托协议UITableViewDelegate的代理对象来设置数据,这两个协议对于表视图来说是非常重要的。接下来,通过一张图来描述这两个协议在创建表视图中的作用,如图3-13所示。
图3-13 创建并为表视图配置数据
从图3-13中可以看出,创建并为表视图配置数据的过程大体可以分为5步,具体如下。
(1)通过调用initWithFrame:style方法初始化表视图,并且设置表视图的样式。
(2)为表视图设置数据源和代理,其中,数据源必须遵守UITableViewDataSource@协议,代理方必须遵守UITableViewDelegate@ 协议。
(3)表视图向数据源发送numberOfSectionsInTableView: 消息,数据源会返回分组的个数,如果表视图有多个分组,那么该方法必须被实现。
(4)表视图向数据源发送numberOfRowsInsection: 消息,数据源会返回每个分组的行数。
(5)数据源接收到tableView:cellForRowAtIndexPath: 这个消息,来设置每个分组中每行要显示的数据,即为单元格填充数据。
掌握了表视图的创建方式后,接下来,通过一个展示汽车品牌的案例来演示如何创建一个简单视图,具体步骤如下。
1.创建工程,设计界面
(1)新建一个Single View Application应用,名称为CarBrand,然后在Main.storyboard界面中添加一个Table View控件,并将View Controller的尺寸设置为3.5-inch,如图3-14所示。
图3-14 添加表视图控件
(2)为Table View设置数据源和代理对象。右键单击storyboard中的Table View控件,将数据源dataSource和代理delegate设置到控制Table View的View Controller上,设置完成后的界面如图3-15所示。
图3-15 为表视图设置数据源和代理
2.对应用程序资源进行配置
表视图需要展示大量的数据,在此,将汽车品牌的所有信息存储在Car.plist文件中,并将plist文件导入Supporting文件,导入后的plist文件结构如图3-16所示。
图3-16 Car.plist文件
图3-16所示的是Car.plist文件中的一部分数据,其中,最外层的Item包含两部分,一部分是Array类型的cars,一部分是String类型的title。其中,cars是一个包含多个Dictionary类型的集合,它里面的每个元素都包含一个String类型的icon和一个String类型的name,分别表示汽车的图标路径和名称。
由于展示汽车品牌时,需要加载汽车的图标,因此,我们将表示汽车图标的png格式图片添加到Images.xcassets文件夹中,添加后的界面如图3-17所示。
图3-17 添加汽车图标
3.创建单元格模型
在plist文件中,每个cars选项都是一个数组,该数组中的每个元素都是一个单元格,将单元格抽成一个模型,同样提供创建单元格的初始化方法。新建单元格的模型类Car,在Car.h文件中声明属性和方法,代码如例3-1所示。
【例3-1】Car.h
1 #import <Foundation/Foundation.h>
2 @interface Car : NSObject
3 // 用于表示汽车名称的属性
4 @property(nonatomic,copy)NSString *name;
5 // 用于表示汽车图标路径的属性
6 @property(nonatomic,copy)NSString *icon;
7 +(instancetype)carWithDict:(NSDictionary *)dict;
8 -(instancetype)initWithDict:(NSDictionary *)dict;
9 @end
在Car.m中对模型的属性进行初始化,代码如例3-2所示。
【例3-2】Car.m
1 #import "Car.h"
2 @implementation Car
3 +(instancetype)carWithDict:(NSDictionary *)dict{
4 return [[self alloc]initWithDict:dict];
5 }
6 -(instancetype)initWithDict:(NSDictionary *)dict{
7 if (self=[super init]) {
8 [self setValuesForKeysWithDictionary:dict];
9 }
10 return self;
11 }
12 @end
4.创建分区模型
根据car.plist文件存储数据的特点,表视图的每个分区都有单元格数组和分区头,这时,我们需要将分区抽成一个模型,并提供一个创建分区的初始化方法。新建分区模型类CarGroup,在CarGroup.h文件中声明属性和方法,代码如例3-3所示。
【例3-3】CarGroup.h
1 #import <Foundation/Foundation.h>
2 @interface CarGroup : NSObject
3 // 用于表示标题的属性
4 @property(nonatomic,copy)NSString *title;
5 // 用于表示单元格数组的属性
6 @property(nonatomic,strong)NSArray *cars;
7 // 用于初始化模型
8 -(instancetype)initWithDict:(NSDictionary *)dict;
9 +(instancetype)groupWithDict:(NSDictionary *)dict;
10 @end
在CarGroup.m中对模型的属性进行初始化,代码如例3-4所示。
【例3-4】CarGroup.m
1 #import "CarGroup.h"
2 #import "Car.h"
3 @implementation CarGroup
4 +(instancetype)groupWithDict:(NSDictionary *)dict{
5 return [[self alloc]initWithDict:dict];
6 }
7 -(instancetype)initWithDict:(NSDictionary *)dict{
8 if (self=[super init]) {
9 // 为标题赋值
10 self.title=dict[@"title"];
11 // 取出原来的字典数组
12 NSArray *arr=dict[@"cars"];
13 NSMutableArray *carsArray=[NSMutableArray array];
14 for (NSDictionary *dict in arr) {
15 Car *car=[Car carWithDict:dict];
16 [carsArray addObject:car];
17 }
18 self.cars=carsArray;
19 }
20 return self;
21 }
22 @end
5.为表视图填充数据
在ViewController.m文件中,加载plist文件中的数据,并通过实现UITableViewDataSource协议中的numberOfSectionsInTableView: 、tableView:numberOfRowsInSection: 和tableView: cellForRowAtIndexPath: 方法为表视图填充数据,代码如例3-5所示。
【例3-5】ViewController.h
1 #import "ViewController.h"
2 #import "CarGroup.h"
3 #import "Car.h"
4 @interface ViewController ()<UITableViewDataSource>
5 @property (weak, nonatomic) IBOutlet UITableView *tableview;
6 @property(nonatomic,strong)NSArray *groups;
7 @end
8 @implementation ViewController
9 - (void)viewDidLoad {
10 [super viewDidLoad];
11 // 设置单元格的高度
12 self.tableview.rowHeight=60;
13 }
14 // 隐藏状态栏
15 -(BOOL)prefersStatusBarHidden{
16 return YES;
17 }
18 // 懒加载plist文件中的数据
19 -(NSArray *)groups{
20 if (_groups==nil) {
21 //1.获取plist文件的路径
22 NSString *path=[[NSBundle mainBundle] pathForResource:
23 @"cars_total.plist" ofType:nil];
24 //2.加载数组
25 NSArray *dictArray=[NSArray arrayWithContentsOfFile:path];
26 //3.将dictArray里面的所有字典转为模型对象,放在新的数组中
27 NSMutableArray *groupArray=[NSMutableArray array];
28 for (NSDictionary *dict in dictArray) {
29 CarGroup *group=[CarGroup groupWithDict:dict];
30 [groupArray addObject:group];
31 }
32 _groups=groupArray;
33 }
34 return _groups;
35 }
36 // 设置表视图的分组个数
37 -(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
38 return self.groups.count;
39 }
40 // 设置表视图中每组的行数
41 -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:
42 (NSInteger)section{
43 CarGroup *group=self.groups[section];
44 return group.cars.count;
45 }
46 // 设置每行要展示的数据
47 -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:
48 (NSIndexPath *)indexPath{
49 static NSString *ID=@"car";
50 // 从缓冲池中获取单元格对象
51 UITableViewCell *cell=[tableView dequeueReusableCellWithIdentifier:ID];
52 if (cell==nil) {
53 cell= [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault
54 reuseIdentifier:ID];
55 }
56 CarGroup *groups=self.groups[indexPath.section];
57 Car *car=groups.cars[indexPath.row];
58 cell.imageView.image=[UIImage imageNamed:car.icon];
59 cell.textLabel.text=car.name;
60 return cell;
61 }
62 // 设置表视图的头部,即
63 -(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:
64 (NSInteger)section{
65 CarGroup *group=self.groups[section];
66 return group.title;
67 }
68 @end
在例3-5中,第18~35行代码使用懒加载获取plist文件中的数据,并将plist文件中的数据封装到数组中;第46~61行代码用于设置每个单元格的数据,其中,第51行代码通过调用tableView的dequeueReusableCellWithIdentifier方法从缓冲池中获取单元格对象,如果缓冲池中没有可使用的单元格对象,则创建新的单元格对象,从而避免重复创建对象,造成内存浪费。
运行程序,结果如图3-18所示。
图3-18 汽车品牌运行结果
多学一招:单元格的重用
在设置表视图的数据时,通常都会在tableView:cellForRowAtIndexPath方法中创建UITableViewCell对象,如果用UITableView显示成千上万条数据,就需要成千上万个UITableViewCell对象的话,就会耗尽iOS设备的内存,此时我们需要重用UITableViewCell对象。
重用UITableViewCell对象的原理比较简单,当滚动列表时,部分UITableViewCell会移出窗口,UITableView会将窗口外的UITableViewCell放入一个对象池中,等待重用,当UITableView要求dataSource返回UITableViewCell时,dataSource首先会查看这个对象池,如果池中有未使用的UITableViewCell,dataSource会使用新的数据配置这个UITableViewCell,然后返回给UITableView,重新显示到窗口中,从而避免创建新对象。TableView提供了一个从对象池中获取UITableViewCell对象的方法,具体示例如下:
UITableViewCell *cell=[tableView dequeueReusableCellWithIdentifier: @"A"];
从上述代码可以看出,当使用dequeueReusableCellWithIdentifier方法获取UITableViewCell对象时,需要传递一个字符串类型的标识符,这是因为对象池中包含很多不同类型的UITableViewCell,如果不为UITableViewCell设置标识符,那么在重用UITableView时,会得到错误类型的UITableViewCell。
UITableViewCell有个NSString *reuseIdentifier属性,可以在初始化UITableViewCell的时候传入一个特定的字符串标识来设置reuseIdentifier(一般用UITableViewCell的类名)。当UITableView要求dataSource返回UITableViewCell时,先通过一个字符串标识到对象池中查找对应类型的UITableViewCell对象,如果有,就重用,如果没有,就传入这个字符串标识来初始化一个UITableViewCell对象,具体示例如下:
cell= [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"A"];
多学一招:NSBundle类
在开发iOS的过程中,开发者一般会先用Xcode和iOS模拟器进行模拟开发,开发者此时把应用程序所要用到的本地资源(图像,声音,nib文件,以及代码文件等)都放在PC端中。但是iOS应用程序本身是用于移动设备的,应用程序中并非所有的资源都是网络实时加载的,如果想要将这些资源加载到移动设备上,需要对这些资源进行整合打包,再通过一定的方式安装到用户的移动设备中。
iOS就提供了NSBundle类来管理这些资源,NSBundle类的实例对象可以获取一个程序的代码和资源在系统中的位置,它可以动态加载和卸载代码,开发者可以通过使用应用程序、框架、插件等项目类型来创建一个NSBundle对象。
应用程序本身就是一个NSBundle对象,在Mac OS X 系统的Finder中,一个应用程序看上去和其他文件没有什么区别,但是实际上它是一个包含了nib文件,编译代码,以及其他资源的目录。 我们把这个目录叫作程序的main bundle。mainbundle是应用程序文件包,它是一个以应用名字命名且以.app为后缀名的文件夹。
3.2.2 实战演练——添加索引
当表视图中有大量的数据时,如果想缩小查找范围,可以通过为表视图添加索引来实现大量数据查询功能,iOS系统中的通信录就是一个表视图,它可以通过索引来查询数据,如图3-19所示。
在图3-19中,右边一栏是索引,索引中的每一个字母代表的是一组数据,当单击索引列时,屏幕显示的内容会定位到对应字母开头的一组数据。例如,A字母开头代表A字母开头的所有单词。
当为表视图添加索引时,若想正确使用索引,需要注意遵循以下原则。
索引标题最好不要与单元格的标题一样,否则索引就失去了它存在的意义。
索引标题要具有简洁代表性,能表示一个分区的共同特点。
如果采用了索引列表,最好就不要再使用附加视图,否则会出现莫名的冲突。单击索引标题时,很容易点到扩展视图。
在iOS中,为表视图添加索引的方式比较简单,直接实现sectionIndexTitlesForTableView方法即可,该方法用于在表格右边建立一列浮动的索引,接下来,在3.2.1小节的基础上,在ViewController.m文件中为表视图添加索引,具体实现代码如下所示:
1 -(NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView{
2 return [self.groups valueForKey:@"title"];
3 }
上述代码中,将CarGroup中的title属性作为索引要显示的数据,运行程序,结果如图3-20所示。
图3-19 带有索引的通信录
图3-20 添加索引后的汽车品牌展示
3.2.3 实战演练——添加搜索栏
当表视图中有大量数据的时候,即表视图中含有很多行的时候,用户很难找到指定范围内的数据。UIKit提供了UISearchBar类,它用于创建搜索栏的实例对象。一般情况下,搜索栏位于表视图的上方,用户可以通过向搜索栏输入相关信息,从而缩小查询范围。搜索栏的样式有很多种,接下来,通过一张表来描述,见表3-3。
表3-3 搜索栏的样式
搜索栏样式 | 功能描述 |
---|---|
基本搜索栏,用于提示用户输入查询关键字,搜索框的Placeholder属性可以设置这个信息 | |
书签按钮搜索栏,一般用于显示用户收藏的书签列表 | |
取消按钮搜索栏,用于用户可以通过单击该按钮激发特定的事件 | |
查询结果搜索栏,一般用于显示最近的搜索结果 | |
附加scope搜索栏,用来创建搜索框下方的分段条,分段条可以创建多个选择按钮,通过单击不同的选择按钮来通知表视图来进一步明确选择范围 |
在表3-3列举的搜索栏样式中,除第一种基本样式外,其他样式都可以直接在属性检查器面板中设置。将对象库中的Search Bar控件拖曳到storyboard面板,查看搜索栏在属性检查器中的Options属性设置,如图3-21所示。
图3-21 搜索栏的Options属性
相比其他控件来说,搜索栏是一个比较复杂的控件,它会涉及事件响应的处理。在iOS中,UISearchBarDelegate是UISearchBar定义的委托协议,该协议定义了许多响应事件的方法,见表3-4。
表3-4 UISearchBarDelegate提供的常用方法
事件类型 | 方法 | 功能描述 |
---|---|---|
编辑输入事件 | –searchBar:textDidChange: | 搜索栏文本发生变化时响应 |
–searchBar:shouldChangeTextInRange:replacementText: | 当前文本即将被特定文本替换时响应 | |
–searchBarShouldBeginEditing: | 搜索栏即将开始输入时响应 | |
–searchBarTextDidBeginEditing: | 搜索栏输入后响应 | |
–searchBarShouldEndEditing: | 搜索栏即将结束输入时响应 | |
–searchBarTextDidEndEditing: | 搜索栏结束编辑时响应 | |
按钮单击事件 | –searchBarBookmarkButtonClicked: | 书签按钮搜索栏的书签按钮被单击时响应 |
–searchBarCancelButtonClicked: | 取消按钮搜索栏的取消按钮被单击时响应 | |
–searchBarSearchButtonClicked: | 软键盘search按钮被单击的时候响应 | |
–searchBarResultsListButtonClicked: | 查询结果搜索栏的查询按钮被单击时响应 | |
Scope按钮单击事件 | –searchBar:selectedScopeButtonIndexDidChange: | scope搜索栏按钮单击发生变化时响应,按钮从左数,第一个按钮Index为0 |
从表3-4中,UISearchBarDelegate的方法主要分为编辑输入事件、按钮单击事件及Scope按钮单击事件,其中按钮单击事件中的4个方法分别作用于不同样式的搜索栏。
接下来,在3.2.2小节的案例上,为汽车品牌的案例添加一个搜索栏,当用户在搜索栏中输入信息后,相应的数据会自动过滤。为汽车品牌添加搜索栏的具体步骤如下所示。
(1)在storyboard中,添加一个Search Bar控件,并调整好TableView的位置,如图3-22所示。
图3-22 添加Search Bar控件
(2)为Search Bar控件设置代理,并且设置一个Search Bar控件对象,然后对ViewController.m文件进行修改,遵守UISearchBarDelegate协议,并且声明一个carFilterArray类来存储搜索过滤之后的单元格数据,carFilterArray是使用懒加载获取过滤后数据的,需要调用UISearchBarDelegate提供的一些方法执行搜索框触发的事件,修改后的ViewController.m文件如例3-6所示。
【例3-6】ViewController.m
1 #import "ViewController.h"
2 #import "CarGroup.h"
3 #import "Car.h"
4 @interface ViewController ()<UITableViewDataSource,UISearchBarDelegate,
5 UITableViewDelegate>
6 @property (weak, nonatomic) IBOutlet UISearchBar *searchbar;
7 @property (weak, nonatomic) IBOutlet UITableView *tableview;
8 @property(nonatomic,strong)NSMutableArray *carFilterArray;
9 @property(nonatomic,strong)NSArray *groups;
10 // 根据关键字进行处理
11 -(void)handleSearchForTerm:(NSString *)searchTerm;
12 @end
13 @implementation ViewController
14 - (void)viewDidLoad
15 {
16 [super viewDidLoad];
17 // 设置单元格的高度
18 self.tableview.rowHeight=60;
19 }
20 // 隐藏状态栏
21 -(BOOL)prefersStatusBarHidden
22 {
23 return YES;
24 }
25 -(NSArray *)groups
26 {
27 if (_groups==nil) {
28 //1.获取plist文件的路径
29 NSString *path=[[NSBundle mainBundle] pathForResource:
30 @"cars_total.plist" ofType:nil];
31 //2.加载数组
32 NSArray *dictArray=[NSArray arrayWithContentsOfFile:path];
33 //3.将dictArray里面的所有字典转为模型对象,放在新的数组中
34 NSMutableArray *groupArray=[NSMutableArray array];
35 for (NSDictionary *dict in dictArray) {
36 CarGroup *group=[CarGroup groupWithDict:dict];
37 [groupArray addObject:group];
38 }
39 _groups=groupArray;
40 }
41 return _groups;
42 }
43 -(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
44 {
45 if (self.searchbar.text.length==0) {
46 return self.groups.count;
47 } else {
48 return 1;
49 }
50 }
51 -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:
52 (NSInteger)section
53 {
54 // 判断搜索栏中是否有信息输入
55 if (self.searchbar.text.length==0) {
56 CarGroup *group = self.groups[section];
57 return group.cars.count;
58 } else { //若无信息输入则显示返回carArray的数据
59 return self.carFilterArray.count;
60 }
61 }
62 // 过滤之后的汽车数据
63 - (NSMutableArray *)carFilterArray
64 {
65 if (!_carFilterArray) {
66 _carFilterArray = [NSMutableArray array];
67 }
68 return _carFilterArray;
69 }
70 -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:
71 (NSIndexPath *)indexPath
72 {
73 static NSString *ID=@"car";
74 UITableViewCell *cell=[tableView dequeueReusableCellWithIdentifier:ID];
75 if (cell==nil) {
76 cell= [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault
77 reuseIdentifier:ID];
78 }
79 // 判断搜索栏中是否有信息输入
80 if (self.searchbar.text.length==0) { //若无信息输入则显示carArray的数据
81 CarGroup *groups=self.groups[indexPath.section];
82 Car *car = groups.cars[indexPath.row];
83 cell.imageView.image = [UIImage imageNamed:car.icon];
84 cell.textLabel.text = car.name;
85 } else {
86 // 若有信息输入则显示carFilterArray的数据
87 Car *car1 = self.carFilterArray[indexPath.row];
88 cell.imageView.image = [UIImage imageNamed:car1.icon];
89 cell.textLabel.text = car1.name;
90 }
91 return cell;
92 }
93 -(NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView{
94 return [self.groups valueForKey:@"title"];
95 }
96 -(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:
97 (NSInteger)section
98 {
99 if (self.searchbar.text.length==0) {
100 CarGroup *group = self.groups[section];
101 return group.title;
102 } else {
103 return nil;
104 }
105 }
106 #pragma mark - UISearchBarDelegate
107 //搜索条输入文字发生变化时触发
108 - (void)searchBar:(UISearchBar *)searchBar textDidChange:
109 (NSString *)searchText
110 {
111 [self handleSearchForTerm:searchText];
112 }
113 //根据输入关键字进行处理
114 - (void)handleSearchForTerm:(NSString *)searchTerm
115 {
116 if (self.searchbar.text.length==0) { //若输入文本信息为空则刷新数据
117 [self.tableview reloadData];
118 } else {
119 // 否则先移除刷新数据
120 [self.carFilterArray removeAllObjects];
121 // 先遍历存放CarGroup模型数组
122 for (CarGroup *group in self.groups)
123 {
124 // 内部遍历存放Car模型数组
125 for (Car *car in group.cars)
126 {
127 NSString *str1 = [car.name uppercaseString] ;
128 NSString *str2 = [searchTerm uppercaseString] ;
129 if ([str1 containsString:str2])
130 {
131 [self.carFilterArray addObject:car];
132 }
133 }
134 }
135 [self.tableview reloadData];
136 }
137 }
138 // 触发单元格后调用代理的方法实现键盘的隐藏
139 -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:
140 (NSIndexPath *)indexPath{
141 [self.searchbar resignFirstResponder];
142 }
143 @end
在例3-6中,第107~112行代码通过调用searchBar:textDidChange: 方法监听搜索框文本的改变,然后在第114~137行代码中对搜索框的输入信息进行判断,并根据查询结果重新加载单元格数据,实现单元格数据的动态更新。
运行程序,在搜索框中输入字母As,结果如图3-23所示。
图3-23 添加搜索栏的汽车品牌展示
在图3-23中,单击搜索栏,软键盘会弹出,当用户在搜索栏中输入数据后,表视图中的单元格数据会随着输入的信息随之刷新,并且在搜索栏的最右边有个删除按钮,当此按钮被单击时,搜索栏的文本输入信息为空,表视图显示所有单元格数据。另外,由于例3-6中添加了隐藏键盘的方法,当单击单元格时,软键盘会消失。
3.3 自定义单元格
随着应用业务需求的多样化,UIKit框架为开发者提供的单元格样式显然不能满足需求,这时,我们可以自定义单元格。在iOS 5之前,自定义单元格可以通过代码和XIB技术实现。但在iOS 5之后,自定义单元格还可以通过storyboard实现,这种方式比XIB更简单一些。接下来,我们带领大家使用Storyboard实现一个自定义的单元格,具体步骤如下。
1.原型分析
使用storyboard实现自定义单元格时,首先需要对要实现的原型图进行分析,先来看一下我们要实现的结果图,如图3-24所示。
图3-24 自定义单元格原型图
图3-24左边所示的是一个自定义的单元格,右边是自定义单元格的设计原型,每个单元格都是由3个Label和1个Image组成。
2.创建项目工程,搭建界面
(1)新建一个Single View Application应用,名称为CustomCells。通常情况下,如果整个页面都是Table View,都会让控制器直接继承自UITableViewController,这时,我们进入viewController .h文件,让控制器直接继承UITableViewController,代码如下所示:
#import <UIKit/UIKit.h>
@interface ViewController : UITableViewController
@end
(2)进入storyBoard界面,删除默认的ViewController,直接拖一个Table View Controller,并将其设置为程序的初始ViewController,如图3-25所示。
图3-25 创建工程
(3)选中图3-25所示的Table View Controller,在身份检查器中将Class所属的类设置为ViewController,将Table View Controller和ViewController相关联,如图3-26所示。
图3-26 将控制器和类进行相关联
3.自定义单元格
(1)在storyboard界面的TableView中,默认都会包含一个Cell,但该Cell是UIKit提供的,它满足不了我们的需求,这时,我们需要自定义一个单元格。自定义单元格需要创建一个类,右击工程名,在弹出的菜单中选择New File…后,在打开的对话框中选择iOS中的Cocoa Touch Class模板,单击“Next”按钮,在弹出对话框中的Class中填写NewCell,在Subclass of中选择UITableViewCell作为父类,如图3-27所示。
图3-27 创建自定义单元格类
(2)单击图3-27所示的“Next”按钮,完成NewCell类的创建后,回到storyboard界面,对单元格的布局进行设置并调整,并将Table View Cell的Identifier属性设置为newCell,如图3-28所示。
图3-28 设计单元格界面
(3)选中图3-28所示的Table View Cell,在身份检查器中将Class所属的类设置为NewCell,如图3-29所示。
图3-29 将Cell控件和NewCell类进行关联
(4)设置完成后,为了便于操作单元格中的每个子控件,需要在NewCell类将各个控件进行关联,关联后的界面如图3-30所示。
图3-30 为控件添加属性
4.对应用程序资源进行配置
(1)将要展示在自定义单元格中的数据存储在news.plist文件中,并将plist文件导入SupportingFiles文件夹,导入后的plist文件结构如图3-31所示。
图3-31 plist文件结构
(2)将应用程序要展示的图片资源放到Images.xcassets中,如图3-32所示。
图3-32 图片资源
5.向自定义单元格中填充数据
进入ViewController.m文件,使用懒加载plist文件中的数据,并通过实现UITableView-DataSource协议中的tableView:numberOfRowsInSection: 和tableView: cellForRowAtIndexPath:方法为表视图填充数据,代码如例3-7所示。
【例3-7】ViewController.m
1 #import "ViewController.h"
2 #import"NewCell.h"
3 @interface ViewController ()<UITableViewDataSource,UITableViewDelegate>
4 @property(nonatomic,strong)NSArray *news;
5 @end
6 @implementation ViewController
7 -(NSArray *)news{
8 if (!_news) {
9 NSString *path=[[NSBundle mainBundle]pathForResource:@"news.plist"ofType:nil];
10 _news=[NSArray arrayWithContentsOfFile:path];
11 }
12 return _news;
13 }
14 - (void)viewDidLoad {
15 [super viewDidLoad];
16 self.tableView.rowHeight=70;
17 }
18 -(BOOL)prefersStatusBarHidden{
19 return YES;
20 }
21 #pragma mark dataSource
22 -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:
23 (NSInteger)section{
24 return self.news.count;
25 }
26 -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:
27 (NSIndexPath *)indexPath{
28 NewCell *cell=[tableView dequeueReusableCellWithIdentifier:@"newCell"];
29 NSDictionary *dic=self.news[indexPath.row];
30 cell.title.text=dic[@"title"];
31 cell.author.text=dic[@"author"];
32 cell.comment.text=[NSString stringWithFormat:@"评论:%@",dic[@"comments"]];
33 cell.icon.image=[UIImage imageNamed:dic[@"icon"]];
34 return cell;
35 }
36 @end
在例3-7中,第7~13行代码用于懒加载plist文件中的数据;第26~35行代码用于设置每行单元格显示的内容,其中,第28行代码是使用标识符newCell来获取NewCell类型的对象,这也是为Cell设置Identifier属性的原因所在。
运行程序,结果如图3-33所示。
图3-33 运行结果图
3.4 静态单元格
当表视图要展示大量数据时,通过加载plist文件中的数据填充单元格的做法显然是很方便的,但是,如果表视图中的数据有限,且结构和内容都不需要动态加载,则使用静态单元格比较简便,静态单元格在实际开发中的应用是非常广泛的。接下来,通过一组图片来描述静态单元格的使用场景,如图3-34所示。
图3-34 静态单元格的界面
图3-34所示的界面都是静态单元格,它们的数据是静态的,不会改变的。默认情况下,我们都会在表视图的属性面板中将content属性设置为static cell,从而完成静态单元格的设置。
接下来,以图3-34所示的第一个界面为例,分步骤讲解如何创建静态单元格,具体如下。
1.创建应用程序,将控制器和相关类进行关联
(1)新建一个Single View Application应用,名称为staticCells。通常情况下,如果整个页面都是Table View,都会让控制器直接继承自UITableViewController,这时,我们进入viewController .h文件,让控制器直接继承UITableViewController,代码如下所示:
#import <UIKit/UIKit.h>
@interface ViewController : UITableViewController
@end
(2)进入storyboard界面,删除默认的ViewController,直接拖一个Table View Controller,默认情况下,TableView下会有一个cell,这个cell是动态的,也是看不见的,如图3-35所示。
图3-35 设置表格视图控制器
(3)选中图3-35所示的Table View Controller,在身份检查器中将Class所属的类设置为ViewController,将Table View Controller和ViewController相关联,如图3-36所示。
图3-36 将控制器与类相关联
2.对应用程序资源进行配置
静态表视图需要数据,在此,将相关的图标资源存储在Images.xcassets下,如图3-37所示。
图3-37 添加图片资源
3.在storyboard中设置样式
(1)进入storyboard界面,选中TableView,在右侧的属性检查器面板中,将Content属性设置为Static Cells,Sections属性暂且设置为1,如图3-38所示。
图3-38 设置TableView属性
(2)设置分区中所含单元格的个数。单击storyboard的Document Outline,选中TableViewSection,然后单击右侧的属性检查器,设置TableViewSection的Rows为1,这样可以使其余的单元格设置变得更加灵活方便,如图3-39所示。
图3-39 设置单元格的行数
(3)选中TableView Cell,设置cell的Style为Basic,同时选择对应的Image,设置cell的Accessory(附加视图类型)为Disclosure Indicator,设置完成后的效果如图3-40所示。
图3-40 设置单元格属性
(4)修改图3-40所示的TextLabel,我们可以通过双击控制器中的单元格中的Title输入,也可以展开Table View Cell的层级列表,将文字Title修改为好友动态,修改后的效果如图3-41所示。
图3-41 设置TextLabel属性
(5)选中图3-41所示的Table View Section,通过快捷键“Command+C”和“Command+V”的方式快速粘贴复制分组,并根据每组cell的个数,快速复制cell,复制完成后,将TableView的Style属性设置为Grouped,效果如图3-42所示。
图3-42 设置分区
(6)重复上述第(3)、(4)步,分别对其余的单元格进行图片和文字的设置。需要注意的是,设置完成后,要想运行程序,需要将当前的ViewController设置为初始的ViewController对象。选中ViewController,在属性检查器面板中勾选Is Initial View Controller属性,发现storyboard界面多了一个箭头,如图3-43所示。
图3-43 设置ViewController为入口
(7)运行程序,结果如图3-44所示。
图3-44 运行结果图
3.5 实战演练——通信录
对于表视图,不仅可以浏览数据,有的应用中还需要修改单元格的数据,如动态删除,插入和移动单元格,接下来,本节将通过一个通信录的案例来讲解如何修改单元格。
3.5.1 实战演练——删除和插入单元格
当用户对表视图的单元格进行删除和插入操作时,首先得根据需求选择不同的编辑模式。当进入编辑模式的时候,单元格的左侧就会出现一个删除或插入模式下的显示图标,如图3-45所示。
图3-45 单元格编辑模式
从图3-45中可以看出,当单元格从正常模式进入删除模式后,左侧会出现一个图标,当单击该图标时,右侧会出现一个Delete按钮,用来确认是否删除单元格;当进入插入模式后,会弹出图标,单击该图标,可以执行相应的插入操作。
需要注意的是,如果想从正常模式进入删除或插入模式,首先需要通过调用setEditing: animated:方法设定表视图进入编辑状态,然后调用表视图的委托协议tableView: editingStyleForRowAtIndexPath: 方法来设定单元格编辑图标的位置,当用户删除或者修改控件时,委托方法向数据源发出tableView: commmitEditingStyle: forRowAtIndexPath: 消息来实现删除或者插入的操作,流程如图3-46所示。
图3-46 删除或插入单元格方法的执行流程
接下来,通过一个通信录的案例来演示如何对表视图进行删除和插入操作,具体步骤如下。
(1)新建一个Single View Application应用,名称为TableViewEditMode,然后在Main. storyboard界面中添加一个Toolbar控件,在Toolbar控件中添加3个Bar Button Item控件,效果如图3-47所示。
图3-47 向storyboard面板添加Toolbar控件
(2)由于Toolbar之上的3个Bar Button Item挤在一起不美观,因此,在3个Bar Button Item中插入两个Flexible Space(可变间距)控件,让这3个Bar Button Item等间距分布在Toolbar中,下方添加一个Table View,如图3-48所示。
图3-48 添加Flexible Space控件和TableView控件
(3)在storyboard的Document Outline中分别单击选中3个Bar Button Item,并在属性检查器中依次将属性Identifier为Trash、Custom和Add,其中,中间的Item的Title属性设置为“联系人列表”,设置完成后的效果如图3-49所示。
图3-49 为Item设置属性
(4)为Table View设置数据源和代理对象。右击storyboard中的Table View控件,将数据源dataSource和代理delegate设置到控制Table View的View Controller上,设置完成后的界面如图3-50所示。
图3-50 为表视图设置数据源和代理
(5)由于表视图中要展示的是联系人的姓名和手机号,因此,我们建立一个表示联系人的模型Person。新建类Person,在Person.h文件中声明属性,代码如例3-8所示。
【例3-8】Person.h
1 #import <Foundation/Foundation.h>
2 @interface Person : NSObject
3 @property (nonatomic,copy)NSString *name;// 表示联系人的姓名
4 @property (nonatomic,copy)NSString *phoneNum;// 表示联系人的电话号码
5 @end
(6)在ViewController.m文件中,设置ViewController遵守UITableViewDataSource和UITableViewDelegate协议,并且声明表示单元格编辑模式的editingStyle属性。为了演示单元格中数据的插入和删除,创建一个存储联系人的数组,并将数组中的数据加载到表视图的单元格中,代码如例3-9所示。
【例3-9】ViewController.m
1 #import "ViewController.h"
2 #import "Person.h"
3 @interface ViewController ()<UITableViewDataSource,UITableViewDelegate>
4 @property (weak, nonatomic) IBOutlet UITableView *tableview;
5 @property (nonatomic,strong)NSMutableArray *persons;
6 @property (nonatomic,readwrite)UITableViewCellEditingStyle editingStyle;
7 - (IBAction)remove:(id)sender;
8 - (IBAction)add:(id)sender;
9 @end
10 @implementation ViewController
11 // 添加单元格数据的方法
12 - (IBAction)add:(id)sender {
13 _editingStyle = UITableViewCellEditingStyleInsert;
14 BOOL result = !self.tableview.isEditing;
15 [self.tableview setEditing:result animated:YES];
16 }
17 // 删除单元格数据的方法
18 - (IBAction)remove:(id)sender {
19 _editingStyle = UITableViewCellEditingStyleDelete;
20 BOOL result = !self.tableview.isEditing;
21 [self.tableview setEditing:result animated:YES];
22 }
23 - (NSMutableArray *)persons
24 {
25 if (_persons == nil) {
26 _persons = [NSMutableArray array];
27 for (int i = 0; i < 30; i++) {
28 Person *p = [[Person alloc]init];
29 p.name = [NSString stringWithFormat:@"Person--%d",i];
30 p.phoneNum = [NSString stringWithFormat:@"%d",10000 +
31 arc4random_uniform(1000000)];
32 [_persons addObject:p];
33 }
34 }
35 return _persons;
36 }
37 - (void)viewDidLoad {
38 [super viewDidLoad];
39 _persons = self.persons;
40 }
41 -(BOOL)prefersStatusBarHidden{
42 return YES;
43 }
44 #pragma mark - dataSource
45 //表视图分区的行数
46 -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:
47 (NSInteger)section{
48 return _persons.count;
49 }
50 // 每一行具体的显示
51 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:
52 (NSIndexPath *)indexPath{
53 //1.定义一个标识
54 static NSString *ID = @"cell";
55 //2.去缓存池中取出可循环利用的cell
56 UITableViewCell *cell = [tableViewdequeueReusableCellWithIdentifier:ID];
57 //3.如果缓存池中没有可循环利用的cell
58 if (cell == nil){
59 cell = [[UITableViewCell alloc]initWithStyle:
60 UITableViewCellStyleValue1 reuseIdentifier:ID];
61 }
62 //4.设置数据
63 Person *p = _persons[indexPath.row];
64 cell.textLabel.text = p.name;
65 cell.detailTextLabel.text = p.phoneNum;
66 return cell;
67 }
68 // 设置单元格编辑模式的方法
69 - (UITableViewCellEditingStyle)tableView:(UITableView *)tableView
70 editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath{
71 return _editingStyle;
72 }
73 // 进入编辑模式
74 -(void)tableView:(UITableView *)tableView
75 commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:
76 (NSIndexPath *)indexPath{
77 //判断单元格的编辑模式
78 if(editingStyle == UITableViewCellEditingStyleDelete) {
79 [self.persons removeObjectAtIndex:indexPath.row];
80 // 刷新表格
81 [tableView deleteRowsAtIndexPaths:@[indexPath]
82 withRowAnimation:UITableViewRowAnimationTop];
83 } else {
84 Person *p = [[Person alloc]init];
85 p.name = @"personAdd";
86 p.phoneNum = @"1383876599";
87 [_persons insertObject:p atIndex:indexPath.row+1];
88 // 保持插入的数据与界面的显示相一致
89 NSIndexPath *path =
90 [NSIndexPath indexPathForRow:indexPath.row + 1inSection:0];
91 [tableView insertRowsAtIndexPaths:@[path]withRowAnimation:
92 UITableViewRowAnimationMiddle];
93 }
94 }
95 @end
在例3-9中,第12~22行代码是单击插入和删除按钮所调用的方法;第23~36行代码使用懒加载,创建了30个Person对象,并且设置了对象的属性;第69~94行代码用于设置单元格的编辑模式,并且进入编辑模式,执行不同模式下的操作,其中第78~83行代码用于实现单元格的删除操作,第84~93行代码用于实现单元格的插入操作。
(7)单击Xcode工具的运行按钮,在模拟器上运行程序。程序运行成功后,单击删除按钮,删除第1个单元格数据,效果如图3-51所示。
图3-51 删除单元格操作
同理,单击插入单元格的按钮,会弹出绿色圆形添加按钮;单击圆形添加按钮之后,对应索引的单元格下面就会插入新的单元格,效果如图3-52所示。
图3-52 插入单元格操作
3.5.2 实战演练——移动单元格
用户在使用的时候会对单元格进行重新排列,将这种改变称之为移动单元格,移动单元格与插入删除单元格类似,都需要单元格先进入编辑模式。移动单元格首先需要进入移动编辑模式,随后单元格内容之后会出现移动按钮,单击移动按钮,可以对单元格进行拖动,移动单元格的模式如图3-53所示。
图3-53 转变为移动模式
同删除或插入单元格类似,当移动单元格时需要实现数据源tableView: canMove Row AtIndexPath: 和tableView: moveRowAtIndexPath:toIndexPath:方法,其中tableView: move RowAtIndexPath:toIndexPath: 方法必须实现,而tableView:canMoveRowAtIndexPath: 方法可以选择性实现,另外,默认情况下,该方法的返回值为YES,表示单元格可以移动,移动单元格的方法执行流程如图3-54所示。
图3-54 移动单元格方法的执行流程
接下来,对3.5.1小节的案例进行修改,在ViewController.m文件中,添加移动单元格的代码,具体代码如下所示。
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:
(NSIndexPath *)indexPath
{
return YES;
}
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:
(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath*)destinationIndexPath
{
// 1.取出要拖动的模型数据
Person *p = _persons[sourceIndexPath.row];
// 2.删除之前行的数据
[_persons removeObject:p];
// 3.插入数据到新的位置
[_persons insertObject:p atIndex:destinationIndexPath.row];
}
单击Xcode工具的运行按钮,在模拟器上运行程序,程序运行后,单击Trash按钮进入编辑模式,然后对单元格进行拖动,效果如图3-55所示。
图3-55 移动单元格操作
3.6 表视图UI设计模式
开发iOS应用时,经常会用到设计模式,例如,代理模式,同理,在表视图的UI设计上,也有两种对应的设计模式,分别是分页模式和下拉刷新模式,这两种模式广泛应用于移动平台,并且已经成为移动平台开发的标准,接下来,本节将针对这两种模式进行详细讲解。
3.6.1 分页模式
在iOS开发中,一次性加载大量数据,不仅影响应用的性能,且易造成网络的堵塞。针对这种问题,表视图提供了分页模式,它通过限定请求数据的数量,将所有数据采用分段请求的方式展示到表视图内。例如,新浪微博页面一次请求20条数据,当翻动屏幕到已显示的20条数据后,应用程序会再次请求20条数据,从而实现分页效果。接下来,看一个使用分页模式的应用,如图3-56所示。
图3-56 分页模式的使用场景
图3-56所示的是一个使用分页模式的应用,该应用在展示列表时,先是请求少量的数据,然后翻动屏幕到显示的最后一条数据后,会再次请求固定数量的数据,实现分页的效果。
根据触发方式的不同,请求可分为主动请求和被动请求两种,关于这两种请求的具体讲解如下所示。
1.主动请求
主动请求指的是满足条件时,再次请求的20条数据是自动发出的,并且一般在表视图的表脚会出现活动指示器,请求结束后,活动指示器会自动隐藏起来,如图3-57所示。
图3-57所示的是主动请求数据的方式,该方式的请求数据是自动发出的,同时带有一个活动指示器。
2.被动请求
被动请求指的是条件满足时,表视图的表脚中会显示一个响应单击事件的控件,这个控件通常会是一个按钮,按钮标签上设有“更多”的字样。单击“更多”按钮时,应用会向服务器发送请求,请求结束后,“更多”按钮会隐藏起来,如图3-58所示。
图3-57 主动请求数据
图3-58 被动请求数据
图3-58所示的是被动请求数据的方式,当单击更多按钮时,应用程序会向服务器请求更多的数据,并且隐藏“更多”按钮。
3.6.2 下拉刷新模式
下拉刷新(Pull-to-Refresh)即为重新刷新表视图或者列表,以此重新加载数据,这种模式广泛应用于移动平台,它与分页的操作刚好相反,当翻动到屏幕顶部后,如果继续向下拉动屏幕,程序会重新请求数据,同时表视图表头部分会出现等待指示器,当请求结束表视图表头消失。例如,网易新闻中使用了下拉刷新模式,如图3-59所示。
图3-59 网易新闻中的下拉刷新
图3-59所示的是网易新闻的下拉刷新的整个过程,为了大家更好地掌握下拉刷新的整个过程,接下来,以微博的下拉刷新为例,对微博的下拉刷新过程进行拆解,这里假设下拉刷新显示的顶部视图名称为refresh panel,下拉刷新的过程如下所示。
(1)随着用户下拉逐渐显示UITableView的顶部视图refresh panel,如图3-60所示。
图3-60 下拉显示顶部refresh panel
(2)继续下拉UITableView,会出现两种情况,具体如下。
① 若下拉到预设位置,状态文字变为“松开即可刷新”,如图3-61所示。
图3-61 状态文字改为“松开即可刷新”
② 若下拉未达到预设位置,用户手指离开屏幕,UITableView弹回,refresh panel重新隐藏起来,代表操作结束。
(3)下拉到预设位置后,用户手指离开屏幕,refresh panel继续保持显示,状态文字变为“加载中”,后台执行更新数据的操作,如图3-62所示。
图3-62 状态文字改为“加载中”
(4)数据更新完成后,重新隐藏refresh panel,刷新操作完成,如图3-63所示。
图3-63 下拉刷新完成后的效果图
随着下拉刷新的广泛应用,很多开源社区中都有下拉刷新的实现代码,可以供大家参考,例如,Github上的git:https://github.com/leah/PullToRefresh.git。
3.6.3 iOS 7的新特性——下拉刷新控件
随着下拉刷新模式的影响力越来越大,苹果不得不将其列入到自己的规范当中,并在iOS 6 API中推出了下拉刷新控件,如图3-64所示。
图3-64 iOS 6中的下拉刷新
图3-64所示的是iOS 6中的下拉刷新,由图可知,iOS 6中的下拉刷新特别像“胶皮糖”,当“胶皮糖”拉断的时候,就会出现活动指示器。
与iOS 6相比,iOS 7的下拉刷新更提倡扁平化设计,活动指示器替换了“胶皮糖”部分,实现了下拉动画的效果,如图3-65所示。
图3-65 iOS 7中的下拉刷新
图3-65所示的是iOS 7中的下拉刷新,由图可知,下拉到预设位置后,活动指示器出现。
iOS中的下拉刷新是使用UIRefreshControl类实现的,它继承于UIControl:UIView,是一个可以和用户交互,仅适用于表视图的活动控件。UIRefreshControl类定义了一系列下拉刷新的属性,接下来,通过一张表来列举UIRefreshControl的常见属性,见表3-5。
表3-5 UIRefreshControl的常见属性
属性声明 |
功能描述 |
---|---|
@property (nonatomic, readonly, getter=isRefreshing) BOOL refreshing; |
判断下拉刷新控件是否正在刷新 |
@property (nonatomic, retain) UIColor *tintColor; |
设置下拉刷新控件的颜色 |
@property (nonatomic, retain) NSAttributedString *attributedTitle; |
设置下拉刷新控件的状态文字 |
表3-5是UIRefreshControl一些常见的属性,其中attributedTitle属性是NSAttributedString类型,该类型的字符串可以分为好几段,分别可将每段字符串编辑成不同的字体类型,如字体颜色。
除此之外,UIRefreshControl类也提供了两个方法,控制下拉刷新的状态,具体的定义方式如下所示:
// 开始刷新
- (void)beginRefreshing;
// 结束刷新
- (void)endRefreshing;
从上述代码可以看出,这两个方法可以改变下拉刷新控件的状态。例如,数据加载完成之后,调用endRefreshing方法可以结束刷新,隐藏下拉刷新控件。
3.6.4 项目实战——下拉刷新时间数据
为了大家更好地掌握下拉刷新控件的使用,接下来,通过一个下拉刷新数据的案例来学习如何使用UIRefreshController实现数据的刷新,具体步骤如下。
1.创建应用程序,设计界面
(1)新建一个Single View Application应用,名称为UIRefreshControl。通常情况下,如果整个页面都是Table View,都会让控制器直接继承自UITableViewController,这时,我们进入viewController .h文件,让控制器直接继承UITableViewController,代码如下所示:
#import <UIKit/UIKit.h>
@interface ViewController : UITableViewController
@end
(2)进入storyboard界面,删除默认的ViewController,直接拖一个Table View Controller,并将其设置为程序的初始ViewController,如图3-66所示。
图3-66 新建UITableViewController界面
(3)选中图3-66所示的Table View Controller,在身份检查器中将Class所属的类设置为ViewController,将Table View Controller和ViewController相关联,如图3-67所示。
图3-67 将控制器与类相关联
2.在ViewController.m文件中实现下拉刷新的功能
(1)界面设置完成后,首先我们在ViewController.m文件中定义一个数组,用于保存表视图中需要显示的数据,并在ViewController.m的实现部分懒加载数组中的内容,代码如下所示。
#import "ViewController.h"
@interface ViewController ()
// 定义一个数组用于保存时间
@property (nonatomic, strong) NSMutableArray *Times;
@end
@implementation ViewController
// 懒加载数组
- (NSMutableArray *)Times
{
if (_Times == nil) {
_Times = [[NSMutableArray alloc] init];
NSDate *nowDate = [[NSDate alloc] init];
[_Times addObject:nowDate];
}
return _Times;
}
@end
(2)在viewDidLoad方法中,创建一个UIRefreshControl,设置UIRefreshControl的标题及添加事件处理方法,代码如下所示。
- (void)viewDidLoad {
[super viewDidLoad];
// 初始化UIRefreshControl
UIRefreshControl *rc = [[UIRefreshControl alloc] init];
// 设置下拉刷新控件的状态文字“下拉刷新”
rc.attributedTitle = [[NSAttributedString alloc]
initWithString:@"下拉刷新"];
// 监听下拉刷新控件
[rc addTarget:self action:@selector(refreshTableView)
forControlEvents:UIControlEventValueChanged];
// 设置视图控制器的refreshControl属性值为rc
self.refreshControl = rc;
}
在上述代码中,首先初始化UIRefreshControl,然后设置下拉刷新控件的标题内容为“下拉刷新”,并使用addTarget: action: forControlEvents方法为UIControlEventValueChanged事件添加处理方法,即refreshTableView方法,最后将创建好的UIRefreshControl放置于表视图中。
(3)创建refreshTableView方法,实现改变下拉刷新控件标题,添加新的数据的功能,代码如下所示:
1 // 下拉刷新状态改变调用的方法
2 - (void)refreshTableView
3 {
4 if (self.refreshControl.refreshing) { // 正在刷新
5 // 设置下拉刷新控件的状态文字为“加载中”
6 self.refreshControl.attributedTitle =
7 [[NSAttributedString alloc]initWithString:@"加载中…"];
8 // 添加新的数据
9 NSDate *date = [[NSDate alloc] init];
10 // 模拟请求完成2秒后,回调callBackMethod方法
11 [self performSelector:@selector(callBackMethod:)
12 withObject:date afterDelay:2];
13 }
14 }
上述代码用于刷新状态下执行的操作,其中第7行代码将下拉刷新控件的标题内容改为“加载中…”,第11行代码使用performSelector:withObject:afterDelay语句延时调用callBackMethod方法,模拟实现网络请求或者数据库查询的操作。
(4)创建callBackMethod方法,用于结束刷新,回到初始状态。这时,新插入的数据将先显示在列表的首行,代码如下所示。
// 请求完数据后回调的方法
- (void)callBackMethod:(id)object
{
// 结束刷新
[self.refreshControl endRefreshing];
// 恢复下拉刷新控件的状态文字
self.refreshControl.attributedTitle = [[NSAttributedString
alloc]initWithString:@"下拉刷新"];
// 将新数据插入到表格首行
[self.Times insertObject:(NSDate*)object atIndex:0];
// 刷新表格
[self.tableView reloadData];
}
(5)实现UITableView中UITableViewDataSource加载数据的方法,分别为列表设置分组的个数、每组的行数及每行显示的数据,代码如下所示。
#pragma mark - UITableViewDataSource
// 总共有多少组
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
// 每一组有多少行
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
return self.Times.count;
}
// 每一行对应的数据内容
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc]
initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:CellIdentifier];
}
// 设置日期格式
NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
[dateFormat setDateFormat: @"yyyy-MM-dd HH:mm:ss"];
// 设置单元格文本内容
cell.textLabel.text = [dateFormat stringFromDate:
[self.Times objectAtIndex:[indexPath row]]];
// 设置单元格图片内容
cell.imageView.image = [UIImage imageNamed:@"sheep.jpg"];
return cell;
}
(6)为了美观,我们使用preferredStatusBarStyle方法隐藏屏幕顶部的状态栏,代码如下所示。
// 隐藏状态栏
- (BOOL)prefersStatusBarHidden
{
return YES;
}
3.在模拟器上运行程序
单击Xcode工具的运行按钮,在模拟器上运行程序。程序运行成功后,下拉刷新页面,程序的运行结果如图3-68所示。
图3-68 下拉刷新时间结果
多学一招:UITableViewController类和UIRefreshControl类
UITableViewController是表视图的控制器类, iOS 6之后,它添加了一个refreshControl属性,这个属性保持了UIRefreshControl的一个对象指针。UIRefreshControl类的refreshControl属性与UITableViewController配合使用,可以不必考虑下拉刷新布局等问题,UITableViewController会将其自动放置于表视图中。
3.7 本章小结
本章首先针对表视图的基础知识进行了讲解,包含表视图的组成、样式设置及相关的协议,然后通过实战演练的方式,使用展示汽车品牌的案例讲解了如何创建表视图、为表视图添加搜索栏和添加索引,使用通信录的案例讲解了如何删除、插入和移动单元格,最后针对表视图中UI设计模式进行了讲解,希望大家通过本章内容的学习,可以熟练掌握表视图的应用,为步入本书后面的知识打下基础。
【思考题】
1 . 简述UITableViewCell的复用原理。
2 . 实现表视图显示需要设置UITableView的什么属性、实现什么协议?
扫描右方二维码,查看思考题答案!