我们都知道tableView有个编辑模式, 该模式下, tableView中的每一行会显示编辑控件和排序控件. 通过这些控件你可以对cell删除和排序, 或者增加一个新的cell. 这些control的外观是独特的, 如下:
当TableView进入编辑模式后, 用户点击了编辑控件, TableView会发送一系列消息给到DataSource和delegate
, 但是只有当你实现了这些方法才会. 这些方法可以让DataSource和delegate可以改进row的外观和行为; 另外通过这些方法可以让他们执行删除和插入操作.
即使tableView处于正常状态, 你也可以对cell的删除和插入, 也可删除section, 这些操作都可以动画展示.
下面内容的第一部分告诉你如何对编辑模式下的TableView中的cell进行编辑, 第二部分告诉你如何同时删除多个cell或section.
在编辑模式下对行的删除和插入
TableView的编辑模式
当TableView进入编辑状态时, tableView会受到setEditing:animated:
消息. 通常(但不一定), 该消息源于用户点击导航栏编辑按钮时发送的动作消息. 可以通过delegate的tableView:editingStyleForRowAtIndexPath:
方法来控制编辑模式下, row中显示编辑控件的样式.
注意:如果view controller中实现了
setEditing:animated:animated:
方法, 那么你可以在该方法中做一些你需要的事情, 比如更新某些button的状态, controller中的这个方法会在TableView中的setEditing:animated:animated:
方法之前的被调用.
当tableView收到setEditing:animated:
消息后, 它会给tableView中所有可见的cell发送相同的消息. 然后发送一系列消息给到DataSource和delegate(前提是实现了相应的方法), 这一过程如下图6-1所示.
当cell收到setEditing:animated:
消息后, 接下来的方法调用序列如下:
- tableView调用DataSource中的
tableView:canEditRowAtIndexPath:
方法(如果已经实现). 在这个方法中可以将一些cell排除在编辑状态之外, 即使这些cell的editingStyle
中的值另有指示. 大多数情况下和APP不需要实现该方法. - tableView调用delegate中的
tableView:editingStyleForRowAtIndexPath:
方法(如果已经实现). 通过该方法可以控制cell的显示的编辑风格和编辑控件, 到了此时, tableView已经完全进入了编辑模式. - 然后用户点击相应的编辑控件, 如果点击删除控件, 那么改行会展示一个删除按钮, 可以让用户确认是否真的删除该行.
- tableView发送
tableView:commitEditingStyle:forRowAtIndexPath:
给到DataSource对象. 尽管该协议方法时可选的, 但是要是实现cell的删除和插入, 你就必须实现这个方法, 另外:- 实现
deleteRowsAtIndexPaths:withRowAnimation:
或者insertRowsAtIndexPaths:withRowAnimation:
来调整TableView的显示 - 更新相应的模型数据数组, 往数组删除相应的数据或者添加相应的数据.
- 实现
当用轻扫cell来展示删除按钮时, 在此过程中方法调用和上图6-1所示有点不同. 当用轻扫cell来删除时, TableView会先检查DataSource是否实现了tableView:commitEditingStyle:forRowAtIndexPath:
方法; 如果实现了该方法, TableView会发送setEditing:animated:
消息给自己后进入编辑模式. 在"swipe to delete"这种模式下, TableView不会展示添加/移动等控件. 因为该事件是由用户驱动的, TableView会调用delegate的两个方法tableView:willBeginEditingRowAtIndexPath:
和tableView:didEndEditingRowAtIndexPath:
. 通过实现这两个方法, 可以适当地更新TableView的外观.
注意:在DataSource的
tableView:commitEditingStyle:forRowAtIndexPath:
方法中, 记得不要调用setEditing:animated:
方法, 如果因为某些原因非得调用, 那么可以使用performSelector:withObject:afterDelay:
来延迟调用该方法.
虽然你可以通过cell中的插入控件来插入新的cell, 当这里有种替代方法, 就是在导航栏中添加一个"Add"按钮, 点击该按钮发送一个action给到viewController, 然后modal出一个新view, 用来给用户输入新的item, 该view会遮住TableView. 当新的item输入完成后, viewController会更新data, 然后reload TableView. 下面的内容会提到该方法.
删除cell的一个例子
该部分内容通过一个例子向你展示TableView的搭建和处理TableView编辑相关的实现. 该demo使用导航栏的控制器结构来管理TableView. 在loadView
方法中, 创建TableView和设置DataSource和delegate, 然后将导航栏的右边的barButton设置为edit button.
self.navigationItem.rightBarButtonItem = self.editButtonItem;
该按钮已经设置好了点击的action为发送setEditing:animated:
消息给到view controller; button的title会在"Edit"和"Done"来回切换. 代码6-1展示了该方法的实现.
代码清单6-1 view controller对setEditing:animated:
方法的反应
- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
[super setEditing:editing animated:animated];
[tableView setEditing:editing animated:YES];
if (editing) {
addButton.enabled = NO;
} else {
addButton.enabled = YES;
}
}
当TableView进入编辑模式下, viewController会让所有的cell显示一个删除控件, 除了最后一个cell外. 最后一个cell会显示一个插入控件. 这些是通过tableView:editingStyleForRowAtIndexPath:
实现的, 如代码6-2.
代码清单6-2 设置行的编辑风格
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath {
SimpleEditableListAppDelegate *controller = (SimpleEditableListAppDelegate *)[[UIApplication sharedApplication] delegate];
if (indexPath.row == [controller countOfList]-1) {
return UITableViewCellEditingStyleInsert;
} else {
return UITableViewCellEditingStyleDelete;
}
}
当用户点击行中的delete控件时, TableView会调用viewController中的tableView:commitEditingStyle:forRowAtIndexPath:
方法, 如代码6-3所示, 在该方法的实现是删除相应的数据项, 然后给tableView发送一个deleteRowsAtIndexPaths:withRowAnimation:
消息.
代码清单6-3 更新数据源和删除相应的行
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
// 如果此行被删除, 那么将其从列表中移除.
if (editingStyle == UITableViewCellEditingStyleDelete) {
SimpleEditableListAppDelegate *controller = (SimpleEditableListAppDelegate *)[[UIApplication sharedApplication] delegate];
[controller removeObjectFromListAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}
添加一个cell的例子
这部分内容告诉你如何往tableView中添加cell. 在本例中, 不是通过cell上的插入控件来引发添加操作, 而是通过在导航栏上添加一个"Add"button来开启添加操作. 下面代码6-4, 展示了viewController的loadView
实现, 将addButton设置为导航栏的右侧barButton.
代码清单6-4 往导航栏中添加一个Add button
addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addItem:)];
self.navigationItem.rightBarButtonItem = addButton;
注意控制器还需要设置控件的标题和控件action的selector. 当用户点击Add按钮时会调用addItem:
方法. 该action方法的实现如6-5所示. 在该方法中, 会创建一个包含一个viewController的navigationController, 然后将其modal出来.
代码清单6-5 Add按钮的action方法
- (void)addItem:sender {
if (itemInputController == nil) {
itemInputController = [[ItemInputController alloc] init];
}
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:itemInputController];
[[self navigationController] presentModalViewController:navigationController animated:YES];
}
在新modal出的controller中, 有个textfield和一个save按钮, 用户输入新的内容后, 点击save按钮会调动controller中的save:
方法, 如下代码6-6所示, viewController会从textfield提取用户输入的信息, 然后使用该信息去更新tableView的data数组.
代码清单6-6 往数据模型数组中添加一个新item
- (void)save:sender {
UITextField *textField = [(EditableTableViewTextField *)[tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]] textField];
SimpleEditableListAppDelegate *controller = (SimpleEditableListAppDelegate *)[[UIApplication sharedApplication] delegate];
NSString *newItem = textField.text;
if (newItem != nil) {
[controller insertObject:newItem inListAtIndex:[controller countOfList]];
}
[self dismissModalViewControllerAnimated:YES];
}
当modal出的view被dismiss后, TableView会被reloaded, 之后新添加的item会被显示出来.
批量插入/删除/排序tableView中的行和组
UITableView
类允许你在同一时间将一组行或section插入/删除, 并且以动画展示. 在代码6-7中, 有8个方法用来批量删除和插入. 注意, 你可以在animation-block的外面调用这些删除和插入方法(就如前面内容中所述的DataSource tableView:commitEditingStyle:forRowAtIndexPath:
方法实现一样).
代码清单6-7 删除/插入的批处理方法
- (void)beginUpdates;
- (void)endUpdates;
- (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation;
- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation;
- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation;
- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation: (UITableViewRowAnimation)animation;
- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation: (UITableViewRowAnimation)animation;
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation;
注意:
reloadSections:withRowAnimation:
和reloadRowsAtIndexPaths:withRowAnimation:
这两个方法是在iOS3.0引入的, 这些方法可以让tableView重载具体的sections和rows, 而不是调用reloadData
来更新整体可见的tableView.
为了动画展示批量插入/删除/重排序, 你需要在beginUpdates
和endUpdates
两个方法调用之间调用前面的批处理方法. 如果你不这样做, 调用批处理方法后, row和section的index可能会变的无效了. 另外beginUpdates
和endUpdates
这两个方法可以嵌套使用.
总而言之, 在调用endUpdates
方法返回后, tableView回去访问DataSource和delegate对象, 请求行和数据. 所以在删除和插入时应该更新tableView背景的数据模型数组.
批量删除和插入的例子
为了批量插入或者删除一组行/组, 第一是准备好数组用来存放rows和sections背后的数据. 当行和组被插入和删除后, 从该数组中获取数据来填充行和组.
接下来是, 调用beginUpdates
方法, 然后是调用insertRowsAtIndexPaths:withRowAnimation:
, deleteRowsAtIndexPaths:withRowAnimation:
, insertSections:withRowAnimation:
, 或者insertSections:withRowAnimation:
, 然后使用endUpdates
方法来提交上述操作. 代码6-8展示了这一操作.
代码清单6-8 批量删除/插入操作
- (IBAction)insertAndDeleteRows:(id)sender {
// original rows: Arizona, California, Delaware, New Jersey, Washington
[states removeObjectAtIndex:4]; // Washington
[states removeObjectAtIndex:2]; // Delaware
[states insertObject:@"Alaska" atIndex:0];
[states insertObject:@"Georgia" atIndex:3];
[states insertObject:@"Virginia" atIndex:5];
NSArray *deleteIndexPaths = [NSArray arrayWithObjects:
[NSIndexPath indexPathForRow:2 inSection:0],
[NSIndexPath indexPathForRow:4 inSection:0],
nil];
NSArray *insertIndexPaths = [NSArray arrayWithObjects:
[NSIndexPath indexPathForRow:0 inSection:0],
[NSIndexPath indexPathForRow:3 inSection:0],
[NSIndexPath indexPathForRow:5 inSection:0],
nil];
UITableView *tv = (UITableView *)self.view;
[tv beginUpdates];
[tv insertRowsAtIndexPaths:insertIndexPaths withRowAnimation:UITableViewRowAnimationRight];
[tv deleteRowsAtIndexPaths:deleteIndexPaths withRowAnimation:UITableViewRowAnimationFade];
[tv endUpdates];
// ending rows: Alaska, Arizona, California, Georgia, New Jersey, Virginia
}
排序操作和Index Paths
你可能注意到了代码6-8中deleteRowsAtIndexPaths:withRowAnimation:
方法要在insertRowsAtIndexPaths:withRowAnimation:
方法后. 但是tableView执行的操作不一定是按照block中的方法调用顺序. 事实上, TableView会将所有的插入操作延后, 直到处理完删除操作. TableView的行为与更新块内调用的重新加载方法相同——在执行动画块之前,对行和节的索引进行重新加载。这种行为和插入/删除/排序的顺序无关.
删除和热loading操作会决定原TableView中的那些行/组需要删除或者更新位置; 插入操作会指定向TableView中加入那些行/组. IndexPath用来标记row和section的位置, 另一方面,插入或移除可变数组中的项可能会影响用于连续插入或移除操作的数组索引;例如,如果在某个索引处插入项,则数组中所有后续项的索引都会递增。
这里有个例子, 假设你有个TableView, 其中有三个section, 每个section中有三个row, 然后实现下面的动画块:
- Begin updates
- 删除索引为(row:1, section:0)处的行
- 删除索引为(section:1)处的组
- 往索引(row:1, section:1)处插入一行
- End updates
图6-2, 展示了该动画块执行的过程