深入 Writing Tools:富文本、语义样式与自定义文本引擎集成
Dive deeper into Writing Tools
2025年6月9日
一句话判断
Writing Tools 今年的核心改进是富文本支持的精细化——display attributes 和 presentation intents 的区分是理解一切的关键。如果你有自定义文本引擎,Writing Tools Coordinator API 给了你完整的集成路径,但这是一份需要仔细对待的 delegate 实现工作。
这场 Session 讲了什么
Dongyuan 从四个方面深入讲解:
新特性概览:ChatGPT 集成支持内容生成和图片创建。支持 visionOS。描述修改后可追加 Follow Up 请求(比如”再温暖一点”)。Writing Tools 可作为 Shortcuts 使用。新增 API:工具栏按钮、标准菜单项、presentation intents、Writing Tools Coordinator。
原生文本视图自定义:工具栏按钮用 UIBarButtonItem(UIKit)或 NSToolbarItem(AppKit)。菜单项可通过 automaticallyInsertsWritingToolsItems = false 关闭自动插入,用 writingToolsItems 获取标准项手动放置。Result Options 决定文本视图支持的格式级别——plainText(纯文本,忽略所有属性)、richText(显示属性如加粗/斜体)、richText + list + table(支持列表和表格)、richText + presentationIntent(语义样式)。
Display Attributes vs Presentation Intents:这是本 Session 最核心的概念。Display attributes 是具体的样式信息(具体字号、颜色),TextEdit 用这种模式。Presentation intents 是语义信息(标题、子标题、引用块、代码块),Notes 用这种模式。Writing Tools 在 presentation intent 模式下尽可能用语义形式表达样式,但某些样式(下划线、上标、下标)无法用 intent 表示,仍会回落为 display attribute。Presentation intents 没有默认样式——你的 app 负责将 intent 转换为具体的显示样式。
自定义文本引擎集成:通过 Writing Tools Coordinator 实现完整体验(原地重写、动画、行内校对标记)。Coordinator 是 UIKit 的 UIInteraction / AppKit 的 NSView 属性。Delegate 方法都是 async 的——因为大文本视图处理底层文本存储可能耗时。关键 delegate 方法:提供 context(文本 + 选区范围,可包含前后段落)、替换文本、返回校对下划线的贝塞尔路径、生成动画预览(macOS 用 NSTextPreview,iOS 用 UITargetedPreview)、响应状态变化。
值得深挖的点
Presentation intents 的适用场景需要判断。如果你的 app 有 Notes 级别的语义样式系统(heading / subheading / blockquote / code block),用 presentationIntent 让 Writing Tools 理解你的文本语义。但如果只是简单的富文本编辑器(加粗/斜体/颜色),richText display attributes 就够了,不需要增加复杂度。
Context 提供的质量直接影响 Writing Tools 效果。Delegate 的 context 方法应该包含当前选区的前后段落——这让模型更好地理解上下文。如果没有选中任何内容,返回整个文档作为 context,选区范围设为光标位置。
动画预览的实现细节容易出错。macOS 需要返回 NSTextPreview(至少一个整体预览,可选按行拆分做更流畅的动画),iOS 用 UITargetedPreview。文本在动画期间需要从文本视图中隐藏,动画结束后再显示——这是为了避免文本和预览图像重叠。
校对标记需要贝塞尔路径。Writing Tools 用你提供的 underline bezier path 绘制校对下划线,用 bounding bezier path 响应点击事件显示行内弹窗。这两个路径的精度直接影响校对体验的视觉质量。
代码片段
// 1. 设置 Writing Tools Coordinator(macOS)
class DocumentView: NSView {
let coordinator = NSWritingToolsCoordinator()
func configureWritingTools() {
coordinator.delegate = self
self.writingToolsCoordinator = coordinator
}
}
// 2. 实现关键 delegate 方法
extension DocumentView: NSWritingToolsCoordinator.Delegate {
// 提供上下文
func writingToolsCoordinator(
_ coordinator: NSWritingToolsCoordinator,
requestsContextFor scope: NSWritingToolsCoordinator.ContextScope,
completion: @escaping ([NSWritingToolsCoordinator.Context]) -> Void
) {
let text = currentAttributedString
let range = selectedRange // 或全文档光标位置
let context = NSWritingToolsCoordinator.Context(
attributedString: text,
range: range
)
completion([context])
}
// 替换文本
func writingToolsCoordinator(
_ coordinator: NSWritingToolsCoordinator,
replace range: NSRange,
in context: NSWritingToolsCoordinator.Context,
with attributedString: NSAttributedString,
completion: @escaping () -> Void
) {
textStorage.replaceCharacters(in: range, with: attributedString)
completion()
}
// 返回校对下划线路径
func writingToolsCoordinator(
_ coordinator: NSWritingToolsCoordinator,
underlinePathsFor range: NSRange,
in context: NSWritingToolsCoordinator.Context,
completion: @escaping ([NSWritingToolsCoordinator.UnderlinePaths]) -> Void
) {
// 计算每个校对修改的下划线贝塞尔路径
let paths = calculateUnderlinePaths(for: range)
completion(paths)
}
}
// 3. Result Options 配置
// Notes 级别:语义样式
textView.writingToolsResultOptions = [.richText, .presentationIntent, .list, .table]
// TextEdit 级别:显示属性
textView.writingToolsResultOptions = [.richText, .list, .table]
// Finder 搜索框:纯文本
textView.writingToolsResultOptions = [.plainText]
最佳实践
-
先判断你的文本视图属于哪个级别。纯文本 → plainText。简单富文本 → richText。有列表/表格 → 加 list + table。有语义样式(heading 等)→ 用 presentationIntent。不要过度使用 presentationIntent,如果你的 app 没有对应的语义样式系统。
-
用 presentationIntent 时实现 requestContexts。如果你的文本视图中有语义样式(比如标题段落),覆写
requestContexts方法,提供包含 presentation intents 的 context,让 Writing Tools 理解你的文本结构。 -
自定义菜单项时使用标准 API。
writingToolsItems会自动跟随系统更新——今年菜单项从 Proofread/Rewrite/Summary 更新了结构,用 API 的 app 自动获得新布局。 -
Coordinator 的 delegate 方法都是 async 的。大文档的文本替换和预览生成可能耗时,确保在 delegate 方法内部正确处理异步操作,不要阻塞主线程。
-
文本变更后通知 Coordinator。外部修改文本后调用
updateRange:withText保持同步,布局变化后调用updateForReflowedText请求新的预览和校对标记。
还有什么值得关注
- Follow Up 请求在 iOS/iPadOS/macOS 26 上支持——描述修改后可追加”更温暖”、“更口语化”等调整
- visionOS 上的 Writing Tools 是新增支持
- Writing Tools 作为 Shortcuts action 可用于自动化工作流
- 校对弹窗点击事件由 bounding bezier path 的点击检测驱动
willChangeToState:completion:方法可用于 undo 分组、停止同步、阻止编辑- Apple 提供了 TextKit 2 自定义引擎的完整示例代码