用 SwiftUI 构建文档类 App
Build document-based apps in SwiftUI
2020年6月25日
一句话判断
SwiftUI 的 DocumentGroup 把文档类 App 的搭建成本从”数周的 UIDocument 配置”降到了几行声明——如果你在做编辑器、笔记或绘图 App,这是 iOS 14 最实用的新 API 之一。
这场 Session 讲了什么
文档类 App(如文本编辑器、绘图工具、代码编辑器)需要处理文件打开、保存、多窗口管理、文档状态恢复等复杂逻辑。过去用 UIDocument + UIDocumentBrowserViewController 实现这些功能需要大量样板代码。
SwiftUI 引入了 DocumentGroup Scene,一行代码就能获得完整的文档管理能力:文件浏览器、文档打开/创建、自动保存、iPadOS 多窗口支持、以及系统级的文档状态恢复。
Session 详细讲解了 FileDocument 协议的实现——你需要定义文档的数据类型、支持的文件格式(通过 UTType)、以及读写方法。SwiftUI 自动处理其余部分。
值得深挖的点
FileDocument 的读写分离设计。 FileDocument 协议要求你实现 init(configuration:) 从文件读取数据,以及 fileWrapper(configuration:) 将数据写入文件。读写操作在后台线程执行,不会阻塞 UI。这种设计与 UIDocument 的 contents(forType:) / load(fromContents:) 类似,但声明更简洁。
UTType 与文件关联。 DocumentGroup 需要你指定支持的文件类型(通过 UTType)。系统会自动将你的 App 注册为该文件类型的处理程序,用户在 Files App 或其他 App 中打开该类型文件时会自动启动你的 App。
代码片段
import SwiftUI
import UniformTypeIdentifiers
// 1. 定义文档模型
struct TextDocument: FileDocument {
var text: String
// 支持的文件类型
static var readableContentTypes: [UTType] {
[.plainText, .utf8PlainText]
}
// 从文件读取
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents,
let string = String(data: data, encoding: .utf8) else {
throw CocoaError(.fileReadCorruptFile)
}
self.text = string
}
// 写入文件
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let data = text.data(using: .utf8)!
return FileWrapper(regularFileWithContents: data)
}
init(text: String = "") {
self.text = text
}
}
// 2. 创建基于文档的 App
@main
struct TextEditorApp: App {
var body: some Scene {
DocumentGroup(newDocument: TextDocument()) { file in
// file.$document 是文档的 Binding
TextEditor(text: file.$document.text)
.font(.body)
}
}
}
// 3. 更复杂的文档 App——支持多种文件格式
struct SketchDocument: FileDocument {
var strokes: [Stroke]
var canvasSize: CGSize
static var readableContentTypes: [UTType] {
// 支持自定义文件类型
[UTType(filenameExtension: "sketch")!]
}
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents else {
throw CocoaError(.fileReadCorruptFile)
}
// 使用 Codable 解码
self = try JSONDecoder().decode(Self.self, from: data)
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let data = try JSONEncoder().encode(self)
return FileWrapper(regularFileWithContents: data)
}
init() {
self.strokes = []
self.canvasSize = CGSize(width: 800, height: 600)
}
}
最佳实践
- 让文档模型实现 Codable。 配合
FileDocument协议,Codable 让读写逻辑只需要几行代码。 - 在
readableContentTypes中列出所有支持的格式。 包括你的自定义格式和可能需要导入的标准格式(如 .txt、.json)。 - 文档数据应该是值类型(struct)。 SwiftUI 的数据流基于值类型,struct 文档可以正确触发视图更新。
- 利用
$documentBinding。DocumentGroup的闭包参数提供文档的 Binding,直接绑定到 SwiftUI 控件即可实现自动保存。
还有什么值得关注
- DocumentGroup 自动在 iPadOS 上支持多窗口,每个窗口打开一个文档
- 系统自动处理文档的脏标记(dirty flag)和自动保存
- macOS 上的 DocumentGroup 自动集成到 Window 菜单
- 文档 App 的状态恢复由系统自动处理,不需要手动保存窗口状态