WWDC Quick Look 💓 By SwiftGGTeam
Get started with Writing Tools

Get started with Writing Tools

观看原视频

Highlight

iOS 18 引入的 Writing Tools 让用户可以在任意文本视图中校对、改写和转换文本,开发者只需使用 UITextView/NSTextView(TextKit 2)或 WKWebView 即可获得完整支持(00:14)。

核心内容

用户在 App 中编辑文本时,若想校对语法或改写措辞,通常需要离开应用、借助外部工具完成,再把内容粘贴回来——流程割裂,格式容易丢失。Apple 在 iOS 18、macOS 15 中推出了 Writing Tools,把校对、改写、摘要、列表转换、表格转换等功能集成到原生文本视图内。用户选中文本后,直接在编辑菜单、浮动栏或右键菜单中调用,建议以行内方式呈现,样式、链接、附件等富文本属性在改写后自动保留(03:39)。

对开发者而言,Writing Tools 是开箱即用的:使用 UITextView、NSTextView(需 TextKit 2)或 WKWebView,系统自动暴露入口。处理文本时系统会自动扩展选区到完整句子以获得更好效果,并通过 attributed string 维持原有属性。开发者可以通过新增的代理方法暂停同步、保护特定文本范围(如代码块、引用),确保应用状态与文本操作保持一致(05:28)。

详细内容

默认行为与 TextKit 版本差异

使用 UITextView 或 NSTextView 时,Writing Tools 的体验取决于 TextKit 版本。TextKit 2 提供完整体验——建议以行内方式显示,可直接在文本视图中预览和应用更改;TextKit 1 则退化为面板模式,仅显示处理结果(03:58)。WKWebView 默认行为为 .limited(面板模式),如需完整体验需在 WKWebViewConfiguration 中显式设置为 .complete08:06)。

监听 Writing Tools 会话

Writing Tools 活跃时会直接修改 text storage,应用应避免在同一时间对文本进行持久化或同步操作。Apple 提供了新的代理方法来监听会话生命周期(05:28):

func textViewWritingToolsWillBegin(_ textView: UITextView) {
    // Take necessary steps to prepare. For example, disable iCloud sync.
}

func textViewWritingToolsDidEnd(_ textView: UITextView) {
    // Take necessary steps to recover. For example, reenable iCloud sync.
}

if !textView.isWritingToolsActive {
    // Do work that needs to be avoided when Writing Tools is interacting with text view
    // For example, in the textViewDidChange callback, app may want to avoid certain things
    // when Writing Tools is active
}
  • textViewWritingToolsWillBegin:会话开始时调用,适合暂停自动保存、文本分析等会干扰 text storage 的操作
  • textViewWritingToolsDidEnd:会话结束时调用,恢复之前暂停的状态
  • isWritingToolsActive:只读属性,用于在回调中判断当前是否有 Writing Tools 会话正在进行

控制行为与接受格式

开发者可以通过 writingToolsBehavior 控制体验模式(07:11):

textView.writingToolsBehavior = .limited  // 仅面板模式
textView.writingToolsBehavior = .none     // 完全禁用

通过 writingToolsAllowedInputOptions 指定文本视图接受的格式类型(07:31):

textView.writingToolsAllowedInputOptions = [.plainText]
textView.writingToolsAllowedInputOptions = [.plainText, .richText, .table]
  • .plainText:仅接受纯文本
  • .richText:接受富文本(包括样式、链接、附件)
  • .table:接受表格转换(需文本视图支持 NSTextTable)

默认假设视图支持纯文本和富文本但不支持表格;若视图能渲染表格,需显式声明。

WKWebView 的配置

WKWebView 的 Writing Tools API 通过 WKWebViewConfiguration 设置,默认行为为 .limited。此外 WKWebView 本身也提供 isWritingToolsActive 属性用于检查会话状态(07:55):

// For `WKWebView`, the `default` behavior is equivalent to `.limited`
extension WKWebViewConfiguration {
    @available(iOS 18.0, *)
    open var writingToolsBehavior: UIWritingToolsBehavior { get set }
}

extension WKWebViewConfiguration {
    @available(macOS 15.0, *)
    open var writingToolsBehavior: NSWritingToolsBehavior { get set }
}

extension WKWebView {
    /// If the Writing Tools behavior on the configuration is `.limited`, this will always be `false`.
    @available(iOS 18.0, macOS 15.0, *)
    open var isWritingToolsActive: Bool { get }
}
  • writingToolsBehavior:在 WKWebViewConfiguration 上设置,决定 WKWebView 的 Writing Tools 体验级别
  • isWritingToolsActive:当行为为 .limited 时此属性始终返回 false;仅在完整体验下才能检测到活跃会话

保护特定文本范围

笔记应用中的代码块、邮件应用中的引用内容不应被改写。通过新代理方法返回需要忽略的 NSRange,Writing Tools 不会对这些范围提出修改建议(08:48):

// Returned `NSRange`s are relative to the substring of the textView's textStorage from `enclosingRange`
func textView(_ textView: UITextView, writingToolsIgnoredRangesIn
        enclosingRange: NSRange) -> [NSRange] {
    let text = textView.textStorage.attributedSubstring(from: enclosingRange)
    return rangesInappropriateForWritingTools(in: text)
}
  • enclosingRange:系统传递的处理范围,返回的 NSRange 相对于该范围的子串
  • attributedSubstring:从 textStorage 中取出子串,用于检测其中的代码块或引用
  • WKWebView 会自动忽略 <blockquote><pre> 标签内的内容(08:56

自定义文本视图支持

对于非 UITextView/NSTextView 的自定义文本视图,iOS/iPadOS 上采用 UITextInteraction 即可免费获得 Writing Tools 菜单项。系统通过 UITextInput 协议读写文本和定位弹出窗口,新增的 isEditable 属性用于指示视图是否可编辑(09:58):

protocol UITextInput {
    @available(iOS 18.0, macOS 15.0, *)
    optional var isEditable: Bool { get }
}

在 macOS 上,自定义视图需实现 NSServicesMenuRequestor 协议并覆盖 validRequestor(forSendType:returnType:),同时为视图添加上下文菜单即可自动获得 Writing Tools 菜单项(11:05):

class CustomTextView: NSView, NSServicesMenuRequestor {
    override func validRequestor(forSendType sendType: NSPasteboard.PasteboardType?,
                                 returnType: NSPasteboard.PasteboardType?) -> Any? {
        if sendType == .string || sendType == .rtf {
            return self
        }
        return super.validRequestor(forSendType: sendType, returnType: returnType)
    }

    nonisolated func writeSelection(to pboard: NSPasteboard,
                                    types: [NSPasteboard.PasteboardType]) -> Bool {
        // Write plain text and/or rich text to pasteboard
        return true
    }

    // Implement readSelection(from pboard: NSPasteboard)
    // as well for editable view
}
  • validRequestor:声明视图支持的剪贴板类型(如纯文本 .string、富文本 .rtf),返回 self 表示该视图可以处理
  • writeSelection:将选区内容写入剪贴板供系统读取
  • readSelection:从剪贴板读取处理后的文本并应用到视图(可编辑视图必需实现)

核心启发

  • 实现 Writing Tools 会话监听:在 textViewWritingToolsWillBegin 中暂停自动保存与同步,在 textViewWritingToolsDidEnd 中恢复,并在关键回调中检查 isWritingToolsActive
    • 为什么值得做:Writing Tools 活跃时会直接修改 text storage,若应用同时进行自动保存或云端同步,会导致并发冲突和数据不一致。
    • 怎么开始:在 UITextViewDelegate/NSTextViewDelegate 中实现两个新代理方法,将需要暂停的逻辑集中管理。
  • 保护不应改写的文本范围:通过 writingToolsIgnoredRangesIn 代理方法返回代码块、引用、固定术语等 NSRange,阻止 Writing Tools 对这些区域提出修改建议。
    • 为什么值得做:用户使用 Writing Tools 校对整段文本时,代码块或引用内容被改写会破坏其原始含义,造成困惑和额外修复工作。
    • 怎么开始:在 delegate 中实现 textView(_:writingToolsIgnoredRangesIn:),解析文本属性返回需要忽略的范围;WKWebView 项目无需额外代码,<blockquote><pre> 标签会被自动忽略。
  • 为 WKWebView 显式启用完整体验:在 WKWebViewConfiguration 中将 writingToolsBehavior 设为 .complete,使 WebView 中的文本编辑器获得行内预览与直接应用更改的能力。
    • 为什么值得做:WKWebView 的默认行为是 .limited(仅面板模式),用户只能看到结果后手动复制粘贴,体验明显低于原生文本视图的行内交互。
    • 怎么开始:创建 WKWebViewConfiguration 时设置 writingToolsBehavior = .complete,然后观察 isWritingToolsActive 确认会话状态。
  • 声明文本视图支持的输入格式:如果自定义文本视图能渲染表格,显式设置 writingToolsAllowedInputOptions 包含 .table;若仅接受纯文本则只声明 .plainText
    • 为什么值得做:系统默认假设文本视图不支持表格,如果你的视图具备渲染能力却不声明,用户将无法使用 Writing Tools 的表格转换功能。
    • 怎么开始:在文本视图初始化时设置 writingToolsAllowedInputOptions = [.plainText, .richText, .table],根据实际渲染能力选择子集。

关联 Session

评论

GitHub Issues · utterances