第十六章——保存、加载数据和应用程序状态【译】

在 iOS 应用程序中有许多方法来保存和加载数据。 本章将介绍一些最常见的机制,以及您在iOS中写入或读取文件系统所需的概念。 接下来,您将更新 Homepwner,使其数据在运行之间保持不变(图16.1)。

图16.1任务切换器中的Homepwner

存档

大多数 iOS 应用程序基本上都会做同样的事情:为用户提供操作数据的界面。 应用程序中的每个对象在此过程中都有作用。 模型对象负责保持用户操纵的数据。 视图对象反映该数据,控制器负责保持视图和模型对象的同步。

因此,保存和加载“数据”几乎总是意味着保存和加载模型对象。

Homepwner 中,用户操作的模型对象是 Item 的实例。 为了让 Homepwner 成为有用的应用程序,Item 的实例必须在应用程序的运行之间持久化。 您将使用 存档(archiving) 来保存和加载 Item 对象。

归档是在 iOS 中持久化模型对象的最常见的方式之一。 归档对象涉及记录其所有属性并将其保存到文件系统。 读档(Unarchiving) 则是从数据中重新创建对象。

需要存档和读档的实例类必须符合 NSCoding 协议并实现其两个必需的方法,encode(with :)init(coder :)

protocol NSCoding {
  func encode(with aCoder: NSCoder)
  init?(coder aDecoder: NSCoder)
}

将对象添加到界面文件(例如故事板( storyboard )文件)中时,将其存档。 在运行时,通过从界面文件读档,将对象加载到内存中。 UIViewUIViewController 均符合 NSCoding 协议,因此无需任何额外的工作即可 存档 和 读档。

另一方面,您的 Item 类目前不符合 NSCoding。 打开 Homepwner.xcodeproj 并在 Item.swift 中添加此协议声明。

class Item: NSObject, NSCoding {

下一步是实现所需的方法。 我们从 encode(with :) 开始。 当一个 Item 被发送消息到 encode(with :) 时,它会将其所有属性编码到作为参数传递的 NSCoder 对象中。 在保存的同时,您将使用 NSCoder 写出数据流。 该流将被组织为键值对并存储在文件系统上。

Item.swift 中,实现 encode(with :) 方法, Item 属性的名称和值将被添加到流中。

func encode(with aCoder: NSCoder) {
  aCoder.encode(name, forKey: "name")
  aCoder.encode(dateCreated, forKey: "dateCreated")
  aCoder.encode(itemKey, forKey: "itemKey")
  aCoder.encode(serialNumber, forKey: "serialNumber")

  aCoder.encode(valueInDollars, forKey: "valueInDollars")
}

要找出用于其他 Swift 类型的编码方法,可以查看 NSCoder 的文档。 无论编码值的类型如何,总是存在一个key,它是一个字符串,用于标识正在编码的属性。 按照惯例,此 key 是要编码的属性的名称。

编码是一个递归过程。 当一个实例被编码(即,当它是 encode(_:forKey :) 中的第一个参数)时,该实例被发送到 encode(with :)。 在执行其 encode(with :) 方法时,它使用 encode(_:forKey :) 对其属性进行编码(图16.2)。 因此,每个实例都对它引用的任何属性进行编码,接着对它引用的引用的属性进行编码,依此类推。

图16.2 编码一个对象

key 的作用是在稍后从文件系统加载该 Item 时检索编码值。 从 存档 加载的对象将被发送到 init(coder :)。 该方法会找出 encode(with:) 内的所有对象,并将它们分配给相应的属性。

Item.swift 中,实现 init(coder :)

required init(coder aDecoder: NSCoder) {
  name = aDecoder.decodeObject(forKey: "name") as! String
  dateCreated = aDecoder.decodeObject(forKey: "dateCreated") as! Date
  itemKey = aDecoder.decodeObject(forKey: "itemKey") as! String
  serialNumber = aDecoder.decodeObject(forKey: "serialNumber") as! String?

  valueInDollars = aDecoder.decodeInteger(forKey: "valueInDollars")

  super.init()
}

请注意,此方法也有一个 NSCoder 参数。 在 init(coder:) 中,NSCoderItem 初始化所要用到的所有数据。 还要注意,您在容器上调用 decodeObject(forKey :) 可以获取对象并使用 decodeInteger(forKey :) 来获得 valueInDollars

在第10章中,我们讨论了构造器链和指派构造器。 init(coder :) 方法不属于这种设计模式。 您将保持 Item 的指派构造器不变,并且 init(coder:) 不会调用它。

Item 的实例现在符合 NSCoding 标准,并且可以使用存档保存到文件系统中并从其中加载。 您现在可以构建应用程序,以确保没有语法错误,但是您还没有办法启动保存和加载。 您还需要在文件系统上存放要保存的 Item 的位置。

应用程序沙盒

每个iOS应用程序都有自己的应用程序沙盒( application sandbox )。 应用程序沙盒是文件系统的一个目录,用于隔离其他的文件。 您的应用程序必须保留在其沙盒中,其他应用程序无法访问其沙盒。 图16.3显示了 iTunes / iCloud 的应用程序沙盒。

图16.3 应用程序沙盒

应用程序沙盒包含多个目录:

Documents/

该目录包含应用程序运行过程生成的并且您希望在运行期间能够持久化的数据。 当设备与 iTunesiCloud 同步时,它会被备份。 如果设备发生问题,则可以从 iTunesiCloud 中恢复此目录中的文件。 在 Homepwner 中,保存所有 item 数据的文件将存储在此处。

Library/Caches/

该目录包含应用程序运行过程生成的并且您希望在运行期间能够持久化的数据。但是,与 Documents 目录不同,当设备与 iTunes 或 iCloud 同步时,它不会被备份。 不备份缓存数据的主要原因是数据可能非常大,会延长同步设备所需的时间。 存储在其他地方的数据(如Web服务器)可以放在此目录中。 如果用户需要恢复设备,则可以从 Web 服务器再次下载该数据。 如果设备的磁盘空间非常小,系统可能会删除此目录的内容。

Library/Preferences/

此目录是存储所有首选项的位置,也是 设置( Settings )应用程序查找应用程序首选项的地方。 Library/PreferencesNSUserDefaults 类自动处理,并在设备与 iTunesiCloud 同步时进行备份。

tmp/

该目录是您在应用程序的运行时间内暂时使用的数据。 当您的应用程序未运行时,操作系统可能会清除此目录中的文件。但是,为了整洁,您应该在不再需要这些数据时从该目录中显式删除文件。 当设备与 iTunesiCloud 同步时,该目录不会被备份。

构建文件URL

来自 HomepwnerItem 的实例将被保存到 Documents 目录中的单个文件中。 ItemStore 将处理对该文件的写入和读取。 为此,ItemStore 需要为此文件构造一个URL。

ItemStore.swift 中实现一个新的属性来存储这个URL。

var allItems = [Item]()
let itemArchiveURL: URL = {
  let documentsDirectories = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
  let documentDirectory = documentsDirectories.first!
  return documentDirectory.appendingPathComponent("items.archive")
}()

该值将使用闭包来设置,而不是直接将值分配给该属性。 你可能还记得你在第4章中使用了 numberFormatter 属性。请注意,这里的闭包具有 () -> URL 的声明,这意味着它不带任何参数,它返回一个 URL 实例。 当 ItemStore 类被实例化时,将会运行此闭包,并将返回值分配给 itemArchiveURL 属性。 使用这样的闭包可以设置需要多行代码的变量或常量的值,这在配置对象时非常有用。 这使得您的代码更易于维护,因为它保留了属性和生成属性所需的代码。

方法 urls(for :) 在文件系统中搜索符合参数给出的标准的 URL。 (仔细检查你的第一个参数是 .documentDirectory 而不是 .documentationDirectory。自动填充的第一个建议是 .documentationDirectory,因此很容易引入这个错误,最后会导致出现错误的 URL。)

在 iOS 中,最后一个参数总是相同的。 (这种方法是从 macOS 借鉴的,其中有更多的选项。)第一个参数是一个 SearchPathDirectory 枚举,它指定了你的 URL 指向的沙盒目录。 例如,搜索 .cachesDirectory 将返回应用程序沙盒中的 Caches 目录。

您可以搜索 SearchPathDirectory 的文档来查找其他选项。 请记住,这些枚举值是由 iOS 和 MacOS 共有的,所以并不是所有这些值都可以在 iOS 上运行。

urls(for :) in :) 的返回值是一个URL数组。 它是一个数组,因为在 macOS 中可能有多个URL满足搜索条件。 然而,在iOS中,只有一个(如果您搜索的目录是正确的沙盒目录)。 因此,存档文件的名称将追加到数组中的第一个也是唯一的 URL。 这将是 Item 实例的存档保存的位置。

NSKeyedArchiver 和 NSKeyedUnarchiver

你现在有一个地方可以保存文件系统上的数据和模型对象了。 最后两个问题是:如何启动保存和加载过程,什么时候执行? 为了保存 Item 的实例,当应用程序 “退出” 时,您将使用类 NSKeyedArchiver

ItemStore.swift 中,实现一个在 NSKeyedArchiver 类上调用 archiveRootObject(_:toFile :) 的新方法。

func saveChanges() -> Bool {
  print("Saving items to: \(itemArchiveURL.path)")
  return NSKeyedArchiver.archiveRootObject(allItems, toFile: itemArchiveURL.path)
}

archiveRootObject(_:toFile :) 方法负责将 allItem 中的每个 Item 保存到 itemArchiveURL 中。 是的,这很简单。 这里是 archiveRootObject(_:toFile :) 的工作原理:

  • 该方法从创建 NSKeyedArchiver 的实例开始。(NSKeyedArchiver 是抽象类 NSCoder 的具体子类。)
  • allItems 上调用 encode(with :) 的方法,并将 NSKeyedArchiver 的实例作为参数传递。
  • 然后 allItems 数组调用 encode(with :) 包含的所有对象,并传递到相同的 NSKeyedArchiver。 因此,Item 的所有实例将其实例变量编码到相同的 NSKeyedArchiver 中(图16.4)。
  • NSKeyedArchiver 将其保存的数据写入路径。

图16.4 allItems数组存档

当用户按下设备上的 Home 按钮时,messageapplicationDidEnterBackground(_ :) 消息被发送到 AppDelegate。 那也是当您想要将 saveChanges 发送到 ItemStore 时候。

打开 AppDelegate.swift 并将一个属性添加到该类以存储 ItemStore 实例。 您将需要一个属性来引用 applicationDidEnterBackground(_ :) 中的实例。

class AppDelegate: UIResponder, UIApplicationDelegate {

  var window: UIWindow?
  let itemStore = ItemStore()

然后更新 application(_:didFinishLaunchingWithOptions :) 以使用此属性而不是本地常量。

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]?) -> Bool {
  // Override point for customization after application launch.

  Create an ImageStore
  let itemStore = ItemStore()

  Create an ImageStore
  let imageStore = ImageStore()

  // Access the ItemsViewController and set its item store and image store
  let navController = window!.rootViewController as! UINavigationController
  let itemsController = navController.topViewController as! ItemsViewController
  itemsController.itemStore = itemStore
  itemsController.imageStore = imageStore

return true
}

由于属性和本地常量命名相同,因此只需删除创建局部常量的代码即可。

现在,仍然在 AppDelegate.swift 中,实现 applicationDidEnterBackground(_ :) 来启动保存 Item 实例。 (此方法可能已经由模板实现了,如果是这样,请确保此代码被添加到现有方法中,而不是编写新的)。

func applicationDidEnterBackground(_ application: UIApplication) {
  let success = itemStore.saveChanges()
  if (success) {
    print("Saved all of the Items")
  } else {
    print("Could not save any of the Items")
  }

}

在模拟器上构建并运行应用程序。 创建一些 Item 的实例,然后按主页按钮离开应用程序。 检查控制台,您应该看到一个日志语句,表明 Item 已被保存。

虽然您还没有将这些 Item 的实例加载到应用程序中,但仍可以验证某些内容是否已保存。

在控制台的日志语句中,找到两个位置,一个记录 itemArchiveURL ,另一个记录是否保存成功。 如果保存不成功,请确认您的 itemArchiveURL 已经被正确创建。 如果 item 已成功保存,请将打印的路径复制到控制台。

打开 Finder 并按 Command-Shift-G。 粘贴您从控制台复制的文件路径,然后按Return键。 您将被带到包含 items.archive 文件的目录。 按 Command-Up(上箭头) 导航到 items.archive 的父目录。 这是应用程序的沙盒目录。 在这里,您可以在应用程序本身旁边看到 DocumentsLibrarytmp 目录(图16.5)。

图16.5 Homepwner的沙盒

沙箱目录的位置可以在应用程序的运行之间改变; 然而,沙盒的内容将保持不变。 因此,您可能需要在处理应用程序时将目录复制并粘贴到 Finder 中。

加载文件

现在我们来看看加载这些文件。 要在应用程序启动时加载 Item 的实例,在创建 ItemStore 时将使用 NSKeyedUnarchiver 类。 在 ItemStore.swift 中,重写 init(),添加以下代码:

init() {
  if let archivedItems = NSKeyedUnarchiver.unarchiveObject(withFile: itemArchiveURL.path) as? [Item] {
    allItems = archivedItems
  }
}

unarchiveObject(withFile :) 方法将创建一个 NSKeyedUnarchiver 的实例,并将位于 itemArchiveURL 的存档加载到该实例中。 然后,NSKeyedUnarchiver 将检查存档中的根对象的类型,并创建该类型的实例。 在这种情况下,类型将是 Item 的数组,因为您使用类型为 [Item] 的根对象创建此存档。 (如果根对象是 Item 的实例,那么 unarchiveObject(withFile :) 将返回一个 Item。)

新创建的数组然后被发送到 init(coder :),如你所猜测的,NSKeyedUnarchiver 作为参数传递。 该数组从 NSKeyedUnarchiver 开始解码其内容( Item 的实例),并将这些对象中的每个对象发送给消息 init(coder :),传递相同的 NSKeyedUnarchiver

构建并运行应用程序。 您的 Item 将可用,直到您明确删除它们。 需要注意的一点是测试保存和加载代码:如果从 Xcode 中杀死 Homepwner,方法 applicationDidEnterBackground(_ :) 将无法获得调用的机会,并且不会保存 Item 数组。 您必须先按住 Home 键,然后通过单击 停止( Stop ) 按钮将其从 Xcode 中删除。

应用程序的状态和转换

Homepwner 中,当应用程序进入 后台(background) 状态时,item 将被 存档。 了解应用程序可能处于哪些状态,导致应用程序在状态之间转换的原因以及如何通过代码通知这些转换是有用的。 此信息总结如图16.6所示。

图16.6 典型的应用程序状态

当应用程序未运行时,它处于 非运行状态(not running state),并且不执行任何代码或在RAM中保留任何内存。

用户启动应用程序后,它进入 活动状态(active state)。 当处于活动状态时,应用程序的界面显示在当前屏幕上,它接受事件和事件处理。

当处于活动状态时,应用程序可以被诸如 SMS消息,推送通知,电话呼叫或警报之类的系统事件暂时中断。 您的应用程序顶部将显示一个叠加层,以处理此事件,并且应用程序进入 非活动状态(inactive state)。 在非活动状态下,应用程序被覆盖而不可见,它正在执行代码,但不会收到事件。 应用程序通常很少会停留在非活动状态。 您可以通过按设备顶部的 “锁定” 按钮强制有效应用程序进入非活动状态。 应用程序将保持非活动状态,直到设备解锁。

当用户按下 Home 键或以某种其他方式切换到其他应用程序时,应用程序进入 后台状态(background state)。 (实际上,在转换到后台状态之前,它暂停在非活动状态。)在后台状态下,应用程序的界面不可见也无法接收事件,但仍可执行代码。 默认情况下,进入后台状态的应用程序在进入 挂起状态(suspended state) 前约有10秒钟。 你的应用程序不应该依赖这个数字; 相反,它应该尽可能快地保存用户数据并释放任何共享资源。

处于挂起状态的应用程序无法执行代码。 您看不到它的界面,并且在暂停时不需要的任何资源都将被销毁。 暂停的应用程序基本上是闪存冻结的,并且当用户重新启动时可以快速解冻。 表16.1总结了不同应用状态的特点。

表16.1 应用程序状态

您可以通过双击主页按钮进入任务切换器(图16.7),查看后台应用程序或挂起的应用程序。 您可以在模拟器中按 Command-Shift-H 两次。 (最近运行的已被终止的应用程序也可能会出现在此显示中。)

图16.7 任务切换器中的后台和挂起的应用程序

只要有足够的系统内存,暂停状态下的应用程序将保持该状态。 当操作系统觉得内存不足时,会根据需要终止挂起的应用程序。 暂停的应用程序并不表示它即将被终止。 它只是从内存中删除。 (应用程序在终止后可能会保留在任务切换器中,但是在点击时将不得不重新启动。)

当应用程序更改其状态时,会在应用程序委托中调用一些方法。 以下是声明应用程序状态转换的 UIApplicationDelegate 协议的一些方法。 (这些也显示在图16.6中。)

optional func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]?) -> Bool
optional func applicationDidBecomeActive(_ application: UIApplication)
optional func applicationWillResignActive(_ application: UIApplication)
optional func applicationDidEnterBackground(_ application: UIApplication)
optional func applicationWillEnterForeground(_ application: UIApplication)

您可以实现这些方法为您的应用程序采取适当的操作。 转换到后台状态是保存任何未完成更改的好地方,因为最后一次应用程序可以在进入暂停状态之前执行代码的时机。 一旦处于暂停状态,应用程序就可能被操作系统终止。

将数据写入文件系统

您在 Homepwner 中的存档会为每个 item 保存并加载 itemKey,但图片呢? 目前,当应用程序进入后台状态时,它们会丢失。 在本节中,您将扩展图片存储,以便在添加图片时保存图片,并根据需要进行读取。

Item 实例的图片也应存储在 Documents 目录中。 您可以使用用户拍摄图片时生成的图片 key 来命名文件系统中的图片。

ImageStore.swift 中使用名为 imageURL(forKey :) 的新方法在文档目录中使用给定的 key 创建一个URL。

func imageURL(forKey key: String) -> URL {

  let documentsDirectories = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
  let documentDirectory = documentsDirectories.first!

  return documentDirectory.appendingPathComponent(key)
}

要保存并加载图片,您将将 JPEG 格式的图片到内存中的缓冲区。 不用仅仅单纯的去创建一个缓冲区,Swift 程序员有一个方便的类来创建,维护和销毁这些缓冲区——DataData 实例保存一些二进制数据,您将使用 Data 来存储图片数据。

ImageStore.swift 中,修改 setImage(_:forKey :) 以获取 URL 并保存图片。

func setImage(_ image: UIImage, forKey key: String) {
  cache.setObject(image, forKey: key as NSString)

  // Create full URL for image
  let url = imageURL(forKey: key)

  // Turn image into JPEG data
  if let data = UIImageJPEGRepresentation(image, 0.5) {
    // Write it to full URL
    let _ = try? data.write(to: url, options: [.atomic])
  }
}

我们来仔细检查这个代码。 函数 UIImageJPEGRepresentation 有两个参数:UIImage 和一个压缩质量。 压缩质量是从 0 到 1 的浮点数,其中 1 是最高质量(最小压缩)。 如果压缩成功,该函数返回 Data 的一个实例,如果不成功则返回 nil

可以通过调用 write(to:options :) 将该 Data 实例写入文件系统。 Data 中保存的字节随后被写入由第一个参数指定的 URL 中。 第二个参数允许将一些选项传递给该方法。 如果存在 .atomic 选项,则将文件写入文件系统的临时位置,一旦写入操作完成,该文件将重命名为第一个参数的URL,替换以前存在的任何文件。 如果写入过程中应用程序崩溃,这也可以防止数据损坏。

值得注意的是,这种向文件系统写入数据的方法不是存档。 虽然可以存档 Data 实例,但是使用 write(to:options :) 方法是将 Data 中的字节直接复制到文件系统。

现在图片存储在文件系统中,ImageStore 将在需要时加载该图片。 UIImage 的构造器
init(contentsOfFile :) 将从给定 URL 的文件中读取图片。

ImageStore.swift 中,更新方法 image(forKey :),以便图片未被加载时, ImageStore 从文件系统加载。

func image(forKey key: String) -> UIImage? {
  return cache.object(forKey: key as NSString)

  if let existingImage = cache.object(forKey: key as NSString) {
    return existingImage
  }

  let url = imageURL(forKey: key)
  guard let imageFromDisk = UIImage(contentsOfFile: url.path) else {
    return nil
  }

  cache.setObject(imageFromDisk, forKey: key as NSString)
  return imageFromDisk
}

什么是 guard 语句? guard 语句是一个条件语句,就像一个 if 语句。 编译器只有在 guard 的条件为 true 时,才会继续执行 guard 语句。 这里,条件是 UIImage 初始化是否成功。 如果初始化失败,则执行 else 块,这样可以提早返回。 如果初始化成功,则在 guard 语句(这里是 imageFromDisk)中绑定的任何变量或常量都可以在 guard 语句之后使用。

上面的代码在功能上等同于以下代码:

if let imageFromDisk = UIImage(contentsOfFile: url.path) {
  cache.setObject(imageFromDisk, forKey: key)
  return imageFromDisk
}

return nil

虽然你可以做到这一点,但是如果你不是必须要使用 if 的话,guard 语句会显得更简洁,更重要的是能以更安全的方式来确保你退出。 使用 guard 也会将失败的语句与要检查的条件直接相关。 这使得代码更容易理解。

您可以将图片保存到磁盘并从磁盘中检索图片,因此您需要做的最后一件事是添加从磁盘中删除图片的功能。

ImageStore.swift 中,确保从存储中删除图片时,也从文件系统中删除了。 (输入代码时会看到一个错误,接下来我们再讨论一下)

func deleteImage(forKey key: String) {
  cache.removeObject(forKey: key as NSString)

  let url = imageURL(forKey: key)
  FileManager.default.removeItem(at: url)
}

我们来看看这个代码生成的错误信息,如图16.8所示。

图16.8 从磁盘上删除图片时出错

这个错误消息让你知道方法 removeItem(at :) 可能会失败,但是你没有处理失败的情况。 我们来解决这个问题。

错误处理

用一种方式来表明创建的方法可能会出错通常是很有用的。 在使用可选项时,您已经看到在本书中表示失败的一种方法。 当您不在乎故障原因时,可选提供了一种简单的方法来表示问题。 考虑从 String 创建 Int

let theMeaningOfLife = "42"
let numberFromString = Int(theMeaningOfLife)

Int 上的这个构造器需要一个 String 参数,并返回一个可选的 IntInt? )。

这是因为字符串可能无法被表示为 Int。 上面的代码将成功创建一个Int,但是下面的代码将不会:

let pi = "Apple Pie"
let numberFromString = Int(pi)

字符串 “Apple Pie” 不能表示为 Int,因此 numberFromString 将包含 nil。 一个可选值很好地代表这里的问题,因为你不在乎为什么它失败。 你只是想知道它是否成功。

当您需要知道某些失败的原因时,可选项将不会提供足够的信息。 想想从文件系统中删除图像——为什么会失败? 也许在指定的 URL 上没有文件,或 URL 格式不正确,或者您没有删除该文件的权限。 有很多原因可能会导致此方法失败,您可能希望以不同的方式处理每种情况。

Swift 提供了一个健壮的错误处理系统,被编译器支持,以确保您可以识别何时出现坏事。 当 Swift 编译器告诉您,从磁盘中删除文件时,您没有处理可能的错误,您已经看到了这一点。

如果一个方法可能会产生一个错误,那么它的方法声明需要使用 throws 关键字来表示。 以下是 removeItem(at :) 的方法定义:

func removeItem(at URL: URL) throws

throws 关键字表示此方法可能会引发错误。 (如果您熟悉在其他语言中抛出异常,注意 Swift 的错误处理与抛出异常不同。)通过使用此关键字,编译器可确保任何使用此方法的人都知道此方法可能会导致错误——甚至更多,重要的是,调用者会处理任何潜在的错误。 这就是为什么编译器会提示您尝试从磁盘中删除文件时你没有处理错误。

要调用可以抛出的方法,可以使用 do-catch语句。 在 do 块中,您可以使用 try 关键字来表明该调用可能会出错。

ImageStore.swift 中,使用 do-catch 语句更新 deleteImage(forKey :) 来调用 removeItem(at :)

func deleteImage(forKey key: String) {
  cache.removeObject(forKey: key as NSString)

  let url = imageURL(forKey: key)

  FileManager.default.removeItem(at: url)
  do {
    try FileManager.default.removeItem(at: url)
  } catch {

  }
}

如果方法真的抛出了错误,那么程序会立即退出 do 块; do 块中没有进一步的代码被执行。 在这一点上,错误被传递给 catch 块,以便以某种方式进行处理。

现在,更新 deleteImage(forKey :) 将错误打印到控制台。

func deleteImage(forKey key: String) {
  cache.removeObject(forKey: key as NSString)

  let url = imageURL(forKey: key)
  do {
    try FileManager.default.removeItem(at: url)
  } catch {
    print("Error removing the image from disk: \(error)")
  }
}

catch 块中,存在一个隐含的错误常量,其中包含描述错误的信息。 你可以选择给这个常数一个明确的名字。

再次更新 deleteImage(forKey :) 以使用被捕获的错误的显式名称。

func deleteImage(forKey key: String) {
  cache.removeObject(forKey: key as NSString)

  let url = imageURL(forKey: key)
  do {
    try FileManager.default.removeItem(at: url)
  } catch let deleteError {
    print("Error removing the image from disk: \( errordeleteError )")
  }
}

还有更多关于错误处理知识,你目前只要掌握这些就足够了。 随着本书的推进,我们将会介绍更多的细节。

ImageStore 完成后,构建并运行应用程序。 选择一个 Item 并拍摄照片,然后退出应用程序到主屏幕(在模拟器上,选择 HardwareHome或按 Shift-Command-H ;在硬件设备上,只需按 Home键)。 再次启动应用程序。 选择相同的 item 将显示所有保存的详细信息——包括您刚刚拍摄的照片。

请注意,拍摄后立即保存图片,只有当应用程序进入后台时,Item 实例才会保存。 您立即保存图片的话,会因为它们太大而不能长时间保存在内存中。

青铜挑战:PNG

并不是每个图像都会保存为 JPEG,试着将图像另存为 PNG

更多:应用程序状态转换

让我们写一些快速的代码来更好地了解不同的应用程序状态转换。

AppDelegate.swift 中,实现应用程序状态转换的委托方法,以便打印出方法的名称。 您将需要添加四个方法。(检查以确保在编写全新的模板之前,模板尚未创建这些方法。)不要在调用 print() 中对方法的名称进行硬编码,请使用 #function 表达式。 在编译时, #function 表达式将会被替换为表示方法名称的 String

func applicationWillResignActive(_ application: UIApplication) {
  print(#function)
}

func applicationDidEnterBackground(_ application: UIApplication) {
  print(#function)
  let success = itemStore.saveChanges()
  if success {
    print("Saved all of the Items")
  } else {
    print("Could not save any of the Items")
  }
}

func applicationWillEnterForeground(_ application: UIApplication) {
  print(#function)
}

func applicationDidBecomeActive(_ application: UIApplication) {
  print(#function)
}

func applicationWillTerminate(_ application: UIApplication) {
  print(#function)
}

最后,将相同的 print() 语句添加到 application(_:didFinishLaunchingWithOptions :) 的顶部。

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]?) -> Bool {
  print(#function)
  ...
}

构建并运行应用程序。 你会看到应用程序发送 application(_:didFinishLaunchingWithOptions :) 然后 是applicationDidBecomeActive(_ :)。 尝试各种动作,查看会导致什么过渡。

按下 Home 键,控制台将报告应用程序暂时失效,然后进入后台状态。 通过在主屏幕或任务切换器中点击其图标重新启动应用程序。 控制台将报告应用程序进入屏幕,然后变为活动状态。

按 Home键 再次退出应用程序。 然后,双击 Home键 打开任务切换器。 刷新 Homepwner 应用程序,关闭此显示屏以退出应用程序。 请注意,在这一点上,您的应用程序委托没有方法被调用——它只是被终止。

更多:读取和写入文件系统

除了存档和 Data 的二进制读写方法之外,还有一些将文件传输到文件系统的方法。 其中一个是 Core Data,将在第22章中出现。另外几个在这里提一提。

使用 Data 对二进制数据有效。 对于文本数据,String 有两种实例方法:write(to:atomically:encoding :)init(contentsOf:encoding :)。 它们的用法如下:

// Save someString to the filesystem
do {
  try someString.write(to: someURL, atomically: true,encoding: .utf8)
} catch {
  print("Error writing to URL: \(error)")
}

// Load someString from the filesystem
do {
  let myEssay = try String(contentsOf: someURL, encoding: .utf8)
  print(myEssay)
} catch {
  print("Error reading from URL: \(error)")
}

请注意,在许多语言中,任何意外的结果都可能会导致异常被抛出。 在 Swift 中,几乎总是使用异常来表示程序员的错误。 抛出异常时,出现错误的信息在 NSException 对象中。 该信息通常只是程序员的一个提示,就像“ 你尝试访问这个数组中的第七个对象,但只有两个”。调用栈的信息(抛出异常时出现)也在 NSException 中。

什么时候使用异常,什么时候使用错误处理? 如果您正在编写一个只能使用奇数作为参数调用的方法,如果使用偶数调用异常,则抛出异常——调用者发生错误,并希望帮助该程序员找到错误。 如果您正在编写一个想要读取特定目录的内容但没有必要权限的方法,请使用 Swift 的错误处理方法,并向调用者发出错误,以表明您无法完成这一非常合理的请求。

可序列化属性列表(Property list serializable) 类型也可以写入文件系统。 可序列化属性列表 的唯一类型是 StringNSNumber (包括原始数据类型,如 IntDoubleBool),DateDataArray <Element>Dictionary <Key:Hashable,Value>。 当使用这些方法将 Array <Element>Dictionary <Key,Value> 写入文件系统时,将创建一个 XML 属性列表。 XML属性列表是标记值的集合,如:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
  <dict>
    <key>firstName</key>
    <string>Christian</string>
    <key>lastName</key>
    <string>Keur</string>
  </dict>
  <dict>
    <key>firstName</key>
    <string>Aaron</string>
    <key>lastName</key>
    <string>Hillegass</string>
  </dict>
</array>
</plist>

XML属性列表是存储数据的便捷方式,因为它们几乎可以在任何系统上读取。 许多Web服务应用程序使用属性列表作为输入和输出。 写入和读取属性列表的代码如下所示:

let authors = [
  ["firstName":"Christian", "lastName":"Keur"],
  ["firstName":"Aaron", "lastName":"Hillegass"]
]

// Write array to disk
if PropertyListSerialization.propertyList(authors, isValidFor: .xml) {
  do {
    let data = try PropertyListSerialization.data(with: authors,format: .xml,options: [])
    data.write(to: url, options: [.atomic])
  } catch {
    print("Error writing plist: \(error)")
  }
}

// Read array from disk
do {
  let data = try Data(contentsOf: url, options: [])
  let authors = try NSPropertyListSerialization.propertyList(from: data, options: [], format: nil)
  print("Read in authors: \(authors)")
} catch {
  print("Error reading plist: \(error)")
}

更多:应用程序包

Xcode 中构建iOS应用程序项目时,您将创建一个 应用程序包(application bundle)。 应用程序包包含应用程序可执行文件和您与应用程序捆绑在一起的任何资源。 资源是故事板文件,图像和音频文件——在运行时使用的任何文件。 将资源文件添加到项目中时,Xcode 足够智能,可以将其与应用程序捆绑在一起。

您如何知道哪些文件与您的应用程序捆绑在一起? 从项目导航器中选择 Homepwner 项目。 查看 Homepwner 目标中的 Build Phases 窗格。 Copy Bundle Resources 下的所有内容将在构建时添加到应用程序包中。

Homepwner 目标组中的每一项都是构建项目时出现的其中一个阶段。 Copy Bundle Resources 阶段是将项目中的所有资源复制到应用程序包中的位置。

在模拟器上安装应用程序后,您可以在文件系统查看应用程序捆绑包。 将应用程序包路径打印到控制台,然后导航到该目录。

print(Bundle.main.bundlePath)

选中应用程序包并右击,然后从上下文菜单中选择 Show Package Contents(图16.9)。

图16.9 查看应用程序包

将出现一个 Finder 窗口,显示应用程序包的内容(图16.10)。 当用户从 App Store 下载您的应用程序时,这些文件将被复制到其设备。

图16.10 应用程序包

您可以在运行时从应用程序的捆绑包中加载文件。 要获取应用程序包中的文件的完整URL,您需要获取对应用程序包的引用,然后请求它获取资源的URL。

// Get a reference to the application bundle
let applicationBundle = Bundle.main

// Ask for the URL to a resource named myImage.png in the bundle
if let url = applicationBundle.url(forResource: "myImage", ofType: "png") {
  // Do something with URL
}

如果您请求的 URL 对应的文件不在应用程序捆绑包中,则此方法将返回为 nil。 如果文件确实存在,则返回完整的URL,您可以使用此URL将该文件加载到适当的类中。

请记住,应用程序包中的文件是只读的。 您不能修改它们,也不能在运行时动态地将文件添加到应用程序包。 应用程序包中的文件通常包括按钮图像,界面声音效果或与应用程序一起提供的数据库的初始状态。 您将在后面的章节中学习在运行时加载这些类型的资源。

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

推荐阅读更多精彩内容

  • 在本章中,您将要添加照片到 Homepwner 应用程序。 您将呈现一个 UIImagePickerControl...
    titvax阅读 610评论 0 1
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • 应用间通信 应用程式只能间接与设备上的其他应用进行通信。您可以使用AirDrop与其他应用程序共享文件和数据。您还...
    nicedayCoco阅读 735评论 0 1
  • 许多 iOS 应用程序向用户显示列表项,并允许用户选择,删除或重新排列列表项。 不管是显示用户地址簿中的人员列表的...
    titvax阅读 1,498评论 2 1
  • 岁月携了沧桑的肚兜 摸了你的脸 光彩不在 朦胧的眼神 胆颤的热泪 幸福的遐想 注定 写在生的渴望里 张望 温柔的棉...
    慕言言的言阅读 151评论 0 3