通过Sample Code逐步学习使用UITableView

在已经接触UITableView并能够对其进行基础的使用之后是时候通过一些Demo来进行练手了。现在,通过一些小Demo来加深UITableView的使用以及运用合理的操作完成一些Demo,其中所涉及到的知识会从最基础的如何使用UITableView开始,随着需求的逐渐的增加而增加。本系列Demo不涉及UITableView的重用机制等深度知识。

本系列中的所有Demo都是源于Apple提供的Sample Code,学习iOS编程最有效最直接的方法就是通过Apple Developer Library,在这里可以找到有Apple提供的Demo、各种开发中使用的类、WWDC视频等等。

你不应该过分依赖于这个Library,它更多地用处是提供一些新知识的查阅文档,某些知识点的Q&A,编码思维/规范之类的,自己还是要对里面的知识进行总结理解。通过查看苹果工程师的代码要反思为什么要这样写,要和自己写的代码作对比,不见得他们写的代码都是很合理的,但这里这绝对是新手学习的一手途径,这比在github上随便search一个demo的价值来的更高。在每一个工程的Readme中都有详细的介绍该Demo的作用以及更新版本之类的信息,并且每个类中都会有简单的英文注释,在需要注意的地方也会有单独的注释。

1.第一阶段

需要的知识

1.UITableView的基本使用
2.NSTimeZone的基本使用(时区的获取)
3.排序的简单使用

主要目的

对UITableView能够进行简单、基础的使用。

对使用何种UITableView类型(Plain、Group)、何种结构(UITableViewController、UIViewController+UITableView)、何种编程方式(纯代码、IB界面)不做要求,但是针对以上所提及的一些方式都要能够很熟练的运用。

Demo要求

  • 要求将所有读取出来的时区使用列表进行展示

效果如下,

具体分析

时区的获取

对于时区类NSTimeZone的使用做一个简单的介绍,这个不是重点。

1
2
3
+ (NSArray<NSString *> *)knownTimeZoneNames;

Returns an array of strings listing the IDs of all the time zones known to the system.

+knownTimeZoneNames方法用于获取目前已知的所有时区的ID,是一个装载着字符串类型的数据,其中的字符串格式如下Africa/Luanda,表示所在州的时区,有的地区可能还会有第三个,比如America/North_Dakota/Beulah

A-Z的升序排序

从获取的已知时区列表中可以看出,所有的给定时区ID都已经按照A-Z升序进行了排列,但是以防万一,还是要求显示的对其进行A-Z的升序排列。

排序方法:

1
2
3
[timeZones sortUsingComparator:^NSComparisonResult(NSString * timeZone1,NSString * timeZone2) {
return [timeZone1 localizedStandardCompare:timeZone2];
}];

当然还有好多种排序方法,自己试试看再找出来两三种排序方法进行A-Z升序排列。

Demo-00

2.第二阶段

需要的知识

1.对UITableView的进一步使用
2.使用对象模型来表征数据
3.排序的进一步使用
4.单例模式

主要目的

进一步学习对UITableView的使用,能够使用模型数据进行具体数据的存储,并通过模型数据对UITableView进行数据源配置,能够使用排序API对特定的需求进行数据的排序,

Demo需求

  • 对已知的时区(TimeZone)按照不同地区(Region)进行展示
  • 同一个地区下展示相同的时区
  • 使用NSObject模型进行时区数据的表征

Demo效果如下,

具体分析

第一个阶段通过操作NSTimeZone类获得了目前已知的所有时区的信息,由数据可以看出来某一些时区的第一个/前的单词是一样的,这个单词是表示当前时区所在地区的名字,一共有11个,下面需要做的就是获取到这11个地区的信息数据,并且将所有属于同一个地区的时区归到该地区下面,最后使用UITableView以Demo要求示意图那样进行显示,同时,对所有的地区进行A-Z的升序排列。

如何进行数据的分离的逻辑要自己进行处理,代码就不贴了。

这之后将获得一个类似于这样的数据结构:

1
2
3
4
5
6
7
8
9
@[
@{@"Africa":@[@"Abidjan",
@"Accra",
...
@"Windhoek"]},
@{@"America":@[...]},
....
@{@"Pacific":@[...]}
]

Demo-10

通过数据结构发现,每一个具体的时区都是一个单独的数据,但是他们都是时区,从面相对象的角度来说,他们是同一种类(Class)中的不同实例对象(Object),所以可以使用NSObject来表示时区这种数据类型。

同时,每一个时区又有其所属的区域,也就是从+knownTimeZoneNames方法获得的时区ID中/分隔的第一个字符串,不同的时区可以属于同一个区域。所以这里使用两个对象模型进行时区数据的表征,

  • 时区区域—-HLLRegion
    • 拥有一系列的具体时区对象—-以NSArray类型进行存储,里面装的是HLLTimeZone对象
    • 还拥有该区域的名称—-以NSString类型进行存储
  • 具体时区—-HLLTimeZone
    • 拥有属于自己的时区名称—-以NSString存储

这时候使用建立模型并且替换掉在上面获得的数据,修改dataSource的一些方法,这样就完成了使用模型表征数据的操作,虽然使用字典-数组-字符串模型可以很简单的进行数据的存储,并且不需要创建新类,但是由于后续还有需要对时区进行一些操作,使用字典-数组-字符串模型不方便进行。

在实际编码的过程中,使用单例模式将区域和时区进行进一步的封装,对外面调用获取时区信息更加简洁方便。HLLTimeZoneManager的API如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+ (instancetype) shareTimeZoneManager;

/**
* 获取已知时区列表
*
* @return 已知时区区域列表
*/
- (NSArray <HLLRegion *>*)knownRegions;

/**
* 获取所有的时区
*
* @return 具体时区列表
*/
- (NSArray <HLLTimeZone *>*)allTimeZones;

Demo-11

一个新需求

现在要求不是按照地区进行Section划分,对所有的时区进行重新的排序。要求所有时区按照A…Z升序进行排序,以每一个时区的首字母进行Section划分,同时要求有侧边栏索引。注,使用UITableViewDataSource-sectionIndexTitlesForTableView方法。

效果如下,

新需求的要求

  • 使用现阶段所学的知识完成上面的新需求
  • 不能使用硬编码

硬编码就比如直接写上A…Z这26个字母这之类的代码,这样照成的后果就是有可能有的字母是不会出现的,但是这个索引还是出现在表格视图中,所以要按照实际的数据进行效果实现。

实现思路和上面的差不多,最后的数据结构也是类似的。

新需求的另外一种实现思路

这里提供一种UIKit中的比较智能的排序归类方法

3.第三阶段

需要的知识

1.自定义UITableViewCell
2.协议-代理模式
3.关于UITableViewHeaderFooterView子类的使用
4.计时器NSTimer的使用

主要目的

学会使用代码和Xib进行自定义固定高度的UITableViewCell,学会使用可以进行重用的UITableViewHeaderFooterView进行section的header自定义显示,在自定义视图的基础上,运用协议-代理模式进行对象之间的事件传递。

Demo需求

由于所有的时区信息排列出来太长,即使使用sectionIndex进行侧边栏索引也还是不方便,这时候的需求是将第二阶段中的按照地区(Region)进行Section排列的Demo改造,改造成类似于QQ好友列表的组折叠效果,点击SectionView可以控制该Section下的Cell的展示与隐藏。

效果如下,

另外,使用自定义的UITableViewCell进行展示时区信息,需要展示的有时区的名称,当前时间等。

效果图如下,

其他要求

  • 使用合理的方案完成抽屉效果
  • 按照要求进行自定义的Cell
  • 分别使用纯代码和IB进行Cell的自定义,并能灵活的使用两种方法进行自定义Cell

具体分析 – 抽屉效果

SectionInfo 部分

对于第一部分,即实现折叠效果,在原有项目的基础上添加一个自定义的UITableViewHeaderFooterView的子类HLLSectionHeaderView和一个用于管理section的类HLLSectioinInfo

其中HLLSectionInfo 的主要作用是:

  • 决定/判断这个section是否处于打开状态
  • 为这个section提供headerView
  • 拥有决定section内容的section对象模型
  • 决定这个section下每一个row的高度是多少

通过他把section的一些细节信息,比如sectionHeaderViewsectionObject等与视图控制器进行解耦,控制器只需要根据sectionInfo提供的section相关的信息进行操作、展示即可。

这个类大概的一个样子是这样

1
2
3
4
5
6
7
8
9
@property (nonatomic ,assign ,getter=isOpen) BOOL open;

// UI
@property (nonatomic) HLLSectionHeaderView * sectionHeaderView;

// Data
@property (nonatomic) HLLRegion * region;

@property (nonatomic) NSMutableArray * rowsHeight;

在设计好这个类之后回到控制器中,将前一个阶段使用HLLTimeZoneManager决定UITableView的显示的代码进行修改。由于每一个HLLSectionInfo对象其实决定的是一个section,并且时区的区域一共有11个,通过timeZoneManager的-knownRegions方法可以获取得到,所以下面要做的就是将sectionInfo和sectionObject结合到一起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NSMutableArray * temp = [NSMutableArray array];

for (HLLRegion * region in [self.manager knownRegions]) {

HLLSectionInfo * sectionInfo = [[HLLSectionInfo alloc] init];
sectionInfo.region = region;
sectionInfo.open = NO;

NSNumber * rowHeight = @(44);
NSInteger rowsCount = region.timeZones.count;

for (NSInteger index = 0; index < rowsCount; index ++) {

[sectionInfo insertObject:rowHeight forRowHeightAtIndex:index];
}
[temp addObject:sectionInfo];
}

这一步里面有一个sectionInfo的实例对象方法- insertObject:forRowHeightAtIndex:,这个方法是为了决定对应section下的每一个row的高度而添加的。这样就将sectionObject(HLLRegion)和sectionInfo(HLLSectionInfo)进行了绑定,另外这里的temp数组需要在其他的地方进行使用,所以视图控制器需要对他进行持有,添加一个数组属性,可以是NSArray也可以是NSMutableArray,最后将temp数组赋值给sectionInfoArray

1
@property (nonatomic ,strong) NSMutableArray * sectionInfoArray;

这之后在数据源方法中要操作的就不再是timeZoneManager了,而是sectionInfoArray,对其中一些方法进行简单的修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
- (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{

HLLSectionInfo * sectionInfo = self.sectionInfoArray[section];
NSInteger timeZones = sectionInfo.region.timeZones.count;

return sectionInfo.isOpen ? timeZones : 0;
}

- (NSInteger) numberOfSectionsInTableView:(UITableView *)tableView{

return self.sectionInfoArray.count;
}

- (UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{

UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:kTimeZoneCellIdentifier forIndexPath:indexPath];

HLLSectionInfo * sectionInfo = self.sectionInfoArray[indexPath.section];
HLLRegion * region = sectionInfo.region;
HLLTimeZone * timeZone = region.timeZones[indexPath.row];

cell.textLabel.text = [NSString stringWithFormat:@"%@",timeZone.localeName];

return cell;
}

- (NSString *) tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section{

HLLSectionInfo * sectionInfo = self.sectionInfoArray[section];
HLLRegion * region = sectionInfo.region;
return region.name;
}

如果运行程序,看到的仅仅是这个样子,只有时区区域的名字,并且不能显示出来UITableViewCell,

到这里都没有对section的Header视图进行一次操作,除了sectionInfo中对其进行了一个属性引用,下面要进行的就是对自定义sectionHeaderView的操作。

SectionHeaderView 部分

这里设计的是这个UITableViewHeaderFooterView的子类,这个视图所拥有的控件有一个按钮可以点击控制视图的开启与关闭、一个展示时区区域的标签和一个展示区域下有多少时区的标签,除此之外,看不见的还有一个tap手势对象

对于一个自定义视图上的Target-Action事件,很明显他自己是能够进行响应,但是如果这个响应操作需要在其他的地方也能够进行响应,或者说是其他的对象也要能够同时响应到这些方法,这时候需要做的就是要让两个或多个对象之间进行通信。具体在这个例子里面就是这个的一个逻辑:sectionHeaderView上的按钮点击事件,视图控制器要知道并且能够在点击事件执行的时候自己也执行一些操作。所以这里使用协议-代理模式,通过协议约束代理对象达到信息传递的目的。

由于代理对象只需要知道HLLSectionHeaderView的点击效果(打开/关闭)就可以了,所以协议部分可以简单的使用两个方法来进行通信:

1
2
3
4
5
6
7
8
9
10
@protocol HLLSectionHeaderViewDelegate <NSObject>

@optional
// Open
- (void) sectionHeaderView:(HLLSectionHeaderView *)sectionHeaderView didOpenAtIndex:(NSInteger)index;

// Close
- (void) sectionHeaderView:(HLLSectionHeaderView *)sectionHeaderView didCloseAtIndex:(NSInteger)index;

@end

如此一来,HLLSectionHeaderView只需要拥有一个遵守该协议的代理对象即可。

为了进行重用,和UITableViewCell一样,UITableViewHeaderFooterView的使用也是需要先注册的,这里使用UINib的实例对象进行注册,并且将具体的xib名称封装进sectionHeaderView内部,对外仅仅提供一个类方法。

具体类设计如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@interface HLLSectionHeaderView : UITableViewHeaderFooterView

// UI
@property (weak, nonatomic) IBOutlet UIButton *button;
@property (weak, nonatomic) IBOutlet UILabel *titleLabel;
@property (weak, nonatomic) IBOutlet UILabel *countLabel;

@property (nonatomic ,weak) id<HLLSectionHeaderViewDelegate>delegate;
@property (nonatomic ,assign) NSInteger section;

+ (UINib *) nib;

@end

先简单的进行按钮和手势点击实现箭头图片的切换,其实也就是按钮的选中与非选中状态之间切换

1
2
3
4
5
6
7
8
9
10
11
12
- (void)awakeFromNib{

[self.button setImage:[UIImage imageNamed:@"ExplainJianTou_Select"] forState:UIControlStateSelected];

UITapGestureRecognizer * tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(toggleOpen:)];
[self addGestureRecognizer:tap];
}

- (IBAction)toggleOpen:(id)sender {

self.button.selected = !self.button.selected;
}

这时候回到控制器中,将-tableView:titleForHeaderInSection:方法去掉,添加一个-tableView:viewForHeaderInSection:方法用于返回我们创建的sectionHeaderView,前提是别忘了让UITableView对其进行注册,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
[_tableView registerNib:[HLLSectionHeaderView nib] forHeaderFooterViewReuseIdentifier:kSectionHeaderViewIdentifier];
...

- (UIView *) tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{

HLLSectionHeaderView * sectionHeaderView = (HLLSectionHeaderView *)[tableView dequeueReusableHeaderFooterViewWithIdentifier:kSectionHeaderViewIdentifier];

HLLSectionInfo * sectionInfo = self.sectionInfoArray[section];
sectionInfo.sectionHeaderView = sectionHeaderView;

sectionHeaderView.titleLabel.text = sectionInfo.region.name;
sectionHeaderView.countLabel.text = [NSString stringWithFormat:@"%lu",(unsigned long)sectionInfo.region.timeZones.count];

return sectionHeaderView;
}

这时候如果运行程序,通过点击sectionHeaderView就可以看见箭头能够响应点击变换图片了。

下面的任务就是通过代理-协议进行实际的打开与关闭操作了。

首先,需要将按钮的点击状态通过代理传出去,在按钮的点击事件-toggleOpen:中进行操作。

1
2
3
4
5
6
7
8
9
10
11
...
if (self.button.selected) {
if (self.delegate && [self.delegate respondsToSelector:@selector(sectionHeaderView:didOpenAtIndex:)]) {
[self.delegate sectionHeaderView:self didOpenAtIndex:self.section];
}
}else{
if (self.delegate && [self.delegate respondsToSelector:@selector(sectionHeaderView:didCloseAtIndex:)]) {
[self.delegate sectionHeaderView:self didCloseAtIndex:self.section];
}
}
...

然后,在-tableView:viewForHeaderInSection:方法中进行代理的添加,使控制器成为sectionHeaderView的代理,并且实现代理协议,也就是那两个协议方法-sectionHeaderView:didOpenAtIndex:-sectionHeaderView:didCloseAtIndex:

1
2
3
4
...
sectionHeaderView.delegate = self;
sectionHeaderView.section = section;
...

对于这两个协议,先说比较难的,当要打开的时候的方法-sectionHeaderView:didOpenAtIndex:,这个方法所要进行的操作是:

  • 将该sectionHeaderView所处的sectionInfo对象进行打开操作,也就是设置open属性为YES
  • 通过sectionInfo对象获得要在这个section下显示的row(HLLTimeZone对象)的个数,以及具体对象,对UITableView添加这些row
  • 如果有其他的sectionHeaderView被打开,对其进行关闭操作,对UITableView删除其他section下的row
  • 最后,更新UI

这里需要设置一个标志位,用于标志当前打开的HLLSectionHeaderView的section,暂时命名为openSectionIndex,初始值设为NSNotFound

这里有一个问题,如何在控制器中去执行HLLSectionHeaderView的箭头开关操作呢?在这之前都是使用button的Target-Action方法直接操作的,调用者都是HLLSectionHeaderView。一个做法是,将Target-Action方法暴漏出来,在代理协议方法中用传过来的sectionHeaderView调用Target-Action方法,这么做不行的一点在于这个Target-Action方法方法内部已经涉及到协议方法的调用了。另外一种方法是不暴漏Target-Action方法同时提供一个简洁的API—-toggleOpenWithUserAction:,使用BOOL值进行控制是否执行协议方法。

那么HLLSectionHeaderView内部需要进行简单的修改。如果是点击操作,不论点击的是按钮还是通过tap手势实现的点击,都传YES;如果是非点击操作,比如这里的要使用HLLSectionHeaderView的实例对象进行操作,都传NO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (IBAction)toggleOpen:(id)sender {

[self toggleOpenWithUserAction:YES];
}

- (void)toggleOpenWithUserAction:(BOOL)action{

self.button.selected = !self.button.selected;

if (action) {

if (self.button.selected) {
...
}else{
...
}
}
}

这之后的打开操作的协议实现具体逻辑代码如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
- (void)sectionHeaderView:(HLLSectionHeaderView *)sectionHeaderView
didOpenAtIndex:(NSInteger)section{

HLLSectionInfo * sectionInfo = self.sectionInfoArray[section];
sectionInfo.open = YES;

// 添加row给tableView
NSInteger rowCount = sectionInfo.region.timeZones.count;
NSMutableArray * indexPathToInsert = [NSMutableArray array];

for (NSInteger row = 0; row < rowCount; row ++) {
[indexPathToInsert addObject:[NSIndexPath indexPathForRow:row inSection:section]];
}

// 如果是已经有sectionHeaderView打开了,就将他下面的row都删除
NSMutableArray * indexPathToDelete = [NSMutableArray array];

if (self.openSectionIndex != NSNotFound) {

// 取出已经打开的section,对其进行关闭操作
HLLSectionInfo * openSectionInfo = self.sectionInfoArray[self.openSectionIndex];
openSectionInfo.open = NO;
[openSectionInfo.sectionHeaderView toggleOpenWithUserAction:NO];
for (NSInteger row = 0; row < openSectionInfo.region.timeZones.count; row ++) {
[indexPathToDelete addObject:[NSIndexPath indexPathForRow:row inSection:self.openSectionIndex]];
}
}

// 设置动画效果,可有可无
UITableViewRowAnimation animationWithDelete;
UITableViewRowAnimation animationWithInsert;

if (self.openSectionIndex == NSNotFound ||
section < self.openSectionIndex) {
animationWithInsert = UITableViewRowAnimationTop;
animationWithDelete = UITableViewRowAnimationBottom;
}else{
animationWithInsert = UITableViewRowAnimationBottom;
animationWithDelete = UITableViewRowAnimationTop;
}

// 更新UI
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:indexPathToDelete withRowAnimation:animationWithDelete];
[self.tableView insertRowsAtIndexPaths:indexPathToInsert withRowAnimation:animationWithInsert];
[self.tableView endUpdates];

// 重置标志位
self.openSectionIndex = section;
}

处理完了打开的操作之后,进行关闭的操作就简单的多了:

  • 将该sectionHeaderView所处的sectionInfo对象进行关闭操作,也就是设置open属性为NO
  • 通过标志位_openSectionIndex或者直接使用协议方法传过来的section获取到已经打开的row的个数
  • 创建要删除的row数组,使用UITableView进行删除操作
  • 更新UI
  • 滞空标志位
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

- (void)sectionHeaderView:(HLLSectionHeaderView *)sectionHeaderView
didCloseAtIndex:(NSInteger)section{

HLLSectionInfo * sectionInfo = self.sectionInfoArray[section];
sectionInfo.open = NO;

NSInteger countOfRowsToDelete = [self.tableView numberOfRowsInSection:_openSectionIndex];

if (countOfRowsToDelete > 0) {

NSMutableArray * indexPathToDelete = [NSMutableArray array];

for (NSInteger row = 0; row < countOfRowsToDelete; row ++) {
[indexPathToDelete addObject:[NSIndexPath indexPathForRow:row inSection:section]];
}
[self.tableView deleteRowsAtIndexPaths:indexPathToDelete withRowAnimation:UITableViewRowAnimationTop];
}
self.openSectionIndex = NSNotFound;
}

这时候运行程序,应该就能够达到demo要求的效果了。

Demo-20

具体分析 – 自定义Cell

Demo-21

4.第四阶段

需要的知识

1.UITableView和UISearchController的结合使用
2.UISearchBar的使用
3.谓词的使用
4.正则表达式

主要目的

掌握UISearchBar以及与其相关类的使用,并能够合理的与UITableView进行结合使用,使用谓词进行数据的过滤,达到搜索的目的。

Demo要求

  • 将UISearchController的UISearchBar作为UITableView的tableViewHeaderView
  • 点击搜索框可以进行搜索内容,按照输入的字符进行检索是否有匹配的时区,并展示在表格视图中
  • 如果没有相应的搜索内容应提供一个视图对用户进行提示

具体分析

在iOS8之前,结合UITableView一起实现搜索功能的控制器是UISearchDisplayController,这个控制器结合UIKit中的UISearchBar来实现应用内的表视图搜索功能,但这是在iOS8之前的,到了iOS8就又有所不同了。在iOS8 之后,UISearchDisplayController被弃用,但是奇怪的是在这个Interface Builder’s位置还是可以找到并且使用它。

这个被弃用的控制器被UISearchController替换了,更奇怪的是在Interface Builder’s中却找不到这个新的控制器类,那这个类必须要使用代码进行初始化了,而不能使用IB。

通过查看UISearchController Class Reference可以知道一个search控制器是结合两个控制器一起工作的,一个用于提供可搜索内容(displays searchable content ),一个用于提供搜索结果的展示(displays search results)。对于第二个控制器来说,可以通过initWithSearchResultsController:方法提供,但是如果传递一个nil的话,就代表第二个控制器和第一个控制器一样。

每一个search控制器都提供一个UISearchBar对象来为用户提供搜索的入口,一般常用的办法就是将这个对象设置为表视图的tableHeaderView属性,当用户点击UISearchBar的时候search控制器就会自动的展示出搜索结果控制器,也就是第二个控制器,并且同时给设置代理searchResultsUpdater对象提供检索内容的输出,这个代理的协议方法只要一个,这个相较于iOS8之前的协议方法简便了许多。

一个简单使用两个控制器进行展示搜索内容以及搜索结果的例子如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 使用自定义的另外一个控制器作为搜索结果控制器
MySearchResultsController* resultsController = [[MySearchResultsController alloc] init];

self.searchController = [[UISearchController alloc] initWithSearchResultsController:resultsController];

// 用当前控制器更新搜索结果
self.searchController.searchResultsUpdater = self;

// 将search 控制器的searchBar设置为UITableView的tableViewHeaderView
self.tableView.tableHeaderView = self.searchController.searchBar;

// It is usually good to set the presentation context.
self.definesPresentationContext = YES;

// <UISearchControllerDelegate> 用于监控result Controller的出现以及消失
self.searchController.delegate = self;

// 可以试着将这个属性进行其他设置看一下效果
self.searchController.dimsBackgroundDuringPresentation = NO; // default is YES

在初始化以及设置了UISearchController之后,一旦进行了搜索操作,就会调用UISearchResultsUpdating协议的-updateSearchResultsForSearchController:方法,并且所有的过滤操作都要在这个方法中进行。

另外要说明的一点是,UISearchControllerUISearchBar不仅仅可以是表视图的tableHeaderView,还可以是当前控制器的navigationItem.titleView,还可以是当前控制器的navigationItem.rightBarButtonItem,还可以什么都不是,仅仅是当一个控件被用户点击的时候present出来search 控制器。这些使用方法在Apple 提供的UIKit Catalog中都有所体现 ,UIKit Catalog是Apple提供的一个关于常用控件使用方法的Sample Code。

这里使用一个简单的demo进行演示一下,还是使用时区,但是是将最直观的展示所有时区的那个demo拿来修改,并且这里的search 控制器的两个控制器使用的是同一个,在-updateSearchResultsForSearchController:方法中进行过滤的操作如下,

1
2
3
4
5
6
7
8
9
10
11
NSString * filterString = self.searchController.searchBar.text;

if (!filterString || filterString.length <= 0) {
self.visibleResults = [self.manager allTimeZones];
}
else {
NSPredicate *filterPredicate = [NSPredicate predicateWithFormat:@"localeName contains %@", filterString];

self.visibleResults = [[self.manager allTimeZones] filteredArrayUsingPredicate:filterPredicate];
}
[self.tableView reloadData];

这里使用到了数组在谓词 --- NSPredicate下的一个分类,一般而言,用到谓词的地方或多或少都要涉及到正则表达式

1
2
3
Predicates wrap some combination of expressions and 

operators and when evaluated return a BOOL.

Predicate在Cocoa框架中常用于查询过滤操作,主要是根据要过滤的数据是否满足一系列的表达式并返回布尔类型的值进行过滤。具体内容可以看Developer Library上关于 Predicate的教程,链接在参考资料中的关于谓词的使用。

简单说一下这里谓词的使用,首先,NSArray有一个关于谓词的分类方法,用于对数组内部所有的对象进行符合谓词条件的过滤输出。使用一个字符串表达式初始化了一个谓词实例对象,用到了一个在谓词中的关键字contains表示含有的意思,localeName是数组中对象的一个属性,谓词的时候是支持KVC的,所以这里使用localeName contains %@self.localeName contains %@是一样的,当数组中对象的localeName属性包含有输入的字符串的时候就会被过滤出来。

一些例子,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// contains
NSArray * array =@[@"lisi",@"zhansan",@"wangwu",@"zhaoliu",@"liu"];

NSPredicate * predicate = [NSPredicate predicateWithFormat:@"self contains 'an'"];

NSArray * result = [array filteredArrayUsingPredicate:predicate];
// result:[@"zhansan",@"wangwu"]


//in取出两个集合中共有的元素
NSArray * filterArray = @[@"lisi",@"tianqi"];

NSPredicate * inPredicate = [NSPredicate predicateWithFormat:@"self in %@",filterArray];

NSArray * inResult = [likeArray filteredArrayUsingPredicate:inPredicate];
// inResult:[@"lisi"]


//正则表达式
NSString * regexStr =@"^zh.+";

NSPredicate * regexPredicate = [NSPredicate predicateWithFormat:@"self matches %@",regexStr];

NSArray * regexResult = [array filteredArrayUsingPredicate:regexPredicate];
// regexResult:[@"zhansan",@"zhaoliu"]

另外,正则表达式也是在进行过滤操作时常常涉及的知识点,关于正则能够做什么,进行文件搜索的时候应该能够感受到他的便捷,另外他还可以用于文本的替换,查找匹配内容的范围等等,下面有一个关于正则表达式很简便实用的教程。

在开发中常常结合谓词和正则表达式的地方是用于检索用户输入内容格式的正确与否,校验手机号、邮箱、银行卡号、身份证号等等;或者通过用户的输入内容进行过滤数据再提供给用户,通讯录搜索、QQ好友搜索等等。

Demo-30

5.第五阶段

需要的知识

1.UITableView的主要数据源方法
2.自定义协议,以及协议-代理模式
3.UITabbarController的使用

主要目的

使用UITabbarContrller展示根据不同要求进行排列的时区信息,同一个UITableView在不同的数据源方法下进行不同的风格显示。

Demo要求

  • 提供如下四种不同的数据源:
    • 第一种是阶段一中的最基础的效果
    • 第二种是按照不同地区进行Section的时区排列效果
    • 第三种是按照A-Z进行Section的时区排列效果
    • 第四种是使用UILocalizedIndexedCollation进行数据分组的A-Z排列效果
  • 使用一个UIViewController,而不是四个

具体分析

表视图的数据源协议可以为表视图提供可以用于展示、修改的模型数据以及用于展示这些数据信息的视图,因此在不需要对数据进行修改的情况下,完全有必要将这些数据的获取以及视图的展示移除出控制器,也就是不让控制器成为表视图的数据源代理—dataSource,使控制器更加的简洁。

具体做法就是使用一个协议来约束数据源对象,数据源对象相应的要遵循表视图的数据源协议,并且实现相应的方法,至于数据的获取以及展示样式的区别都在数据源对象中进行。就是这么简单。

Demo-40

参考资料:

代码
Guide
文章