表视图是UIKit框架中最常用的组件之一,并且是iOS平台上用户体验的组成部分。 表格视图做一件事,并且做得很好,显示了项目的有序列表。 UITableView
类是继续学习UIKit框架的好地方,因为它结合了Cocoa Touch和UIKit的几个关键概念,包括视图,协议和可重用性。
数据源和委托
UITableView
类是UIKit框架的关键组件之一,已高度优化以显示项目的有序列表。 可以定制表视图并使其适合各种用例,但是基本思想保持不变,只显示项目的有序列表。
UITableView
类仅负责将数据显示为行列表。 正在显示的数据由表视图的数据源对象管理,可通过表视图的dataSource
属性进行访问。 数据源可以是符合UITableViewDataSource
协议(Objective-C协议)的任何对象。 正如我们将在本文后面看到的那样,表视图的数据源通常是视图控制器,该控制器管理表视图是其子视图的视图。
同样,表格视图仅负责检测表格视图中的触摸。 它不负责响应触摸。 表格视图还具有delegate
属性。 每当表视图检测到触摸事件时,它都会将该触摸事件通知给其委托。 表格视图的代表负责响应触摸事件。
通过让数据源对象管理其数据以及委托对象处理用户交互,表视图可以专注于数据表示。 结果是一个高度可重用和高性能的UIKit组件,该组件与MVC(模型-视图-控制器)模式完全吻合,我们在本系列的前面已经讨论过。 UITableView
类继承自UIView
,这意味着它仅负责显示应用程序数据。
数据源对象与委托对象相似,但不相同。 委托对象由委托对象委托对用户界面的控制。 但是,数据源对象是数据的委派控制。
表格视图要求数据源对象提供应显示的数据。 这意味着数据源对象还负责管理它向表视图提供的数据。
表格视图组件
UITableView
类继承自UIScrollView
, UIScrollView
是一个UIView
子类,它提供了对显示大于应用程序窗口大小的内容的支持。
UITableView
实例由行组成,每行包含一个单元格, UITableViewCell
的实例或其子类。 与此相反的对应UITableView
在OS X, NSTableView
,实例UITableView
是一个列宽。 嵌套数据集和层次结构可以通过结合使用表视图和导航控制器( UINavigationController
)来显示。 我们将在本系列的下一篇文章中讨论导航控制器。
我已经提到过,表视图仅负责显示数据源对象提供的数据和检测触摸事件,这些事件被路由到委托对象。 表格视图不过是管理多个子视图(表格视图单元)的视图。
一个新项目
创建一个新的Xcode项目并向您展示如何设置表格视图,向其填充数据并使其对触摸事件做出响应,这比让您的理论繁重更好,而且更加有趣。
打开Xcode,创建一个新项目( File> New> Project ... ),然后选择Single View Application模板。
将项目命名为“ 表视图” ,分配组织名称和标识符,然后将“ 设备”设置为iPhone 。 告诉Xcode您要将项目保存到哪里,然后点击Create 。
新项目应该看起来很熟悉,因为我们在本系列的前面选择了相同的项目模板。 Xcode已经为我们创建了一个应用程序委托类AppDelegate
,它还为我们提供了一个视图控制器类ViewController
。
添加表格视图
构建并运行该项目,以了解我们从何开始。 在模拟器中运行应用程序时看到的白屏是Xcode在情节提要中为我们实例化的视图控制器的视图。
将表视图添加到视图控制器视图的最简单方法是在项目的主故事板上。 打开Main.storyboard并在右侧找到对象库 。 浏览对象库,然后将UITableView
实例拖到视图控制器的视图中。
如果表格视图的尺寸不能自动调整以适合视图控制器视图的边界,请通过拖动表格视图边缘的白色正方形来手动调整其尺寸。 请记住,仅当选择了表格视图时,白色方块才可见。
向表视图添加必要的布局约束,以确保表视图跨越其父视图的宽度和高度。 如果您已经阅读了有关自动版式的上一篇文章,这应该很容易。
将表视图添加到视图控制器的视图中,这几乎就是我们要做的全部工作。 生成并运行项目以在模拟器中查看结果。 由于表格视图尚无任何数据可显示,因此您仍将看到白色视图。
表格视图具有两种默认样式:普通样式和分组样式。 要更改表格视图( 普通 )的当前样式,请在情节提要中选择表格视图,打开“ 属性”检查器 ,然后将样式属性更改为“ 分组” 。 对于本项目,我们将使用普通表视图,因此请确保将表视图的样式切换回普通表。
连接数据源和代理
您已经知道表视图应该具有数据源和委托。 目前,表格视图没有数据源或委托。 我们需要将dataSource
和表视图的delegate
出口连接到符合UITableViewDataSource
和UITableViewDelegate
协议的对象。
在大多数情况下,该对象是视图控制器,它管理表视图是其子视图的视图。 在情节提要中选择表格视图,打开右侧的Connections Inspector ,然后将其从dataSource
出口(右侧的空圆圈)拖动到View Controller 。 对delegate
出口执行相同的操作。 现在,我们的视图控制器已连接起来,充当数据源和表视图的委托。
如果按原样运行该应用程序,它将几乎立即崩溃。 稍后将阐明其原因。 在仔细研究UITableViewDataSource
协议之前,我们需要更新ViewController
类。
表格视图的数据源和委托对象需要分别符合UITableViewDataSource
和UITableViewDelegate
协议。 正如我们在本系列文章的前面所看到的,协议在该类的超类之后列出。 多个协议用逗号分隔。
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
...
}
创建数据源
在开始实施数据源协议的方法之前,我们需要一些数据在表视图中显示。 我们将数据存储在一个数组中,因此我们首先向ViewController
类添加一个新属性。 打开ViewController.swift并添加[String]
类型的属性fruits
。
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var fruits: [String] = []
...
}
在视图控制器的viewDidLoad()
方法中,我们用水果名称列表填充fruits
属性,稍后将在表视图中显示该名称。 在将视图控制器的视图及其子视图加载到内存后,将自动调用viewDidLoad()
方法,因此该方法名为。 因此,这是填充fruits
数组的好地方。
override func viewDidLoad() {
super.viewDidLoad()
fruits = ["Apple", "Pineapple", "Orange", "Blackberry", "Banana", "Pear", "Kiwi", "Strawberry", "Mango", "Walnut", "Apricot", "Tomato", "Almond", "Date", "Melon", "Water Melon", "Lemon", "Coconut", "Fig", "Passionfruit", "Star Fruit", "Clementin", "Citron", "Cherry", "Cranberry"]
}
UIViewController
类( ViewController
类的超类)还定义了一个viewDidLoad()
方法。 ViewController
类重写 UIViewController
类定义的viewDidLoad()
方法。 这由override
关键字指示。
覆盖超类的方法永远不会没有风险。 如果UIViewController
类在viewDidLoad()
方法中做一些重要的事情怎么办? 在覆盖viewDidLoad()
方法时,如何确保不破坏任何内容?
在这样的情况下,它是先调用重要viewDidLoad()
在别人做任何事情之前超类的方法viewDidLoad()
方法。 关键字super
引用超类,我们向其发送一条viewDidLoad()
消息,调用超类的viewDidLoad()
方法。 这是一个很重要的概念,因此请确保在继续之前先了解这一点。
数据源协议
因为我们将视图控制器分配为表视图的数据源对象,所以表视图会询问视图控制器应显示的内容。 表格视图希望从其数据源获得的第一条信息是其应显示的节数。
表格视图通过在其数据源上调用numberOfSectionsInTableView(_:)
方法来实现此目的。 这是UITableViewDataSource
协议的可选方法。 如果表视图的数据源未实现此方法,则表视图假定它仅需要显示一个部分。 无论如何,我们都实现了此方法,因为在本文后面将需要它。
您可能想知道“什么是表格视图部分?” 表格视图部分是一组行。 例如,iOS上的“联系人”应用程序根据姓氏或名字的首字母对联系人进行分组。 每组联系人形成一个节,该节的前面是节顶部的节标题和/或节底部的节脚 。
numberOfSectionsInTableView(_:)
方法接受一个参数tableView
,该参数是将消息发送到数据源对象的表视图。 这很重要,因为如果需要,它允许数据源对象成为多个表视图的数据源。 如您所见, numberOfSectionsInTableView(_:)
的实现非常简单。
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
现在,表视图知道需要显示多少个部分,它会询问其数据源每个部分包含多少行。 对于表视图中的每个部分,表视图都会向数据源发送tableView(_:numberOfRowsInSection:)
。 此方法接受两个参数,即发送消息的表视图和表视图想要知道行数的节索引。
如下所示,此方法的实现非常简单。 我们首先声明一个常数numberOfRows
,然后通过在数组上调用count
来为其分配fruits
数组中的项目count
。 我们在方法末尾返回numberOfRows
。
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let numberOfRows = fruits.count
return numberOfRows
}
此方法的实现非常简单,因此我们也可以使其更加简洁。 查看下面的实现,以确保您了解已更改的内容。
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return fruits.count
}
如果我们尝试以当前状态编译项目,则编译器将引发错误。 该错误告诉我们ViewController
类不符合UITableViewDataSource
协议,因为我们尚未实现该协议的必需方法。 表格视图希望数据源ViewController
实例为表格视图中的每一行返回一个表格视图单元格。
我们需要实现tableView(_:cellForRowAtIndexPath:)
,这是UITableViewDataSource
协议的另一种方法。 该方法的名称具有很强的描述性。 通过将此消息发送到其数据源,表视图向其数据源查询由该方法的第二个参数indexPath
指定的行的表视图单元格。
在继续之前,我想花一点时间来谈论NSIndexPath
类。 如文档所述 ,“ NSIndexPath
类表示嵌套数组集合树中特定节点的路径。” 此类的一个实例可以容纳一个或多个索引。 在表视图的情况下,它保存项目所在部分的索引以及该项目在该部分中的行。
表格视图的深度永远不会超过两个级别,第一个级别是该部分,第二个级别是该部分中的行。 即使NSIndexPath
是Foundation类,UIKit框架也向该类添加了一些额外的方法,这些方法使使用表视图更加容易。 让我们检查tableView(_:cellForRowAtIndexPath:)
方法的实现。
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath)
// Fetch Fruit
let fruit = fruits[indexPath.row]
// Configure Cell
cell.textLabel?.text = fruit
return cell
}
重用表格视图单元格
在本系列的前面,我告诉您视图是iOS应用程序的重要组成部分。 但是您还应该知道,就其消耗的内存和处理能力而言,视图是昂贵的。 使用表视图时,尽可能重用表视图单元很重要。 通过重用表格视图单元格,表格视图不必在每次新行都需要表格视图单元格时从头开始初始化新的表格视图单元格。
在屏幕外移动的表格视图单元格不会被丢弃。 通过在初始化期间指定重用标识符,可以将表视图单元标记为重用。 当标记为重用的表视图单元格移出屏幕时,该表视图将其放入重用队列中以备后用。
当数据源向其表视图询问新的表视图单元并指定重用标识符时,该表视图首先检查重用队列,以检查具有指定重用标识符的表视图单元是否可用。 如果没有可用的表视图单元格,则表视图将实例化一个新的单元格并将其传递到其数据源。 那就是在第一行代码中发生的事情。
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath)
表格视图的数据源通过向其发送dequeueReusableCellWithIdentifier(_:forIndexPath:)
消息向表格视图询问表格视图单元。 此方法接受我前面提到的重用标识符以及表视图单元格的索引路径。
编译器会告诉您cellIdentifier
是“未解析的标识符”。 这仅表示我们正在使用尚未声明的变量或常量。 在fruits
属性的声明上方,为cellIdentifier
添加以下声明。
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
let cellIdentifier = "CellIdentifier"
var fruits: [String] = []
...
}
表格视图如何知道如何创建新的表格视图单元格? 换句话说,表视图如何知道用于实例化新表视图单元的类? 答案很简单。 在情节提要中,我们创建一个原型单元,并为其提供重用标识符。 现在开始吧。
创建原型单元
打开Main.storyboard ,选择之前添加的表视图,然后打开右侧的Attributes Inspector 。 “ 原型单元”字段当前设置为0 。 通过将其设置为1创建一个原型单元。 现在,您应该在表格视图中看到一个原型单元。
选择原型单元并查看右侧的“ 属性”检查器 。 样式当前设置为Custom 。 将其更改为Basic 。 基本表格视图单元格是包含一个标签的简单表格视图单元格。 这对于我们正在构建的应用程序很好。 回到ViewController
类之前,将Identifier设置为CellIdentifier 。 该值应与我们刚才声明的为cellIdentifier
常量分配的值相同。
配置表视图单元
下一步涉及使用在fruits
数组中存储的数据填充表格视图单元格。 这意味着我们需要知道从fruits
数组使用什么元素。 这又意味着我们需要以某种方式知道表视图单元格的行或索引。
tableView(_:cellForRowAtIndexPath:)
方法的indexPath
参数包含此信息。 如前所述,它还有一些额外的方法可以简化表视图的工作。 这些方法之一是row
,它返回表视图单元格的行。 我们通过询问获取正确的水果fruits
阵列项目在indexPath.row
,用斯威夫特的便利标语法。
// Fetch Fruit
let fruit = fruits[indexPath.row]
最后,我们将表视图单元格的textLabel
属性的文本设置为从fruits
数组中获取的水果名称。 UITableViewCell
类是UIView
子类,并且具有许多子视图。 这些子视图之一是UILabel
的实例,我们使用此标签在表视图单元格中显示水果的名称。 textLabel
属性是否为nil
取决于UITableViewCell
的样式。 这就是为什么textLabel
属性后面带有问号的原因。 这就是众所周知的可选链接。
// Configure Cell
cell.textLabel?.text = fruit
tableView(_:cellForRowAtIndexPath:)
方法期望我们返回UITableViewCell
类(或其子类)的实例,这就是我们在方法末尾所做的事情。
return cell
运行应用程序。 现在,您应该具有一个功能齐全的表视图,其中填充了存储在视图控制器的fruits
属性中的水果名称数组。
栏目
在查看UITableViewDelegate
协议之前,我想通过向表视图中添加节来修改UITableViewDataSource
协议的当前实现。 如果水果列表随着时间的推移而增长,那么最好按字母顺序对水果进行排序,并根据每个水果的第一个字母将其分组为多个部分,这将更好并且更加用户友好。
如果我们要向表视图中添加节,则当前的水果名称数组将无法满足要求。 取而代之的是,数据需要细分为多个部分,每个部分中的结果均按字母顺序排序。 我们需要一本字典。 声明一个新的属性,开始alphabetizedFruits
,类型[String: [String]]
在ViewController
类。
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
let cellIdentifier = "CellIdentifier"
var fruits: [String] = []
var alphabetizedFruits = [String: [String]]()
...
}
在viewDidLoad()
,我们使用fruits
数组创建水果字典。 字典中的每个字母应包含一组水果。 如果该字典没有任何结果,我们将忽略字典中的一个字母。
override func viewDidLoad() {
super.viewDidLoad()
fruits = ["Apple", "Pineapple", "Orange", "Blackberry", "Banana", "Pear", "Kiwi", "Strawberry", "Mango", "Walnut", "Apricot", "Tomato", "Almond", "Date", "Melon", "Water Melon", "Lemon", "Coconut", "Fig", "Passionfruit", "Star Fruit", "Clementin", "Citron", "Cherry", "Cranberry"]
// Alphabetize Fruits
alphabetizedFruits = alphabetizeArray(fruits)
}
该字典是在辅助方法alphabetizeArray(_:)
的帮助下创建的。 它接受fruits
数组作为参数。 乍一看, alphabetizeArray(_:)
方法可能有点不知所措,但实际上它的实现非常简单。
// MARK: -
// MARK: Helper Methods
private func alphabetizeArray(array: [String]) -> [String: [String]] {
var result = [String: [String]]()
for item in array {
let index = item.startIndex.advancedBy(1)
let firstLetter = item.substringToIndex(index).uppercaseString
if result[firstLetter] != nil {
result[firstLetter]!.append(item)
} else {
result[firstLetter] = [item]
}
}
for (key, value) in result {
result[key] = value.sort({ (a, b) -> Bool in
a.lowercaseString < b.lowercaseString
})
}
return result
}
我们创建了一个可变字典, result
类型为[String: [String]]
,我们用它来存储字母化水果的数组,每个字母的字母一个数组。 然后,我们遍历array
的项目,该方法的第一个参数,并提取该项目名称的首字母,使其大写。 如果result
已经包含该字母的数组,则将该项附加到该数组。 如果没有,我们将创建一个包含项的数组并将其添加到result
。
现在,将根据第一个字母对项目进行分组。 但是,这些组不是按字母顺序排列的。 这就是第二个for
循环中发生的情况。 我们遍历result
并按字母顺序对每个数组排序。
如果没有完全清楚的实现alphabetizeArray(_:)
不要担心。 在本教程中,我们专注于表视图,而不是创建按字母顺序排列的水果列表。
节数
有了新的数据源之后,我们要做的第一件事就是更新numberOfSectionsInTableView(_:)
。 在更新的实现中,我们要求字典alphabetizedFruits
为其键。 这给我们一个包含字典每个键的数组。 keys
的项目数等于表视图中的节数。 就这么简单。
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
let keys = alphabetizedFruits.keys
return keys.count
}
我们还需要更新tableView(_:numberOfRowsInSection:)
。 正如我们在做numberOfSectionsInTableView(_:)
,我们要求alphabetizedFruits
其键和排序结果。 对键数组进行排序很重要,因为字典的键值对是无序的。 这是数组和字典之间的主要区别。 这经常会使刚接触编程的人绊倒。
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let keys = alphabetizedFruits.keys
// Sort Keys
let sortedKeys = keys.sort({ (a, b) -> Bool in
a.lowercaseString < b.lowercaseString
})
// Fetch Fruits
let key = sortedKeys[section]
if let fruits = alphabetizedFruits[key] {
return fruits.count
}
return 0
}
然后,我们从与tableView(_:numberOfRowsInSection:)
的第二个参数section
对应的sortedKeys
中获取键。 我们使用键通过可选绑定为当前节获取水果数组。 最后,我们返回结果数组中的项目数。
我们需要对tableView(_:cellForRowAtIndexPath:)
进行的更改是相似的。 我们仅更改获取表视图单元格在其标签中显示的水果名称的方式。
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath)
// Fetch and Sort Keys
let keys = alphabetizedFruits.keys.sort({ (a, b) -> Bool in
a.lowercaseString < b.lowercaseString
})
// Fetch Fruits for Section
let key = keys[indexPath.section]
if let fruits = alphabetizedFruits[key] {
// Fetch Fruit
let fruit = fruits[indexPath.row]
// Configure Cell
cell.textLabel?.text = fruit
}
return cell
}
如果要运行该应用程序,则不会看到任何部分标题,例如在“联系人”应用程序中看到的标题。 这是因为我们需要告诉表视图它应该在每个节标题中显示什么。
最明显的选择是显示每个部分的名称,即字母的字母。 最简单的方法是实现tableView(_:titleForHeaderInSection:)
,这是UITableViewDataSource
协议中定义的另一种方法。 在下面查看其实现。 它类似于tableView(_:numberOfRowsInSection:)
。 运行该应用程序以查看带有部分的表视图。
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
// Fetch and Sort Keys
let keys = alphabetizedFruits.keys.sort({ (a, b) -> Bool in
a.lowercaseString < b.lowercaseString
})
return keys[section]
}
代表团
除了UITableViewDataSource
协议外,UIKit框架还定义了UITableViewDelegate
协议,该协议是表视图的委托需要遵循的协议。
在情节提要中,我们已经将视图控制器设置为表视图的委托。 即使我们尚未实现UITableViewDelegate
协议中定义的任何委托方法,该应用程序也可以正常工作。 这是因为UITableViewDelegate
协议的每个方法都是可选的。
不过,能够响应触摸事件会很不错。 每当用户触摸一行时,我们都应该能够将相应水果的名称打印到Xcode的控制台上。 即使这不是很有用,它也会向您展示委托模式的工作方式。
实现此行为很容易。 我们要做的就是实现UITableViewDelegate
协议的tableView(_:didSelectRowAtIndexPath:)
方法。
// MARK: -
// MARK: Table View Delegate Methods
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
// Fetch and Sort Keys
let keys = alphabetizedFruits.keys.sort({ (a, b) -> Bool in
a.lowercaseString < b.lowercaseString
})
// Fetch Fruits for Section
let key = keys[indexPath.section]
if let fruits = alphabetizedFruits[key] {
print(fruits[indexPath.row])
}
}
现在应该很熟悉获取与所选行相对应的水果的名称。 唯一的区别是,我们将水果的名称打印到Xcode的控制台上。
我们使用alphabetizedFruits
词典查找相应的水果可能会让您感到惊讶。 为什么不向表格视图或表格视图单元格询问水果的名称? 这是一个很好的问题。 让我解释一下会发生什么。
表格视图单元格是一个视图,其唯一目的是向用户显示信息。 除了如何显示外,它不知道正在显示什么。 表格视图本身不负责了解其数据源,它只知道如何显示其包含和管理的节和行。
这个例子是另一个很好的说明,说明了我们在本系列前面看到的模型-视图-控制器(MVC)模式的关注点分离。 除了如何显示之外,视图对应用程序数据一无所知。 如果您要编写可靠且健壮的iOS应用程序,了解并尊重这种职责分离非常重要。
结论
一旦了解了表视图的行为并了解了所涉及的组件(例如表视图与之交谈的数据源和委托对象),表视图就不会变得那么复杂。
在本教程中,我们仅瞥见了表视图的功能。 在本系列的其余部分中,我们将重新访问UITableView
类,并探索更多的难题。 在本系列的下一部分中,我们将介绍导航控制器。
如果您有任何问题或意见,可以将其留在下面的评论中,或通过Twitter与我联系。
翻译自: https://code.tutsplus.com/tutorials/ios-from-scratch-with-swift-table-view-basics--cms-25160