SwiftUI:辅助功能——简单介绍
简介
使您的应用程序无障碍意味着要采取措施以确保每个人都可以充分使用它,无论他们的个人需求如何。例如,如果他们是盲人,那么您的应用应该可以与系统的VoiceOver(旁白)系统配合使用,以确保您的UI可以顺利阅读。
SwiftUI免费为我们提供了大量功能,因为它的VStack
和HStack
布局系统自然会形成视图流。但是,它并不完美,您随时可以添加一些额外信息来帮助iOS无障碍系统,这可能会有所帮助。
通常,测试应用程序的最佳方法是启用VoiceOver支持并在真实设备上运行该应用程序——如果您的应用程序与VoiceOver配合使用效果很好,那么您很有可能已经远远超出了iOS应用程序的平均水平。
无论如何,在该技术项目中,我们将研究一些辅助功能技术,然后研究我们之前进行的一些项目,以了解它们如何升级。
目前,请使用Single View App模板创建一个新的iOS应用。您应该在真实设备上运行该项目,以便可以启用VoiceOver真实功能。
使用有用的标签标识视图
在该项目的文件中,我放置了从Unsplash下载的四张图片。Unsplash 文件名由图片ID和摄影师的姓名组成,因此,如果将其拖到资产目录中,则会看到它们的名称,例如“ales-krivec-15949”等。这本身不是问题,事实上,我认为这可能是记住资产来源的有用方法。但是,这确实给屏幕阅读器带来了问题。
要开始使用VoiceOver,我们将创建一个简单的视图,该视图随机浏览资产目录中的四张图片。将ContentView
结构体修改如下:
struct ContentView: View {
let pictures = [
"ales-krivec-15949",
"galina-n-189483",
"kevin-horstmann-141705",
"nicolas-tissot-335096"
]
@State private var selectedPicture = Int.random(in: 0...3)
var body: some View {
Image(pictures[selectedPicture])
.resizable()
.scaledToFit()
.onTapGesture {
self.selectedPicture = Int.random(in: 0...3)
}
}
}
那里没有什么复杂的,但已经有助于说明两个严重的问题。
如果您尚未在iOS设备的“设置”应用中启用VoiceOver,请立即启用:“设置”>“辅助功能”>“VoiceOver”,然后将其打开。
重要说明:VoiceOver切换开关的正下方是有关如何使用它的说明。您过去的常规轻击和滑动操作不再以相同的方式进行,因此请阅读这些说明!
现在,在您的设备上启动我们的应用,然后尝试在图片上点击一次以将其激活。如果您认真听VoiceOver,您应该会听到两个问题:
- 读出“Kevin Horstmann 一四一七零五”不仅对用户没有帮助,因为它根本无法描述图片,而且实际上令人迷惑——长串数字弊大于利。
- 阅读完上面的字符串后,VoiceOver然后说“image”。是的,它是一幅图像,但是它也可以充当按钮,因为我们添加了
onTapGesture()
修饰符。
这些问题中的第一个是SwiftUI试图给我们提供开箱即用的明智的可访问性行为的副作用:当给定图像时,它会自动使用图像的文件名作为文本来读出。
通过附加两个修饰符:.accessibility(label :)
和.accessibility(hint :)
,我们可以控制VoiceOver为给定视图读取的内容。它们都采用包含我们想要的任何内容的文本视图,但是它们具有不同的用途:
- label 标签会被立即读取,并且应该是一小段文字。如果此视图从用户数据中删除某项,则可能会说“删除”。
- hint 提示会在短暂的延迟后读取,并应提供有关视图用途的更多详细信息。例如,它可能会说“从收件箱中删除电子邮件”。
无障碍标签正是我们解决第一个问题所需要的,因为这意味着我们可以保留图像名称,而让VoiceOver读出有助于用户的内容。
首先,将第二个图像描述数组添加为ContentView
的属性:
let labels = [
"Tulips",
"Frozen tree buds",
"Sunflowers",
"Fireworks",
]
现在,将此修饰符附加到图像上:
.accessibility(label: Text(labels[selectedPicture]))
无论存在什么图像,VoiceOver都可以读取正确的标签。当然,如果您的图片不是随机更改,则可以直接在修改器中输入标签。
第二个问题是图像被识别为图像。这是不言而喻的事实,但它也无济于事,因为我们在其上附加了轻按手势,因此实际上是一个按钮。
我们可以使用另一个修饰符.accessibility(addTraits:)
来解决第二个问题。这使我们可以向VoiceOver提供一些幕后信息,以描述视图的工作方式,在本例中,通过添加此修饰符可以告诉我们图像也是按钮:
.accessibility(addTraits:.isButton)
如果需要,您也可以删除图像特征,因为它并没有多有用:
.accessibility(removeTraits: .isImage)
通过这些更改,我们的UI可以更好地工作:VoiceOver现在可以读取图像内容的有用描述,还可以使用户意识到图像也是按钮。
无障碍访问内容的隐藏和分组
如果您与活跃的VoiceOver用户一起度过了几分钟,您将很快学到两件事:它们非常擅长在用户界面上导航,并且它们还经常将阅读速度设置得非常快-比您或我使用的速度更快。
在设计用户界面时,必须同时考虑这两个方面:这些用户不仅仅是出于好奇而尝试VoiceOver,而是依靠它来访问您的应用的VoiceOver高级用户。因此,重要的是要确保我们的UI尽可能消除混乱,以便用户可以快速浏览它,而不必听VoiceOver读取无用的说明。
除了设置标签和提示,我们还有几种方法可以控制VoiceOver读出的内容。我要特别关注三个:
- 将图像标记为对VoiceOver不重要。
- 隐藏无障碍访问系统中的视图。
- 将多个视图分组为一个。
例如,我们可以通过使用Image(decorative:)
告诉SwiftUI特定的图像在那里使UI看起来更好。无论是简单的要点还是应用中的吉祥物动画,它实际上都无法传达任何信息,因此Image(decorative:)
告诉SwiftUI,VoiceOver应该忽略它。
像这样使用它:
Image(decorative: "字符")
如果图像具有某些重要特征,例如.isButton
——高亮显示时会说出“按钮”,并且如果我们附加了有效的轻击手势,则会使图像无法通过VoiceOver进行访问——但它不会读出图像的filename
作为自动的VoiceOver标签。除非您随后添加了标签或提示。
如果要更进一步,可以使用.accessibility(hidden :)
修饰符,该修饰符使所有视图对于辅助功能系统完全不可见:
Image(decorative: "character")
.accessibility(hidden: true)
使用该修改器,无论图像具有什么特征,该图像都对VoiceOver变得不可见。显然,只有在相关视图确实没有添加任何内容的情况下才应使用此选项——如果您将视图放置在屏幕外以使用户当前不可见,则应将其标记为VoiceOver也无法访问。
从VoiceOver隐藏内容的最后一种方法是通过分组,这使我们可以控制系统如何读取多个相关视图。例如,考虑以下布局:
VStack {
Text("Your score is")
Text("1000")
.font(.title)
}
VoiceOver将其视为两个不相关的文本视图,因此根据用户选择的内容,它将显示为“您的分数是”或“ 1000”。这两个都无济于事,这就是.accessibilityElement(children:)
修饰符出现的位置:我们可以将其应用于父视图,并要求它将子级组合为单个可访问性元素。
例如,这将导致两个文本视图一起阅读:
VStack {
Text("Your score is")
Text("1000")
.font(.title)
}
.accessibilityElement(children: .combine)
当子视图包含单独的信息时,这确实很好,但是在我们的情况下,子视图确实应该作为一个单独的实体阅读。因此,更好的解决方案是使用.accessibilityElement(children:.ignore)
,以便VoiceOver不可见子视图,然后为父视图提供自定义标签,如下所示:
VStack {
Text("Your score is")
Text("1000")
.font(.title)
}
.accessibilityElement(children: .ignore)
.accessibility(label: Text("Your score is 1000"))
值得尝试这两种方法,以了解它们在实践中的不同之处。使用.combine
会在两段文字之间添加一个停顿,因为它们不一定设计为可以一起阅读。使用.ignore
和自定义标签意味着可以一次阅读所有文本,而且更加自然。
读取控件的值
默认情况下,SwiftUI为其用户界面控件提供VoiceOver读数,尽管这些通常很好,但有时它们根本无法满足您的需求。
一个很好的例子是Slider
控件,VoiceOver会以一系列百分比的形式读出它。如果您使用百分比,那么这是有道理的,但是如果您没有使用百分比,则可以使用accessibility(value:)
修饰符覆盖VoiceOver读出的值,以提供一些替代文本。
为了说明这一点,下面是一个滑块,要求用户输入介于0到50之间的估算值:
@State private var estimate = 25.0
var body: some View {
Slider(value: $estimate, in: 0...50)
.padding()
}
如果运行,您会听到VoiceOver会将值读取为百分比,这毫无意义。为了解决这个问题,我们可以使用accessibility(value:)
修饰符提供自定义文本,如下所示:
.accessibility(value: Text("\(Int(estimate))"))
这在SwiftUI在值更改时无法很好地更新UI的地方尤其重要。例如,除非您专门附加了自己的.accessibility(value:)
修饰符,否则步进更改时它现在不会读出值。我希望这只是一个可在不久的将来修复的错误,但是您可以自己使用以下代码来解决:
@State private var rating = 3
var body: some View {
Stepper("Rate our service: \(rating)/5", value: $rating, in: 1...5)
}
运行上面代码时,至少现在,您可以选择步进器并向上或向下滑动以更改值,但是VoiceOver不会在更改时读出值。我们可以通过添加值的自定义读数来解决此问题,如下所示:
.accessibility(value: Text("\(rating) out of 5"))
即使在Apple修复了该错误之后(我相信他们会!),能够控制精确的VoiceOver读数也非常重要。
译自
Accessibility: Introduction
Identifying views with useful labels
Hiding and grouping accessibility data
Reading the value of controls