使用 UIDocument 管理 APP 的文档存取 | 咖啡时间

swift 发布于 2024年04月17日
关于 UIDocument

对于普通的文件操作,我们最常用到的应该就是 NSFileManager 类了,这个类提供了大部分文件操作用到的特性。比如我们要创建一个文件,可以这样:

let docPath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true).first

let filePath = docPath?.stringByAppendingString("/file.txt")

let content = "text file"
let fileData:NSData = content.dataUsingEncoding(NSUTF8StringEncoding)!
NSFileManager.defaultManager().createFileAtPath(filePath!, contents: fileData, attributes: nil)

类似这样的操作我们都可以通过 NSFileManager 来完成。不过 NSFileManager 是面向文件的操作。如果我们要开发一个笔记应用,我们更多的精力会放到处理文档而不是文件

比如,使用 NSFileManager 进行操作,如果我们的文档内容比较大,就需要将文件的读写放到异步线程中进行。而这些操作,都需要我们手工的进行处理。更不用说关于文档的自动保存,以及资源同步等这些问题的处理上。

NSFileManager 对于文档操作的逻辑,还是偏底层。所以苹果为我们提供了一个更好的抽象 UIDocument,它将这些文档操作的同步问题,以及异步性能问题都帮我们处理好了。这样,我们只要专注于文档相关的操作就行了。 我们再来看一下,使用 UIDocument 如何进行文档相关的操作。

首先,我们先要定义 UIDocument 的子类,然后实现 loadFromContents 方法:

class MyDocument: UIDocument {

override func loadFromContents(contents: AnyObject, ofType typeName: String?) throws {

guard let fileData = contents as? NSData else {
return
}

if let string = NSString(data: fileData, encoding: NSUTF8StringEncoding) {

print(string)

}


}

}

loadFromContents 是必须要实现的,因为我们后面的读取操作都依赖于这个方法的实现。接下来,我们可以使用我们定义的 MyDocument 类进行文档的读取了:

if let docURL:NSURL = NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.DocumentDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask).first {

let fileURL = docURL.URLByAppendingPathComponent("file.txt")
let document = MyDocument(fileURL: fileURL)

document.openWithCompletionHandler { success in

print("open finished")

}

}

大家看到了吧, UIDocument 的构造方法,使用的是 URL 机制,我们首先使用

NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.DocumentDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask)

这个方法,回去了文档目录的 URL, 然后再对 URL 进行一些拼接处理:

let fileURL = docURL.URLByAppendingPathComponent("file.txt")

大家可能会感到奇怪,为什么不直接用路径,而要用 URL 呢。因为 UIDocument 设计的初衷是专为文档操作而设计的。一个文档,它有可能是存储在咱们 APP 的本地路径,也有可能存放在某个云存储系统中的文档,比如存放在 iCloud 中的文档。

从这里也可以看出, UIDocument 对文档操作的抽象做的非常的完善。

最后我们构建 MyDocument 对象(这个类继承自 UIDocument), 然后调用它的 openWithCompletionHandler 方法,即可将文件打开了:

let document = MyDocument(fileURL: fileURL)

document.openWithCompletionHandler { success in

print("open finished")

}

openWithCompletionHandler 方法是一个异步执行的方法,它接受一个闭包作为参数。当它完成文档的读取后,会调用我们定义在的 MyDocument 类中的 loadFromContents 方法,还会调用传入它的闭包。这样我们就可以对读取的内容进行处理了。简单的话可以将它存储为类的属性,以便后面使用:

override func loadFromContents(contents: AnyObject, ofType typeName: String?) throws {

guard let fileData = contents as? NSData else {
return
}

if let string = NSString(data: fileData, encoding: NSUTF8StringEncoding) {

self.document = string //存储为该类的属性

}


}
为何使用 UIDocument

看到这里,大家可能会想了,使用 UIDocument 好像反而复杂了。我以前直接读取文件内容几行代码就搞定了。用了这个还需要自己定义一个类,而且写的代码也变多了,那么它有什么好处呢?

确实是这样,UIDocument 并不适用于所有的情况,如果你只是操作一些小文件,确实用诸如这样的读取操作就可以很简单的完成了:

let content = NSString(contentsOfFile: filePath, encoding: NSUTF8StringEncoding)

UIDocument 更加适用于文档类的操作需求。比如你准备开发一个记事本或者字处理 APP,这类的 APP 操作的文档相对就比较复杂了。比如你操作的文档可能会有几兆大小,这时候你就需要异步线程来读取文档,否则界面就会产生卡顿。还有,如果你的文档对一致性要求特别高,比如你在开发一个 HTML 文档预览的 APP,你要时刻确保文档的格式,并且及时的对文档进行保存,以防止文件丢失。

UIDocument 所适应的就是上述这种情形。假如我们要进行异步的文档读取,如果使用一般的方法,那么至少也需要用 GCD 来调用一下:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {

let content = NSString(contentsOfFile: filePath, encoding: NSUTF8StringEncoding)

dispatch_async(dispatch_get_main_queue()) {

self.textView = content

}

}

这还只是最简单的纯文本读取情况,如果你还需要处理文件内容格式的处理,就会更加复杂了。比如你读取的是一个 PDF 文档或者某些具有二进制格式的文档,那么你还要处理文档的解析操作。再比如,你的文档处理文档云存储,你也需要进行额外的工作。

UIDocument 的出现,就是为了帮助大家更快捷的处理这种问题。相比上面的,我们要手工调用多线程操作来说,这种形式是不是就显得简单多了:

document.openWithCompletionHandler { success in

print("open finished")

}

所有的异步操作处理,都由 UIDocument 来完成,并且还会返回给我们是否读取成功的状态。我们要做的,只是专注的处理文档操作相关的逻辑就行了。

进一步了解 UIDocument

了解完 UIDocument 读取文件内容的方法,现在我们来看一下如何将编辑后的内容保存起来,首先我们还是需要实现一个 UIDocument 的方法 contentsForType

class MyDocument: UIDocument {

override func contentsForType(typeName: String) throws -> AnyObject {

return self.content.dataUsingEncoding(NSUTF8StringEncoding)!

}

}

contentsForType 的实现也很简单,将我们文档的内容转换成 NSData 类型,然后返回。NSDocument 就会根据这个返回值,将文件存储到相应的位置,那么咱们回调函数实现完了,接下来就可以调用 UIDocument 的保存方法了:

document.saveToURL(document.fileURL, forSaveOperation: UIDocumentSaveOperation.ForOverwriting){ success in

print("save finished")

}

保存的调用也很简单,我们使用 saveToURL 方法,传入要保存的 URL 位置,以及保存选项 UIDocumentSaveOperation.ForOverwriting。 顾名思义,这个保存操作会覆盖之前的文件。同样的,如果是新建文件的话,还可以使用 UIDocumentSaveOperation.ForCreating 选项。这个保存方法仍然接受一个闭包参数。当保存方法完成后,也会调用这个闭包,来表示保存操作是否成功。

使用 UIDocument 来操作文档,就像是预先定义好一系列规则,比如如何读取文件内容,如何保存文件内容。我们继承自 UIDocument 的子类,就像是这个规则定义的主体。定义好规则后,我们就可以直接调用 UIDocument 的文档操作方法来操作文档了。

结语

UIDocument 的这种将文档读取保存的实现与文档物理操作相互分离的方式能让我们更加专注于文档处理本身。我们不必费心去处理异步读取,文档一致性之类的问题。我们只需要按照 UIDocument 规定的标准,将各个操作实现好,即可完成文档处理的整体逻辑。这样即便我们在以后需要对文档处理的细节做修改,也能做到最小的影响其他代码。

UIDocument 更加适合文档操作类的 APP,你处理的文档相对的复杂一些,比如记事本,或者类似 Pages 这样的字处理 APP,这样会发挥出 UIDocument 真正的作用。如果你只是简单的保存一些非常小的文本文件,那么就不太适合使用 UIDocument,直接用简单的文件操作即可。


如果你觉得这篇文章有帮助,还可以关注微信公众号 swift-cafe,会有更多我的原创内容分享给你~

本站文章均为原创内容,如需转载请注明出处,谢谢。
关注微信公众号
发现更多精彩
swift-cafe