如何在显示表单时滚动 UITableView






4.55/5 (13投票s)
本文详细介绍了在 UITableView 中显示表单所需的 UITableViewDelegate 方法,以实现完全支持。
引言
Xcode 文档在描述滚动表视图时的 UIKit 交互方面明显不足。《iOS 表视图编程指南》会让人认为,填充表视图所需的一切就是实现 viewDidLoad
和 tableView:cellForRowAtIndexPath
方法。关于滚动,唯一提到的内容是“滚动表视图也会为每个新出现的行调用 tableView:cellForRowAtIndexPath:
”。其中有关于使用数据填充表、管理选择、插入和删除行的章节,但没有专门讨论支持滚动的内容。
本文介绍 UITableViewDelegate
协议中用于显示和与表单交互以支持滚动所必需的方法。这些方法还提供了一种更优雅的方式来初始化将要显示的单元格的数据。
背景
用表视图呈现表单以捕获用户输入可能看似简单:将 UITableViewController
控件拖到故事板上。将表视图内容设置为静态单元格。配置节数和单元格数。将适当的控件拖到每个单元格中以捕获相应的应用程序数据。在屏幕上放置一个保存按钮,可能是在导航栏中。将按钮连接到 UITableViewController
的子类,并包含一个保存操作方法。在保存方法中添加代码,以读取每个单元格中的数据,将其传输到适当的应用程序变量,并将数据存储到模型中。
这似乎很简单。如果表单中的单元格数量不足以填满设备屏幕高度,那么大部分(如果不是全部)逻辑都在保存方法中。如果不是,那么 **只有** **可见** 单元格代表的数据才能在表视图中显示。随着用户滚动,屏幕外的单元格会被移除,新出现的单元格会被添加。
您可能会问,这为什么会使事情复杂化?这是一个表单,所以应用程序只需要在用户点击保存等按钮时采取行动的数据。以下是只在保存方法中传输视图数据的后果。当保存方法从表视图的顶部访问到 S 底部时,tableView:cellForRowAtIndexPath:
会为未显示的行返回 nil
。无法访问不可见的单元格,因此无法将用户输入的所有数据传输到模型中。
另外,对于使用“动态原型”的表,随着新单元格滚动到视图中,它们会显示屏幕外被移除的单元格。例如,如果您有一个包含 20 个相同单元格的表,每个单元格都有一个 UITextField
,并且第一个单元格的文本字段包含字符“1”,那么从底部滚入的单元格的文本字段也将包含字符“1”,如果您不在 tableView:cellForRowAtIndexPath:
方法中初始化单元格。第一个单元格会被重复使用以显示滚动进来的单元格,但为了正确初始化,您必须在单元格滚动出屏幕时存储数据。
请注意,对于使用“静态单元格”的表,这不是问题。UITableView
和 UITableViewController
的默认行为会维护相应单元格在进出视图时的值。虽然我不确定支持此行为的逻辑,但我知道在调用 tableView:cellForRowAtIndexPath:
时,不可见的单元格会从表中移除,并为它们返回 nil
。因此,在单元格滚动出视图时捕获这些单元格的数据以在单元格超出可见范围时访问是必要的。
因此,为了正确实现带 UITableView
的表单,必须处理两种用例。单元格数据需要在滚动出视图时保存(对“动态原型”和“静态单元格”都必需),并且需要在滚动回来时恢复(仅对带“动态原型”的表必需,但我会明确恢复它们)。当这两种情况得到妥善处理时,保存方法就可以访问并存储所有输入的数据。
使用代码
现在您已经了解了问题的背景,我将在一个保存 20 名同学列表的应用程序中演示解决方案。每个单元格都有一个 UITextField
控件。在此示例中,保存会将每个同学的值写入系统日志。有两个 UITableViewController
的子类:一个用于演示静态单元格的逻辑,名为 StaticClassListController
;另一个用于演示“动态原型”的逻辑,名为 DynamicClassListController
。
为了在单元格滚动进出视图时维护同学列表,DynamicClassListController
有一个包含 20 个元素的字符串数组,名为 classmateNames
。
NSString *classmateNames[20];
在 viewDidLoad
中,该数组被初始化为所有 20 个元素的 nil
。
当行 **从** 视图中 **移除** 时,数据会保存到相应的数组索引中。这在 DynamicClassListController
的 tableView:didEndDisplayingCell:forRowAtIndexPath:
方法中处理。
-(void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
UITextField* txtField = (UITextField*)[cell viewWithTag:1];
classmateNames[[indexPath row]] = txtField.text;
}
当行 **进入** 视图时,数据会从相应的数组索引中恢复。这在 tableView:willDisplayCell:forRowAtIndexPath:
方法中处理。对于带“静态单元格”的表视图,此方法不是必需的。
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
UITextField *txtFieldClassMate = (UITextField*)[cell viewWithTag:1];
NSUInteger row = [indexPath row];
txtFieldClassMate.text = classmateNames[row];
}
保存方法的工作逻辑是通过读取相应行的 UITextField
的值(如果可访问),否则读取 classmateNames
数组中相应行索引的值。该数组仅保存 **不可见** 单元格的最新数据。由于用户可能在单元格数据上次传输到数组后更改了值,因此有必要直接从 **可见** 单元格中获取数据。
- (IBAction)Save:(id)sender {
UITableViewCell *cell; // The current cell
NSString *name; // Set to the current value of the UITextField in the current cell
for (int i = 0; i < 20; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
cell = [self.tableView cellForRowAtIndexPath:indexPath];
if (cell != nil) {
name = ((UITextField*)[cell viewWithTag:1]).text;
} else if (classmateNames[i] != nil) {
name = classmateNames[i];
} else {
name = @"Empty";
}
NSLog(@"Value for cell at row %d: %@", i+1, name);
}
}
简而言之,要求如下:
- 使用
tableView:didEndDisplayingCell:forRowAtIndexPath:
检测单元格何时滚动出视图,并将单元格数据保存到临时存储。 - 使用
tableView:willDisplayCell:forRowAtIndexPath:
检测单元格何时滚动回视图,并在存在时从临时存储中恢复单元格数据。 - 最后,当用户在表单上执行操作(例如保存)时,**优先** 使用 **可见** 单元格中的数据,而不是临时存储中的数据。
关注点
还有一个额外的注意事项需要处理。滚动视图的默认值是“不要自动消失”。结果是,当单元格滚动出视图且其 UITextField
是第一响应者时,不会调用 didEndDisplayingCell
。如果用户在单元格仍然离开视图时点击保存,cellForRowAtIndexPath
将返回 nil
,因此数据将无法访问。要纠正此问题,请将键盘属性设置为“拖动时消失”。
教程中通常会在 tableView:cellForRowAtIndexPath:
方法中初始化单元格的数据,但另一种方法是在 tableView:willDisplayCell:forRowAtIndexPath:
中初始化它们,并将单元格创建逻辑保留在 tableView:cellFoRowAtIndexPath:
中。当然,在应用程序开发中,有很多方法和原因可以完成一项任务。了解替代方案可以让我们为独特的设计需求创建最优雅的解决方案。
摘要
表单只是应用程序关心检测单元格何时滚动进出视图的众多原因之一。对于那些单元格数量超过屏幕高度的表单,检测它们至关重要。