【总结回顾】iOS Apprentice Tutorial 2:Checklists(五)

这是***【总结回顾】iOS Apprentice Tutorial 2:Checklists ***系列的第五篇文章,前几篇文章请见(一) 、(二)、(三)、(四)。

本篇文章总结本书的第七、八章( Saving and loading the checklist itemsMultiple checklists)中的重点内容,从126页到172页。第七章以数据持久化的内容为主,第八章主要是增加了一个嵌套清单,之前已经学过如何创建 table view controller,作者尽可能地用另外一种方法实现同样的效果。

50. 数据持久化三件事

1) 找到可以存放文件的路径,创建文件。

找到路径首页要了解一下iOS的沙盒机制,每个App都有自己的文件目录,不能进入其他App的文件目录里。沙盒机制能够保护手机不受手机病毒的干扰。

所以,App可以存储数据的文件目录名字为“Document”,Document里的内容会和iTunes或iCloud同步。当发布新的版本后,Document里的内容仍然在。App目录里除了Document之外还有其他的文件夹?有,Library和tmp两个文件夹。Library里都是是cache文件,和偏好设置文件。Library是由系统控制管理的。tmp文件夹里都是临时文件,tmp里的文件都会时不时地被系统清理删除。

所以,我们就把数据存储到Document里。

那么,接下来需要的做2件事情:找路径、创建存储文件

    //找路径
    func documentsDirectory() -> String {
        let paths = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)
        return paths[0]
    }

    //在找到的路径里创建文件
    func dataFilePath() -> String {
        return(documentsDirectory() as NSString).stringByAppendingPathComponent("某某.plist")
    }

注意:.DocumentDirectoryDocumentationDirectory的区别。

我们创建的文件的扩展名为.plist,plist表示Property List ,是XML文件格式,能够存储结构化数据。

2)把 数据 存放到文件中,每当用户改变了数据时,改变后的数据也能同步存放到数据中。

我们保存数据需要用到 NSCoder,可以将数据储到结构化格式文件里。将对象转换成文件,再将文件转换回来的过程,就是 Serialization(序列化)。

  func saveChecklists() {
    let data = NSMutableData()
    let archiver = NSKeyedArchiver(forWritingWithMutableData: data)
    archiver.encodeObject(lists, forKey: "Checklists")
    archiver.finishEncoding()
    data.writeToFile(dataFilePath(), atomically: true)
  }

方法saveChecklists()用了两步将 items 数组转换成了二进制数据:
a. NSKeyedArchiver能将数组和 ChecklistItem 转换成二进制文件然后写入对应的文件里。

b. data放置在NSMutableData对象里,然后将自己写入文件所在的路径中

最后一点,NSKeyedArchiver知道如何encode一个数组对象,但是并不了解 ChecklistItem,所以,需要让 ChecklistItem 遵守 NSCoding 协议才可以。也就是说,凡是NSKeyedArchiver要encode的对象,都要遵守 NSCoding 协议。或者说,你想让某个对象使用 NSCoder 系统,就要让这个对象遵守 NSCoding 协议。有关 NSCoding 的知识点请见 #53。

3)应用启动时能够加载数据(取数据)。

  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()
      }
    }
  }

51. NSCoding 协议

协议里有两个方法是必须要实现的:

  • func encodeWithCoder(aCoder: NSCoder) 用来saving 或者 encoding 对象。(存)
  • init?(coder aDecoder: NSCoder)初始化方法,用于创建新的对象,通过从 plist 文件里 loading 或者 decoding 对象来创建对象。(取)
  func encodeWithCoder(aCoder: NSCoder) {
    aCoder.encodeObject(name, forKey: "Name")
    aCoder.encodeObject(items, forKey: "Items")
  }

NSKeyedArchiver尝试encodeChecklistItem对象时,NSKeyedArchiver会给ChecklistItem发送encodeWithCoder(coder)消息。

  required init?(coder aDecoder: NSCoder) {
    name = aDecoder.decodeObjectForKey("Name") as! String
    items = aDecoder.decodeObjectForKey("Items") as! [ChecklistItem]
    super.init()
  }

52. 调试bug小技巧

有时候出现bug时,Xcode会转换到 debugger 情景下,显示哪一行代码导致了程序崩溃。不过有时候会显示是 AppDelegate 的问题,如下图:

【总结回顾】iOS Apprentice Tutorial 2:Checklists(五)_第1张图片

这对改bug来说可没有什么帮助。那怎么办呢?见下图:

【总结回顾】iOS Apprentice Tutorial 2:Checklists(五)_第2张图片

Breakpoint navigator -> 点击 +

然后再次 Run,Xcode就会显示真正导致crash的代码行了。

53.题外话

  • 作者推荐了一个Mac软件:TextWrangler。
  • 在Xcode里如果遇到看不懂的方法,按住Alt/Option键,点击这个代码即可出现帮助信息。
  • 帮你区分两个文件中代码异同的小工具:Xcode -> Open Developer Tool -> FileMerge

54. Initializers 构造器

在创建新的对象时,才需要 init 方法。比如:当用户点击+时,用init()来创建 ChecklistItem,用init?(coder) 将 ChecklistItems存储到硬盘上。

init() 的标准步骤:

init() {
    //给常量或变量实例赋值
    super.init()
    //其他初始化代码,比如调用方法,写在这里就好。必须在super.init()之后,不然报错
}

init 方法不用 func 关键词开头。

override initrequired init?, 一个对象A是对象B的子类,如果要在对象A里添加init方法,前面需要有 override或者required,比如:

  init(name: String) {
    self.name = name
    super.init()
  }

  override init() {
    super.init()
  }

当init有问号时,表示当 init 失败时,会返回nil。如果plist文件里没有足够的信息,decoding一个对象就会失败。

当你声明一个变量或者常量时,需要给常量或变量一个初始值。

如果声明了变量,却没有给出初始值,只给出了类型,比如:

var checked: Bool

这样的话,必须要在init方法里给变量赋值。不然,Swift会报错(Optional 类型的变量除外)。

在给所有的变量常量实例都赋值后,就可以调用 super.init() 方法来初始化这个对象的superclass(父类)。之后,就可以写其他初始化代码,比如调用某些方法,必须在super.init()之后,不然报错。

虽然 Swift 的初始化规则看起来比较复杂,还好,要你你忘了提供init方法,编译器会提示你的。

最后以 table view controller 举例, table view controller 和很多其他的对象一样,会有多个 init 方法:

  • init?(coder):view controller 自动从 storyboard 中载入
  • init(nibName, bundle):你想手动从一个nib文件中载入 view controller
  • init(style):你想不使用 storyboard 或 nib 来创建 table view controller。
required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
}

注意到 init?(coder)的参数有些奇怪了吗,外部标签和内部标签和其他的方法不太一样。coder标签是方法名字的一部分,方法参数是aDecoder
当年调用super.init方法,用coder标签表示super的初始化方法的参数,从aDecoder来的对象作为参数的值。这句话可能不太好理解,可能是翻译错了,附上原文:

When you call super.init, you use the label coder to refer to the parameter of super's init method, and the object from aDecoder as that parameter's value.

总结一下 init 方法三步骤:
1)确保实例变量有值
2)调用 superclass 的 init(),
3)调用其他的方法

55. 创建 table view cell 的四种方法

方法一:使用 prototype cells

在storyboard中找到cell,输入identifier:ChecklistItem,然后写代码:

let cell = tableView.dequeueReusableCellWithIdentifier("ChecklistItem", forIndexPath: indexPath)

注意dequeueReusableCellWithIdentifier方法里有参数forIndexPath,只能用在 prototype cells 中。

方法二:使用静态cell(static cells)

已经确定有哪些cell,而且内容不会变动。

方法三:使用nib文件

nib,也就是XIB,有点像是迷你型的storyboard,里面包含定制的 UITableViewCell 对象。

方法四:手动创建


    let cellIdentifier = "Cell"
    if let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) {
      return cell
    } else {
      return UITableViewCell(style: .Default, reuseIdentifier: cellIdentifier)
    }

注意dequeueReusableCellWithIdentifier方法里没有参数。

这样可能不太深刻,实际使用的时候是什么样子呢?如下:

func cellForTableView(tableView: UITableView) -> UITableViewCell {
    let cellIdentifier = "Cell"

    if let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) {
        return cell
    } else {
        return UITableViewCell(style: .Default, reuseIdentifier: cellIdentifier)
    }
}
 
  override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = cellForTableView(tableView)

    let checklist = lists[indexPath.row]
    cell.textLabel!.text = checklist.name
    cell.accessoryType = .DetailDisclosureButton
    
    return cell
  }

总之,对于 UITabieViewCell,我有一个忠告:
尽可能的复用cell(reuse cells)
尽可能的复用cell(reuse cells)
尽可能的复用cell(reuse cells)
重要的事情说三遍。

56. 新方法之点击跳转界面的同时传值(一)

首先,storyboard中,黄点拖动(见下图),输入Identifier:ShowChecklist。

【总结回顾】iOS Apprentice Tutorial 2:Checklists(五)_第3张图片

然后写代码:

  override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {

    performSegueWithIdentifier("ShowChecklist", sender: nil)
  }

上面代码中有 sender,借用sender可以传值(这个功能是重点,省时省力好帮手),如下:

  override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    let checklist = lists[indexPath.row]
    performSegueWithIdentifier("ShowChecklist", sender: checklist)
  }

理解这两行代码非常关键。

当然,还不能忘了 prepare�ForSegue(sender) 方法。

override func prepareForSegue(sender: UIStoryboardSegue, sender: AnyObject?) {
   if segue.identifier == "ShowChecklist" {
       let controller = segue.destinationViewController as! ChecklistViewController {
           controller.checklist = sender as! Checklist //看,用上 sender 了!
       }
   }
}

当然了,ChecklistViewController里一定要声明(声明里为什么要有叹号,在后面会提及):

var checklist: Checklist!

好了,上面就是所有的步骤了。

接下来说一下上述步骤中涉及的一些知识点,先看图,看看实际上 perform 一个 sugue 涉及多少步骤,然后讲解知识点:


【总结回顾】iOS Apprentice Tutorial 2:Checklists(五)_第4张图片
  • 调用顺序。viewDidLoad()prepareForSegue()之后调用,也就是说,先调用prepareForSegue(),然后再调用viewDidLoad()
  • checklist为什么要有叹号。加一个叹号可以允许 checklist 暂时为 nil 直到 viewDidLoad() 被调用。

在#59里,会介绍另外一种也就是第三种跳转页面并且传值的方法。

57. 创建自己的构造器(init 方法)

var list = Checklist()
list.name = "Name of the checklist"

想把上面的两行变成下面这一行,该怎么做呢?

list = Checklist(name: "Name of the checklist")

需要写一个自己的 init 方法,让 name 作为一个参数:

init(name: String) {
    self.name = name
    super.init()
}

这个构造器的作用就是把参数 name 赋值给实例变量(self.name)。
self.name 指的是当前 Checklist 对象的变量 name。

创建这个构造器的好久就是,可以保证每次我创建新的 Checklist 对象时,都一定会有 name 属性。

58. Type Cast(类型检查)

类型检查的目的是让 Swift 把某个值拥有不同的数据类型。

59. 新方法之点击跳转界面的同时传值(二)

点击 cell 里的 Accessory,除了用 storyboard 之外,还可以用:

  override func tableView(tableView: UITableView, accessoryButtonTappedForRowWithIndexPath indexPath: NSIndexPath) {

  }

点击 cell 的 Accessory 跳转界面并且传值的方法如下:
先到 storyboard 中找到你要跳转的目的地界面,然后如下图;


在 Storyboard ID 中输入对应的 Identity,然后写代码:

  override func tableView(tableView: UITableView, accessoryButtonTappedForRowWithIndexPath indexPath: NSIndexPath) {
    let navigationController = storyboard!.instantiateViewControllerWithIdentifier("ListDetailNavigationController") as! UINavigationController
    
    let controller = navigationController.topViewController as! ListDetailViewController
    controller.delegate = self
    
    let checklist = dataModel.lists[indexPath.row]
    controller.checklistToEdit = checklist
    
    presentViewController(navigationController, animated: true, completion: nil)
  }

其中关键代码两行:

let navigationController = storyboard!.instantiateViewControllerWithIdentifier("ListDetailNavigationController") as! UINavigationController

 presentViewController(navigationController, animated: true, completion: nil)

这个方法非常好用~

你可能感兴趣的:(【总结回顾】iOS Apprentice Tutorial 2:Checklists(五))