这是***【总结回顾】iOS Apprentice Tutorial 2:Checklists ***系列的第六篇文章,前几篇文章请见(一) 、(二)、(三)、(四)、(五)。
本篇文章总结本书的第九、十章( Putting to-do items into the checklists、Using NSUserDefaults to rememberstuff)中的重点内容,主要优化了存储方法,以及如何记录用户最后使用的界面的位置等等,从173页到204页。
数据结构从下图中看,可能会更容易理解:
60. 存储方法优化
之前用户的每一次点击之后都要保存,这样写起来不仅繁琐,而且有时候会忘记某个点击效果,就没法保存了。
所以我们这次使用的方法是,在App被 terminate 之前保存数据,在 AppDelegation 文件里,有两个方法就能够涵盖所有需要保存的情况:
func applicationDidEnterBackground(application: UIApplication)
和
func applicationWillTerminate(application: UIApplication)
实际上代码如下
...
let dataModel = DataModel()
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
let navigationController = window!.rootViewController as! UINavigationController
let controller = navigationController.viewControllers[0] as! AllListsViewController
controller.dataModel = dataModel
return true
}
...
func applicationDidEnterBackground(application: UIApplication) {
saveData()
}
func applicationWillTerminate(application: UIApplication) {
saveData()
}
func saveData() {
dataModel.saveChecklists()
}
注意代码中使用的是let controller = navigationController.viewControllers[0] as! AllListsViewController
,不是之前出现的topViewController
,因为后者只是当前正在展示在界面上的 view controller。
而 DataModel 的代码为:
import Foundation
class DataModel {
//声明变量并赋值
var lists = [Checklist]()
//初始化方法。(注意初始化方法里没有super.init(),因为DataModel没有父类superclass)
init() {
loadChecklists()
}
//找路径
func documentsDirectory() -> String {
let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
return paths[0]
}
//新建文件
func dataFilePath() -> String {
return (documentsDirectory() as NSString).stringByAppendingPathComponent("Checklists.plist")
}
//存数据
func saveChecklists() {
let data = NSMutableData()
let archiver = NSKeyedArchiver(forWritingWithMutableData: data)
archiver.encodeObject(lists, forKey: "Checklists")
archiver.finishEncoding()
data.writeToFile(dataFilePath(), atomically: true)
}
//取数据
func loadChecklists() {
let path = dataFilePath()
if NSFileManager.defaultManager().fileExistsAtPath(path) {
if let data = NSData(contentsOfFile: path) {
let unarchiver = NSKeyedUnarchiver(forReadingWithData: data)
lists = unarchiver.decodeObjectForKey("Checklists") as! [Checklist]
unarchiver.finishDecoding()
}
}
}
}
Checklist 的代码如下:
import UIKit
class Checklist: NSObject, NSCoding {
var name = ""
var items = [ChecklistItem]()
init(name: String) {
self.name = name
super.init()
}
required init?(coder aDecoder: NSCoder) {
name = aDecoder.decodeObjectForKey("Name") as! String
items = aDecoder.decodeObjectForKey("Items") as! [ChecklistItem]
super.init()
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(name, forKey: "Name")
aCoder.encodeObject(items, forKey: "Items")
}
}
ChecklistItem 的代码如下:
import Foundation
class ChecklistItem: NSObject, NSCoding {
var text = ""
var checked = false
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
text = aDecoder.decodeObjectForKey("Text") as! String
checked = aDecoder.decodeBoolForKey("Checked")
super.init()
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(text, forKey: "Text")
aCoder.encodeBool(checked, forKey: "Checked")
}
func toggleChecked() {
checked = !checked
}
}
61. var 和 let 的区别
Swift 中,value types(值类型) 和 reference types(引用类型)有显著的区别,在let使用方面也有一些不同。
对值类型而言,var后面是值,可以变。let后面也是值,不能变。
我们在 class 类定义的 objects,都是引用类型。对引用类型而言,引用类型的变量或者常量实际上没有包含一个真正的对象,只包含了这个对象的引用。如果用let声明引用类型常量,那么此常量不能再指向其他的对象了,但是此常量对象本身是可以变化的。
如果还没搞定什么时候用let什么时候用var,作者给出了一个小方法:全都用let,然后编译器告诉你某个应该用var声明,再去改成var。
如果是 optional,必须用 var
如果是 optional,必须用 var
如果是 optional,必须用 var
63. NSUserDefaults记录用户看到了哪里
需求如下:记录用户看到了哪个页面,下次打开App的时候,还在上次离开的地方。
我们使用 NSUserDefaults 来记录这个数据。NSUserDefaults 有些像是 词典Dictionary,每个键对应一个值。NSUserDefaults 只能记录小量的数据,大的数据是不能使用NSUserDefaults的。
为了实现需求,我们需要做三件事情:
(1)
当segue从主界面到清单详细内容界面时,记录下所选清单的index,存到NSUserDefaults里。
写入值的方法为:
NSUserDefaults.standardUserDefaults().setInter(indexPath.row, forKeys: "ChecklistIndex")
(2)
当用户点击返回按钮返回到主界面时,需要去掉存到NSUserDefaults里的index值。可以赋值-1,表示在主界面。(注意:NSUserDefaults里不能使用optional)
首先要知道用户点击了返回按钮,然后才能赋值。如何才能知道用户点击了返回按钮呢?方法如下:
让主界面view controller 成为 navigation controller 的delegate,这样主界面就能够监听用户点击返回按钮事件了。类的开头写上delegate的名字UINavigationControllerDelegate
,在viewDidAppear()
写上navigationController?.delegate = self
。
然后可以实现delegate中的方法了:
func navigationController(navigationController: UINavigationController, willShowViewController viewController: UIViewController, animated: Bool) {
if viewController === self {
NSUserDefaults.standardUserDefaults().setInter(indexPath.row, forKeys: "ChecklistIndex")
}
}
任何时候只要navigation controller要滑到新的页面时,这个方法都会被调用。
调用条件很重要,附上原文:
This method is called whenever the navigation controller will slide to a new screen.
(3)
如果App在启动后NSUserDefaults的值不是-1,需要自动跳转到NSUserDefaults里记录的页面。
首先要从NSUserDefaults取出值,才能知道跳转到哪个界面,读取值的方法为:
let index = NSUserDefaults.standardUserDefaults().integerForKey("ChecklistIndex")
跳转界面的方法为:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
navigationController?.delegate = self
let index = dataModel.indexOfSelectedChecklist
if index >= 0 && index < dataModel.lists.count {
let checklist = dataModel.lists[index]
performSegueWithIdentifier("ShowChecklist", sender: checklist)
}
}
每次在view controller可见后,UIKit都会自动调用 viewDidAppear 方法。
为什么使用 viewDidAppear 方法而不是 viewDidLoad ?这里非常
64. Equal 或者 identical 的异同
===三个等号:检查三个等号两边的对象是否为同一个对象
==两个等号:在检查两个等号两边的变量是否有相同的值
65. Defensive programming 防御性/防错性程序设计
可是在用户第一次运行App的时候,NSUserDefaults里还没有值,这时候运行App,就会报错。因为NSUserDefaults的取值方法integerForKey()如果没有找到key对应的值,就会返回数字零,可是在这里,0是index的值,index 0 那行用户还没有创建呢。自然就崩溃了。
所以,我们要给NSUserDefaults设置一个初始值,在DataModel里写就好:
func registerDefaults() {
let dictionary = [ "ChecklistIndex": -1]
NSUserDefaults.standardUserDefaults().registerDefaults(dictionary)
}
然后在DataModel初始化方法里调用上面这方法就可以了。其实NSUserDefaults的读取也可以放到DataModel里,毕竟是MVC里的M嘛。
读取NSUserDefaults可以用Swift最新的一个语法:
var indexOfSelectedChecklist: Int {
get {
return NSUserDefaults.standardUserDefaults().integerForKey("ChecklistIndex")
}
set {
NSUserDefaults.standardUserDefaults().setInteger(newValue, forKey: "ChecklistIndex")
NSUserDefaults.standardUserDefaults().synchronize()
}
}
NSUserDefaults.standardUserDefaults().synchronize()
的作用是:每当indexOfSelectedChecklist
的值发生改变的时候,强制NSUserDefaults保存数值,保证NSUserDefaults和.plist文件处于同步状态。
当计算机尝试读取indexOfSelectedChecklist
的值的时候,get闭包里的代码会被执行,当尝试给indexOfSelectedChecklist
赋予新值的时候,set闭包里的代码就会执行。这样#63中的方法可以更新如下:
let index = dataModel.indexOfSelectedChecklist
dataModel.indexOfSelectedChecklist = indexPath.row
dataModel.indexOfSelectedChecklist = -1
第二个Defensive programming例子,看if后面跟的条件:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
navigationController?.delegate = self
let index = dataModel.indexOfSelectedChecklist
if index >= 0 && index < dataModel.lists.count {
let checklist = dataModel.lists[index]
performSegueWithIdentifier("ShowChecklist", sender: checklist)
}
}
66. 创建默认的清单数据
如果App下载后第一次打开就有默认数据,有一个名为list的清单,同时App还会自动跳转到这个名为list的清单里面,可以直接添加内容(items)。如何实现需求呢?
func registerDefaults() {
let dictionary = [ "ChecklistIndex": -1,"FirstTime": true ]
NSUserDefaults.standardUserDefaults().registerDefaults(dictionary)
}
func handleFirstTime() {
let userDefaults = NSUserDefaults.standardUserDefaults()
let firstTime = userDefaults.boolForKey("FirstTime")
if firstTime {
let checklist = Checklist(name: "List")
lists.append(checklist)
indexOfSelectedChecklist = 0
userDefaults.setBool(false, forKey: "FirstTime")
userDefaults.synchronize()
}
}
init() {
loadChecklists()
registerDefaults()
handleFirstTime()
}