WWDC Quick Look 💓 By SwiftGGTeam
What's new with text and text interactions

What's new with text and text interactions

观看原视频

Highlight

iOS 17 和 macOS Sonoma 带来了全新的文本选择 UI、UITextSelectionDisplayInteraction 轻量选择方案、可定制的文本项交互、TextKit 2 列表支持,以及针对多语言的动态行高和换行改进,让富文本体验和国际化支持更完善。

核心内容

选择 UI 全面重设计

所有平台都有了全新的文本光标。切换输入语言时,光标位置会显示内联的交互式切换器。范围选择时的选区手柄更符合人体工程学。还引入了全新的放大镜(loupe),帮助在大段文本中精确放置光标。

使用 UITextView 和 UITextField 的应用自动获得这些更新。使用 UITextInteraction 的自定义文本视图也能获得新选择 UI。

UITextSelectionDisplayInteraction:不接手势的选择 UI

对于有高度自定义文本 UI 的应用,如果无法采用 UITextInteraction(它自带手势处理),iOS 17 提供了 UITextSelectionDisplayInteraction。它只提供选择 UI——光标、光标附件、选区高亮、选区手柄——不包含手势。

这是一个新的 UIInteraction 子类,可以安装到任何 UIView 上。需要提供实现 UITextInput 协议的对象,用来获取选择状态。选择状态变化时调用 setNeedsSelectionUpdate(),交互对象会自动更新所有视图。

所有选择视图都可以替换,需要时可以自定义行为。

UITextLoupeSession:独立的放大镜

除了 UITextSelectionDisplayInteraction,iOS 17 还新增了独立的放大镜 API。它可以用在任何视图上,不需要 UITextSelectionDisplayInteraction 或 UITextInput。

推荐用 UIPanGestureRecognizer 来驱动。手势开始时调用 UITextLoupeSession.begin(at:),移动时调用 move(to:),结束时调用 invalidate()

文本项交互的增强

UITextView 中链接、附件和自定义文本范围的交互现在可以完全定制。UITextViewDelegate 新增了方法来自定义 primary action 或提供菜单。

Text item 包括:

  • NSTextAttachment(文本附件)
  • NSLinkAttributeName(链接)
  • UITextItemTagAttributeName(自定义范围)

UITextItemTagAttributeName 可以把任意文本范围标记为可交互项。然后实现 menuConfigurationFor:defaultMenu: 返回自定义菜单,或实现 primaryAction 方法改变默认点击行为。返回 nil 则禁用默认行为。

TextKit 2 的列表支持

TextKit 2 终于支持列表了。无序列表和有序列表都支持,有序列表包括罗马数字、字母序号和十进制序号。系统会根据设备语言自动本地化编号格式。

NSParagraphStyletextLists 属性定义哪些段落应用列表样式。系统根据换行符自动编号,UITextView 会根据 typing attributes 自动传播段落样式。

macOS 听写 UI

macOS Sonoma 引入了全新的听写指示器。说话时带尾随发光效果,静止时显示麦克风图标。滚动时指示器会吸附到滚动视图边缘,并提供返回当前输入位置的按钮。

标准 NSTextView 自动获得这些行为。自定义文本视图需要用 NSTextInsertionIndicator 替换自己画的插入点。它是一个可定制的 NSView 子类,外观(颜色、大小)可调整,默认跟随当前强调色。

设置 effectsViewInserter 块,让系统把效果视图插入到你的文档视图层级中。系统会自动定位效果视图并随光标移动更新。文本视图 resign responder 时把 displayMode 设为 .hidden 隐藏光标。

文本插入时,如果正在使用听写,帧更新会自动带动画发光效果。不需要发光效果时,从 automaticModeOptions 中移除 .showEffectsView

切换输入模式时,光标下方会显示语言选择 UI。需要覆盖位置时,在 NSTextInputClient 实现中指定 preferredTextAccessoryPlacement

光标滚动出屏幕时,系统显示 scroll-away indicator 指示光标相对位置,并提供返回听写位置的入口。自定义视图需要遵循 NSTextInputClient,实现 selectionRectdocumentVisibleRect 属性,并在滚动开始/结束时调用 textInputClientWillStartScrollingOrZoomingwillEndScrollingOrZooming

国际化改进

动态行高

很多语言的 ascender 和 descender 超出拉丁字母的行高范围,导致文字被裁剪。iOS 17 会自动调整 UILabel 和 UITextField 的行高来容纳这些语言。

这个行为只在特定条件下触发:

  • 设备配置了需要额外行高的语言
  • 影响整个 UI,包括固定行高的语言(如英语),保持视觉一致性
  • 只对使用 text styles 的文本元素生效,自定义字体保持固定行高

要获得这个行为:

  • preferredFont(forTextStyle:) 创建字体
  • 避免给文本元素设置 clipsToBounds
  • 确保 UI 能响应高度变化,其他控件保持对齐

换行改进

中文、德文、日文、韩文的换行规则做了大幅改进,根据 text style 应用不同规则。

例如韩文:以前标题中的单词可能被拆到两行,现在 title text style 会阻止单词内换行,只在词边界换行。body text style 仍允许在单词内换行。

德文长单词在窄布局中以前会按字符换行,现在会在 morpheme 边界自动断词,布局更平衡。

要获得这些改进,使用 text styles。

详细内容

添加选择显示交互

01:46

// 创建选择显示交互,提供 UITextInput 实现
let selectionInteraction = UITextSelectionDisplayInteraction(textInput: document)

// 添加到显示选择 UI 的视图
textContainerView.addInteraction(selectionInteraction)

// 选择状态变化时更新
document.selectionChanged = {
    selectionInteraction.setNeedsSelectionUpdate()
}

关键点:

  • UITextSelectionDisplayInteraction 只提供选择 UI,不包含手势
  • 需要实现 UITextInput 协议的对象来提供选择状态
  • setNeedsSelectionUpdate() 在选择状态变化时调用
  • 所有选择视图可替换

管理放大镜会话

03:16

var loupeSession: UITextLoupeSession?

@objc func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
    let location = gesture.location(in: textView)
    let selectionWidget = cursorView
    
    switch gesture.state {
    case .began:
        loupeSession = UITextLoupeSession.begin(
            at: location,
            in: selectionWidget,
            from: textView
        )
    case .changed:
        loupeSession?.move(to: location)
    case .ended, .cancelled:
        loupeSession?.invalidate()
        loupeSession = nil
    default:
        break
    }
}

关键点:

  • UITextLoupeSession.begin(at:in:from:) 创建放大镜会话
  • move(to:) 随手势更新位置
  • invalidate() 结束时销毁会话
  • 可以用在任何视图上,不限于文本视图

为文本项配置菜单

05:56

func textView(
    _ textView: UITextView,
    menuConfigurationFor textItem: UITextItem,
    defaultMenu: UIMenu
) -> UITextItem.MenuConfiguration? {
    // 返回 nil 禁用默认菜单
    // 返回自定义菜单配置
    return .init(menu: UIMenu(children: [
        UIAction(title: "Open in App") { _ in
            self.handleLink(textItem.link)
        }
    ]))
}

关键点:

  • menuConfigurationFor:defaultMenu: 自定义文本项的菜单
  • 返回 nil 禁用默认行为
  • 可以包含 inline preview

标记自定义文本范围为可交互

05:32

let attributedString = NSMutableAttributedString(string: "Tap here for more info")
attributedString.addAttribute(
    UITextItemTagAttributeName,
    value: "info-tag",
    range: NSRange(location: 0, length: 8)
)

关键点:

  • UITextItemTagAttributeName 标记任意文本范围为可交互
  • 标记后该范围会触发 text item 的 delegate 回调
  • 可以用来给非链接文本添加交互

TextKit 2 列表

06:46

let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.textLists = [NSTextList(markerFormat: .decimal, options: 0)]

let attributedString = NSMutableAttributedString(string: "First item\nSecond item\nThird item")
attributedString.addAttribute(
    .paragraphStyle,
    value: paragraphStyle,
    range: NSRange(location: 0, length: attributedString.length)
)

关键点:

  • NSParagraphStyle.textLists 定义列表样式
  • NSTextList(markerFormat:options:) 创建列表格式
  • 支持 .decimal.lowercaseLetter.uppercaseLetter.lowercaseRoman.uppercaseRoman.bullet
  • 系统根据 locale 自动本地化
  • UITextView 自动传播 typing attributes

macOS 文本插入指示器

08:28

let insertionIndicator = NSTextInsertionIndicator()
documentView.addSubview(insertionIndicator)

insertionIndicator.effectsViewInserter = { view, placement in
    documentView.addSubview(view)
}

// 隐藏光标
textViewDidResignFirstResponder {
    insertionIndicator.displayMode = .hidden
}

关键点:

  • NSTextInsertionIndicator 替换自定义光标绘制
  • effectsViewInserter 让系统插入听写效果视图
  • displayMode 控制显示/隐藏
  • 默认跟随 accent color

核心启发

  1. 用系统选择 UI 替换自定义实现

    • 做什么:把自定义文本视图里的光标、选区手柄、放大镜换成系统提供的组件
    • 为什么值得做:自动跟随系统样式更新,减少维护成本,获得新功能(如听写发光效果)
    • 怎么开始:iOS 用 UITextSelectionDisplayInteraction,macOS 用 NSTextInsertionIndicator
  2. 给文本中的关键词添加交互菜单

    • 做什么:在 UITextView 中给特定关键词(如人名、地点、产品名)添加自定义菜单
    • 为什么值得做:用户长按就能执行相关操作,不需要离开当前界面
    • 怎么开始:用 UITextItemTagAttributeName 标记范围,实现 menuConfigurationFor:defaultMenu: delegate 方法
  3. 在富文本编辑器中加入列表支持

    • 做什么:给文本编辑器添加有序/无序列表功能
    • 为什么值得做:TextKit 2 原生支持,自动编号、自动本地化,不需要自己维护列表状态
    • 怎么开始:设置 NSParagraphStyle.textLists,用 NSTextList 定义格式
  4. 确保多语言下的文本不被裁剪

    • 做什么:检查应用中的 UILabel 和 UITextField 是否使用了 text styles,是否设置了 clipsToBounds
    • 为什么值得做:泰语、印地语等语言的字符会超出标准行高,裁剪后影响可读性
    • 怎么开始:把自定义字体换成 preferredFont(forTextStyle:),移除 clipsToBounds = true
  5. 给 macOS 文本编辑器加上听写支持

    • 做什么:让自定义 macOS 文本视图支持系统听写功能
    • 为什么值得做:用户可以用语音输入,获得发光效果、边缘吸附、scroll-away indicator 等原生体验
    • 怎么开始:用 NSTextInsertionIndicator 替换自定义光标,设置 effectsViewInserter,实现 NSTextInputClient 的相关属性

关联 Session

评论

GitHub Issues · utterances