iOS 从零开始使用 Swift:表格视图基础

iOS 从零开始使用 Swift系列:
探索 iOS SDK
探索 Foundation 框架
使用 UIKit 的第一步
自动布局基础
表格视图基础
导航控制器和视图控制器层次结构
iOS 上的数据持久性和沙盒
构建购物清单应用程序 1
构建购物清单应用程序 2
下一步该去哪里

表格视图是 UIKit 框架中最常用的组件之一,也是 iOS 平台上用户体验不可或缺的一部分。表视图只做一件事,它们做得很好,呈现一个有序的项目列表。该UITableView课程是继续我们的 UIKit 框架之旅的好地方,因为它结合了 Cocoa Touch 和 UIKit 的几个关键概念,包括视图、协议和可重用性。

数据源和委托

该类 UITableView 是 UIKit 框架的关键组件之一,针对显示有序的项目列表进行了高度优化。表格视图可以定制并适应广泛的用例,但基本思想保持不变,呈现有序的项目列表。

该类 UITableView 仅负责将数据呈现为行列表。显示的数据由表视图的数据源 对象管理,可通过表视图的dataSource属性访问。数据源可以是任何符合 UITableViewDataSource 协议的对象,即Objective-C协议。正如我们将在本文后面看到的那样,表视图的数据源通常是管理视图的视图控制器,表视图是其子视图。

同样,table view 只负责检测 table view 中的触摸。它不负责响应触摸。表视图也有一个 delegate 属性。每当 table view 检测到一个触摸事件时,它都会通知它的委托该触摸事件。表格视图的委托负责响应触摸事件。

通过让数据源对象管理其数据和委托对象处理用户交互,表格视图可以专注于数据呈现。结果是一个高度可重用和高性能的 UIKit 组件,它与我们在本系列前面讨论的 MVC(模型-视图-控制器)模式完全一致。该类 UITableView 继承自 UIView,这意味着它只负责显示应用程序数据。

数据源对象类似于委托对象,但并不完全相同。委托对象由委托对象委托对用户界面的控制。然而,数据源对象被委托控制数据。

表视图向数据源对象询问它应该显示的数据。这意味着数据源对象还负责管理它提供给表视图的数据。

表视图组件

该类 UITableView 继承自 UIScrollView,一个 UIView 子类,支持显示大于应用程序窗口大小的内容。

实例由 UITableView 行组成,每行包含一个单元格、其实例 UITableViewCell 或其子类。UITableView 与OS X 上 的对应物相比 , 的NSTableView实例 UITableView 是一列宽。嵌套数据集和层次结构可以通过使用表视图和导航控制器 ( UINavigationController) 的组合来显示。我们将在本系列的下一篇文章中讨论导航控制器。

我已经提到表视图只负责显示数据,由数据源对象传递,并检测触摸事件,这些事件被路由到委托对象。表格视图只不过是管理多个子视图的视图,即表格视图单元格。

一个新项目

与其让你理论过多,不如创建一个新的 Xcode 项目,向你展示如何设置表格视图、填充数据并让它响应触摸事件,这会更好——也更有趣。

打开 Xcode,创建一个新项目(File > New > Project…),然后选择 Single View Application 模板。

将项目命名为 Table Views,分配组织名称和标识符,并将 Devices设置 为 iPhone。告诉 Xcode 您要将项目保存在哪里,然后点击 Create

新项目应该看起来很熟悉,因为我们在本系列的前面选择了相同的项目模板。Xcode 已经为我们创建了一个应用程序委托类, AppDelegate它还为我们提供了一个视图控制器类,可以从它开始 ViewController

添加表视图

构建并运行项目,看看我们从什么开始。当您在模拟器中运行应用程序时看到的白屏是 Xcode 在情节提要中为我们实例化的视图控制器的视图。

将表格视图添加到视图控制器视图的最简单方法是在项目的主故事板中。打开Main.storyboard 并找到  右侧的Object Library 。浏览对象库并将一个实例拖到 UITableView 视图控制器的视图中。

如果表格视图的尺寸没有自动调整以适应视图控制器视图的边界,则通过拖动表格视图边缘的白色方块来手动调整其尺寸。请记住,白色方块仅在选择表格视图时可见。

将必要的布局约束添加到 table view 以确保 table view 跨越其父视图的宽度和高度。如果您已经阅读了上一篇关于自动布局的文章,这应该很容易。

这几乎就是我们向视图控制器的视图添加表格视图所需要做的所有事情。构建并运行项目以在模拟器中查看结果。你仍然会看到一个白色的视图,因为表格视图还没有任何数据要显示。

表格视图有两种默认样式,普通样式和分组样式。要更改 table view ( Plain )的当前样式,请在 storyboard 中选择 table view,打开 Attributes Inspector并将样式属性更改为 Grouped。对于这个项目,我们将使用普通的表格视图,因此请确保将表格视图的样式切换回普通的。

连接数据源和委托

你已经知道表视图应该有一个数据源和一个委托。目前,表格视图没有数据源或委托。我们需要将表格视图的 dataSource 和 delegate 出口连接到符合 UITableViewDataSource 和 UITableViewDelegate 协议的对象。

在大多数情况下,该对象是管理视图的视图控制器,表视图是其子视图。选择storyboard中的table view,打开右边的 Connections Inspector  ,从 dataSource outlet(右边的空圈) 拖到View Controllerdelegate 对插座做同样的事情 。我们的视图控制器现在连接起来充当数据源和表视图的委托。

如果您按原样运行应用程序,它几乎会立即崩溃。稍后将清楚其原因。在仔细查看 UITableViewDataSource 协议之前,我们需要更新ViewController类。

表格视图的数据源和委托对象需要分别遵守 UITableViewDataSource 和 UITableViewDelegate 协议。正如我们在本系列前面所见,协议列在类的超类之后。多个协议用逗号分隔。

import UIKit
 
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
 
  ...
 
}

创建数据源

在我们开始实现数据源协议的方法之前,我们需要在表格视图中显示一些数据。我们将数据存储在一个数组中,所以让我们首先向ViewController该类添加一个新属性。打开 ViewController.swift 并添加一个 fruits类型为 的属性[String]

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 该类  定义  的 方法 。这由关键字指示。viewDidLoad()UIViewControlleroverride

重写超类的方法永远不会没有风险。如果 UIViewController 类在方法中做了一些重要的事情 viewDidLoad() 怎么办?我们如何确保在重写 viewDidLoad() 方法时不会破坏任何东西?

在这种情况下,重要的是首先调用 viewDidLoad() 超类的方法,然后再在方法中执行任何其他 操作viewDidLoad() 。关键字 super 引用超类,我们向它发送一条消息 viewDidLoad(),调用 viewDidLoad() 超类的方法。这是一个需要掌握的重要概念,因此请确保在继续之前了解这一点。

数据源协议

因为我们将视图控制器指定为表视图的数据源对象,所以表视图会询问视图控制器它应该显示什么。表格视图想要从其数据源获得的第一条信息是它应该显示的部分的数量。

表视图通过调用 numberOfSectionsInTableView(_:) 其数据源上的方法来完成此操作。这是 UITableViewDataSource 协议的可选方法。如果表格视图的数据源没有实现这个方法,表格视图假定它只需要显示一个部分。无论如何,我们都实现了这个方法,因为我们将在本文后面需要它。

您可能想知道“什么是表格视图部分?” 表格视图部分是一组行。例如,iOS 上的联系人应用程序根据名字或姓氏的第一个字母对联系人进行分组。每组联系人形成一个部分,该部分前面有一个  位于该部分顶部的 部分标题和/或位于该部分底部的部分页脚 。

该 numberOfSectionsInTableView(_:) 方法接受一个参数,即向 tableView数据源对象发送消息的表视图。这很重要,因为它允许数据源对象在必要时成为多个表视图的数据源。如您所见,实现 numberOfSectionsInTableView(_:) 非常简单。

func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return 1
}

现在表格视图知道它需要显示多少个部分,它会询问它的数据源每个部分包含多少行。对于表视图中的每个部分,表视图都会向数据源发送一条消息 tableView(_:numberOfRowsInSection:)。这个方法接受两个参数,表视图发送消息和表视图想知道行数的部分索引。

该方法的实现非常简单,如下所示。我们首先声明一个常量 , 并 通过调用数组 numberOfRows为其分配数组中的项目数  。我们  在方法结束时返回。fruitscountnumberOfRows

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 。Prototype Cells字段当前 设置为 0通过将其设置为1创建原型单元格 。您现在应该在表格视图中看到一个原型单元格。

选择原型单元格并查看 右侧的属性检查器。Style当前 设置为 Custom。将其更改为 Basic。基本表格视图单元格是包含一个标签的简单表格视图单元格。这对我们正在构建的应用程序来说很好。在我们回到ViewController课堂之前,将 Identifier设置为 CellIdentifier。该值应该与分配给cellIdentifier我们刚才声明的常量的值相同。

配置表格视图单元

下一步涉及使用存储在 fruits 数组中的数据填充表格视图单元格。这意味着我们需要知道要使用 fruits 数组中的哪个元素。这反过来意味着我们需要知道表格视图单元格的行或索引。

该 方法的indexPath 参数 tableView(_:cellForRowAtIndexPath:) 包含此信息。正如我之前提到的,它有一些额外的方法可以使使用表视图更容易。其中一种方法是 row,它返回表格视图单元格的行。我们通过使用 Swift 方便的下标语法向fruits 数组中的 item询问正确的水果 。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(_:) 如果实施不完全清楚,请不要担心 。在本教程中,我们专注于表格视图,而不是创建按字母顺序排列的水果列表。

Number of Sections节数

有了新的数据源,我们需要做的第一件事就是更新 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
}

然后我们从中获取与的第二个参数 sortedKeys对应 的键。我们使用键来获取当前部分的水果数组,使用可选绑定。最后,我们返回结果数组中的项目数。sectiontableView(_:numberOfRowsInSection:)

我们需要做的改变 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 协议,即 table view 的委托需要遵守的协议。

在故事板中,我们已经将视图控制器设置为表视图的委托。即使我们没有实现 UITableViewDelegate 协议中定义的任何委托方法,应用程序也可以正常工作。这是因为 UITableViewDelegate 协议的每个方法都是可选的。

不过,能够响应触摸事件会很好。每当用户触摸一行时,我们应该能够将相应水果的名称打印到 Xcode 的控制台。尽管这不是很有用,但它会向您展示委托模式的工作原理。

实现这种行为很容易。我们所要做的就是实现协议的 tableView(_:didSelectRowAtIndexPath:) 方法 UITableViewDelegate 。

// 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课程并探索更多的难题。在本系列的下一部分中,我们来看看导航控制器。

ios-from-scratch-with-swift-table-view-basics–cms-25160