文本与文本交互的新变化
What's new with text and text interactions
2023年6月5日
一句话判断
如果你的应用涉及自定义文本编辑,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 的又一个理由。