SwiftUI 富文本编辑:AttributedString、选择区与格式化
Xcode 26 与 iOS 26 的 SwiftUI 测试版让 TextEditor 可以围绕 AttributedString 和选择区构建富文本体验,相关签名仍可能变化。与其先做一排粗体、斜体和颜色按钮,更重要的是定义文档允许哪些语义、选择如何变化、格式操作怎样撤销,以及保存格式是否能跨版本读取。
文档模型不是工具栏状态
粗体按钮的高亮不是事实来源。事实来源是当前选择覆盖的属性:全部为粗体、全部不是,还是混合状态。文档模型保存富文本,选择模型描述插入点或范围,格式命令读取两者并产生一次可撤销编辑。这样键盘快捷键、菜单命令和工具栏按钮能复用相同意图。
struct NoteEditor: View {
@State private var text = AttributedString("Start writing…")
@State private var selection: TextSelection?
var body: some View {
TextEditor(text: $text, selection: $selection)
.toolbar {
ToolbarItemGroup {
Button("Bold", systemImage: "bold") {
applyBold(to: selection, in: &text)
}
Button("Italic", systemImage: "italic") {
applyItalic(to: selection, in: &text)
}
}
}
}
}
这段代码表达架构,具体测试版类型与初始化器应以当前 SDK 为准。applyBold 不应直接依赖 SwiftUI 视图,而应是可测试的文档操作。没有选择时,可以改变之后输入的属性或作用于当前词,但产品必须选择一种一致规则,并通过按钮状态和辅助说明让用户知道结果。
只允许产品真正支持的属性
AttributedString 能承载多种属性,不代表文档格式必须保存任意字体、颜色和附件。笔记应用可能只需要标题、强调、链接和列表;评论框也许只允许强调与代码。定义白名单能简化渲染、导出、搜索和同步,还能避免粘贴网页内容时把不可控字体与前景色带进主题系统。
粘贴或导入时先标准化:保留允许的语义,移除未知属性,把绝对字号映射到产品样式。链接需要验证 scheme;附件要检查类型、大小和存储位置;来自外部的富文本始终是不可信输入。显示层不应执行隐藏在属性或链接中的任意行为。
正确处理 Unicode 范围
富文本范围不能用用户看到的字符数或 UTF-16 偏移随意换算。表情、组合音标和不同脚本会让“第几个字符”产生多个答案。选择与属性修改应使用 AttributedString 自己的索引,并把持久化标记锚定在文档语义上。每次编辑之后,旧索引可能失效;不要跨任意修改长期缓存范围。
测试数据应包含 emoji、家族组合符号、中文、阿拉伯文、换行和混合方向文本。若格式操作在这些样本中只改到半个组合字符,往往说明代码越过了正确的索引边界。
撤销、自动保存与协作是不同问题
一次点击“加粗”应成为一次撤销步骤,而不是每个属性 run 各一步。输入合并、格式切换和粘贴要有清晰事务边界。自动保存负责把已接受的文档快照持久化,不应该让磁盘写入反过来打断输入;可以在模型层去抖,并在页面关闭或进入后台时显式刷新。
多人协作需要操作标识、冲突策略或专门的协作数据结构,仅有 AttributedString 绑定并不会自动解决并发编辑。第一版先把单机编辑、撤销和序列化做确定,再决定是否引入协作层,能避免把 UI 范围误当作网络协议。
可访问的格式控件
格式按钮要有可读名称、选中或混合状态以及足够点击区域。外接键盘用户需要常见快捷键和正确焦点顺序。动态字体下,自定义字号应相对文本样式缩放;不要把“标题”仅实现为固定大号字体。VoiceOver 应能读出链接和文本语义,但过度播报每个属性也会妨碍连续阅读,需用真实文档检查。
结论
可维护的富文本编辑器从受限文档模型开始:选择区是输入,格式是可测试命令,属性有白名单,Unicode 索引不被偷偷转换,撤销与持久化边界明确。测试版 API 最好包在编辑器模块内;即使正式 SDK 调整了签名,文档语义、命令和测试仍然可以保留。