VoiceOver

iOS 拥有各种辅助技术,但核心是 VoiceOver —— 苹果的屏幕阅读器系统。这显然主要有利于盲人和视障用户,但它的作用远不止于此:VoiceOver 会告诉你系统如何解释你的 UI,包括可见的内容和排序方式。

VoiceOver 用户非常擅长在用户界面中导航,而且他们还经常将阅读速度设置得非常快。在设计 UI 时,考虑到这两点非常重要:这些用户不仅仅是出于好奇而尝试 VoiceOver,而是依靠它来访问您的应用程序的 VoiceOver 高级用户。因此,我们必须确保 UI 消除尽可能多的混乱,以便用户可以快速浏览,而不必听 VoiceOver 阅读无用的描述。

如果您尚未在 iOS 设备上的“设置”应用中启用 VoiceOver,请立即执行以下操作:“设置” > “辅助功能” > “VoiceOver” 将其打开。或者您可以随时激活 Siri 并要求启用或禁用 VoiceOver。VoiceOver 开关的正下方是有关如何使用它的说明。


SwiftUI 试图提供开箱即用的可用性功能,例如当给定一个 Image 图像时,它会自动使用图像的“文件名”作为 VoiceOver 要读出的文本。但如果想要自定义 VoiceOver 读出的内容,可以通过两个修饰符:.accessibilityLabel().accessibilityHint()

accessibilityLabel 辅助标签

accessibilityHint 辅助提示

Button("Add", systemName: "person.crop.circle") {
    print("Adding friend…)
}
.accessibilityLabel("Add to group")
.accessibilityHint("Add Jess to the Bridgerton Fans chat group.")

//如果不想iOS读出一些特殊字符,可以用字符串的替换方法先处理一遍。例如把.换成空格停顿
.accessibilityLabel(crewMember.astronaut.name.replacingOccurrences(of: ".", with: ""))

面向 iOS 18 及更高版本,accessibilityLabel() 有另一个变体非常有用。

我们可以为它提供一个闭包来自定义 SwiftUI 的默认标签,从而允许我们添加现有的描述。例如,下面的代码将读取 SwiftUI 的默认文本标签,但在其前面添加单词“警告”以反映文本为粗体和红色的事实:

Text("This is an important message")
    .fontWeight(.bold)
    .foregroundStyle(.red)
    .accessibilityLabel { label in
		    Text("Warning:")
        label
    }

accessibilityAddTraits 添加特征

当 Image 视图,加上了 onTapGesture 修饰符后,它仍然会被程序识别为图像,于是在 VoiceOver 读出的最后,会读出“图像”一词。但实际上由于我们在它上面附加了点击手势,所以它实际上是一个按钮。这时我们可以使用修饰符 .accessibilityAddTraits() 来解决正确识别的问题。我们可以向 VoiceOver 提供一些额外的幕后信息来描述该视图的工作原理。

//例如以上例子,可以添加 accessibilityAddTraits 修饰符,来告诉程序该图像也是一个按钮:
Image(pictures[selectedPicture])
		.resizable()
    .scaledToFit()
    .onTapGesture {
		    selectedPicture = Int.random(in: 0...3)
    }
    .accessibilityLabel(labels[selectedPicture])
		.accessibilityAddTraits(.isButton)

accessibilityRemoveTraits 删除特征

如果你愿意,你也可以删除具体的特征,虽然实际上体验不出什么区别:

//比如,当图像被用作按钮时,我们不希望辅助系统读出图像特征,那就可以移除
Image(...)
.accessibilityRemoveTraits(.isImage)

元素选择尽可能符合语义

虽然上面的做法可以让 Image 实现类似按钮的可用性。但如果我们只使用常规按钮而不是带有点击手势的图像,则根本不需要自行添加和删除特征。所以我们应该尽可能使用按钮而不是 onTapGesture() ,那样更符合语义:

//这意味着代码修改如下:
Button {
    selectedPicture = Int.random(in: 0...3)
} label: {
    Image(pictures[selectedPicture])
        .resizable()
        .scaledToFit()
}
.accessibilityLabel(labels[selectedPicture])

进阶技巧

除了设置标签和提示之外,还有多种方法可以控制 VoiceOver 读出的内容:

单独朗读字符

大多数文本都可以作为单词阅读,但某些特殊文本(例如密码、股票代码和某些数字)必须通过 VoiceOver 逐个字母地阅读才能发挥作用。在 SwiftUI 中,可以使用 speechSpellsOutCharacters() 修饰符启用此功能,其用法如下:

Text("abCayer-muQai")
    .speechSpellsOutCharacters()

此修饰符返回另一个 Text 实例,这意味着您可以根据需要将其链接到其他仅 Text 修饰符。当将辅助功能元素分组在一起时,它的效果特别好,因此它们被视为一个整体,如下所示:

VStack {
    Text("Your password is")
    Text("abCayer-muQai")
        .font(.title)
        .speechSpellsOutCharacters()
}
.accessibilityElement(children: .combine)

decorative 标记图像为不重要

例如,可以使用 Image(decorative:) 告诉 SwiftUI 特定图像只是为了让 UI 看起来更好。无论是简单的要点还是应用程序吉祥物角色跑来跑去的动画,它实际上并没有传达任何信息,因此 Image(decorative:) 告诉 SwiftUI 它应该被 VoiceOver 忽略。

Image(decorative: "character")

accessibilityHidden 从辅助系统中隐藏

如果您想更进一步,可以使用 .accessibilityHidden() 修饰符,这使得任何视图对于辅助功能系统完全不可见。

使用该修改器,无论图像具有什么特征,它都对 VoiceOver 不可见。显然,只有当相关视图确实没有添加任何内容时,您才应该使用此选项 - 如果您将视图放置在屏幕外,因此当前用户看不到它,则您也应该将其标记为 VoiceOver 无法访问。

Image(decorative: "character")
    .accessibilityHidden(true)

accessibilityElement

.combine 将子视图归组

使用 .accessibilityElement(children: .combine) 可以将子视图归成一组,让他们一起被读出。

// 参考以下布局: VoiceOver 会将其视为两个不相关的文本视图,因此根据用户的选择,它会显示“您的分数是”或“1000”;
// 这两者都没有帮助,这就是 .accessibilityElement(children:) 修饰符的用武之地;
// 我们可以将其应用于父视图,并要求它将子视图组合成单个可访问性元素;

VStack {
    Text("Your score is")
    Text("1000")
        .font(.title)
}

//例如,这将导致两个文本视图一起阅读,并在它们之间短暂停顿:
VStack {
    Text("Your score is")
    Text("1000")
        .font(.title)
}
.accessibilityElement(children: .combine)

.ignore 让子视图不可见

VStack {
    Text("Your score is")
    Text("1000")
        .font(.title)
}
.accessibilityElement(children: .ignore)
.accessibilityLabel("Your score is 1000")

<aside> 💡 使用 .combine 会在两段文本之间添加暂停,而使用 .ignore 和自定义标签可以一次读取所有文本,会更加自然。

</aside>


accessibilityInputLabels 处理语音输入

确保应用能够与 Voiceover 良好配合后,下一步就是确保它也能处理语音输入 ,该技术允许用户通过对应用说话来控制您的应用。语音输入允许用户通过名称或数字激活控件,名称会根据您所呈现的内容自动生成。这是一个简单的例子:

Button("Tap Me") {
    print("Button tapped")
}
//因为屏幕上有“Tap Me”,所以可以通过说“Press Tap Me”来激活它。

//这很简洁,但事情往往更复杂。例如,假设您有带有不同总统姓名的按钮,如下所示:
Button("John Fitzgerald Kennedy") {
    print("Button tapped")
}

//如果能识别出“Tap Kennedy”甚至“Tap JFK”岂不是很棒吗?认出这三个人怎么样?
//这是 SwiftUI 需要我们使用 accessibilityInputLabels() 修饰符提供一些额外帮助的地方。它接受可以附加到我们的按钮的字符串数组,因此用户可以通过多种方式触发它。因此,要使用三个不同的短语触发按钮,我们将使用以下命令:
Button("John Fitzgerald Kennedy") {
    print("Button tapped")
}
.accessibilityInputLabels(["John Fitzgerald Kennedy", "Kennedy", "JFK"])

//目标是帮助用户使用他们认为自然的任何方式激活您的控件 - 您可以提供任意数量的字符串,iOS 将侦听所有字符串。

读取控件的值

默认情况下,SwiftUI 为其用户界面控件提供 VoiceOver 读出,尽管这些通常很好,但有时它们并不适合您的需要。在这些情况下,我们可以使用 accessibilityValue() 修饰符将控件的值与其标签分开,也可以使用 accessibilityAdjustableAction() 指定自定义滑动操作。

//例如构建一个视图,显示由各种按钮控制的某种输入,例如自定义步进器:
//这可能是您想要的点击交互方式,但对于 VoiceOver 来说不是很好的体验,因为所有用户每次点击其中一个按钮时都会听到“递增”或“递减”
struct ContentView: View {
    @State private var value = 10
    var body: some View {
        VStack {
            Text("Value: \\(value)")
            Button("Increment") {
                value += 1
            }
            Button("Decrement") {
                value -= 1
            }
        }
    }
}