给待办事项分类
这个app的名称叫做Checklists是有原因的:它允许你拥有多条待办分类,目前为止,我们都只能添加待办条目,但是不久之后你就会拥有添加分类的功能,你可以先增加一些分类,比如会议,日程等分类,然后再向每个分类下增加具体的待办项目。
我们要做以下几件事情:
1、添加一个新的界面展示分类。
2、创建一个新的界面,可以使用户添加或者编辑新的分类
3、当你点击具体一个分类时,显示其中的待办事项
4、对分类进行保存和读取
两个新的界面意味着我们要两个新的视图控制器。
1、AllListsViewController展示用户所有的分类。
2、ListDetailViewController,使用户可以添加,编辑分类以及为分类增加一个图标
首先你要添加的是AllListsViewController。这也是这个app新的主界面。
当你完成后,app看起来会是这个样子:
这个界面和你之前创建的界面非常相似。它也是一个table view controller。
从现在开始,我会将这个新的主界面称为“分类”界面,而将之前的展示待办事项列表的界面称为“事项”界面,以示区别。
在工程导航器中右击Checklists分组(黄色文件夹图标的那个),然后选择New File,选择Cocoa Touch Class模版(iOS标签下的)。
然后按照下面的示例选择选项:
Class:AllListsViewController
Subclass of:UITableViewController
Also create XIB file:Uncheck this(不要勾选)
Language:Swift
注意:确保Subclass of这一栏填写的是UITableViewController,而不是UIViewController。同时注意一下,Xcode会自动将AllListsViewController重命名为AllListsTableViewController,多了一个Table,你需要稍微修改一下。
点击Next,然后点击Create提交。
Xcode的table view controller模版中会预置一些代码,但是也许你用不到它们。模版只是把认为你需要的东西都提前列了出来,所以首先我们要把它们都删干净。
你还是先要自己造一点数据,让app能先跑起来。和你知道的一样,我每做一小步都会运行app测试一下,如果运行效果正常,那么就进入到下一步。
打开AllListsViewController.swift,删除掉numberOfSections(in)方法。没有这个方法的时候,列表就只会有一个分节。
将tableView(numberOfRowsInSection)修改为:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
接下来修改tableView(cellForRowAt),注意一下,这个方法在文件里有,只是被注释掉了,你可以把注释取消掉就可以了。并且修改为下面这个样子:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = makeCell(for: tableView)
cell.textLabel!.text = "List \(indexPath.row)"
return cell
}
在ChecklistViewController中你是在界面建造器中设计cell单元,但是在AllListsViewController中,我们使用另外一种方式,用代码来设计cell单元。
你需要根据编译器的提示添加下面这个方法:
func makeCell(for tableView: UITableView) -> UITableViewCell {
let cellIdentifier = "Cell"
if let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) {
return cell
} else {
return UITableViewCell(style: .default, reuseIdentifier: cellIdentifier)
}
}
一会我会详细的讲这些代码的作用,但是现在你要知道你在这里也使用了tableView.dequeueReusableCell(withIdentifier)。如果它返回nil,那么就是说没有可重用的cell,这时你要使用UITableViewCell(style, reuseIdentifier)来新建一个。
把这段代码单独分离出来后会起到保持tableView(cellForRowAt)简洁的作用。
把AllListsViewController.swift中的其他注释部分全部删掉,它们没有任何作用,只会让代码看起来乱糟糟的。
最后一步就是添加新的view controller到故事模版上。
打开故事模版并且拖拽一个新的Table View Controller到画布上,把它放到离第一个navigation controller近的地方。
按住ctrl并且从第一个navigation controller拖拽到这个新的视图控制器:
在弹出的菜单上选择Relationship Segue分节下的root view controller:
这样会把前期存在的navigation controller和ChecklistViewController之间的链接断开,这样一来,Checklists就不再是主界面了。
选择新的这个table view controller并且打开身份检查器,在Class中输入AllListsViewController。
双击这个新的视图控制器的导航栏,并且将它重命名为Checklists。
这样在Xcode的略缩面板中All Lists View Controller的视图控制器会被重命名为Checklists,这可能会使你有些困惑,因为我们已经有一个Checklists了,我们稍后会处理这个问题。
你可以整理一下故事模版,调整下新视图控制器的位置,让它们看起来美观点,把它们放到一排去。
就像我之前提到过的,你在这里不要为这个table view使用标准cell单元,如果你已经用了,那么非常不错,并且作为一个练习你可以之后重写代码来使用标准cell单元,但是这次我要为你展示一个新的方法来生成table view cells。
把All Lists View Controller中的空的prototype cell删掉,选定以后按delete键就可以了。
然后选定视图控制(黄色圆圈图标按钮的那个),然后按住ctrl键拖拽到Checklist View Controller中去,创建一个Show转场。
这样就在从All Lists界面到Checklist界面见加入了一个推入的转换。同时也把右边的导航栏放回到Checklist界面。
双击右边的导航栏,修改标题为Name of the Checklist。这只是预置的文本,它可以帮你区分略缩面板中的各个视图控制器。
⚠️:略缩面板中不显示视图控制器对象的名称,而只是显示导航栏的文本,这一点Xcode需要改进,否则非常容易把人弄晕。
当我们说到All Lists View Controller时,就是指略缩面板中的Checklists Scene。
而Checklist View Controller现在则是略缩面板中的Name of the Checklist Scene。
注意一下,这个转场没有和任何按钮或者table view cell关联。
并且在All Lists界面上也没有任何东西给你点击,换而言之就是说你无法触发这个转场。这就意味着你要通过编程的方式来触发它。
选定新的转场,并且打开属性检查器,在identifier输入ShowChecklist。
这里你还可以看到这个转场的Kind(类型)为Show (e.g.Push),因为执行这个转场时,你正在将Checklist View Controller推到导航层的上面。
打开AllListsViewController.swift,添加tableView(didSelectRowAt)方法:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: "ShowChecklist", sender: nil)
}
回忆一下,这个table view委托方法是当用户点击某一行的时候被触发。
之前,点击某一行时,会自动触发一个转场,因为你将转场和cell单元链接起来了。然而,这个新的table view并没有使用cell单元,因此你需要手动执行转场。
这非常简单:只需要调用performSegue(withIdentifier,sender)就可以了。
运行app,它看起来应该是这个样子:
点击某一行后我们非常熟悉的ChecklistViewController就滑动到屏幕中了。
你可以点击导航栏上左边的“Back”回到主界面上。现在你应该真正体会到导航控制器的强大了。
在All Lists界面中放入内容
你将要把Checklist View Controller中的大部分功能复制到All Lists界面中。
这里将会有一个➕号按钮用户添加新的分类,可以通过滑动删除某个分类,可以通过点击详细信息按钮来编辑某个分类。
当然,你还要将分类对象保存到一个plist文件中。
因为之前这些步骤我们已经详细讲解过了,所以这次我们会将的稍微快一点。
我们首先要创建一个数据模型来代表分类,就叫做Checklist好了,之前的代表具体某条待办事项的数据模型叫做ChecklistItem。
用Cocoa Touch Class模版添加一个新的文件,将其命名为Checklist并且设置为NSObject的子类。
就像ChecklistItem一样,你需要把Checklist作为NSObject的子类,因为NSCoder系统在存储和读取时,对象必须是这种类型。
给Checklist.swift一个叫做name的属性:
import UIKit
class Checklist: NSObject {
var name = ""
}
接下来,你需要一个数组来保存AllLIstsViewController的Checklist对象。
在AllListsViewController.swift中添加一个新的实例变量。
var lists: [Checklist]
这个数组就用于存储Checklist对象。
⚠️:你也可以这样声明数组:
var lists: Array<Checklist>
在Swift代码中你会见到这两种声明方式,它们的作用是一样的。
你可以给这个新的数组添加一点测试数据,可以通过init?(coder)来实现这一目的。记住当UIKit从故事模版中读取视图控制器时会自动调用这个方法。
打开AllListsViewController.swift,像下面这样就可以实现了(先不要急着动手去做,先阅读一遍,当需要你敲代码的时候,我会通知你的)
required init?(coder aDecoder: NSCoder) {
// 1
lists = [Checklist]()
// 2
super.init(coder: aDecoder)
// 3
var list = Checklist()
list.name = "Birthdays"
lists.append(list)
// 4
list = Checklist()
list.name = "Groceries"
lists.append(list)
list = Checklist()
list.name = "Cool Apps"
lists.append(list)
list = Checklist()
list.name = "To Do"
lists.append(list)
}
这和你在ChecklistViewController中添加测试数据的方法非常相似。下面是每一步的讲解。
1、给lists变量一个值。你也可以写成lists= Array<Checklist>(),我比较喜欢方括号的那个版本。
2、调用init?(coder)父类的初始化方法。没有这一步,就不能从故事模版中读取出这个视图来。但是你也不用太担心,如果你真的忘了这个步骤,Xcode会提醒你的。
创建一个新的Checklist对象,给它一个名称,并且将它添加到数组中。
4、重复创建多个Checklist对象。因为list是一个变量,所以你可以复用它。
注意一下,你每次创建一个新的Checklist对象,都重复了同样的两个步骤:
list = Checklist()
list.name = "Name of the check
看起来每一个你创建的Checklist对象都有一个名称。你可以通过自己写一个init方法,将name作为一个参数,然后你就可以将这两行合并为一行了,就像下面这样:
list = Checklist(name: "Name of the checklist")
打开Checklist.swift并且添加新的init方法:
init(name: String) {
self.name = name
super.init()
}
这个初始化用了一个参数name,并且将它传递给实例变量name。
因为参数名称和实例变量都叫做name,所以你使用self.name来引用实例变量。
如果你像下面这样写代码:
init(name: String) {
name = name
super.init()
}
编译器就会死给你看,因为它分不清那个name是参数,而哪个name是实例变量了。
为了消除歧义,你在实例变量name前加了self.回忆一下,self引用你当前所处的对象,所以self.name就代表Checklist中的实例变量name。
回到AllListsViewController.swift,然后添加init?(coder)方法,这次你要动手去做了。
required init?(coder aDecoder: NSCoder) {
lists = [Checklist]()
super.init(coder: aDecoder)
var list = Checklist(name: "Birthdays")
lists.append(list)
list = Checklist(name: "Groceries")
lists.append(list)
list = Checklist(name: "Cool Apps")
lists.append(list)
list = Checklist(name: "To Do")
lists.append(list)
}
这比最初我展示给你看的那一版简单了许多,并且它保证了新的Checklist对象的name属性总是有值的。
注意一下,你不会写成下面这个样子:
var list = Checklist.init(name: "Birthdays")
虽然方法的名称叫做init,但是它不是标准方法。你在使用初始化方法的时候只需要像下面这样写:
var object = ObjectName(parameter1: value1, parameter2: value2, . . .)
根据你指定的参数,Swift会自动找到相应的init方法。
明白了吗?我们进入到下一步吧。
将tableView(numberOfRowsInSection)修改为下面这个样子:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return lists.count
}
然后修改tableView(cellForRowAt),来在cell中填进刚才的数据:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = makeCell(for: tableView)
let checklist = lists[indexPath.row]
cell.textLabel!.text = checklist.name
cell.accessoryType = .detailDisclosureButton
运行app,看起来会是这个样子:
这个界面还有很多剩下的工作要做,这里仅仅是一个开始。