Build document-based apps in SwiftUI
Swift & UI 进阶 24m

用 SwiftUI 构建文档类 App

Build document-based apps in SwiftUI

2020年6月25日

在 Apple 官方观看视频

一句话判断

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 文档可以正确触发视图更新。
  • 利用 $document Binding。 DocumentGroup 的闭包参数提供文档的 Binding,直接绑定到 SwiftUI 控件即可实现自动保存。

还有什么值得关注

  • DocumentGroup 自动在 iPadOS 上支持多窗口,每个窗口打开一个文档
  • 系统自动处理文档的脏标记(dirty flag)和自动保存
  • macOS 上的 DocumentGroup 自动集成到 Window 菜单
  • 文档 App 的状态恢复由系统自动处理,不需要手动保存窗口状态
WWDC 2020