Highlight
Apple 提供 Accessibility Custom Content API,让数据密集型界面把核心信息交给 VoiceOver 直接朗读,把补充信息放进 More Content rotor 供用户按需访问。
核心内容
一个列表单元格看起来很普通:狗的照片、名字、品种、性格描述、受欢迎程度、年龄、体重、身高。
VoiceOver 默认会按界面顺序朗读这些内容。用户滑到第一只狗时,会听到名字、品种、长句描述、排名、年龄、体重、身高。再滑到下一只狗,又重复一次。
问题出在信息密度。朗读太多,用户要在一长串语音里找重点。朗读太少,用户又拿不到完整信息。不同用户关心的信息也不同:有人先看年龄,有人先看体重,有人只想快速扫过列表。
Apple 在 Accessibility framework 中提供 Accessibility Custom Content API。开发者可以把元素拆成两层:辅助功能标签只保留核心内容,例如狗名和品种;详细字段放入自定义内容,交给 More Content rotor。
结果很直接。VoiceOver 聚焦到单元格时先说“Bailey, beagle. More content available.” 用户想听细节时,打开 More Content rotor,再逐项浏览年龄、受欢迎程度、体重、身高和描述。
这个能力解决的是数据密集界面的节奏问题。默认朗读保持短,详细内容仍然可访问,用户自己决定什么时候听。
详细内容
用 More Content rotor 承载补充信息
(01:50)
Accessibility Custom Content API 由 Accessibility framework 提供,演讲说明它在所有 Apple 平台可用。它的目标是让辅助技术按“适量”呈现信息。
在 iOS 上,用户把 VoiceOver rotor 转到 More Content,然后上下滑动浏览补充内容。在 macOS 上,VoiceOver 光标停在元素上后,可以用 VO + Command + / 打开 More Content 菜单。
关键 API 是 AXCustomContentProvider 和 AXCustomContent。
import UIKit
import Accessibility
class DogTableViewCell: UITableViewCell, AXCustomContentProvider {
var coverImage: UIImageView!
var name: UILabel!
var type: UILabel!
var desc: UILabel!
var popularity: UILabel!
var age: UILabel!
var weight: UILabel!
var height: UILabel!
override var accessibilityLabel: String? {
get {
guard let nameLabel = name.text else { return nil }
guard let typeLabel = type.text else { return nil }
return nameLabel + ", " + typeLabel
}
set { }
}
var accessibilityCustomContent: [AXCustomContent]! {
get {
let notes = AXCustomContent(label: "Description", value: desc.text!)
let popularity = AXCustomContent(label: "Popularity", value: popularity.text!)
let age = AXCustomContent(label: "Age", value: age.text!)
let weight = AXCustomContent(label: "Weight", value: weight.text!)
let height = AXCustomContent(label: "Height" , value: height.text!)
return [age, popularity, weight, height, notes]
}
set { }
}
}
关键点:
import Accessibility引入 Accessibility framework,AXCustomContent和AXCustomContentProvider来自这里。DogTableViewCell实现AXCustomContentProvider,表示这个 cell 可以提供自定义辅助内容。accessibilityLabel只返回name和type,让 VoiceOver 聚焦时先读最重要的信息。AXCustomContent(label:value:)把一个字段做成键值对,例如"Age"和age.text!。accessibilityCustomContent返回数组,数组顺序决定 VoiceOver 在 More Content rotor 中呈现字段的顺序。
用 importance 把关键信息提前朗读
(06:49)
有些补充信息仍然应该默认朗读。演讲里的例子是狗的年龄。对于想领养的人来说,年龄可能是决策字段,藏在 rotor 里会增加操作成本。
AXCustomContent 有 importance 属性。把它设为 .high 后,VoiceOver 聚焦到元素时会直接朗读这个字段,同时它仍然会出现在 More Content rotor 中。
import UIKit
import Accessibility
class DogTableViewCell: UITableViewCell, AXCustomContentProvider {
var coverImage: UIImageView!
var name: UILabel!
var type: UILabel!
var desc: UILabel!
var popularity: UILabel!
var age: UILabel!
var weight: UILabel!
var height: UILabel!
override var accessibilityLabel: String? {
get {
guard let nameLabel = name.text else { return nil }
guard let typeLabel = type.text else { return nil }
return nameLabel + ", " + typeLabel
}
set { }
}
var accessibilityCustomContent: [AXCustomContent]! {
get {
let notes = AXCustomContent(label: "Description", value: desc.text!)
let popularity = AXCustomContent(label: "Popularity", value: popularity.text!)
let age = AXCustomContent(label: "Age", value: age.text!)
age.importance = .high
let weight = AXCustomContent(label: "Weight", value: weight.text!)
let height = AXCustomContent(label: "Height" , value: height.text!)
return [popularity, age, weight, height, notes]
}
set { }
}
}
关键点:
let age = AXCustomContent(label: "Age", value: age.text!)创建年龄字段。age.importance = .high把年龄标记为高优先级。- VoiceOver 聚焦 cell 时会读出狗名、品种和年龄。
return [popularity, age, weight, height, notes]保留年龄在 rotor 中的访问入口。
在 SwiftUI 中添加自定义内容
(07:47)
WWDC21 也把同一能力带到 SwiftUI。最小做法是在视图上使用 accessibilityCustomContent modifier,传入 label、value 和可选的 importance。
struct SampleView: View {
var body: some View {
VStack {
Text(name)
Text(description)
}
.accessibilityElement(children: .combine)
.accessibilityCustomContent("Description", description, importance: .high)
}
}
关键点:
VStack里有Text(name)和Text(description)两段文本。.accessibilityElement(children: .combine)把子元素合并成一个辅助功能元素。.accessibilityCustomContent("Description", description, importance: .high)把描述字段加入自定义内容。importance: .high让这个描述在元素聚焦时直接被 VoiceOver 呈现。
整理 SwiftUI cell 的默认朗读内容
(08:10)
SwiftUI 会自动从 Text 推断辅助功能内容。数据密集型 cell 里,这会让描述和各个数值字段自动进入朗读队列。
演讲的处理方式是:先对补充字段使用 accessibilityHidden(true),避免它们进入默认 label;再把同样的数据通过 accessibilityCustomContent 加回来。
import SwiftUI
import Accessibility
struct DogCell: View {
var dog: Dog
var body: some View {
VStack {
HStack {
dog.image
.resizable()
VStack(alignment: .leading) {
Text(dog.name)
.font(.title)
Spacer()
Text(dog.type)
.font(.body)
Spacer()
Text(dog.description)
.fixedSize(horizontal: false, vertical: true)
.font(.subheadline)
.foregroundColor(Color(uiColor: UIColor.brown))
.accessibilityHidden(true)
}
Spacer()
}
.padding(.horizontal)
HStack(alignment: .top) {
VStack(alignment: .leading) {
HStack {
Text(dog.popularity)
Spacer()
Text(dog.age)
Spacer()
Text(dog.weight)
Spacer()
Text(dog.height)
}
.foregroundColor(Color(uiColor: UIColor.darkGray))
.accessibilityHidden(true)
}
Spacer()
}
.padding(.horizontal)
Divider()
}
.accessibilityElement(children: .combine)
.accessibilityCustomContent("Age", dog.age, importance: .high)
.accessibilityCustomContent("Popularity", dog.popularity)
.accessibilityCustomContent("Weight", dog.weight)
.accessibilityCustomContent("Height", dog.height)
.accessibilityCustomContent("Description", dog.description)
}
}
关键点:
Text(dog.description)仍然显示在界面上,但.accessibilityHidden(true)阻止它进入默认朗读。- 包含
popularity、age、weight、height的HStack也被隐藏,避免 VoiceOver 一次读完所有数值。 .accessibilityElement(children: .combine)把整个VStack作为一个可聚焦元素。.accessibilityCustomContent("Age", dog.age, importance: .high)让年龄默认朗读,并保留在 More Content rotor 中。- 后续四个
.accessibilityCustomContent把受欢迎程度、体重、身高和描述放入 rotor。
用 AccessibilityCustomContentKey 复用键
(08:57)
如果同一个自定义内容值会在 SwiftUI 层级中多处更新或移除,演讲建议创建 AccessibilityCustomContentKey 扩展。这样可以复用键,避免在不同位置重复写本地化字符串。
import SwiftUI
import Accessibility
extension AccessibilityCustomContentKey {
static var age: AccessibilityCustomContentKey {
AccessibilityCustomContentKey("Age")
}
}
struct DogCell: View {
var dog: Dog
var body: some View {
VStack {
HStack {
dog.image
.resizable()
VStack(alignment: .leading) {
Text(dog.name)
.font(.title)
Spacer()
Text(dog.type)
.font(.body)
Spacer()
Text(dog.description)
.fixedSize(horizontal: false, vertical: true)
.font(.subheadline)
.foregroundColor(Color(uiColor: UIColor.brown))
.accessibilityHidden(true)
}
Spacer()
}
.padding(.horizontal)
HStack(alignment: .top) {
VStack(alignment: .leading) {
HStack {
Text(dog.popularity)
Spacer()
Text(dog.age)
Spacer()
Text(dog.weight)
Spacer()
Text(dog.height)
}
.foregroundColor(Color(uiColor: UIColor.darkGray))
.accessibilityHidden(true)
}
Spacer()
}
.padding(.horizontal)
Divider()
}
.accessibilityElement(children: .combine)
.accessibilityCustomContent(.age, dog.age, importance: .high)
.accessibilityCustomContent("Popularity", dog.popularity)
.accessibilityCustomContent("Weight", dog.weight)
.accessibilityCustomContent("Height", dog.height)
.accessibilityCustomContent("Description", dog.description)
}
}
关键点:
extension AccessibilityCustomContentKey给自定义内容键增加静态属性。AccessibilityCustomContentKey("Age")创建可复用的年龄键。.accessibilityCustomContent(.age, dog.age, importance: .high)使用键对象替代字符串。- 复用键后,层级中其他位置可以更新或移除同一项内容。
核心启发
-
做什么:给商品列表做“核心信息 + 详细参数”的 VoiceOver 结构。 为什么值得做:商品卡片通常包含价格、规格、库存、促销和描述,默认朗读容易变长。Accessibility Custom Content 可以把规格和描述放入 More Content rotor。 怎么开始:让 cell 实现
AXCustomContentProvider,accessibilityLabel只保留商品名和价格,规格字段用AXCustomContent(label:value:)返回。 -
做什么:给宠物领养、房源、二手车这类筛选列表设置高优先级字段。 为什么值得做:年龄、面积、里程等字段会直接影响选择,用户不应每次都打开 rotor 才能听到。 怎么开始:为关键字段创建
AXCustomContent,设置importance = .high,再检查 VoiceOver 聚焦时的朗读顺序。 -
做什么:把 SwiftUI 复杂卡片的视觉文本和辅助功能文本分开维护。 为什么值得做:卡片可以继续显示丰富排版,VoiceOver 默认只读核心内容,补充字段通过
accessibilityCustomContent提供。 怎么开始:对描述和参数区域加.accessibilityHidden(true),在外层容器上使用.accessibilityElement(children: .combine)和多次.accessibilityCustomContent。 -
做什么:为会重复更新的字段定义
AccessibilityCustomContentKey。 为什么值得做:同一字段可能在父视图和子视图中被设置、更新或移除,复用键可以减少字符串重复。 怎么开始:扩展AccessibilityCustomContentKey,创建static var,再把.accessibilityCustomContent("Age", ...)改成.accessibilityCustomContent(.age, ...)。
关联 Session
- SwiftUI Accessibility: Beyond the basics — 讲解 SwiftUI 辅助功能预览、自定义控件语义、导航分组和 rotor。
- Support Full Keyboard Access in your iOS app — 介绍如何让 iOS 应用支持完整键盘导航。
- Bring accessibility to charts in your app — 讲解图表如何为辅助技术提供可导航的数据结构。
- Create accessible experiences for watchOS — 展示 watchOS 应用如何处理动态字体、辅助功能和 SwiftUI 可访问体验。
评论
GitHub Issues · utterances