【总结回顾】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 的问题,如下图:

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

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。

然后写代码:

  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 涉及多少步骤,然后讲解知识点:


  • 调用顺序。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)

这个方法非常好用~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,186评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,858评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,620评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,888评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,009评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,149评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,204评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,956评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,385评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,698评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,863评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,544评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,185评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,899评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,141评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,684评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,750评论 2 351

推荐阅读更多精彩内容

  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,136评论 30 470
  • 代码创建UIWindow对象 Xcode7之后使用代码创建UIWindow对象: //创建UIWindow对象 s...
    云之君兮鹏阅读 1,313评论 0 2
  • 这本书看了几遍也记不清了,总之好多遍,第一遍照着书中的代码敲了一遍,结果还出现了好多bug,删掉重新敲,没有问题之...
    sing_crystal阅读 899评论 4 5
  • 都因为你 我相信世间有一种热情像太阳一样! 都是因为你 我相信世间有一种温柔像月亮一样! 爱的世界有你! 就没有地...
    刘文娟阅读 405评论 0 1
  • 最近几年,发现自己的泪点低了很多…不知道是年纪大了,对生活、生命的感慨多了还是怎么了…很容易被感动,也很容易因为一...
    筱淼渺阅读 150评论 0 0