前言:作为一名swift初学者,我希望能通过写文章的方式来更好地帮助自己学习,更希望能得到你的建议和批评。
如有纰漏之处,还望不吝赐教。
一.简介
在使用一些不需要服务端支持的App时,我发现部分App通过使用iCloud文档存储功能来满足应用数据云存储的需求,如饥荒的游戏数据存储、素记的日记存储等,用户可以在自己iCloud账号下的任何设备访问或修改App的这部分数据,十分方便。
二.基本概念
在iOS iCloud存储中,苹果提供了三个功能,分别是:
- Key-value storage
- iCloud Documents
- CloudKit
1.Key-value storage
顾名思义,这是一个类似于iOS里NSUserDefaults的通过键值对来保存简单数据的功能,这种Property-list数据格式适合存储一些非关键数据,如用户配置。
2.iCloud Documents
这个功能提供了文件及目录的数据类型,这个特性决定了此功能相较于Key-value storage更适合进行关键数据的存储。
3.CloudKit
相较于前两个,CloudKit就要复杂得多,这是苹果为开发者提供的一整套数据库工具,类似于Maxleap这类第三方云服务。开发者通过苹果提供的Cloud dashboard网站可以配置所需的表结构,并通过在代码中导入CloudKit进行数据库操作。
可以看出,这三个数据库工具是苹果为了满足不同层次的数据云存储场景而设置的,所以在使用前要根据自己的需要来选择相对应的工具。对我而言,iCloud Documents更能满足我的需求,下面将开始iCloud Documents的学习。
三.iCloud Documents的深入学习
简单说来,iCloud Documents只需要两个类就可以实现其功能:
1.类名:NSMetadataQuery
功能:定位数据,也就是查询文件列表。
2.类名:UIDocument
功能:文件操作,对某一个文件进行操作,包括新增文件。
四.iCloud Documents功能的简单实现
1.准备工作
1.1新建项目
1.2开启iCloud功能
如图:
这里需要注意的是如果在Capabilities里没有找到这个选项的话,你需要的是一个付费的苹果开发者账号。
而配置里的Containers就像iOS的沙盒目录一样,将app内不同的文件目录分开,如果你需要多个容器的话请自行配置,这里我选择默认容器。
1.3在你的iOS设备上登录iCloud账号
1.4创建一个简单的列表视图,增加输入、保存、修改、删除这四个按钮
2.检测 iCloud 可用性
使用 iCloud Documents 之前,需要检测当前设备是否开启了 iCloud 功能。
这里通过获取iCloudDocuments路径来进行可用性判断:
func iCloudDocumentURL() -> URL? {
let fileManager = FileManager.default
if let url = fileManager.url(forUbiquityContainerIdentifier: nil) {
return url.appendingPathComponent("Documents")
}
return nil
}
3.查询文件列表
确认iCloud可用之后,我们查询Documents目录下的子目录。这一步,通过NSMetadataQuery这个类来实现:
private var query : NSMetadataQuery = NSMetadataQuery()
func loadDocuments()->Bool{
let baseURL = self.iCloudDocumentURL()
guard baseURL != nil else {
return false
}
let center = NotificationCenter.default
query.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
query.predicate = NSPredicate(value: true)
center.addObserver(self, selector: #selector(metadataQueryDidFinishGathering), name: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: nil)
self.query.enableUpdates()
query.start()
return true
}
searchScopes这个属性用来设置所需查询的目录,NSMetadataQueryUbiquitousDocumentsScope为子目录,也就是Documents下的目录或文件。predicate可根据自己的需求设置查询条件。
查询的结果通过在通知中心注册观察者来监听,注册后就可以开始查询。
监听到查询结束的通知后,建议关闭查询操作。
这样,我们就得到了Documents目录下的所有子文件,子文件以NSMetadataItem的实例存在query.results数组里。接下来,通过对NSMetadataItem实例调用value(forAttribute: NSMetadataItemURLKey)这样的方法来获取子文件的URL、文件名、修改时间等信息。代码如下:
func metadataQueryDidFinishGathering() {
query.disableUpdates()
query.stop()
let center = NotificationCenter.default
center.removeObserver(self)
var diaryList = Array<Any>()
if (query.resultCount == 1) {
let item = query.results.first as! NSMetadataItem
let fileURL = item.value(forAttribute: NSMetadataItemURLKey) as! URL
let document = XDocument(fileURL: fileURL )
document.open(completionHandler: { (success) in
for dic in document.diaries{
let diary = Diary(dic: dic)
diaryList.append(diary)
}
self.delegate?.queryDocumentsComplete(results: diaryList)
document.close(completionHandler: nil)
})
}else{
self.delegate?.queryDocumentsComplete(results: diaryList)
}
}
示例代码中仅在Documents目录下保存了一个文件,所以处理查询结果时也只是操作这一个文件,你可以根据自己的需求对results遍历及操作。在上述代码中可以看到,获取到子文件的路径后,我是通过继承自UIDocument的XDocument这个类进行文件读取的,具体原因在下面详述。
4.实现UIDocument的方法进行文件操作
iCloud 的官方文档中强制要求使用者对文件的操作通过 NSFileCoordinator 和 NSFilePresenter 来进行的,而UIDocument就是对这两个类的封装,由于iCloud的文件可以在用户的不同设备进行操作,所以为了读写安全,我们需要使用UIDocument对文件进行操作。
需要注意的是,UIDocument不能直接使用,我们需要自己实现对文件内容的操作逻辑,即继承UIDocument并实现contents(forType typeName: String)和load(fromContents contents: Any, ofType typeName: String?) 这两个方法。这两个回调方法前者是用来自定义所需保存的数据,后者是用来在我们将获取的数据解析后保存起来。代码如下:
let kArchiveKey = "Diary"
import UIKit
class XDocument: UIDocument {
var diaries : Array<Dictionary<String, Any>>!
override func contents(forType typeName: String) throws -> Any {
let data = NSMutableData.init()
let archiver:NSKeyedArchiver = NSKeyedArchiver.init(forWritingWith: data)
archiver.encode(self.diaries, forKey: kArchiveKey)
archiver.finishEncoding()
return data
}
override func load(fromContents contents: Any, ofType typeName: String?) throws {
let unarchiver:NSKeyedUnarchiver = NSKeyedUnarchiver.init(forReadingWith: contents as! Data)
self.diaries = unarchiver.decodeObject(forKey: kArchiveKey) as! Array
unarchiver.finishDecoding()
}
}
代码中,我将Diary这个类转成Dictionary后所保存在的数据列表用NSKeyedArchiver转成Data保存起来。
这样我们可以使用XDocument这个类对我们的数据进行保存及修改,记得在读写操作前分别调用UIDocument的open及close方法。
新建文件或者修改文件都是通过save(to url: URL, for saveOperation: UIDocumentSaveOperation, completionHandler: ((Bool) -> Swift.Void)? = nil)这个方法进行的,区别在于UIDocumentSaveOperation这个枚举,
一目了然。
具体代码如下:
func save(diaries:Array<Diary>){
let baseURL = self.iCloudDocumentURL()
if baseURL != nil{
var dataList = Array<Dictionary<String, Any>>()
for diary in diaries {
let dic = diary.convertToDictionary()
dataList.append(dic)
}
var url = self.iCloudDocumentURL()
url = url?.appendingPathComponent("saveData")
let document = XDocument(fileURL: url!)
document.open { (success) in
if(success){
document.diaries = dataList
document.save(to: url!, for: .forOverwriting) { (success) in
if success{
print("Overwrit success")
document.close(completionHandler: nil)
}
}
}else{
document.diaries = dataList
document.save(to: url!, for: .forCreating) { (success) in
if success{
print("Creat success")
document.close(completionHandler: nil
}
}
}
}
}
}
这样,我们就简单的实现了iCloud documents的数据存储。
如果demo中有任何问题或疑问,请告诉我,谢谢。