iOS开发中的单选与多选

在前端开发中如果要拥有一个单选或者多选功能十分简单,因为HTML中有现成的标签可以很方便的实现单选或者多选效果,比如这样写上几句代码就能拥有最原始的选择效果。

See the Pen selected by Rocky (@Yrocky) on CodePen.

但是在iOS开发中就没有这么方便的控件了,如果要完成单选或者多选的功能还需要一些逻辑编码,并且可以选择的方案还是有很多的。

在iOS开发中经常用于实现单选或者多选功能的控件是UITableView,并且,通过这个控件可以完成一个App80%的界面。而微信则是一个全部使用UITableView构建的App。下面将会针对于单选和多选进行一个探究学习,学习SDK中提供有哪些API能够使用,并且结合这些API如何总结出合适的方法完成单选多选功能。

单选

单选其实还是很简单的,在将UITableView添加到控制器中的时候选择任意一个cell都会看到被选择的cell变为灰色,这是系统提供的,不需要开发者进行设置。谈不上好看,但正是由于不是很好看,才留给了开发者很大的自定义的空间。

这个选择样式系统是根据设置的selectionStyle属性决定的,这是一个枚举类型,默认的是UITableViewCellSelectionStyleBlue,但是通过选择代理方法获取cell并且打印出来的selectionStyle属性却是UITableViewCellSelectionStyleDefault!!

1
2
3
4
5
6
7
8
9
@property (nonatomic) UITableViewCellSelectionStyle   selectionStyle;             
// default is UITableViewCellSelectionStyleBlue.

typedef NS_ENUM(NSInteger, UITableViewCellSelectionStyle) {
UITableViewCellSelectionStyleNone,
UITableViewCellSelectionStyleBlue,
UITableViewCellSelectionStyleGray,
UITableViewCellSelectionStyleDefault NS_ENUM_AVAILABLE_IOS(7_0)
};

除此之外,UIKit中还为UITableView提供了单选、多选、在编辑状态下的单选、在编辑状态下的多选的布尔值属性,以及其他的选择操作API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@property (nonatomic) BOOL allowsSelection;

@property (nonatomic) BOOL allowsSelectionDuringEditing;

@property (nonatomic) BOOL allowsMultipleSelection;

@property (nonatomic) BOOL allowsMultipleSelectionDuringEditing;

@property (nonatomic, readonly, nullable) NSIndexPath *indexPathForSelectedRow

@property (nonatomic, readonly, nullable) NSArray<NSIndexPath *> *indexPathsForSelectedRows;

- (void)selectRowAtIndexPath:(nullable NSIndexPath *)indexPath animated:(BOOL)animated scrollPosition:(UITableViewScrollPosition)scrollPosition;

- (void)deselectRowAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated;

默认情况下是允许单选的,也就是说allowsSelection属性即使不设置也可以进行单选。如果要进行多选allowsMultipleSelection属性必须设置为YES,并且当UITableView处于多选状态下的时候,如果重复点击同一个cell可以让cell在选中与非选中状态之间切换,单选的状态下就不行,一旦选择了就一直有一个被选择。

使用这几个属性结合UITableViewCell的accessoryType属性可以轻易的完成单选、多选以及单选可回退等选择操作。

及简方法实现选择操作

单选和多选

要达到单选或者多选操作,需要做的是在UITableView的代理方法-tableView:didSelectRowAtIndexPath:中对选择的cell进行选中标记,以及-tableView:didDeselectRowAtIndexPath中对取消选中的cell取消选中标记,这里使用UITableViewCell的accessoryType属性进行选中与否的设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-tableView:didSelectRowAtIndexPath:

UITableViewCell * cell = [tableView cellForRowAtIndexPath:indexPath];

if (cell.selected) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}else{
cell.accessoryType = UITableViewCellAccessoryNone;
}

-tableView:didDeselectRowAtIndexPath

UITableViewCell * cell = [tableView cellForRowAtIndexPath:indexPath];

if (!cell.selected) {
cell.accessoryType = UITableViewCellAccessoryNone;
}

如果这样做了的话,在进行滑动表视图的时候会出现cell重用的现象,可以在要进行重用的时候根据cell的selected属性进行决定accessoryType显示样式

1
2
3
4
5
6
7
8
9
-tableView:cellForRowAtIndexPath:

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

if (cell.selected) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}else{
cell.accessoryType = UITableViewCellAccessoryNone;
}

做完了这些,可以在对UITableView进行设置属性的地方决定是要单选allowsSelection还是多选allowsMultipleSelection

1
2
3
_tableView.allowsSelection = YES;
// or
_tableView.allowsMultipleSelection = YES;
单选回退

前面说了,如果是多选状态,可以回到一个都没有选中的状态,但是如果是单选状态,一旦选择就回不到原始一个都没有选中的状态。如果要实现一个单选可以回退到原始状态的效果怎么办呢?

这时候需要借助一个标记位,用来标记选中的cell所在的位置,这个标记位使用NSIndexPath的实例对象来表示,当点击cell的时候对该cell所处的位置和标记位进行比较,如果相同的话就取消掉cell的选中效果,并且将标记位置为nil,同时要对UITableView进行取消选中当前indexPath下的cell操作,这样做的目的是为了在使用indexPathForSelectedRow属性获取选中cell的时候能够获取到正确的数据;如果和标记位不相同就添加选中状态,并重新赋值标记位。将-tableView:didSelectRowAtIndexPath:方法使用一下代码进行替换,同时对当前控制器添加一个selectedIndexPath属性作为标记位

1
2
3
4
5
6
7
8
9
10
11
12
-tableView:didSelectRowAtIndexPath:

UITableViewCell * cell = [tableView cellForRowAtIndexPath:indexPath];

if (_selectedIndexPath == indexPath) {
cell.accessoryType = UITableViewCellAccessoryNone;
[tableView deselectRowAtIndexPath:_selectedIndexPath animated:YES];
_selectedIndexPath = nil;
}else{
cell.accessoryType = UITableViewCellAccessoryCheckmark;
_selectedIndexPath = indexPath;
}

其他地方还是和单选一样的,这样就可以实现单选回退操作了。

以上的代码

借助标记位实现单选操作

当然也可以使用一个NSIndexPath的实例对象作为标记位来实现单选操作,在点选每一个cell的时候进行标记的重新赋值,在cellForRow方法中根据标记位来决定哪一个cell显示选中状态。

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
-tableView:cellForRowAtIndexPath:

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier forIndexPath:indexPath];
if (_selectedIndexPath && _selectedIndexPath == indexPath) {

cell.accessoryType = UITableViewCellAccessoryCheckmark;
}else{
cell.accessoryType = UITableViewCellAccessoryNone;
}

...

-tableView:didSelectRowAtIndexPath:

UITableViewCell * cell = [tableView cellForRowAtIndexPath:indexPath];

if (_selectedIndexPath) {

// 取消上一次选择的accessoryType
cell = [tableView cellForRowAtIndexPath:self.selectedIndexPath];
cell.accessoryType = UITableViewCellAccessoryNone;
}

// 设置这一次选择的accessoryType
cell = [tableView cellForRowAtIndexPath:indexPath];
if (cell.accessoryType) {
cell.accessoryType = UITableViewCellAccessoryNone;
} else {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}

// 保存indexPath
self.selectedIndexPath = indexPath;

[tableView performSelector:@selector(deselectRowAtIndexPath:animated:) withObject:indexPath afterDelay:0.5];

----

// 或者用短一点的,将该方法中以上写的代码替换成下面这样,但是要考虑一下这样有可能会出现的后果
_selectedIndexPath = indexPath;

[tableView reloadData];

这些做法比较基础,也比较简单。如果是在没有进行自定义Cell,并且对选择样式也没什么要求,一个对号也能行的那种情况下,这样做就行了,没必要多折腾了。但是,往往一个App的局部样式跟整体的样式是相关的,比如微信主色调是一种绿色的,并且设计师由于美观要使用一个带有圆圈的对号来表示选中状态,这样的话一个简单的对号显然已经不能满足需求了。在进行自定义Cell的时候,需要考虑将选中和非选中的样式进行迁移,不要在控制器中进行,换到在Cell中进行。

自定义cell中实现单选操作

使用自定义的Cell,需要在实现类里的-setSelected:animated:方法进行判断,如果不需要有Cell的选中样式可以在-tableView:cellForRowAtIndexPath:中将选中样式设为none。

1
2
3
4
5
6
7
8
9
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {

[super setSelected:selected animated:animated];
if (selected) {
self.checkoutImageView.image = [UIImage imageNamed:@"CheckBox_HL"];
}else{
self.checkoutImageView.image = [UIImage imageNamed:@"CheckBox"];
}
}

效果如图,

这种做法说起来也是比较取巧,在需求不是很复杂的情况下使用还是可以的,如果一旦牵扯到很多的业务逻辑就不要使用这种办法了。

以上的代码

上面说的都是在一个Section下进行单选,如果换个前提条件,是要求在多个Section下进行全部Cell的单选呢?那如果又增加了难度,要求是在多个Section下进行每一个Section内部的单选呢?

首先,这个在多个Section下全局单选功能和单个Section下的单选功能是一样的,重要的是下面这个在每一个Section下进行单选操作。

多个Section下每个Section内部的单选功能

以上的代码

多选

和单选一样,如果多选任务要求的是多个Section下的全局多选呢?如果是多个Section下的每个Section内部的多选呢?这样的话又要如何实现呢?思考一下。

当认真思考之后你会发现,这样的做法实在是无聊。

一个购物车

在做了这么多的单选和多选操作之后,用一个实实在在的例子进行一下演练,涉及到一个很实际的应用场景:挑选购物车中的物品进行结账。

在你手机中某一家电商类App的购物车中会有同一个商家的多种物品,也会出现所有的商品都不是同一个商家这种情况。当你有钱的时候,你可以全选购物车中的所有物品进行结账;当你不是很有钱的时候,只能够买其中的一部分,你可以选择同一个商家下面的所有商品,同时你又比较纠结,你也可以挑选多个商家的某一些商品进行结账;当你实在是没有钱了,但又想买买买,所有钱只能够买一件的时候,你在购物车中挑选了一件进行结账。

用户的行为就是最直观的项目需求,翻译成编码需求大概是这样:

  • 表视图是由许多Sections的,每一个Section下又有若干Rows
  • 可以对全局的Row单选、多选、全选
  • 可以多某一个Section进行单选、多选、全选
  • 可以跨Section进行多选
  • 当有至少一个Row没有被选中的时候,全选按钮就不应该是选中状态
  • 当某一个Section下仅仅有一个Row的时候,并且选中了该Row的时候,当前Section也应该被选中
  • 当点击全选按钮的时候,所有的Row都应该进入全选状态,并且对应的Section的HeaderView也要变成选中状态

应该就这些了吧。。。

初始工程已经写好了,这个初始工程使用了plist文件提供了假数据供表视图使用,同时使用了一些Xib进行构建局部视图的样式,这在开发中会成为便捷开发的部分,同时也会在进行小功能demo的时候节省大量的时间用来写无聊至极的布局代码。但是,这个初始工程还是有很多问题的,比如,没有很好地解决Cell重用问题,当点击一个靠上部分或者靠下部分的按钮,然后滑动表格,会发现,其他地方的按钮别选中了!!典型的cell重用问题,这个会在后面进行解决重用问题;还有里面在每一个xib对应的视图中都用到了那个具有特殊状态的按钮,并且在每个类中都写了重复的代码,写三遍的代码就要考虑重构。同时,这个工程仅仅提供一个展示,没有具体的单选以及多选或者全选功能,这些都会在接下来进行实现。

先看看初始工程长什么样子吧,而且最终的工程也是长这个样子的,不过功能部分是看不出来的,体验才能察觉出来。

感觉给自己挖了个大坑!!!!!!

在实际的开发中,一个购物车要考虑的情形比这复杂的多了,这里仅仅是简化到了最简单的购物车功能。

参考资料: