- 项目链接:Checklists
1.功能与ViewController
介绍
1. 查看已有待办分类,并可以新增、编辑待办分类,并为该分类选择图标
- 每个分类显示其中还未完成的项目,这是
AllListsViewController
- 新增、编辑待办分类,这是
ListDetailViewController
- 为分类选择图标,这是
IconPickerViewController
2. 每个分类中有它的待办清单,可以新增、编辑待办事项,并可选择提醒时间、是否提醒
- 可以点击行来标记是否完成该事项,这是
ChecklistsViewController
- 可以新增、编辑待办事项并选择提醒时间,这是
ItemDetailViewController
3. 使用plist
保存待办数据
4. 使用UserDefault
和导航控制器代理方法实现保存和恢复上次的浏览位置
5. 用户可以为每条待办事项选择提醒时间,并会按时收到本地通知
2.数据结构
2.1 对于待办事项
-
应包含的变量:
事项ID、事项名、是否完成、提醒时间、是否提醒;其中事项ID是用于设置本地提醒时的identifier
-
应包含的方法:
重写init
(方便新增待办事项)、重写deinit
(删除待办事项时本地通知也要移除)、切换是否完成、设置本地通知、移除本地通知
代码如下,方法的具体实现省略,见源文件
class ChecklistClass: NSObject, Codable{
//成员变量
var itemID = 0
var text = ""
var flag = false
var shouldRemind = false
var dueDate = Date()
//方法
init(text: String, flag: Bool) {
}
deinit {
}
func changeFlag() {
}
func setNotification() {
}
func removeNotification() {
}
}
2.2 对于待办分类
-
应包含的变量:
分类名、分类图标名、事项数组 -
应包含的方法:
重写init
(方便新增待办分类)、计算未完成的待办事项数
class ChecklistKindClass: NSObject, Codable {
var name = ""
var iconName = "No Icon"
var items = [ChecklistClass]()
init(name: String, iconName: String = "No Icon") {
}
func cntNoflag() -> Int {
}
}
2.3 顶层数据结构
- 顶层数据用来存放待办分类数组、实现数据的存储和读取,并封装一些方法(如存取用户上次点击的待办分类序号、计算下一条事项ID)供外部方法调用。
AppDelegate
是整个App中最顶层的对象,因此让AppDelegate
持有DataModel是最合理的,它可以向任何需要DataModel的视图控制器传递这一对象
class DataModel {
var lists = [ChecklistKindClass]()
//数据持久化
func documentsDirectory() -> URL {
}
func dataFilePath() -> URL {
}
func loadLists(){
}
func saveLists(){
}
//初始化时加载plist数据
init() {
loadLists()
}
//存取上一次访问的分类序号
var indexOfLastList : Int {
get {
return UserDefaults.standard.integer(forKey: "listIndex")
}
set{
UserDefaults.standard.setValue(newValue, forKey: "listIndex")
}
}
//类方法直接用类名调用,ChecklistClass的初始化时调用生成不重复的事项ID
class func nextItemID() -> Int {
let userDefaults = UserDefaults.standard
//一开始没有ItemID,返回0;
let itemID = userDefaults.integer(forKey: "ItemID")
userDefaults.set(itemID + 1, forKey: "ItemID")
userDefaults.synchronize()
return itemID
}
}
3.ViewController
关系图与转场
3.1 ViewController
关系图简介
- 以下“
xxViewController
”简称“xxVC
”
-
AllListsVC
与ChecklistsVC
在同一个导航控制器中,在AllListsVC
中点击某分类行进入ChecklistsVC
,ChecklistVC
点击左上角可返回上一级 -
在
ChecklistsVC
界面,点击右上角“+”或点击某行的Detail Disclosure,可进入ItemDetailVC
进行添加/编辑该项目;ChecklistsVC
通过ItemDetailVC
的导航控制器找到它 -
在
AllListsVC
界面,点击右上角“+”或点击某行的Detail Disclosure,可进入listDetailVC
进行添加/编辑该分类;AllListsVC
通过listDetailVC
的导航控制器找到它。
关于为什么要将
ItemDetailVC
嵌入一个新的导航控制器中:
- 这里使用
present modally
转场方式,是一个弹出的页面效果。如果不嵌入导航控制器,无法显示该页的title
(添加还是编辑)、也无法在顶部添加done、cancel的Label
- 如果不给
ItemDetailVC
加导航控制器,也可以使用show
转场,不过这种的话它与ChecklistsVC
处于同一个导航控制器下,就不是页面弹出的效果而是页面层级的关系- 我个人觉得添加、编辑待办事项用弹出页面的方式更好一些,而不像待办分类
AllListsVC
和待办事项ChecklistsVC
那两个层级的页面关系;同理,添加、编辑待办分类的listDetailVC
也嵌入到导航控制器中。- 此外,如果是
present modally
转场,应该在done、cancel(完成编辑、取消编辑)的代理方法中用dismiss;如果是show
转场,它们处于同一个导航控制器,应该用navigationController
的popViewController
方法,因为导航控制器是一个类似栈的结构,是ViewController
的容器,栈顶放的是当前显示的ViewController
。
3.2 通过Storyboard转场
- 以添加、编辑待办事项为例(
ChecklistsVC
转场至ItemDetailVC
)
- 按住ctrl,连接“+”的
Label
和ItemDetailVC
的导航控制器,选择“Action Segue”中的present modally
,并在属性器中填入转场ID:addItemSegue
- 按住ctrl,连接
Table View Cell
和ItemDetailVC
的导航控制器,选择“Accessory Action”中的present modally
,并在属性器中填入转场ID:editItemSegue。
- 注意要选中cell与导航控制器相连(而不是cell中的
Label
),因为那个Detail Disclosure的accessory是cell的属性
- 代码及注释如下,将
ChecklistVC
设为ItemDetailVC
的delegate
是为了反向传值
override func prepare(for segue:UIStoryboardSegue, sender: Any?) {
if segue.identifier == "addItemSegue" {
//ItemViewVC是它所在的导航控制器的第一个VC,通过找到导航控制器找到ItemDetailVC
let nvController = segue.destination as! UINavigationController
let controller = nvController.topViewController as! ItemDetailViewController
controller.delegate = self
}
//正向传值,让ItemDetailVC知道是add还是edit
else if segue.identifier == "editItemSegue" {
let nvController = segue.destination as! UINavigationController
let controller = nvController.topViewController as! ItemDetailViewController
controller.delegate = self
if let indexPath = tableView.indexPath(for: sender as! UITableViewCell){
controller.itemToEdit = checklistKind.items[indexPath.row]
}
}
}
3.3 通过Table View Delegate
手动转场
- 以进入某个待办分类为例(
AllListsVC
转场至ChecklistsVC
)
- 按住ctrl,连接
AllListsVC
和ChecklistsVC
,选择“Manual Segue”中的show
,并在属性器中填入转场ID:showItemSegue - 重写
AllListsVC
Table View的代理方法,在点击某一行时手动触发转场,并正向传值告诉ChecklistsVC
选择的是哪个待办分类
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath:IndexPath) {
let checklistKind = dataModel.lists[indexPath.row]
//向ChecklistViewController传checklistKind对象
performSegue(withIdentifier: "showItemSegue", sender: checklistKind)
}
3.4 通过Table View Delegate
与present
方法
- 以编辑某个待办分类为例,从
AllListsVC
到ListDetailVC
- 在Storyboad中,给
ListDetailVC
的导航控制器填入ID - 重写
AllListsVC
Table View的代理方法,当点击某一行的Accessory时被调用:Storyboard根据上面的ID实例化一个视图控制器(就是ListDetailVC
的导航控制器
)并展现在屏幕上,效果和转场类似。
//从AllListsVC到ListDetailVC
//ListDetailVC需要向AlllistVC反向传值,因此需要设置代理;edit也需要正向传值
//ListDetailVC通过其导航控制器找到
override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
let nvController = storyboard!.instantiateViewController(withIdentifier: "ListDetailNavigationController") as! UINavigationController
let controller = nvController.topViewController as! ListDetailViewController
controller.delegate = self
let checklistKind = dataModel.lists[indexPath.row]
controller.listToEdit = checklistKind
present(nvController, animated: true, completion: nil)
}
- 这里将
ListDetailVC
嵌入一个单独导航控制器的原因上面已经提到了,在使用present
时是一个弹出的效果,如果不嵌入导航控制器无法显示title
的顶部Label
; - 如果不用
present
,则不用将ListDetailVC
嵌入一个单独的导航控制器:用导航控制器的pushViewController
方法(相当于转场中的show
),相应的要将实例化视图控制器的ID填入ListDetailVC
中,代码如下:
override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
let controller = storyboard!.instantiateViewController(withIdentifier: "ListDetailNavigatio