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 终于支持列表了。无序列表和有序列表都支持,有序列表包括罗马数字、字母序号和十进制序号。系统会根据设备语言自动本地化编号格式。
用 NSParagraphStyle 的 textLists 属性定义哪些段落应用列表样式。系统根据换行符自动编号,UITextView 会根据 typing attributes 自动传播段落样式。
macOS 听写 UI
macOS Sonoma 引入了全新的听写指示器。说话时带尾随发光效果,静止时显示麦克风图标。滚动时指示器会吸附到滚动视图边缘,并提供返回当前输入位置的按钮。
标准 NSTextView 自动获得这些行为。自定义文本视图需要用 NSTextInsertionIndicator 替换自己画的插入点。它是一个可定制的 NSView 子类,外观(颜色、大小)可调整,默认跟随当前强调色。
设置 effectsViewInserter 块,让系统把效果视图插入到你的文档视图层级中。系统会自动定位效果视图并随光标移动更新。文本视图 resign responder 时把 displayMode 设为 .hidden 隐藏光标。
文本插入时,如果正在使用听写,帧更新会自动带动画发光效果。不需要发光效果时,从 automaticModeOptions 中移除 .showEffectsView。
切换输入模式时,光标下方会显示语言选择 UI。需要覆盖位置时,在 NSTextInputClient 实现中指定 preferredTextAccessoryPlacement。
光标滚动出屏幕时,系统显示 scroll-away indicator 指示光标相对位置,并提供返回听写位置的入口。自定义视图需要遵循 NSTextInputClient,实现 selectionRect 和 documentVisibleRect 属性,并在滚动开始/结束时调用 textInputClientWillStartScrollingOrZooming 和 willEndScrollingOrZooming。
国际化改进
动态行高
很多语言的 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
核心启发
-
用系统选择 UI 替换自定义实现
- 做什么:把自定义文本视图里的光标、选区手柄、放大镜换成系统提供的组件
- 为什么值得做:自动跟随系统样式更新,减少维护成本,获得新功能(如听写发光效果)
- 怎么开始:iOS 用
UITextSelectionDisplayInteraction,macOS 用NSTextInsertionIndicator
-
给文本中的关键词添加交互菜单
- 做什么:在 UITextView 中给特定关键词(如人名、地点、产品名)添加自定义菜单
- 为什么值得做:用户长按就能执行相关操作,不需要离开当前界面
- 怎么开始:用
UITextItemTagAttributeName标记范围,实现menuConfigurationFor:defaultMenu:delegate 方法
-
在富文本编辑器中加入列表支持
- 做什么:给文本编辑器添加有序/无序列表功能
- 为什么值得做:TextKit 2 原生支持,自动编号、自动本地化,不需要自己维护列表状态
- 怎么开始:设置
NSParagraphStyle.textLists,用NSTextList定义格式
-
确保多语言下的文本不被裁剪
- 做什么:检查应用中的 UILabel 和 UITextField 是否使用了 text styles,是否设置了 clipsToBounds
- 为什么值得做:泰语、印地语等语言的字符会超出标准行高,裁剪后影响可读性
- 怎么开始:把自定义字体换成
preferredFont(forTextStyle:),移除clipsToBounds = true
-
给 macOS 文本编辑器加上听写支持
- 做什么:让自定义 macOS 文本视图支持系统听写功能
- 为什么值得做:用户可以用语音输入,获得发光效果、边缘吸附、scroll-away indicator 等原生体验
- 怎么开始:用
NSTextInsertionIndicator替换自定义光标,设置effectsViewInserter,实现 NSTextInputClient 的相关属性
关联 Session
- What’s new in UIKit — UIKit 新特性概览
- What’s new in AppKit — AppKit 新特性,包含 macOS 文本光标改进
- Unleash the UIKit trait system — Trait 系统升级,与文本渲染相关
评论
GitHub Issues · utterances