版本记录
版本号 | 时间 |
---|---|
V1.0 | 2018.10.25 星期四 |
前言
数据的持久化存储是移动端不可避免的一个问题,很多时候的业务逻辑都需要我们进行本地化存储解决和完成,我们可以采用很多持久化存储方案,比如说
plist
文件(属性列表)、preference
(偏好设置)、NSKeyedArchiver
(归档)、SQLite 3
、CoreData
,这里基本上我们都用过。这几种方案各有优缺点,其中,CoreData是苹果极力推荐我们使用的一种方式,我已经将它分离出去一个专题进行说明讲解。这个专题主要就是针对另外几种数据持久化存储方案而设立。
1. 数据持久化方案解析(一) —— 一个简单的基于SQLite持久化方案示例(一)
2. 数据持久化方案解析(二) —— 一个简单的基于SQLite持久化方案示例(二)
开始
首先看一下写作环境
Swift 4.2, iOS 12, Xcode 10
在iOS中有很多方法可以将数据保存到磁盘 - 原始文件API,Property List Serialization
,Core Data
,Realm
等第三方解决方案,当然还有NSCoding
。
对于需要大量数据的应用程序,Core Data
或Realm
通常是最佳选择。对于轻量数据要求,NSCoding
通常更好,因为它更容易采用和使用。
Swift 4还提供了另一种轻量级选择:Codable
。这与NSCoding
非常相似,但存在一些差异。
如果您需要一种简单的方法将数据保存到磁盘,NSCoding
和Codable
都是不错的选择!但是,它们都不支持查询或创建复杂的对象图。如果您需要这些,则应使用Core Data
,Realm
或其他数据库解决方案。
NSCoding
的主要缺点是需要你依赖Foundation。 Codable
不依赖于任何框架,但您必须在Swift中编写模型才能使用它。
Codable
还提供了NSCoding
没有的几个丰富功能。例如,您可以使用它轻松地将模型序列化为JSON。
许多Foundation
和UIKit
类都使用NSCoding
,因为它自iOS 2.0开始就存在 - 是的,十几年!大约一年前,在Swift 4中添加了Codable
,由于许多Apple类都是用Objective-C编写的,因此很可能很快就不会更新以支持Codable
。
无论您选择在应用程序中使用NSCoding
还是Codable
,最好了解两者是如何工作的。在本教程中,您将学习有关NSCoding
的所有知识!
你将开展一个名为“Scary Creatures”
的示例项目。这个应用程序可以让你保存生物的照片并评估它们的可怕程度。 但是,此时它不会保留数据,因此如果重新启动应用程序,则添加的所有生物都将丢失。 因此,该应用程序还不是很有用......
在本教程中,您将使用NSCoding
和FileManager
保存并加载每个生物的数据。 之后,您将熟悉NSSecureCoding
以及它可以做些什么来改善您的应用程序中的数据加载。
首先,在Xcode中打开入门项目并浏览项目中的文件:
您将使用的主要文件是:
-
ScaryCreatureData.swift
包含有关生物,名称和等级的简单数据。 -
ScaryCreatureDoc.swift
包含有关该生物的完整信息,包括数据,缩略图图像和该生物的完整图像。 -
MasterViewController.swift
显示所有存储生物的列表。 -
DetailViewController.swift
显示所选生物的细节,并允许您对其进行评分。
构建并运行以了解应用程序的工作方式。
Implementing NSCoding - 实现NSCoding
NSCoding
是一种协议,您可以在数据类上实现该协议,以支持将数据编码和解码到数据缓冲区中,然后数据缓冲区可以保留在磁盘上。
实现NSCoding
实际上非常容易 - 这就是为什么你会发现它有用的原因。
首先,打开ScaryCreatureData.swift
并将NSCoding
添加到类声明中,如下所示:
class ScaryCreatureData: NSObject, NSCoding
然后将这两个方法添加到类中:
func encode(with aCoder: NSCoder) {
//add code here
}
required convenience init?(coder aDecoder: NSCoder) {
//add code here
self.init(title: "", rating: 0)
}
您需要实现这两个方法以使类符合NSCoding
。 第一种方法编码对象。 第二个解码数据以实例化新对象。
简而言之,encode(with:)
是编码器,init(coder:)
是解码器。
注意:此处关键字
convenience
的存在不是NSCoding
的要求。 它就在那里,因为你在初始化程序中调用了一个指定的初始化程序init(title:rating :)
。 如果您尝试删除它,Xcode会给您一个错误。 如果您选择自动修复它,编辑器将再次添加相同的关键字。
在实现这两个方法之前,为了代码组织,在类的开头添加以下枚举:
enum Keys: String {
case title = "Title"
case rating = "Rating"
}
虽然看似非常微不足道,但它是值得的。 Codable
键使用String
数据类型。 如果没有编译器捕获你的错误,字符串很容易拼错。 通过使用枚举,编译器将确保您始终使用一致的键名称,Xcode将在您键入时为您提供代码完成。
现在,你准备好了有趣的部分。 添加以下内容进行encode(with:)
:
aCoder.encode(title, forKey: Keys.title.rawValue)
aCoder.encode(rating, forKey: Keys.rating.rawValue)
encode(_:forKey :)
将作为第一个参数提供的值写入并将其绑定到键。 提供的值必须是符合NSCoding
协议的类型。
将以下代码添加到init?(coder:)
的开头:
let title = aDecoder.decodeObject(forKey: Keys.title.rawValue) as! String
let rating = aDecoder.decodeFloat(forKey: Keys.rating.rawValue)
这恰恰相反。 您从指定键提供的NSCoder
对象中读取值。 由于您要保存两个值,因此您希望再次读取相同的两个值以正常恢复应用程序。
您现在需要使用解码的内容实际构建生物的数据。 替换此行:
self.init(title: "", rating: 0)
使用下面这行
self.init(title: title, rating: rating)
OK! 使用这几行代码,ScaryCreatureData
类符合NSCoding
。
Loading and Saving to Disk - 加载和存储到磁盘
接下来,您需要添加代码来访问磁盘,读取和写入存储的生物数据。
为了提高性能 - 在构建应用程序时始终牢记这一点 - 您不会立即加载所有数据。
1. Adding the Initializer - 添加初始化程序
打开ScaryCreatureDoc.swift
并将以下内容添加到类的末尾:
var docPath: URL?
init(docPath: URL) {
super.init()
self.docPath = docPath
}
docPath
将存储ScaryCreatureData
信息在磁盘上的位置。 这里的诀窍是你应该在第一次访问它时将信息加载到内存中,而不是初始化对象。
但是,如果要创建一个全新的生物,则此路径将为nil
,因为尚未为该文档创建文件。 您将在旁边添加Bookkeeping
代码,以确保在创建新生物时设置此代码。
2. Adding Bookkeeping Code - 添加簿记代码
将此枚举添加到ScaryCreatureDoc
的开头,紧跟在大括号后面:
enum Keys: String {
case dataFile = "Data.plist"
case thumbImageFile = "thumbImage.png"
case fullImageFile = "fullImage.png"
}
接下来,将thumbter
的getter
替换为:
get {
if _thumbImage != nil { return _thumbImage }
if docPath == nil { return nil }
let thumbImageURL = docPath!.appendingPathComponent(Keys.thumbImageFile.rawValue)
guard let imageData = try? Data(contentsOf: thumbImageURL) else { return nil }
_thumbImage = UIImage(data: imageData)
return _thumbImage
}
接下来,使用以下命令替换fullImage
的getter
:
get {
if _fullImage != nil { return _fullImage }
if docPath == nil { return nil }
let fullImageURL = docPath!.appendingPathComponent(Keys.fullImageFile.rawValue)
guard let imageData = try? Data(contentsOf: fullImageURL) else { return nil }
_fullImage = UIImage(data: imageData)
return _fullImage
}
由于您要将每个生物保存在自己的文件夹中,因此您将创建一个帮助程序类,以提供存储该生物文档的下一个可用文件夹。
创建一个名为ScaryCreatureDatabase.swift
的新Swift文件,并在文件末尾添加以下内容:
class ScaryCreatureDatabase: NSObject {
class func nextScaryCreatureDocPath() -> URL? {
return nil
}
}
你会在一段时间内为这个新类添加更多内容。 现在,返回ScaryCreatureDoc.swift
并将以下内容添加到类的末尾:
func createDataPath() throws {
guard docPath == nil else { return }
docPath = ScaryCreatureDatabase.nextScaryCreatureDocPath()
try FileManager.default.createDirectory(at: docPath!,
withIntermediateDirectories: true,
attributes: nil)
}
createDataPath()
正如其名称所说的那样。 它使用数据库中的下一个可用路径填充docPath
属性,并且仅当docPath
为nil
时才创建该文件夹。 如果不是,这意味着它已经正确发生。
3. Saving Data - 保存数据
接下来,您将添加逻辑以将ScaryCreateData
保存到磁盘。 在createDataPath()
的定义之后添加此代码:
func saveData() {
// 1
guard let data = data else { return }
// 2
do {
try createDataPath()
} catch {
print("Couldn't create save folder. " + error.localizedDescription)
return
}
// 3
let dataURL = docPath!.appendingPathComponent(Keys.dataFile.rawValue)
// 4
let codedData = try! NSKeyedArchiver.archivedData(withRootObject: data,
requiringSecureCoding: false)
// 5
do {
try codedData.write(to: dataURL)
} catch {
print("Couldn't write to save file: " + error.localizedDescription)
}
}
这是这样做的:
- 1) 确保
data
中存在某些内容,否则只需return
,因为没有任何内容可以保存。 - 2) 调用
createDataPath()
以准备保存创建的文件夹中的数据。 - 3) 构建要写入信息的文件的路径。
- 4) 编码
data
,ScaryCreatureData
的一个实例,您之前使其符合NSCoding
。 您现在将requiresSecureCoding
设置为false
,但稍后您将进行此操作。 - 5) 将编码数据写入在步骤3中创建的文件路径。
接下来,将此行添加到init(title:rating:thumbImage:fullImage:)
的末尾:
saveData()
这可确保在创建新实例后保存数据。
很好! 这样可以保存数据。 好吧,该应用程序仍然不会实际保存图像,但您将在本教程后面添加它。
4. Loading Data - 加载数据中
如上所述,我们的想法是在您第一次访问它时将信息加载到内存中,而不是在初始化对象时加载。 如果你有很长的生物列表,这可以改善应用程序的加载时间。
注意:
ScaryCreatureDoc
中的属性都是通过带有getter
和setter
的私有属性访问的。 初学者项目本身并没有从中受益,但它已经添加,以便您继续执行后续步骤。
打开ScaryCreatureDoc.swift
并使用以下内容替换data
的getter:
get {
// 1
if _data != nil { return _data }
// 2
let dataURL = docPath!.appendingPathComponent(Keys.dataFile.rawValue)
guard let codedData = try? Data(contentsOf: dataURL) else { return nil }
// 3
_data = try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(codedData) as?
ScaryCreatureData
return _data
}
这就是加载先前通过调用saveData()
创建的已保存ScaryCreatureData
所需的全部内容。 这是它的作用:
- 1) 如果数据已经加载到内存中,只需返回它。
- 2) 否则,请将已保存文件的内容作为一种
Data
读取。 - 3)
Unarchive
先前编码的ScaryCreatureData
对象的内容并开始使用它们。
您现在可以从磁盘保存和加载数据! 但是,在应用程序准备发布之前还有更多内容。
5. Deleting Data - 删除数据
应用程序还应允许用户删除生物,也许留下来太可怕了。
在saveData()
的定义之后立即添加以下代码:
func deleteDoc() {
if let docPath = docPath {
do {
try FileManager.default.removeItem(at: docPath)
}catch {
print("Error Deleting Folder. " + error.localizedDescription)
}
}
}
此方法只删除包含文件的整个文件夹,其中包含生物数据。
Completing ScaryCreatureDatabase - 完成ScaryCreatureDatabase
您之前创建的ScaryCreatureDatabase
类有两个作业。 第一个,你已经写了一个空方法,是提供下一个可用的路径来创建一个新的生物文件夹。 它的第二项工作是加载你之前保存的所有存储的生物。
在实现这两个功能之一之前,您需要一个帮助方法来返回应用程序存储生物的位置 - 数据库实际位于何处。
打开ScaryCreatureDatabase.swift
,并在开始类花括号后面添加此代码:
static let privateDocsDir: URL = {
// 1
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
// 2
let documentsDirectoryURL = paths.first!.appendingPathComponent("PrivateDocuments")
// 3
do {
try FileManager.default.createDirectory(at: documentsDirectoryURL,
withIntermediateDirectories: true,
attributes: nil)
} catch {
print("Couldn't create directory")
}
return documentsDirectoryURL
}()
这是一个非常方便的变量,它存储数据库文件夹路径的计算值,您在此处将其命名为“PrivateDocuments”。以下是它的工作原理:
- 1) 获取应用程序的
Documents
文件夹,这是所有应用程序都具有的标准文件夹。 - 2) 构建指向包含所有内容的数据库文件夹的路径。
- 3) 如果文件夹不存在则创建该文件夹并返回路径。
您现在已准备好实现上述两个函数。 您将从保存的文档加载数据库开始。 将以下代码添加到类的底部:
class func loadScaryCreatureDocs() -> [ScaryCreatureDoc] {
// 1
guard let files = try? FileManager.default.contentsOfDirectory(
at: privateDocsDir,
includingPropertiesForKeys: nil,
options: .skipsHiddenFiles) else { return [] }
return files
.filter { $0.pathExtension == "scarycreature" } // 2
.map { ScaryCreatureDoc(docPath: $0) } // 3
}
这会加载存储在磁盘上的所有.scarycreature
文件,并返回一个ScaryCreatureDoc
项目数组。 在这里,你这样做:
- 1) 获取数据库文件夹的所有内容。
- 2) 过滤列表仅包含以.scarycreature结尾的项目。
- 3) 从筛选的列表中加载数据库并将其返回。
接下来,您要正确返回用于存储新文档的下一个可用路径。 用这个替换nextScaryCreatureDocPath()
的实现:
// 1
guard let files = try? FileManager.default.contentsOfDirectory(
at: privateDocsDir,
includingPropertiesForKeys: nil,
options: .skipsHiddenFiles) else { return nil }
var maxNumber = 0
// 2
files.forEach {
if $0.pathExtension == "scarycreature" {
let fileName = $0.deletingPathExtension().lastPathComponent
maxNumber = max(maxNumber, Int(fileName) ?? 0)
}
}
// 3
return privateDocsDir.appendingPathComponent(
"\(maxNumber + 1).scarycreature",
isDirectory: true)
与之前的方法类似,您获取数据库的所有内容,过滤它们,附加到privateDocsDir
并返回它。
跟踪磁盘上所有项目的简单方法是按编号命名文件夹;通过查找名为最高编号的文件夹,您可以轻松地提供下一个可用路径。
注意:使用数字只是为基于文档的数据库命名和跟踪文件夹的一种方法。 只要每个文件夹都有一个唯一的名称,您就可以选择其他方式,这样就不会意外地用新的文件替换现有的项目。
好的 - 你差不多完成了! 是时候尝试了。
Trying It Out! - 试一试!
在运行应用程序之前,在privateDocsDir
的类属性定义的末尾return
之前添加此行:
print(documentsDirectoryURL.absoluteString)
当应用程序在模拟器中运行时,这将帮助您准确了解计算机上包含文档的文件夹的位置。
现在,运行应用程序。 从控制台复制值,但跳过“file://”
部分。 路径应以“/ Users”
开头,以“/ PrivateDocuments”
结尾。
打开Finder应用程序。 从菜单导航,Go ▸ Go to Folder
并在对话框中粘贴路径:
打开文件夹时,其内容应如下所示:
您在此处看到的项目是由MasterViewController.loadCreatures()
创建的,这是在初学者项目中为您实现的。 每次运行应用程序时,它都会在磁盘上添加更多文档......这实际上并不正确! 发生这种情况是因为在加载应用程序时您没有从磁盘读取数据库的内容。 你马上解决这个问题,但首先,你需要实现更多的东西。
如果用户在表视图上触发删除,则还需要从数据库中删除该生物。 在同一个文件中,用以下代码替换tableView(_:commit:forRowAt :)
的实现:
if editingStyle == .delete {
let creatureToDelete = creatures.remove(at: indexPath.row)
creatureToDelete.deleteDoc()
tableView.deleteRows(at: [indexPath], with: .fade)
}
最后一件事你需要考虑:你完成了添加和删除功能,但是编辑呢? 别担心......它就像实现删除一样简单。
打开DetailViewController.swift
并在rateViewRatingDidChange(rateView:newRating :)
和titleFieldTextChanged(_ :)
的末尾添加以下行:
detailItem?.saveData()
这只是告诉ScaryCreatureDoc
对象在用户界面中更改其信息时保存自己。
Saving and Loading Images - 保存和加载图像
该生物应用程序剩下的最后一件事是保存和加载图像。 您不会将它们保存在列表文件本身中;将它们作为普通图像文件保存在其他存储数据旁边会更方便,所以现在你要编写代码。
在ScaryCreatureDoc.swift
中,在类的末尾添加以下代码:
func saveImages() {
// 1
if _fullImage == nil || _thumbImage == nil { return }
// 2
do {
try createDataPath()
} catch {
print("Couldn't create save Folder. " + error.localizedDescription)
return
}
// 3
let thumbImageURL = docPath!.appendingPathComponent(Keys.thumbImageFile.rawValue)
let fullImageURL = docPath!.appendingPathComponent(Keys.fullImageFile.rawValue)
// 4
let thumbImageData = _thumbImage!.pngData()
let fullImageData = _fullImage!.pngData()
// 5
try! thumbImageData!.write(to: thumbImageURL)
try! fullImageData!.write(to: fullImageURL)
}
这有点类似于之前在saveData()
中编写的内容:
- 1) 确保存储图像;否则,没有必要继续执行。
- 2) 如果需要,创建数据路径。
- 3) 构建指向磁盘上每个文件的路径。
- 4) 将每个图像转换为PNG数据表示,以便您在磁盘上写入。
- 5) 将生成的数据写入磁盘的各自路径中。
项目中有两点要调用saveImages()
。
第一个是初始化程序init(title:rating:thumbImage:fullImage:)
。 打开ScaryCreatureDoc.swift
,并在此初始化程序结束时,在saveData()
之后,添加以下行:
saveImages()
第二点是在DetailViewController.swift
里面的imagePickerController(_:didFinishPickingMediaWithInfo:)
。 您将找到一个调度闭包,您可以在其中更新detailItem
中的图像。 将此行添加到闭包的末尾:
self.detailItem?.saveImages()
现在,您可以保存,更新和删除生物。 该应用程序已准备好保存您将来可能遇到的所有恐怖和非恐怖的生物。
如果你现在要构建并运行并从磁盘恢复可怕的生物,你会发现有些人拥有图像而其他人没有,就像这样:
使用Xcode调试控制台中打印的路径,查找并删除PrivateDocuments
文件夹。 现在构建并运行一次。 你会看到他们的图像的初始生物:
当你保存你的生物时,你无法看到你已经保存了什么。 打开MasterViewController.swift
并将loadCreatures()
的实现替换为:
creatures = ScaryCreatureDatabase.loadScaryCreatureDocs()
这会从磁盘加载生物而不是使用预先填充的列表。
构建并再次运行。 尝试更改标题和评级。 当您返回主屏幕时,应用程序会将更改保存到磁盘。
Implementing NSSecureCoding - 实现NSSecureCoding
在iOS 6中,Apple推出了一些基于NSCoding
的新功能。 您可能已经注意到,您解码了存档中的值,以将它们存储在如下行的变量中:
let title = aDecoder.decodeObject(forKey: Keys.title.rawValue) as! String
读取值时,它已经加载到内存中,然后将其转换为您应该知道的数据类型。 如果出现问题并且先前写入的对象的类型无法转换为所需的数据类型,则该对象将完全加载到内存中,然后转换尝试将失败。
诀窍是行动的顺序;虽然应用程序根本不会使用该对象,但该对象已经完全加载到内存中,然后在失败的转换后释放。
NSSecureCoding
提供了一种加载数据的方法,同时在解码时验证其类,而不是之后。 最好的部分是它非常容易实现。
首先,在ScaryCreatureData.swift
中,使类实现协议NSSecureCoding
,以便类声明如下所示:
class ScaryCreatureData: NSObject, NSCoding, NSSecureCoding
然后在类的末尾添加以下代码:
static var supportsSecureCoding: Bool {
return true
}
这就是您遵守NSSecureCoding
所需的全部内容,但您尚未从中获益。
用以下代码替换encode(with :)
实现:
aCoder.encode(title as NSString, forKey: Keys.title.rawValue)
aCoder.encode(NSNumber(value: rating), forKey: Keys.rating.rawValue)
现在,用这个替换init?(coder :)
的实现:
let title = aDecoder.decodeObject(of: NSString.self, forKey: Keys.title.rawValue)
as String? ?? ""
let rating = aDecoder.decodeObject(of: NSNumber.self, forKey: Keys.rating.rawValue)
self.init(title: title, rating: rating?.floatValue ?? 0)
如果查看新的初始化代码,您会注意到这个decodeObject(of:forKey :)
与decodeObject(forKey :)
不同,因为它所采用的第一个参数是一个类。
不幸的是,使用NSSecureCoding
要求你在Objective-C
中使用string和float对应对象;这就是使用NSString
和NSNumber
的原因,然后将值转换回Swift String
和Float
。
最后一步是告诉NSKeyedArchiver
使用安全编码。 在ScaryCreatureDoc.swift
中,更改saveData()
中的以下行:
let codedData = try! NSKeyedArchiver.archivedData(withRootObject: data,
requiringSecureCoding: false)
替换成下面
let codedData = try! NSKeyedArchiver.archivedData(withRootObject: data,
requiringSecureCoding: true)
在这里,您只需将true
传递给requiresSecureCoding
而不是false
。 这告诉NSKeyedArchiver
在归档对象及其后代时强制执行NSSecureCoding
。
注意:以前使用
NSSecureCoding
编写的文件现在不兼容。 您需要删除以前保存的所有数据或从模拟器中卸载应用程序。 在实际场景中,您必须迁移旧数据。
后记
本篇主要讲述了基于NSCoding的持久化存储,感兴趣的给个赞或者关注~~~