如何在UITableView中设计动态显示行






4.13/5 (6投票s)
描述了在UITableView中实现动态行的设计方法。
引言
您是否对在 Cocoa Touch 表单(如日历应用的日期选择器)中支持动态显示行的方案感到满意?如果您正在寻找另一种方法,本文或许正是您所寻找的。
此技术的目标是消除用于识别行的含糊不清的条件语句。虽然无法完全消除对单元格可见性的测试,但可以将其减少到基本条件。该技术的基础是使用映射表来识别表单中所有行的位置。通过这种方法,在识别静态行和动态行时没有区别。
在本文的其余部分,我将引导您完成一个简单的应用程序,该应用程序 **只** 用于演示此技术。该应用呈现一个包含两行的表单:柑橘类和瓜类。点击柑橘类或瓜类行将在其下方显示一个带有相应水果列表的选择器控件。再次点击该行将移除选择器并显示所选水果。
Using the Code
该应用程序使用 UIFruitSelectionController
(UITableViewController
的子类)来演示此技术。它负责显示水果选择和相应的选择器。
首先,定义支持该方法所需的变量和常量。该解决方案需要一个数组 rowMap
来标识每行的位置,以及一个数组 rowsPerSection
来标识表中每节的行数。此解决方案只有一个节。有两个布尔变量初始化为 no,isMelonPickerDisplayed
和 isCitrusPickerDisplayed
,用于标识各自的选择器何时显示。
常量定义了节的数量、节标识符、它们各自的行标识符和行数。需要两个常量来标识水果节中的行数。第一个行数 kFruitFormFruitSectionExpandedCount
表示当所有选择器控件都可见时节中行的数量,同样,kFruitFormFrutSectionCollapsedCount
表示当所有选择器控件都隐藏时节中行的数量。
其余常量标识节的行。需要注意的一个重要方面是,命名行的常量必须按顺序排列,并与所有行都显示时它们的位置相关联。
enum {
kFruitFormSectionCount = 1, // only one section in this solution
kFruitFormSectionFruits = 0, // the fruit section id
};
enum {
kFruitFormFruitSectionExpandedCount = 4,
kFruitFormFruitSectionCollapsedCount = 2,
kFruitFormFruitSectionRowCitrus = 0,
kFruitFormFruitSectionRowCitrusPicker = 1,
kFruitFormFruitSectionRowMelon = 2,
kFruitFormFruitSectionRowMelonPicker = 3,
};
@interface UIFruitSelectionController ()
{
@private
NSInteger rowMap[kFruitFormFruitSectionExpandedCount]; // maps row type to table position
NSInteger rowsPerSection[kFruitFormSectionCount]; // total count of rows in per section
Boolean isMelonPickerDisplayed; // Yes if the melon picker is displayed
Boolean isCitrusPickerDisplayed; // No if the Citrus picker is displayed.
}
@end
接下来,在 viewDidLoad
方法中初始化 UIFruitSelectionController
的对象变量。
最初,选择器行的表位置等于呈现并移除它们的相应行。此条件可用于标识选择器何时隐藏。我更喜欢使用标志的清晰度,而不是测试相等的映射表行位置。标志使代码更具可读性。
// Pickers are initially not displayed
isMelonPickerDisplayed = isCitrusPickerDisplayed = false;
// Initialize the location of each row type in the table
rowMap[kFruitFormFruitSectionRowCitrus] = kFruitFormFruitSectionRowCitrus;
rowMap[kFruitFormFruitSectionRowCitrusPicker] = kFruitFormFruitSectionRowCitrus;
rowMap[kFruitFormFruitSectionRowMelon] = kFruitFormFruitSectionRowCitrus+1;
rowMap[kFruitFormFruitSectionRowMelonPicker] = kFruitFormFruitSectionRowCitrus+1;
rowsPerSection[kFruitFormSectionFruits] = kFruitFormFruitSectionCollapsedCount;
self.tableView.estimatedRowHeight = 2; // work around for bug in tableView
以下是返回节数和每节行数的方法。它们使用前面设置的常量和变量来返回值。
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
// Return the number of sections.
return kFruitFormSectionCount;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
// Return the number of rows in the section.
return rowsPerSection[section];
}
cellForRowAtIndexPath
方法测试要提供的单元格并返回它。用于识别返回哪个单元格的条件与静态单元格和动态单元格之间没有区别。为了使此工作正常,必须让选择器检查紧跟在呈现它的行之后;否则,如果顺序颠倒,选择器将显示。
在从函数返回之前,它会测试返回的行是否为选择器行之一,并调用其 init
方法。这是必需的,因为选择器是子类,并且 UIPickerViewDataSource
和 UIPickerViewDelegate
接口在其中定义。调用此方法是为了初始化 UIPickerView
的数据源和委托属性。本文中未对此进行说明。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *cellIdentifier;
NSInteger row = [indexPath row];
NSInteger section = [indexPath section];
switch (section) {
case kFruitFormSectionFruits:
if (rowMap[kFruitFormFruitSectionRowCitrus] == row) {
cellIdentifier = @"CitrusCellId";
} else if (rowMap[kFruitFormFruitSectionRowCitrusPicker] == row) {
cellIdentifier = @"CitrusPickerCellId";
} else if (rowMap[kFruitFormFruitSectionRowMelon] == row) {
cellIdentifier = @"MelonCellId";
} else if (rowMap[kFruitFormFruitSectionRowMelonPicker] == row) {
cellIdentifier = @"MelonPickerCellId";
}
break;
default:
break;
}
UIFruitCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier
forIndexPath:indexPath];
if ((row == rowMap[kFruitFormFruitSectionRowCitrusPicker] && isCitrusPickerDisplayed)) {
[cell.pickerCitrus init];
} else if ((row == rowMap[kFruitFormFruitSectionRowMelonPicker] && isMelonPickerDisplayed)) {
[cell.pickerMelon init];
}
return cell;
}
选择器在 didSelectRowAtIndexPath
方法中插入和移除。在移除选择器行之前,选择将从选择器复制并分配给显示行。接下来,从选择器行开始,以及其后的每一行,rowMap
的位置都会递减。两个辅助例程 incrementRowMapAt
和 decrementRowMapAt
将在本文后面进行说明。
if (rowMap[kFruitFormFruitSectionRowCitrus] == row) { // Citrus selection row tapped?
if (isCitrusPickerDisplayed) { // Citrus picker is displayed so
isCitrusPickerDisplayed = NO; // after this routine exits the citrus picker is no longer displayed
// create the index for the citrus picker
NSIndexPath * removePath = [NSIndexPath indexPathForRow:rowMap
[kFruitFormFruitSectionRowCitrusPicker] inSection:kFruitFormSectionFruits];
// Retrieve a copy of the cell
UIFruitCell *citrusSelection = (UIFruitCell*)[tableView cellForRowAtIndexPath:removePath];
// Copy the selection to the row that displays it.
cell.lblCitrus.text = citrusSelection.pickerCitrus.selection;
--rowsPerSection[kFruitFormSectionFruits]; // one less row after the picker is removed.
// all the rows from the picker and after shift up in the table by one position
[self decrementRowMapAt:kFruitFormFruitSectionRowCitrusPicker];
// Remove the picker from the table.
[tableView deleteRowsAtIndexPaths:@[removePath] withRowAnimation:UITableViewRowAnimationMiddle];
}
}
didSelectRowAtIndexPath
中添加选择器的逻辑类似。此方法的重要方面是从选择器开始,以及其后的每一行,递增行位置。
isCitrusPickerDisplayed = YES; // Set indicating the citrus picker is on display
++rowsPerSection[kFruitFormSectionFruits]; // once inserted there is one more row in the table
// all the rows from the picker and after shift down in the table by one position
// to make room for the cell
[self incrementRowMapAt:kFruitFormFruitSectionRowCitrusPicker];
// Create the index path for the picker
NSIndexPath * insertPath = [NSIndexPath indexPathForRow:rowMap[kFruitFormFruitSectionRowCitrusPicker]
inSection:kFruitFormSectionFruits];
// Insert the picker after the row that when tapped presents it
[tableView insertRowsAtIndexPaths:@[insertPath] withRowAnimation:UITableViewRowAnimationMiddle];
有两个辅助方法可以递增和递减 rowMap
。它们从传入的行位置开始循环遍历映射表,直到到达末尾并执行相应的操作。
- (void) incrementRowMapAt: (NSInteger) index {
for (NSInteger i = index; i < kFruitFormFruitSectionExpandedCount; i++) {
++rowMap[i];
}
}
- (void) decrementRowMapAt: (NSInteger) index {
for (NSInteger i = index; i < kFruitFormFruitSectionExpandedCount; i++) {
--rowMap[i];
}
}
至此,关于使用映射数组在表单中定位动态行的描述就完成了。下载压缩的项目文件以评估此方法。
历史
- 2015 年 9 月 30 日 - 由于添加了可下载的项目文件,已删除指向 github 的链接
- 2015 年 9 月 25 日 - 添加了可下载的示例项目文件
- 2015 年 6 月 23 日 - 修复了一些标点错误
- 2015 年 6 月 20 日 - 首次发布