Highlight
SwiftUI 的
DocumentGroup让 App 用一个 Scene 声明文档打开与管理能力,并由系统提供文档浏览器、标准文档命令、macOS 状态跟踪与 Handoff、iOS 文档浏览和分享界面。
核心内容
用户在 Finder 和 Files app 里整理文件,会用标签、云文件提供方和外接存储设备。他们打开一个文件时,期待 App 直接编辑原始文档;如果 App 只是把文件导入自己的数据库,用户改到的是副本,原始文件不会跟着变。
这类能力不只属于专业创作工具。Pixelmator、Keynote 和 Final Cut Pro 的主要工作都围绕文档管理展开;Xcode、Mail、Console 也会在主界面之外打开工程、EML 文件或崩溃报告。关键点是打开并编辑文件本身,而不是把文件吞进 App 私有数据里。
SwiftUI 把这个能力放进 DocumentGroup。App 仍然由 App、Scene 和 View 组成;当 Scene 换成 DocumentGroup,你声明的是:这个 App 支持打开和管理某类文档。SwiftUI 接着补上平台行为,包括 macOS 的状态跟踪与 Handoff,以及 iOS 的文档浏览器、导航栏搜索和分享入口。
演示从一个 iPad Playground 里的绘图原型开始。Tina 把它放进名为 ShapeEdit 的多平台 App,先配置系统能识别的文档类型,再让文档模型实现 FileDocument,最后把默认 TextEditor 换成画布。到结尾时,这个绘图 App 已经可以保存绘图并稍后重新打开。
详细内容
DocumentGroup 把文档管理接入 Scene
(02:00)SwiftUI application 由 App、Scene 和 View 组合而成。添加文档支持时,新的 Scene 类型是 DocumentGroup。它可以单独作为 App 的一个 Scene,也可以和多个 DocumentGroup 或 WindowGroup 组合在同一个 App 里。
(02:12)官方 Code tab 里的最小例子是一个文本编辑器:
@main
struct TextEdit: App {
var body: some Scene {
DocumentGroup(newDocument: TextDocument()) { file in
TextEditor(text: file.$document.text)
}
}
}
关键点:
@main标出 App 入口,body返回一个 Scene。DocumentGroup(newDocument: TextDocument())声明新建文档时使用的基础文档。file是DocumentGroup闭包传入的文件上下文。file.$document.text提供可读写绑定,TextEditor直接编辑文档里的文本。- transcript 在 06:49 说明,这个绑定让 SwiftUI 知道文本何时更新,并负责注册 undo 和标记文档 dirty state。
文档类型先让系统认识文件
(04:43)Xcode 的文档型 App 模板会在 Info.plist 里加入 Document Type。里面的 identifier 是 Uniform Type Identifier(统一类型标识符),系统用它把磁盘上的文件和 App 关联起来。
(05:07)plain text 这类类型由别处声明,App 只需要导入。ShapeEdit 要定义自己的绘图格式,所以要填写 Exported Type Identifier:给格式写描述,让它符合 public data 和 public content,再分配文件扩展名。
(08:24)transcript 特别区分了 importedAs 和 exportedAs。导入类型是 computed variable,因为它的值可能随着已安装 App 变化;导出类型由当前 App 拥有,可以声明为常量。
FileDocument 定义磁盘上的文档
(07:41)ShapeEdit 的文档类型是一个 value type,并遵守 FileDocument 协议。这个协议代表磁盘上的文档,第一步是定义 readableContentTypes,它是一个 UTType 数组。SwiftUI 用这个数组判断用户要打开的文件类型是否被 App 支持。
(09:11)读取文档时,初始化方法会拿到 FileWrapper 和 contentType。演示里删除模板代码后使用 JSONDecoder,因此文档类型需要遵守 Codable。
(09:43)写出文档时,方法同样接收目标 FileWrapper 和 contentType。FileWrapper 是 inout 参数,可以创建新的 wrapper,也可以更新已有 wrapper。演示里使用 JSONEncoder 把文档写回文件。
(10:12)文档模型是 struct,因此保留值类型的 copy-on-write 行为。transcript 给出的实际收益是:用户还在画图时,App 已经可以开始保存。
从文本模板换成绘图文档
(10:32)文档支持准备好后,演示把 Playground 里的画布代码加进项目。项目里有一个 graphic type 用来描述图形属性,也有一个 Canvas View 用来显示图形。
(10:56)接着把文档的数据类型从 text 改成 graphic type,加入初始图形,再把 TextEditor 替换成 canvas。最后运行的 ShapeEdit 可以保存绘图,也可以之后再打开同一份绘图。
核心启发
-
文本文档编辑器
做什么:做一个打开原始文本文件的轻量编辑器。 为什么值得做:
DocumentGroup已经处理打开和管理文档,TextEditor可以直接绑定到file.$document.text。 怎么开始:先用官方 snippet 跑通DocumentGroup(newDocument:),再让文档类型遵守FileDocument,把可读类型放进readableContentTypes。 -
绘图或白板 App
做什么:把 iPad Playground 里的绘图原型迁移成可保存文件的多平台 App。 为什么值得做:session 的 ShapeEdit 演示正是从画布原型开始,最后保存并重新打开绘图。 怎么开始:先定义 graphic type 和 Canvas View,再把文档数据从 text 换成 graphic type,读写层用
JSONDecoder和JSONEncoder处理。 -
日志或崩溃报告查看器
做什么:给一个普通工具 App 增加打开 EML、crash report 或日志文件的入口。 为什么值得做:transcript 明确提到 Mail 和 Console 主要界面并非文档型,但仍支持打开额外文件。 怎么开始:保留原来的
WindowGroup,再组合一个DocumentGroup专门处理文件打开场景。 -
自定义格式的项目文件
做什么:为设计稿、工程配置或创作项目定义自己的文件格式。 为什么值得做:系统需要 UTI 才能把磁盘文件和 App 关联起来,ShapeEdit 通过 Exported Type Identifier 声明自己的格式。 怎么开始:在 Info.plist 的 Document Type 和 Exported Type Identifier 中使用同一个类型标识,让该类型符合 public data 和 public content,并分配扩展名。
关联 Session
- App essentials in SwiftUI — 先理解 App、Scene 和 View 的层级;本 session 在讲
DocumentGroup时直接引用它。 - Data Essentials in SwiftUI — 补足 SwiftUI 数据流背景,尤其是 binding 如何把文档变化传给视图。
- What’s new in SwiftUI — 从年度总览看 app、scene、DocumentGroup 和 Uniform Type Identifiers 在 SwiftUI 2020 中的位置。
- Stacks, Grids, and Outlines in SwiftUI — 继续处理 ShapeEdit 这类编辑器的列表、层级和检查器界面。
- Build a SwiftUI view in Swift Playgrounds — 对应 ShapeEdit 最初的 iPad Playground 原型阶段。
评论
GitHub Issues · utterances