制作table view cell的几种方法
在AllListsViewController中创建table view cell的方法比在ChecklistViewController中略复杂一些。在后者中你仅仅是通过简单的一个语句就获得了一个新的table view cell:
let cell = tableView.dequeueReusableCell(withIdentifier: "ChecklistItem",for: indexPath)
但是在AllListsViewController中为了实现同样的目的我们写了一大堆代码:
let cellIdentifier = "Cell"
if let cell =
tableView.dequeueReusableCell(withIdentifier: cellIdentifier) {
return cell
} else {
return UITableViewCell(style: .default,reuseIdentifier: cellIdentifier)
}
这里我们还是调用了dequeueReusableCell(withIdentifier),只是以前我们在故事模版中放置了cell并且给了它一个身份标示,而这次没有。
如果这个table view找不到任何可重用的cell,这个方法会返回nil,这时你不得不手动创建cell,这就是else后面跟的代码的作用。
这实际上是两种不同类型的dequeueReusableCell(...),其中一个有IndexPath参数而另一个没有。在AllListsViewController中我们使用的是没有IndexPath参数的这一个。两者的区别在于有IndexPath参数的这一个仅用于标准cell。如果在AllListsViewController中使用有IndexPath参数的这个方法,app就会崩溃掉。
制作cell有四种方法:
1、使用标准cell。这是最简单也是最快的一种方法。我们在ChecklistViewController中做的就是。
2、使用静态cell。你在Add/Edit界面中使用的就是静态cell。静态cell最大的优势就是不用给它提供数据源方法,适用于你提前知道cell内容的情况。
3、使用nib文件。一个nib(也叫做XIB)就像一个迷你的仅仅包含一个自定义的UITableViewCell对象的故事模版。这和使用标准cell非常相似,只是你是在故事模版之外使用它。
4、手动创建,就是我们在AllListsViewController中使用的方法。在早期的iOS版本中,只有这一种方法。这种方法要复杂一些,但是更加灵活。
当你手动创建一个cell时,你需要指定一个确定的cell style,就会得到一个已经包含标签和图片的预置布局的cell。
在All Lists View Controller中,你使用了“Default”style,在稍后你会将它切换为“Subtitle”,这会在主标签的下方,给你一个小一点的次级标签。
使用标准cell style意味着你不需要设计你自己的cell布局。对于大多数app而言标准cell已经足够用了。
标准cell和静态cell都可以使用标准cell style。标准cell和静态cell的默认style都是“Custom”,这种style要求你使用自己的标签,但是你可以通过界面建造器将它改变为内建的style。
最后,你需要注意的是:有时我看到其他人是这样写代码的,使用代码为每一行创建一个新的cell而不是试着重用cell。你千万不要这样做!一定要首先向table view请求看看是否有可以重用的cell,使用dequeueReusableCell(...)这个方法。
为每一行都创建一个新的cell,会使app变慢,创建一个对象总是比重用一个对象要慢。所以为每一行都创建一个新的cell会占据大量内存,为了用户着想,你也应该重用cell。
查看待办事项分类
目前,由AllListsViewController中的lists数组组成的数据模型包含了少量的Checklist对象。数据模型中同时还有来自ChecklistViewController的items数组,其中包含ChecklistItem对象。
你也许已经注意到了,当你点击任何一行时,无论是哪一行,都会展示一模一样的待办事项。
而实际上,每个待办事项分类,都应该对应不同的待办事项内容。我们之后会完成这一工作。
首先,我们来设置好映射被选择的待办事项分类的名称,作为界面的标题。
打开ChecklistViewController.swift,添加一个实例变量:
var checklist: Checklist!
过会我再讲为什么这必须是个可选型。
还是在ChecklistViewController.swift中,将viewDidLoad()方法修改为:
override func viewDidLoad() {
super.viewDidLoad()
title = checklist.name
}
这一步的作用是改变界面的标题,就是导航栏的标题,将导航栏的标题修改为Checklist对象的名称。
当执行转场时,你会将这个checklist对象给到ChecklistViewController。
打开AllListViewController.swift,将tableView(didSelectRowAt)修改为:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let checklist = lists[indexPath.row]
performSegue(withIdentifier: "ShowChecklist", sender: checklist)
}
和以前一样,你使用performSegue()来执行转场。这个方法之前有一个参数sender,之前是nil。现在你用来传递用户点击的那一行的Checklist对象。
你可以在sender参数中放置任何东西。如果你通过故事模版执行转场(而不是像现在这样手动转场),那么sender就会引用被触发的空控件,例如用于Add按钮的UIBarButton对象或者用于列表中某一行的UITableViewCell。
但是因为你是通过手动开始转场的,所以你可以在sender中放入最方便的对象。
将Checklist对象放入sender参数时,还不会将这个对象给到ChecklistViewController。这一步发生在“prepare-for-segue”中,你还没有在代码里写这个方法。
在AllListsViewController.swift中添加以下方法:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowChecklist" {
let controller = segue.destination as! ChecklistViewController
controller.checklist = sender as! Checklist
}
}
你之前应该见到过这个方法。prepare(for:sender:)在转场执行后立即被调用。你可以在这里,在新的视图还没有在屏幕上可视化之前设置新视图的属性。
⚠️:转场的目标是ChecklistVieController,不是UINavigationController,这和之前有点不同。
到Add/edit界面的转场是一种modally presented(这个真心不知道怎么翻译)方式,针对与嵌入导航控制器中的视图控制器。
而这次是“Push”型的转场,直接转到Checklist View Controller。
看看故事模版就知道在All Lists界面和Checklist界面之间没有导航控制器。这个转场直接从一个视图转到另一个。
在prepare(for:sender:)中,你需要将被点击行的Checklist对象给到ChecklistViewController。这就是为什么之前你将Checklist对象放入sender参数中的原因。(你也可以将Checklist对象临时存储到一个实例变量里,但是把这个对象放入sender参数中更加简单)
所有这一切发生在ChecklistViewController被加载前,ChecklistViewController被实例化时的一瞬间。这就是说它的viewDidLoad()方法在prepare(for:sender:)之后被调用。
在这一时刻,这个视图控制器的checklist属性被来自sender的Checklist对象填充,并且viewDidLoad()可以据此修改界面的标题。
涉及转场的步骤这一系列过程解释了为什么checklist属性被声明为可选型。因为直到调用viewload()前,它都是nil。
nil通常不是Swift中允许的变量取值,但是可选型例外。
之前我们声明可选型时用的是问号,这里是一个感叹号,感叹号的作用和问号非常类似,区别在于用感叹号时,你不需要用if let去对它进行解包。
使用这种隐式解包可选型时,需要非常小心,因为它们没有任何保护措施。
运行app,点击一个待办事型分类,转入的屏幕界面的标题会显示为这个待办事项分类的名称。
注意一点,把Checklist对象给到ChecklistViewController并不会形成一个拷贝。
你仅仅是传递这个对象的一个引用到视图控制器,用户对Checklist对象做出的任何变更,都会体现在AllListsViewController上。
这两个视图控制器读取的都是同一个Checklist对象。过会在Checklist中添加新的ChecklistItem时,这一点会成为你的便利条件。
类型扮演(type cast)
在prepare(for:sender:)中,你写了这样的代码:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
...
controller.checklist = sender as! Checklist
...
}
这里的as!是什么呢?
如果你足够细心的话,你会注意到“as something”已经出现过好几次了。这就是类型扮演(type cast)
类型扮演通知Swift解释具有不同数据类型的值。这和电影中的某一个演员正好相反,在电影中一个演员只扮演一个角色,而在swift中,类型扮演的实际作用就是改变了对象的角色。
上面的方法中,sender的参数是Any?,这意味着这个参数可以是任何类型的对象:一个UIBarButtonItem,一个UITableViewCell,或者一个Checklist对象。感谢这里的问号,使得它甚至可以为nil。
但是controller.checklist总是期待一个合适的Checklist对象,它无法处理其他对象,比如UITableViewController,因此,swift需要你只能把Checklist对象放入checklist属性中。
通过“sender as! Checklist”,你告诉了Swift它可以安全的将sender作为Checklist对象处理。
另一个类型扮演的例子是:
let controller = segue.destination as! ChecklistViewController
转场的destination(目的地)属性引用转场结束时接受到的视图控制器,显然 ,苹果的工程师无法提前预言这个视图控制器就是我们命名的ChecklistViewController。
所以你不得不在读取任何这个对象的属性前,先将它由通用类型UIViewController扮演为这个app中存在的ChecklistViewController。
在举一个例子,在loadChecklistItems()中:
items = unarchiver.decodeObjectForKey("ChecklistItems")
as! [ChecklistIt]
NSKeyedUnarchiver将"ChecklistItems"键值下冻结的对象解码到一个数组中,但是你必须告诉swift这确实是一个包含ChecklistItem对象的数组。
没有类型扮演的的话,swfit会认为这是任何类型,这样就会造成和items数组的数据类型不相容的事情发生。
还有一种使用as?的类型扮演,这是用于可选型的类型扮演,或者说这个类型扮演可能会为nil。我们会在后面接触到这种例子。
如果你不太理解这些内容也不要担心,我们会通过大量的例子让你消化这个内容。
你使用类型扮演的最终原因是,iOS架构的通信原理是由Object-C写成的,swift在类型上的要求比OC要宽松一些,在OC中你需要更加精确的指明类型。
添加和编辑待办事项分类
让我们快速完成添加和编辑待办事项分类功能。这是另一个拥有静态cell的UITableViewController。
如果之前的代码你已经了然于心了,那么现在工作对你就是小菜一碟!
在工程导航器中新增一个Cocoa Touch Class模版或者直接新增一个swift文件,取名为ListDetailViewController。
将模版中原有的内容都删掉,替换为下面的语句:
import UIKit
protocol ListDetailViewControllerDelegate: class {
func listDetailViewControllerDidCancel(_ controller: ListDetailViewController)
func listDetailViewController(_ controller: ListDetailViewController,didFinishAdding checklist: Checklist)
func listDetailViewController(_ controller: ListDetailViewController,didFinishEditing checklist: Checklist)
}
class ListDetailViewController: UITableViewController,UITextFieldDelegate {
@IBOutlet weak var textField: UITextField!
@IBOutlet weak var doneBarButton: UIBarButtonItem!
weak var delegate: ListDetailViewControllerDelegate?
var checklistToEdit: Checklist?
}
我仅仅是把ItemDetailViewController.swift中的内容拷贝过来改了改名字。同时注意一下,你现在要处理的是Checklist对象,而不是ChecklistItem。
添加一个viewDidLoad()方法:
override func viewDidLoad() {
super.viewDidLoad()
if let checklist = checklistToEdit {
title = "Edit Checklist"
textField.text = checklist.name
doneBarButton.isEnabled = true
}
}
这样当用户编辑已经存在的待办事项分类时,可以将界面的标题修改为Edit Checklist,并且将被修改的待办事项分类的名称放入text field。
同时也添加一个viewWillAppear()方法,用于自动弹出小键盘:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
textField.becomeFirstResponder()
}
然后给Cancel按钮以及Done按钮添加动作方法:
@IBAction func cancel() {
delegate?.listDetailViewControllerDidCancel(self)
}
@IBAction func done() {
if let checklist = checklistToEdit {
checklist.name = textField.text!
delegate?.listDetailViewController(self, didFinishEditing: checklist)
} else {
let checklist = Checklist(name: textField.text!)
delegate?.listDetailViewController(self, didFinishAdding: checklist)
}
}
这些代码对你应该非常熟悉了。这和之前的编辑及添加待办事项界面几乎一模一样。
为了在done()方法中创建新的Checklist对象,你使用了Checklist的init(name)方法,并且将textField.text作为参数传入到name中。
你不能像下面这样去实现这个目的,这样做是达不到预期效果的:
let checklist = Checklist()
checklist.name = textField.text!
因为Checklist不具备一个没有任何参数的init()方法,所以Checklist()会返回一个报错。它只有一个init(name)方法,所以你每次创建一个新的Checklist对象时,都必须用这个方法进行初始化。
同时确保用户无法选择text field所在行的cell:
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
return nil
}
最后添加text field的委托方法,根据用户的输入是否为空来启用或者禁用Done按钮。
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let oldText = textField.text! as NSString
let newText = oldText.replacingCharacters(in: range, with: string) as NSString
doneBarButton.isEnabled = (newText.length > 0)
return true
}
这也是你在ItemDetailViewController中做过一次的事。
让我们在界面建造器中为这个新的视图控制器制作用户界面。
打开故事模版,拖拽一个Navigation Controller到画布中并且将它放置在其他视图控制器的下面。
界面建造器已经假定你要嵌入一个table view controller到导航控制器中,这样就为你省了不少事。
选定新的table view controller(名字叫做“root view controller”的那个)并且打开身份检查器。将class中填写为ListDetailViewController。
将导航栏的标题由“Root View Controller”修改为Add Checklist。(如果双击不好使的话,你可以在纲要面板中选定Root View Controller然后在属性检查器中进行改名)
添加Cancel和Done按钮并且将按钮和动作方法链接起来。同时将Done按钮和doneBarButton链接起来,并且取消选定Enable选项。
小贴士:如果你无法将 bar button拖拽到导航栏上,也可以直接往略缩面板里拖。
选中table view,然后在属性检查器中设置Static Cells,和style设置为Grouped。然后删除掉多余的两个cell。
拖拽一个Text Field到cell中,然后对其进行如下配置:
Border Style: none
Font size: 17
Placeholder text: Name of the List
Adjust to Fit: disabled
Capitalization: Sentences
Return Key: Done
Auto-enable Return key: check
然后将这个Text Field和textField outlet链接起来。
然后按住ctrl将Text Field拖拽到视图控制器上,在弹出窗口中选择delegate。这样这个视图控制器就是text field的委托了。
打开text field的链接检查器,将Did End on Exit拖拽到代表视图控制器的黄色圆圈图标上,在弹出窗口中选择done。
(以上步骤如果不熟悉,可以回头去看看之前的课程,这些步骤我们都详细做过一遍)
最终的结果回到All Lists View Controller(就是叫做Checklists的那个),并且拖拽一个bar button上去,并且将这个button设置为Add。
按住ctrl拖拽这个新的Add按钮到下面的导航控制器上,并且在弹出窗口选择Present Modally segue。
选择这个新的转场,并且将其命名为AddChecklist。
你的故事模版现在看起来应该是这个样子:
全家福,三个导航控制和四个table view controller坚持一下,就快完了。你还需要将AllListsViewController做成ListDetailViewController的委托。我们之前也做过一次类似的事情。
通过在All Lists view controller的class声明行中添加ListDetailViewControllerDelegate来使得它遵循这一协议。
打开AllListsViewController.swift:
AllListsViewController: UITableViewController,ListDetailViewControllerDelegate {
还是在AllListsViewController.swift中,扩展一下prepare(for:sender:),
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowChecklist" {
let controller = segue.destination as! ChecklistViewController
controller.checklist = sender as! Checklist
} else if segue.identifier == "AddChecklist" {
let navigationController = segue.destination as! UINavigationController
let controller = navigationController.topViewController as! ListDetailViewController
controller.delegate = self
controller.checklistToEdit = nil
}
}
第一个if中的内容不要改动,从else if开始添加新内容。
这段代码的作用和以前一样,旬斋导航控制器中的视图控制器,并且设置它的delegate为self。
在AllListsViewController.swift的底部,添加协议方法:
func listDetailViewControllerDidCancel(_ controller: ListDetailViewController) {
dismiss(animated: true, completion: nil)
}
func listDetailViewController(_ controller: ListDetailViewController, didFinishAdding checklist: Checklist) {
let newRowIndex = lists.count
lists.append(checklist)
let indexPath = IndexPath(row: newRowIndex, section: 0)
let indexPaths = [indexPath]
tableView.insertRows(at: indexPaths, with: .automatic)
dismiss(animated: true, completion: nil)
}
func listDetailViewController(_ controller: ListDetailViewController, didFinishEditing checklist: Checklist) {
if let index = lists.index(of: checklist) {
let indexPath = IndexPath(row: index, section: 0)
if let cell = tableView.cellForRow(at: indexPath) {
cell.textLabel!.text = checklist.name
}
}
dismiss(animated: true, completion: nil)
}
这些方法会在用户点击Cancel或者Done按钮时被调用。
这些代码你都应该很熟悉才对,我们之前都有完整的做过一次。
同时添加table view的数据源方法来允许用户删除某一条记录:
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
lists.remove(at: indexPath.row)
let indexPaths = [indexPath]
tableView.deleteRows(at: indexPaths, with: .automatic)
}
运行app,现在你可以新增或者删除待办事项分类了:
好玩吧⚠️:如果app崩溃了,那么就检查一下是不是所有的链接都做好了。任何一点细节的丢失,都会导致app崩溃。
你还无法对已经存在的条目进行修改,然我们来完成这最后一点代码。
之前我们也是通过转场的方式进入到编辑界面,但是这一次我们不这样做,我们要通过手动的方式来从故事模版中读取这个新的视图控制器,多掌握一些方法总是好的。
打开AllListsViewController.swift,添加一个tableView(accessoryButtonTappedForRowWith)方法。这个方法是table view的委托方法之一,其作用就和名字一样一目了然。
override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
let navigationController = storyboard!.instantiateViewController(withIdentifier: "ListDetailNavgationController") as! UINavigationController
let controller = navigationController.topViewController as! ListDetailViewController
controller.delegate = self
let checklist = lists[indexPath.row]
controller.checklistToEdit = checklist
present(navigationController, animated: true, completion: nil)
}
在这个方法内,你为Add/Edit Checklist界面创建了新的视图控制器对象,并且将其展现在屏幕上。这和转场的作用大致相似。这个视图控制器被嵌入到故事模版中,并且你请求故事模版对象读取它。
你是在哪里获取这个故事模版对象的呢?每个视图控制器都有一个storyboard属性来引用这个视图控制器是从哪个故事模版中被读取的。你可以使用这个属性来故事模版的所有功能,比如实例化其他视图控制器。
这个storyboard属性是可选型,因为视图控制器并不全部从故事模版中读取,但是我们眼下的这个是,所以我们使用感叹号对其解包。因为我们可以确定在我们这个app中storyboard不会为nil,所以直接用感叹号强制解包就可以,而不需要用if let的方式。
调用instantiateViewController(withIdentifier)时用到了一个字符串“ListDetailNavigationController”,这就是请求故事模版创建新视图控制器的方式,在我们这个例子中,这个新的视图控制器就是包含ListDetailViewController的导航控制器。
你可以直接实例化ListDetailViewController,但是ListDetailViewController是嵌入在导航控制器内部的,如果直接实例化它而不管导航控制器的话,你就无法看到界面标题,以及Done和Cancel按钮。
打开故事模版,选择指向List Detail View Controller的导航控制器,然后打开身份检查器,将Storyboard ID填写为ListDetailNavigationController:
运行app,点击某一行上的详细信息按钮试试,如果app崩溃了,重新保存一下故事模版再运行一次。
练习:设置List Detail View Controller的identifier为ListDetailNavigationController,而不是导航控制器,然后运行app看看会发生什么,试着解释一下为什么会这样,如果你可以解释的话,那么证明你已经掌握了这些内容。
⚠️:你还能跟上我的步伐吗?
如果你对这一切非常茫然并且想要放弃的话,千万要打消这个念头。
学习新的东西本来就是一个枯燥的过程,编程尤其如此。你可以关掉电脑,去睡一觉,过几天以后再重新打开看看。
说不定就灵关一闪的明白了起来。
网友评论
过会我再讲为什么这必须是个可选型。
原文中是
" I’ll explain why the exclamation mark is necessary in a moment. "
也许是我学swift不深, 我认为?才是可选型, ! 是强制解包. 所以我理解为这里的Checklist!是必须有值, 而不是可选型.是我自己的误解.
作者翻译应该是对的.
type cast 翻译类型扮演觉得有点q, 原文中确实举了演员扮演各种角色, 虽然常见的翻译可能是类型转换.
续赞~
override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
let navigationController = storyboard!.instantiateViewController(withIdentifier: "ListDetailNavgationController") as! UINavigationController
let controller = navigationController.topViewController as! ListDetailViewController
controller.delegate = self
let checklist = lists[indexPath.row]
controller.checklistToEdit = checklist
present(navigationController, animated: true, completion: nil)
}
中的Identifier拼错了: "ListDetailNavigationController", 少了个 i
报错: use of undeclared type "ChecklistViewController" 时, 很可能是在之前写名字时和"ChecklistsViewController"搞错了,一个有s另一个没有.