本文是Intermediate iOS 11 Programming with Swift 4系列 的 第 二 篇.
如果您想在UITableView中显示大量的记录,您最好重新考虑如何显示数据的方法。随着行数的增加,表视图变得难以处理。改进用户体验的一种方法是将数据组织成部分。通过将相关数据分组在一起,可以为用户提供更好的访问方式。
此外,还可以在表视图中实现索引列表。索引表视图或多或少与纯样式表视图相同。惟一的区别是它在表视图的右边包含一个索引。索引表在iOS应用程序中非常常见。最著名的例子是iPhone的内置联系人应用。通过提供索引滚动,用户可以立即访问表的特定部分,而无需滚动每个部分。
让我们看看如何向一个简单的表应用程序中添加节和索引列表。如果您对UITableView实现有基本的了解,那么添加节和索引列表并不太难。基本上,您需要处理UITableViewDataSource协议中定义的这些方法:
numberofSections (in:) 方法 - 返回表视图中所有的部分。通常,我们将section的数量设置为1。如果你想要有多个部分,把这个值设置为大于1的数字.
tableView(_ :titleForHeaderInsection:) 方法 - 返回不同部分的标题。如果您不喜欢为部分分配标题,则此方法是可选的.
tableView(_ : numberOfRowsInSection:) 方法 - 返回特定部分中的行总数.
tableView(_ : CellForRowAt:) 方法——如果您知道如何在UITableView中显示数据,那么这种方法对您来说不应该是陌生的。它返回特定部分的表数据.
sectionIndexTitles( for :) 方法——返回出现在表视图右边的索引列表中的索引标题。例如,您可以返回包含从a到Z值的字符串数组.
tableView(_ : sectionForSectionIndexTitle: at: ) 方法——返回当用户点击特定索引时,表视图应该跳转到的部分索引.
没有比给你看一个例子更好的解释实现的方法了。和往常一样,我们将构建一个Simple App,它将使您对索引列表实现有更好的理解.
Demo App
首先,让我们快速浏览一下我们将要构建的演示应用程序。这是一个非常简单的应用程序,在标准的表格视图中显示动物列表。这个应用程序并没有列出所有的动物,而是将它们分组到不同的区域,并显示一个索引列表,以便快速访问. 下面的屏幕截图显示了演示应用程序的最终交付.
下载Xcode项目模板
演示的重点是实现部分和索引列表.
因此,您可以从这里下载项目模板.这个模板已经包含了你需要的一切。如果构建模板,您将有一个应用程序在表视图中显示动物列表(但没有节和索引)。稍后,我们将修改应用程序,将数据分组到节中,并向表中添加索引列表.
显示部分UITableView
好的,让我们开始吧。如果您打开IndexTableDemo项目,动物数据将在数组中定义:
let animals = ["Bear", "Black Swan", "Buffalo", "Camel", "Cockatoo", "Dog", "Donkey", "Emu", "Giraffe", "Greater Rhea", "Hippopotamus", "Horse", "Koala", "Lion", "Llama", "Manatus", "Meerkat", "Panda", "Peacock", "Pig", "Platypus", "Polar Bear", "Rhinoceros", "Seagull", "Tasmania Devil", "Whale", "Whale Shark", "Wombat"]
我们会根据动物名字的第一个字母将数据分成几个部分。有很多方法可以做到这一点。一种方法是用字典手工替换动物数组,如下所示:
let animals: [String: [String]] = ["B" : ["Bear", "Black Swan", "Buffalo"],
"C" : ["Camel", "Cockatoo"],
"D" : ["Dog", "Donkey"],
"E" : ["Emu"],
"G" : ["Giraffe", "Greater Rhea"],
"H" : ["Hippopotamus", "Horse"],
"K" : ["Koala"],
"L" : ["Lion", "Llama"],
"M" : ["Manatus", "Meerkat"],
"P" : ["Panda", "Peacock", "Pig", "Platypus", "Polar Bear"],
"R" : ["Rhinoceros"],
"S" : ["Seagull"],
"T" : ["Tasmania Devil"],
"W" : ["Whale", "Whale Shark", "Wombat"]]”
在上面的代码中,我们把动物数组变成了字典。动物名字的第一个字母被用作钥匙。与相应键相关联的值是一个动物名称数组。
我们可以手动创建字典,但是如果我们可以通过编程的方式从animals数组中创建索引,那不是很好吗?让我们看看怎么做。
首先,在AnimalTableViewController类中声明两个实例变量:
var animalsDict = [String: [String] ] ()
var animalSectionTitles = [String] ()
我们初始化用于存储动物的空字典和用于存储表的节标题的空数组。章节标题是动物名字的首字母(如B)。
因为我们想从animals数组生成一个字典,所以我们需要一个helper方法来处理生成。在AnimalTableViewController类中插入以下方法:
在此方法中,我们遍历动物数组中的所有项。对于每一项,我们首先提取动物名字的第一个字母。要获取特定位置的索引(例如string . index),您必须向字符串本身询问startIndex,然后调用index方法以获得所需的位置。在本例中,目标位置为1,因为我们只对第一个字符感兴趣。
在Swift 3中,您使用字符串的substring(to:)方法来获取一个新字符串,该字符串包含直到给定索引为止的字符。现在,在Swift 4中,该方法已被弃用。相反,您可以使用如下这样的下标将字符串分割为子字符串:
let animalKey = String(animal[..<firstLetterIndex])
aimal[..< firstLetterIndex] 将动物字符串切片到指定的索引。在上面的例子中,它意味着提取第一个字符。您可能想知道为什么我们需要使用字符串初始化来包装返回的子字符串。在Swift 4中,当您将一个字符串分割为一个子字符串时,您将得到一个子字符串实例。它是一个临时对象,与原始字符串共享其存储。为了将子字符串实例转换为字符串实例,需要使用String()对其进行包装.
如前所述,动物名字的第一个字母被用作字典的键。字典的值是该特定键的动物数组。因此,一旦我们获得了这个键,我们要么创建一个新的动物数组,要么将这个项目附加到一个现有的数组中。这里我们展示了前四次迭代的animalsDict值:
迭代 #1: animalsDict["B"] = ["Bear"]
迭代 #2: animalsDict["B"] = ["Bear", "Black Swan"]
迭代 #3: animalsDict["B"] = ["Bear", "Black Swan", "Buffalo"]
迭代 #4: animalsDict["C"] = ["Camel"]
在完全生成animalsDict之后,我们可以从字典的键中检索section标题。
要检索字典的键,只需调用keys方法。但是,返回的键是无序的。Swift的标准库提供了一个名为sort的函数,该函数根据所提供的排序闭包的输出返回已知类型的值的排序数组。
闭包接受相同类型的两个参数(在本例中是字符串),并返回一个Bool值,以声明第一个值在第一个值被排序后是应该出现在第二个值之前还是之后。如果第一个值出现在第二个值之前,它应该返回true。
编写排序闭包的一种方法是:
animalSectionTitles = animalSectionTitles.sorted( by: { (s1:String, s2:String) -> Bool in
return s1 < s2
})
您应该非常熟悉闭包表达式语法。在闭包的主体中,我们比较两个字符串值。如果第二个值大于第一个值,则返回true。例如,s1的值是B, s2的值是E,因为B小于E,闭包返回true,表示B应该出现在E之前。
如果您仔细阅读前面的代码片段,您可能会奇怪为什么我要这样编写排序闭包:
animalSectionTitles = animalSectionTitles.sorted(by: { $0 < $1 })
它是编写内联闭包的一种快捷方式。这里$0和$1指的是第一个和第二个字符串参数。如果您使用简短的参数名称,您可以省略几乎所有的闭包,包括参数列表和关键字;您只需要编写闭包的主体。
Swift 3提供了另一个排序函数sort。这个函数和排序后的函数很相似。排序函数不是返回排序的数组,而是对原始数组进行排序。您可以用下面的代码替换代码行:
animalSectionTitles.sort(by: { $0 < $1 })
创建助手方法后,更新viewDidLoad方法以调用它:
下一步,改变numberOfSections(in:)方法,并返回section的总数:
要在每个部分显示标题标题,我们需要实现tableView(_:titleForHeaderInSection:)方法。每次显示一个新节时,都会调用此方法。基于给定的section索引,我们返回相应的section标题
这很简单,对吧?接下来,我们必须告诉表视图特定部分中的行数。在AnimalTableViewController中更新tableView(_:numberOfRowsInSection:)方法.
当应用程序开始在表视图中呈现数据时,每当显示新节时,就调用tableView(_:numberOfRowsInSection:)方法。根据节索引,我们可以获取节标题并将其用作检索该节的动物名称的键。然后我们返回该部分的动物名称的总数。在上面的代码中,我们使用guard关键字来确定字典是否为特定的animalKey返回一个有效的数组。如果不是,我们返回0.
在这种情况下,guard关键字特别有用。在继续执行之前,我们希望确保animalValues包含一些值。而且,它使代码更清晰、更可读。
最后,修改tableView(_:cellForRowAt:)方法如下:
indexPath参数包含当前行号,以及当前的节索引。因此,基于section索引,我们检索section title(例如:“B”)并使用它作为检索该部分动物名称的键。代码的其余部分非常简单。我们只需获取动物的名字并将其设置为细胞标签。imageFilename变量的计算方法是将动物名称转换为小写字母,然后用下划线替换所有出现的空格。
好了,你准备好了!点击Run按钮,你会得到一个包含部分但没有索引列表的应用。
向UITableView 添加列表索引
那么,如何向表视图添加索引列表呢? 同样,这比您想象的要简单,并且只需几行代码就可以实现。只需添加sectionindextitle (for:)方法并返回一个section索引数组。这里我们将使用章节标题作为索引。
override func sectionIndexTitles(for tableView: UITableView) -> [String]? {
return animalSectionTitles
}
就是这样! 重新编译并运行应用程序。您应该在表的右边找到索引。有趣的是,您不需要任何实现,而且索引已经工作了!尝试点击任何索引,您将被带到表的特定部分.
添加 A -- Z 列表索引
看来我们什么都做过了。但是为什么我们一开始就提到了tableView(_:sectionForSectionIndexTitle:at:)方法呢?
目前,索引列表不包含整个字母表。它只显示那些被定义为动物字典的键的字母。有时,您可能希望在索引列表中显示A-Z。让我们在animaltableviewcontroller中声明一个名为animalindextitle的新变量:
let animalIndexTitles = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]
接下来,更改sectionIndexTitle (for:)方法并返回animalIndexTitle数组,而不是animalSectionTitle数组
override func sectionIndexTitles(for tableView: UITableView) -> [String]? {
return animalIndexTitles
}
现在,重新编译并运行这个应用程序。太酷了!该应用程序将索引从A显示到Z。
但是等一下,它不能正常工作!如果你尝试点击索引“C”,应用程序会跳转到“D”部分。如果你点击索引“G”,它会指向“K”部分。下面显示新旧索引之间的映射。
好,你可能会注意到,索引的数量大于部分的数量,和UITableView对象不知道如何处理索引。您的职责是实现tableView(_:sectionForSectionIndexTitle:at:)方法,并在选中特定索引时显式地告诉表视图区段编号。增加以下新方法:
override func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
guard let index = animalSectionTitles.index(of: title) else {
return -1
}
return index
}
基于所选的索引名(即title),我们定位了animalsectiontitle的正确节索引。在Swift中,您使用名为index(of:)的方法来查找数组中特定项的索引。
实现的整个要点是验证给定的标题是否可以在animalsectiontitle数组中找到,并返回相应的索引。然后,表视图移动到相应的部分。例如,如果标题是B,我们检查B是否是有效的节标题并返回索引1。如果找不到标题(例如A),我们返回-1。
重新编译并运行应用程序。索引列表现在应该可以工作了.
定制部分标题
通过覆盖UITableView类和UITableViewDelegate协议中定义的一些方法,您可以轻松地定制节头。在这个演示中,我们将做一些简单的更改:
更改节标题的高度
更改节标题的字体和背景颜色
要更改区段标题的高度,您可以简单地覆盖tableView(_:heightForHeaderInSection:)方法并返回首选的高度.
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 50
}
在显示节头视图之前,将调用tableView(_:willDisplayHeaderView:forSection:)方法。该方法包含一个名为view的参数。这个视图对象可以是自定义头视图,也可以是标准头视图。在我们的演示, 我们只使用标准的header视图,它是UITableViewHeaderFooterView对象。一旦有了header视图,就可以更改文本颜色、字体和背景颜色。
再次运行应用程序。头视图应该用您喜欢的字体和颜色进行更新。
总结
当您需要显示大量记录时,将数据组织到节中并提供索引列表以方便访问是简单而有效的。在本章中,我们已经介绍了索引表的实现。现在,我相信您应该知道如何向您的表视图添加区段和索引列表.
你可以在这里下载原作者的 Xcode 项目.