What's new with text and text interactions
System & Services 进阶 20m

文本与文本交互的新变化

What's new with text and text interactions

2023年6月5日

在 Apple 官方观看视频

一句话判断

如果你的应用涉及自定义文本编辑,iOS 17 的 UITextSelectionDisplayInteraction 和 macOS 14 的 NSTextInsertionIndicator 能让你的自定义文本视图获得和系统一样的光标、选区、放大镜和听写 UI——不用自己造轮子了。

这场 Session 讲了什么

UIKit 团队的 James Magahern 全面介绍了 2023 年平台在文本交互方面的更新,涵盖 iOS、macOS 和国际化三个方向。

选择 UI 的全面重设计:所有平台都有了全新的文本光标样式。切换输入语言时会在光标位置显示内联切换器,范围选择时的选区手柄更加符合人体工程学,还引入了全新的放大镜(loupe)来帮助在大段文本中精确放置光标。使用 UITextView 和 UITextField 的应用自动获得这些更新。

UITextSelectionDisplayInteraction:对于有高度自定义文本 UI 的应用,如果无法采用 UITextInteraction,现在有了一个新的轻量选择方案——它只提供选择 UI 而不包含手势处理,可以安装到任何 UIView 上。

文本项交互的增强:UITextView 中链接、附件和自定义文本范围的交互现在可以完全定制。你可以改变默认的点击行为,或者为特定的文本项添加自定义菜单。新增了 UITextItemTagAttributeName,可以把任意文本范围标记为可交互项。

TextKit 2 的列表支持:终于支持了有序列表和无序列表,包括罗马数字、字母序号和十进制序号,且自动根据设备语言环境进行本地化。

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

值得深挖的点

UITextSelectionDisplayInteraction 的定位:它本质上是一个”UI only”的选择层,适合那些已经实现了自己的文本渲染和手势逻辑、但不想自己画光标和选区的应用。关键在于它只管显示,手势逻辑完全由你自己控制——通过调用 setNeedsSelectionUpdate 来驱动 UI 更新。

UITextLoupeSession 的生命周期:放大镜不再是系统自动弹出的固定 UI,而是变成了一个你可以主动控制的 session。通过 pan gesture recognizer 驱动,begin → move → invalidate 三步走。这意味着你可以在任何视图上使用放大镜,不限于文本场景。

文本项标签的扩展能力:UITextItemTagAttributeName 的引入意味着你可以让文本中的任何范围都成为可交互项——不局限于链接和附件。比如在翻译应用中,可以给特定词汇打标签,点击后弹出翻译候选菜单。

列表的自动编号和本地化:使用 NSParagraphStyle 的 textLists 属性设置列表后,系统会根据换行符自动编号。编号样式会跟随设备的 locale 设置自动调整——这对多语言应用来说是免费的国际化支持。

代码片段

// UITextSelectionDisplayInteraction - 自定义文本视图获得系统选择 UI
class MyCustomTextView: UIView, UITextInput {
    let selectionInteraction = UITextSelectionDisplayInteraction(document: self)

    override func didMoveToWindow() {
        super.didMoveToWindow()
        addInteraction(selectionInteraction)
    }

    // 当选区状态变化时通知 interaction 更新
    func selectionDidChange() {
        selectionInteraction.setNeedsSelectionUpdate()
    }
}
// 放大镜的使用
@objc func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
    let location = gesture.location(in: cursorView)

    switch gesture.state {
    case .began:
        // 创建放大镜 session,指定位置和光标视图
        loupeSession = UITextLoupeSession.begin(
            at: location,
            cursor: cursorView,
            in: coordinateSpace
        )
    case .changed:
        // 移动放大镜
        loupeSession?.move(to: location)
    case .ended, .cancelled:
        // 关闭放大镜
        loupeSession?.invalidate()
        loupeSession = nil
    default:
        break
    }
}
// 为 UITextView 中的链接添加自定义菜单
func textView(_ textView: UITextView,
              menuConfigurationFor textItem: UITextItem,
              defaultMenu: UIMenu) -> UITextItem.MenuConfiguration? {
    // 只对链接类型的文本项添加自定义菜单
    guard textItem.type == .link else { return nil }

    let customMenu = UIMenu(children: [
        UIAction(title: "在应用内打开") { _ in
            // 在应用内导航到对应页面
        },
        UIAction(title: "复制链接") { _ in
            UIPasteboard.general.string = textItem.content
        }
    ])
    return UITextItem.MenuConfiguration(menu: customMenu, preview: nil)
}

// 用标签标记自定义可交互文本范围
let attributedString = NSAttributedString(
    string: "专业术语",
    attributes: [.textItemTag: "technicalTerm"]
)
// TextKit 2 列表支持
let listStyle = NSMutableParagraphStyle()
listStyle.textLists = [NSTextList(
    markerFormat: .decimal,  // 十进制序号
    options: 0
)]
let listText = NSAttributedString(
    string: "第一项\n第二项\n第三项",
    attributes: [.paragraphStyle: listStyle]
)

最佳实践

  • 不要自己画光标和选区:iOS 17 和 macOS 14 都提供了系统级的选择 UI 组件,用它们比自己实现更一致、更省力,而且自动跟随系统样式更新。
  • 文本项菜单要克制:不是每个可交互文本都需要菜单。返回 nil 可以静默地禁用默认行为,只在真正需要的时候才提供自定义菜单。
  • 利用系统的列表本地化:如果你的应用需要显示列表内容,直接用 NSParagraphStyle 的 textLists 而不是自己实现编号逻辑——系统会根据 locale 自动调整编号格式。
  • macOS 自定义文本视图要适配新听写 UI:用 NSTextInsertionIndicator 替换自己画的插入点,这样听写的发光效果和边缘吸附行为就自动有了。
  • 听写指示器的边缘吸附是自动的:滚动视图内的标准文本控件自动获得这个行为,但自定义实现需要手动处理。

还有什么值得关注

  • Session 提到了重要的国际化更新,虽然具体内容在转录中不完整,但 TextKit 2 列表的自动本地化是其中一部分。
  • UITextItem 的类型从原来的 link 和 attachment 扩展到了自定义标签,这为富文本编辑器、阅读器等应用打开了很多可能性。
  • macOS 的听写 UI 与 iOS 保持一致的视觉语言——这是 Apple 在统一跨平台文本交互体验的又一步。
  • 放大镜 API(UITextLoupeSession)独立于文本系统存在,理论上可以用于任何需要精确位置选择的场景,不限于文本光标。
  • 如果你的应用还在用 TextKit 1,列表支持是迁移到 TextKit 2 的又一个理由。
WWDC 2023