There is no learning without trying lots of ideas and failing lots of times.
- Jonathan Ive
到目前为止,我们仅仅专注于在一个 table view 里显示数据。我猜你应该在想我们怎样才能与 table view 互动和检测行选择的。这是我们这章将要讨论的。
我们将继续改进我们在之前章节构建的 FoodPin app,添加一些增强功能:
- 当用户按单元格的时候弹出一个菜单。这个菜单提供两个选项:Call和I’ve been here。
- 当用户选择“I’ve been here”时显示一个心形的图标。
通过实施这些新功能,你还将学习如何使用 UIAlertController,它通常用于在 iOS apps 里显示警告。
理解 UITableViewDelegate 协议
当我们在第八章首次构建 SimpleTable app,我们添加了2个委托:UITableViewDelegate 和 UITableViewDataSource,到 RestaurantTableViewController 类里。我已经和你们讨论过 UITableViewDataSource 协议但是仅仅提到了 UITableViewDelegate 协议。
像之前说的,委托模式在 iOS 编程里是非常常见的。每个委托负责一个特定角色或者任务来保持系统简单和干净。当一个对象需要执行特定的任务时,它依赖于另一个对象来处理它。这在软件设计里通常被称作“关注点分离(separation of concerns)”。
UITableView 类提供这个设计概念。这两个协议为了不同的目的而设计。UITableViewDataSource 协议定义方法,被用来管理表格数据。它依赖于委托(delegate)所提供的表格数据(table data)。另一方面,UITableViewDelegate 协议负责设置table view的页眉和页脚部分,同时也处理单元格选择和单元格重新排序。
为了管理行选择(row selection),我们将在 UITableViewDelegate 协议里执行一些方法。
阅读文档
在执行方法之前,你可能想知道:
我们如何知道 UITableViewDelegate 里哪个方法将被执行呢?
答案就是“阅读文档”。你已经获得了免费访问苹果官方 iOS 开发者文档(https://developer.apple.com/library/ios/)的权利。作为 iOS 开发者,你需要适应阅读 API 文档。地球上没有一本书能覆盖关于 iOS SDK 的所有事情。大多数时间当我们想要学习更多的关于类或者协议,我们需要看 API 文档。苹果提供了简单的方法来访问在 Xcode 里的文档。所有你需要做的是把光标放置在类或者协议上(如 UITableViewController)然后按住’control-command-?’。这将打开一个弹出类的细节比如它已经添加的协议。
点击 UITableViewDelegate 将进一步打开一个文档浏览器。从那里,你会发现协议中定义的所有办法。
通过粗略的看文档,你会发现下面的方法来管理行选择:
- tableView(_:willSelectRowAtIndexPath:)
- tableView(_:didSelectRowAtIndexPath:)
这两个方法都是为行选择设计的。唯一的不同是,当指定行被选择的时候,会调用tableView(:willSelectRowAtIndexPath:)。你可以使用这个方法来放置特定的单元格。你使用 tableView(:didSelectRowAtIndexPath:) 方法。在用户选择一行后调用这个方法来跟进行选择。我们将在选择行以后实现这个方法来执行额外添加的任务(如弹出一个菜单)。
通过实现协议管理行选择(Row Selections)
Okey,解释得足够多了。让我们来到有趣的部分然后写一些代码。在 FoodPin 工程里,打开 RestaurantTableViewController.swift 文件然后在 RestaurantTableViewController 类里实现tableView(_:didselectRowAtIndexPath:) 方法:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
//创建一个选项菜单作为动作表单
let optionMenu = UIAlertController(title: nil, message: “what do you want to do?”, preferredStyle: .ActionSheet)
//添加动作到菜单
let cancelAction = UIAlertAction(title: “Cancel”, style: .Cancel, handler: nil)
optionMenu.addAction(cancelAction)
//显示菜单
self.presentViewController(optionMenu, animated: true, completion: nil)
}
上面的代码通过实例化一个 UIAlertController 对象来创建一个选项菜单。当用户点击table view 里的任何行,这个方法将自动被调用来弹出动作表单显示“你想要做什么”的信息和一个取消按钮。尝试运行工程来进行快速的测试。app 应该可以检测到触摸。
更多的关于 UIAlertController
在我们继续之前,让我们讨论下更多关于 UIAlertController 类。UIAlertController 类在 iOS8里第一次被介绍来替换旧版本的 iOS SDK 里的 UIAlertview和 UIActionSheet 类。它是为显示警告信息给用户而设计的。
提及前面章节的代码片段,你可以通过 preferredStyle 参数来制定 UIAlertController 对象的样式。你也可以设置它自己的值到.ActionSheet或者.Alert。下图显示警告样式的例子。
除了显示信息给用户以外,你也可以行动给警报控制器来给用户一个回应的方法。要做到这点,你应该创建一个 UIAlertAction 对象,这个对象有着你首选的标题,样式和代码块来执行行动。在代码片段里,我们创建一个 标题为’Cancel’,样式为’.Cancel’的cancelAction 类。当用户选择取消动作的时候不会有任何执行。因此,处理程序被设置成空值(nil)。在 UIAlertAction 对象创建后,你可以通过使用 addAction 方法来非配它给警告控制器。
当警告控制器正确配置的时候,你可以用 presentViewController 方法简单的介绍它。
这是你如何用 UIAlertController 类来介绍一个警告。作为一个初学者,你可能有一些问题:
- 我如何知道当创建一个 UIAlertController 对象时 preferredStyle 参数值是可用的?
- 点(.)语法看起来很新鲜。它应该写成 UIAlertControllerStyle.ActionsSheet 吗?
这些都是好问题。
第一个问题,再说一次答案是“参考文档”。在 Xcode 里,你可以把指针放到 preferredStyle 参数上然后按 control-command-?。Xcode 将显示方法声明。你可以进一步点击 UIAlertControllerStyle 来阅读 API 参考文档。就像你下图看到的,UIAlertControllerStye 是一个枚举,它定义了两个可能的值:ActionSheet 和 Alert。
Quick note:枚举在 Swift 里是一个常见的格式,它为这种格式定义了一列可能的值。UIAlertControllerStyle 是一个好例子。
我们可以用 UIAlertControllerStyle.ActionSheet 或者 UIAlertControllerStyle.Alert 来查阅值。所以当你创建一个 UIAlertController 时你可以写像这样的代码:
let optionMenu = UIAlertController(title: nil, message: “What do you want to do?”), preferredStyle: UIAlertControllerStyle.ActionSheet)
上面的代码没有一点错。Swift 给开发者一个速记的办法,帮助我们打更少的代码。因为 preferredStyle 参数的格式已经知道(如 UIAlertControllerStyle),Swift 让你用更短的.语法来省略 UIAlertControllerStyle。这就是为什么我们像这样实例化 UIAlertController 对象:
let optionMenu = UIAlertController(title: nil, message: “What do you want to do?”, preferredStyle: .ActionSheet)
这同样适用于 UIAlertActionStyle。UIAlertActionStyle 是一个有着3个可能值的枚举:Default,Cancel和Destructive。当创建cancelAction对象时,我们同样使用简写语法:
let cancelAction = UIAlertAction(title: “Cancel”, style: .Cancel, handler: nil)
添加动作到警告控制器
现在让我们添加两个更多的动作到警告控制器:
- “Call”动作-打电话给被选择的餐厅。我们将填入一个伪造的电话号码显示“Call 123-000-x”。
- “I’ve been here” 动作 - 当被选择的适合,这个选项添加一个复选框给被选择的餐厅。
在 tableView(_:didSelectRowAtIndexPath:) 方法里,为“Call”添加下面的代码。你可以在 cancelAction 的初始值之后插入代码:
let callActionHandler = {(action:UIAlertAction!) -> Void in
let alertMessage = UIAlertController(title: “Service Unavailable”, message: “sorry, the call feature is not availabel yet. Please retry later.”, preferredStyle: .Alert)
alertMessage.addAction(UIAlertAction(title: “OK”, style: .Default, handler:nil))
self.presentViewController(alertMessage, animated: true, completion: nil)
}
let callAction = UIAlertAction(title: “call” + “123-000-(indexPath.row)",style: UIAlertActionStyle.Default, handler: callActionHandler)
optionMenu.addAction(callAction)
在上面的代码里,你可能对 callActionHandler 对象不熟悉。像之前提到的,你可以在创建一个 UIAlertAction 对象的时候制定一个代码块作为处理程序。当用户选择行动的时候将执行这个代码块。那意味着我们对于 取消按钮没有任何后续行动。
对于 callAction 对象,我们用 callActionHandler 来分配它。代码块显示一个警告,告诉用户打电话的特征还不可用。
在 Swift 里,这个代码块被称作闭包(Closure)。Closure是独立的方法块,它可以在你的代码里传递。这和 Objective-C 里的块(blocks)非常相似。像上面的例子,提供行动闭包的一个方法是用代码块的值作为常量或变量来声明它。代码块的第一部分对于处理程序参数的定义是一样的。in 关键词表示闭包定义的参数和返回类型已经完成,闭包的主体将开始。下图说明了一个闭包的语法。
callAction 对象的标题是一个假设的电话号码。它是由选中的索引行连接’123-000-‘生成的。如你所见在代码里,Swift 允许开发者用加号(+)来联系字符串。所有你需要的是用括号括起来,前面加反斜杠():
“Call” + “123-000-(indexPath.row)”
Quick note:在 Playgrounds 章已经介绍过字符串的串联。如果你翻到第二章的联系,是时候再次访问它了。或者,你可以参考附件。
随着 Call 动作的实现,为”I’ve been here"动作添加下面的代码行:
let isVisitedAction = UIAlertAction(title: “I’ve been here”, style: .Default, handler: {
(action:UIAlertAction) ->Void in
let cell = tableView.cellForRowAtIndexPath(indexPath)
cell?.accessoryType = .Checkmark
})
optionMenu.addAction(isVisitedAction)
上面的方法给你展示了另一种方法来使用闭包。你可以写一个内联的闭包作为处理程序的参数。这是让代码更清晰,更可读的首选方法。
Swift 里的可选项
你可能想知道问号是做什么用的。单元格在 Swift 里被认为是一个可选项。在 Swift 里介绍了一个新的格式叫做可选项(Optional)。可选项简单的意思是“这里有一个值”或者“这里根本没有值”。单元格通过 tableView 返回。cellForRowAtIndexPath 是一个可选项。用问号来访问 accessoryType 单元格的特性。在这种情况下,Swift 将检查单元格是否存在,如果单元格存在,允许你来设置 accessoryType 的值。在大多数情况下,当你访问一个可选的属性时,Xcode 的自动补全特性会为你添加问号。为了学习更多的关于可选项,你可以进一步的参考附录。
当一个用户选择”I’ve been here”选项,我们添加给选中的单元格添加一个复选框。在 table view 单元格里,右边部分是留给辅助视图的。有4种类型的内置辅助视图包括展开指示器 (disclosure indicator),详情展开按钮(detail disclosure button),复选框(checkmark)和细节(detail)。在这种情况下,我们使用 checkmark 作为指示器。
代码块的第一行使用 indexPath 检索所选的单元格,它包括了所选单元格的索引。第二行代码用一个复选标记更新了 accessoryType 单元格的性质。
编译运行 app。按一个餐厅然后选择其中一个行为,它将展现一个复选标记或者警告给你。
现在,当你选择一行,高亮显示灰色和保持选中的行。在 tableView(_:didSelectRowAtIndexPath:) 方法的最后添加下面的代码来取消选定的行。
tableView.deselectRowAtIndexPath(indexPath, animated: false)
我们遇到了 Bug
app 看起来很好。但是如果你近距离观察它,app里有一个 bug。你用’I’ve been here'标记了’Cafe Deadend’餐厅。如果你往下滚,你会找到另一个餐厅(如Palomino Espresso)同样包含一个复选框。发生了什么问题?为什么 app 会添加额外的复选框?
像每个程序员一样,我讨厌 bug 尤其是当面临一个工程快要交货的时候。但是 bug 总是能帮助我提高我的编程技巧。如果你继续学习你也会遇到很多 bug。习惯它吧。
出现这个问题是由于单元格被重复使用,这个我们在之前的章节已经讨论过了。例如,table view 有30个单元格。由于性能原因,UITableView 可能只创造了10个单元格,当你滚动表的时候来重复使用他们,来代替创造30个单元格。这种情况下,UITableView 重复使用第一个单元格(最初当做一个复选框用于Cafe Deadend)来显示另一个餐厅。在我们的代码里,当 table view 重复使用同样的单元格时,我们仅仅更新了图片视图和标签。附属视图并没有更新。因此,下一个餐厅重复使用同样的单元格共用同样的附属视图。如果附属视图包含一个复选框,那个餐厅同样带着一个复选框。
我们如何解决这个 bug?
我们必须找到另一种方式来跟踪检查项。创造另一个数组来保存被检查的餐厅怎么样?在 RestaurantTableViewController.swift 文件里,声明一个 Boolean 数组:
var restaurantIsVisited = [Bool](count: 21, repeatedValue: false)
Swift 里Bool是一个数据类型,拥有一个 布林(Boolean)值。Swift 提供2个布林(Boolean)值:true 和 false。我们声明restaurantIsVisited数组来保留一个 Bool 值的合集。每一个数组中的值显示是否对应的餐厅被标记为”I’ve been here”。例如,我们可以观察 restaurantIsVisited[0]的值来Cafe Deadend 是否已经被检查或者没有。
数组里的值被初始化为 false。换句话说,条目默认是没有检查的。上面的代码行用重复的值显示一个方法来初始化一个数组在 Swift 里。初始值如下:
var restaurantIsVisited = [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false]
我们必须做一些改变来修复 bug。首先,当一个餐厅被检查时我们需要更新 Bool 数组的值。在 isVisitedAction 对象的处理程序中添加一行代码:
let isVisitedAction = UIAlertAction(title: “I’ve been here”, style: .Default, handler: {
(action:UIAlertAction!) -> Void in
let cell = tableView.cellForRowAtIndexPath(indexPath)
cell?.accessoryType = .Checkmark
self.restaurantIsVisited[indexPath.row] = true
})
代码非常直白。我们把被选的值从false变成 true。最后,在return cell之前添加一些代码行来更新在 tableView(_:cellForRowAtIndexPath:) 方法附属视图:
if restaurantIsVisited[indexPath.row] {
cell.accessoryType = .Checkmark
} else {
cell.accessoryType = .None
}
现在,再次编译运行 app。现在你的 bug 应该解决了。
你可以进一步使用三元条件运算符来把上面的 if 条件简化成一行代码(?:):
cell.accessoryType = restaurantIsVisited[indexPath.row] ? .Checkmark : .None
三元条件运算符是为评估简单的条件做的一个高效的速写。