原创文章,如需转载请在下面留言让我知道。不留言不在开头标明出处链接的坏同学,1字1元索赔
一直想谈谈 iOS 中的 Accessibility,很多做了多年 iOS 开发的同学竟然不知道这是什么。仔细想想,我自己也不是特别清楚,只知道这个东西对残疾人和应用的自动化测试都很重要,今天恰好看到了一篇非常不错的英文博客,那就顺便翻译一下吧
文章阅读时间大约30分钟
为什么要用 Accessibility?
在我们开始编码之前,一定要理解 Accessibility 带来的好处。
- 使用 Accessibility 设计我们的应用,不论是使用 KIF 框架还是 Xcode 中的 UI Testing 进行测试,都会更加容易。
- 你还可以通过让更多群体的人使用你的 App,从而拓宽你的市场和用户基础。
- 如果你在政府机构工作,必须遵守508 条例,该条例规定所有用户都必须可以使用任何软件或技术。(译者注:条例508是1973年颁布的美国劳工康复法案(Rehabilitation Act)的修正条例,这条联邦法例规定残疾人有权使用所有联邦政府发展、促成、维持或使用的电子和信息技术。)
- 在你的应用中实现 Accessibility,表示你愿意为每个用户做更多的努力,这是件好事。
- 很高兴了解到你正在做一件让某个人的生活发生微小而又显著的改变的事
说服你了吗?
各界人士,各个年龄段和来自不同背景的人都在使用智能手机上的 App,其中包括残疾人。在设计 App 的时候要考虑到可用性,以帮助那些视觉、行动、学习或听力有障碍的人使用你的 App。
在这篇 iOS accessibility 教程中,我们会把一个已有的 App 变得让视力障碍的人更容易使用。在这过程中,你会学到:
- 如何使用 VoiceOver(旁白)(译者注:旁白是一款可与 app 中的对象进行交互的屏幕阅读器,使用该功能的用户即使看不到界面,也可以操控界面。)
- 如何使用 Accessibility Inspector 检查你的 App
- 如何使用 UIKit 实现 accessibility 元素
- 如何为残疾人构建一个更好的用户体验
这个教程需要用到 Xcode 11.3 和 Swift 5.1。本教程假定你已经了解了基础的 Swift 开发。如果你还没接触过Swift,可以先学习一下这本书 Swift Apprentice。
注意:你得有一个真机才能使用 VoiceOver。辅助功能现在还不支持模拟器。
开始
在这个教程里面,我们会用到一个已经做好的,叫食谱(Recipe)的应用。这个应用包括一个食谱列表,每种食谱都一个制作难度等级。你还可以给自己做的菜进行评价。
点击原文下载链接(或使用:https://pan.baidu.com/s/1XcYX2fp2YwNJUDs5zy6wQg 密码:33gp),这里面包括我们要用到的所有东西。打开 begin 文件夹中的 Recipe.xcodeproj。
在你的设备上运行这个应用之前,需要修改一下工程的签名配置。
在 Xcode 左侧导航栏点击 Recipe 工程,之后选择同名的 target。选择 Signing & Capabilities 页面,之后在顶部点击 Debug。最后从下拉菜单选择你的 Team。
了解一个这个 Recipe 应用
现在,构建运行这个 App,熟悉一下它的功能
根视图控制器是一个包含图片的食谱列表,还有食物描述和制作难度等级。点击一个食谱,可以看到一个大图和食物的原料、制作方法。
还有更好玩的,你可以划掉原料列表中的条目,以确保你已经加入了必要的食材。还可以点击右上角的喜欢/不喜欢的emoji表情,表示你喜不喜欢自己做的食物。
食谱 App 的幕后
花几分钟自己熟悉一下 begin 文件夹中的工程源码。以下是一些要点:
- Main.Storyboard 包含 App 的所有视图场景。你应该已经注意到了,所有的 UI 元素都是标准的 UIKit 控件和视图。控件已经开启了辅助功能。
- RecipeListViewController.swift 管理根列表(table)视图,显示所有的食谱。使用一个
Recipe
对象做数据源(data source)。 - Recipe.swift 是数据模型对象,表示一个食谱。它包括加载所有食谱数组的工具方法,这些食谱将贯穿我们使用整个应用。
- RecipeCell.swift 是食谱列表根视图控制器的单元格(cell)。它会基于
Recipe
模型对象的数据,显示食谱的难度级别、名称和照片。 - RecipeInstructionViewController.swift 包括详情视图的控制器代码,用于展示食物的大图、原材料和烹饪方法。它有一个
UISegmentedControl
,用于切换原料和烹饪方法,其中烹饪方法是一个列表视图并用到了InstructionViewModel
。 - InstructionViewModel.swift 扮演着
RecipeInstructionsViewController
的数据源的角色。包括原材料和烹饪方法的描述、复选框(check box)的状态信息。 - InstructionCell.swift 定义了一个单元格,包含一个标签 (label) 和一个复选框,用于烹饪方法和原材料的列表。当我们勾选了复选框,这个单元格会把自己的文字画上中划线。
至此,你已经理解了这个应用的运行原理,是时候考虑怎么让它更加易用了。
开启 VoiceOver
iOS 自带 VoiceOver 这个屏幕阅读工具,它让用户无需观看屏幕就可以和软件进行交互。是专门为视力有问题的人士设计的。
VoiceOver 让视力受损的用户可以听到屏幕上的可视内容,并和它们进行交互。VoiceOver 会响应手势,并以声音的方式向用户传达屏幕上的内容或用户选择的内容。本质上,VoiceOver 是 UI 和用户触控输入之间的链接。
使用 VoiceOver 最便捷的方法是打开你设备上的设置,点击辅助功能 ▸ 辅助功能快捷键,之后选择 VoiceOver。
这会创建一个快捷键,此时你可以连按三次 home 键或侧面按键(全面屏的新设备没有 home 键时),在一个真机上开关 VoiceOver。
注意:在 VoiceOver 旁边有很多其他的辅助功能,包括反转颜色、增加对比度、色彩滤镜、降低白点值、缩放、切换控制等。在这个教程中,我们把精力主要放在 VoiceOver 上。
现在你已经启动了 VoiceOver,试一试吧。
如何使用 VoiceOver
VoiceOver 带有一些方便的手势预设,可以很轻松地浏览一个应用。下面是一些常用的应用内使用 VoiceOver 的手势:
- 单击屏幕上的任意位置,VoiceOver 会大声念出项目的辅助功能属性中的识别信息 (identifying information)。
- 向左或向右轻扫 (swipe),VoiceOver 会选择下一个可视辅助功能项目并大声念出该项目。右扫则向右向下移动,左扫移动方向相反。
- 向下轻扫可以逐个地念出每个字母(或汉字、符号等)。
- 双击选中聚焦的项目。
- 三指轻扫,向左或向右轻扫,可以在不同页面视图间前进或后退(译者注:例如网页的前进后退,或桌面左右滑动等)
想要查看完整的 VoiceOver 手势列表,请访问苹果官方 VoiceOver 手势手册。好了,现在你知道 VoiceOver 如何工作了,但是你的 App 和 VoiceOver 一起使用会是什么样的?我们在下一步中马上就会进行测试。
在食谱应用中尝试 VoiceOver
在真机上构建并运行应用,之后连按三次 home 键启动 VoiceOver。向左和向右轻扫以访问食谱列表。VoiceOver 会从左上至右下读取元素。从顶部的标题开始,然后是每个食谱的名称和配图的名称。
但是 VoiceOver 的使用体验有点问题:
- 图像,每个单元格中的图片描述都是这个,这个描述可没有太大作用。(译者注:实际测试中,iPhone X上系统可能会对图片内容自动进行识别并播报,但更老的iPhone 5s中则只会播报“图像”)
- VoiceOver 并没有念出每个食谱的难度等级,所以这个特征对视障人士就失去了意义。
现在我们已经发现了一些问题,你可能想立刻解决这些问题。但是在解决之前,我们得先了解一点辅助功能的工作原理。
辅助功能属性 (Accessibility Attributes)
要支持辅助功能,辅助功能属性是必须要实现的核心部分。VoiceOver 读取我们 App 中的元素信息,然后大声念给用户听。如果辅助功能属性没有配置得当,VoiceOver 就不能念出我们应用的必要细节。
一个辅助功能属性 (attribute) 有5种内容 (property):
- 标签(Label):一种简明的识别控件或视图的方式。例如返回按钮和食谱图像。
- 特性(Traits):特性描述了元素的状态、行为或用法。例如,一个按钮的特性可能是selected。
- 提示(Hint):描述元素可以完成的动作。例如:显示食谱详情。
- 尺寸(Frame):屏幕内元素的
CGRect
格式尺寸。VoiceOver 会念出CGRect
的内容。 - 值(Value):元素的值。例如,进度条或者滑块当前的值可能会念为:百分之5。
大部分UIKit元素已经把上面这些属性为我们做好了;你只需要再改进一些细节,以提升用户体验。如果我们创建了一个自定义的组件,大部分的属性就得自己动手设置了。
注意:这个食谱App使用的是标准的UIKit视图和控件,其中大部分辅助功能已经启用了,最多只需要我们修改一下属性字符串就可以了。对于那些用自定义元素的工程,请确保已经读过了我们的 iOS Accessibility Tutorial: Making Custom Controls Accessible 教程。
现在我们已经搞清楚 VoiceOver 是从哪里为用户获取的信息了,是时候了解一下这个可以帮我们发现并修复 App 中辅助功能缺陷的新工具了:Accessibility Inspector。
Accessibility Inspector 的使用
如果你正在改进一款 App 的辅助功能,找出可改进之处是一件既费时又容易出错的工作。幸运的是,有一个叫做 Accessibility Inspector 的工具可以帮助我们,这个工具的功能如下:
- 贯穿我们的应用整个运行期间,找出辅助功能的常见问题。
- 让我们可以在检查模式下,查看 UI 元素的辅助功能属性。
- 无需离开我们的应用,即可进行辅助功能元素的实时预览。
- 支持所有平台,包括 macOS、iOS、watchOS 和 tvOS。
在食谱App上使用 Accessibility Inspector 之前,先来看一下这个工具。首先,在 Xcode 菜单栏中点击 Xcode ▸ Open Developer Tool ▸ Accessibility Inspector 启动该工具。
这个检查器工具看起来应该是类似这样的:
Target Chooser 可以选择你想要进行检查的设备。可以是你的 MacBook Pro、iOS 设备或者模拟器。
可以在模拟器中进行辅助功能元素的实时预览。因为实时预览比听 VoiceOver 更快,所以这里是我们这篇教程的主战场。
在模拟器中构建并运行 App,然后把 Accessibility Inspector 的 target 修改为我们的模拟器:
现在我们已经打开了这个工具,可以看一下它有哪些非常实用的功能。
Inspection Pointer 的使用
注意:撰写本文时,最新版本的 Xcode 11.3 有个 bug,使用这个工具的时候可靠性不是很好。
选择 Inspection Pointer,就是界面中看起来像准星标志的按钮,这个功能类似我们在真机上开启了 VoiceOver。当我们开启了这个功能,就可以把鼠标指针悬停在任意 UI 元素上查看其属性。直接使用键盘和模拟器交互时,Inspection Pointer 不会起作用。
Inspection Detail 面板中包含了所有我们需要查看并和 App 中的辅助功能属性交互的内容:
- Basic: 显示当前高亮元素的属性内容。
- Actions: 我们可以在这里执行类似点击按钮、滚动视图这样的动作。点击这个面板中的 Perform 按钮,会在设备上执行某个动作。
- Element: 显示当前元素的类、内存地址和视图控制器。在撰写本文时,这里有时候工作不太正常。
- Hierarchy: 显示元素的视图层级树,让我们可以更容易地理解复杂的视图关系。
使用 Quicklook 检查 Xcode 中的音频
Xcode 11 在 Inspection Detail 面板中新增了一项功能,顶部的 Quicklook 里,我们可以在Xcode中听到本该在设备上听到的内容。这意味着我们可以在没有真机的情况下,检查用户使用我们 App 时会听到的内容。
当 App 在模拟器中运行时,点击播放按钮,Accessibility Inspector 会循环念出 App 中每个元素的描述。
如果你喜欢手动检查每个元素,可以点击 Quicklook 中的 暂停 按钮或 喇叭 按钮。点击上一曲或下一曲按钮,可以以你自己的节奏挨个检查每个元素。
(译者注:此处原文中有一张GIF动图,超出了图片大小限制,可点击超链接自行查看)
用这个功能,可比在真机上运行 App 然后再用 VoiceOver 快多了,特别是在开发阶段。一如既往,你可能还是想在真机上测试 App 的所有辅助功能。
通过审计 (Audit) 功能找出问题
Accessibility Inspector 中最有用的功能之一就是它的审计,它会给我们提示 App 辅助功能中的警告。来试一下这个功能,保持模拟器中的App正常运行在食谱列表页面。点击 Audit 图标,之后点击 Run audit。
可以看到 Accessibility Inspector 给出了一些警告,包括我们有些元素没有描述信息等。
点击某个警告后,相关的元素会在模拟器中、审计页面底部被高亮显示。
在我们的这个 App 中,单元格中的图像视图没有描述信息。所以 VoiceOver 不能为我们的用户播报图像的描述。
点击修改建议图标,就是圆圈里面一个问号的图标,对于每个警告都会给出一个修改建议。稍后我们会根据这些建议做修改。
点击眼睛图标会为应用截图。这对于需要创建准确的 bug 报告的测试人员很有用。
Accessibility Inspector 中还有一些实用的辅助功能设置。下面,我们来快速浏览一下这些功能。
额外的 Accessibility Inspector 设置
尽管这超出了这篇教程的范围,但是对于加深 Accessibility Inspector 的了解是有帮助的,我们可以测试下面的辅助功能设置:
- 反转颜色
- 增强对比度
- 减弱透明度
- 减弱动态效果
- 修改字号
不用再使用设置App去开启这些功能。Accessibility Inspector 现在只有这5种设置,但是苹果计划在未来添加更多设置。
Accessibility Inspector 可以为你测试应用节省时间。但是请记住,你还是应该手动地使用 VoiceOver 测试一下实际的用户体验如何。最后这一步可以帮你找出所有 Accessibility Inspector 没发现的问题。
现在我们已经了解了 Accessibility Inspector 的功能,接下来改造一下我们的 App 吧。
让食谱 App 更好用
在我们之前使用 VoiceOver 测试这个 App 的时候,你应该还记得图像的描述信息没太大用处。audit 工具已经提示了原因:image view 没有辅助功能标签。我们现在来修复一下。
在 Xcode 中打开 RecipeCell.swift,在文件底部增加下列代码:
// MARK: Accessibility
extension RecipeCell {
func applyAccessibility(_ recipe: Recipe) {
// 1
foodImageView.accessibilityTraits = .image
// 2
foodImageView.accessibilityLabel = recipe.photoDescription
}
}
上面的代码基于单元格的Recipe
对象补充了缺失的辅助功能属性。下面是它的原理:
-
accessibilityTraits
属性的设置,覆盖了辅助功能元素之前的类型特征。在这里,.image
表示他是一个图像。 - 在 VoiceOver 中会使用
accessibilityLabel
中的内容去描述这个元素。这里我们把他设置成了recipe.photoDescription
,这是一个图片的内容描述字符串。
现在,我们想要把这种设置同样应用到其他食谱单元格中。找到RecipeCell
类中的configureCell(_:)
。在方法的最后增加下面这行内容:
applyAccessibility(recipe)
每次创建一个新单元格,这行代码都会把 recipe 对象中的相关信息设置到辅助功能属性中。
在真机上构建并运行 App,连续点击3次 home 键启动 VoiceOver。测试一下菜单列表的图像描述是不是更加有意义了。
(译者注:原文中此处有YouTube视频,科学上网,自行查看)
好多了!相比简单地听到“图像”二字,可以听到图像的完整描述了。现在用户可以更加形象地想象出食物,而不是因为不知道图像上是什么而感到沮丧。
在模拟器中运行这个 App,再次打开 Accessibility Inspector 并进入食谱列表。清空所有警告,然后点击 Run Audit。
哇塞~没有描述信息的警告了耶!成功地为图像添加描述之后,这个视图的核心内容已经可以被完全访问了。
现在,是时候让食谱的难度等级变得更加易于访问了。
转换难度标签
在 Accessibility Inspector 里,我们已经注意到了有一种警告是 potentially inaccessible text(可能无法访问的文本),也就是说难度标签对于视力受损的用户是不可见的(译者注:难度标签看起来像是图片,实际是 emoji 表情,本质上是文本)。怎么修复这些警告呢?我们得让这些标签更加易于访问,并且要用更加有意义的描述去更新标签的辅助功能属性。
下一步,到 RecipeCell.swift 文件中,在applyAccessibility(_:)
方法的最后增加下面的代码:
// 1
difficultyLabel.isAccessibilityElement = true
// 2
difficultyLabel.accessibilityTraits = .none
// 3
difficultyLabel.accessibilityLabel = "Difficulty Level"
// 4
switch recipe.difficulty {
case .unknown:
difficultyLabel.accessibilityValue = "Unknown"
case .rating(let value):
difficultyLabel.accessibilityValue = "\(value)"
}
下面是代码的解释:
-
isAccessibilityElement
是一个标志,为true
时会让这个元素对于辅助功能来说是可见的。对于大多数 UIKit 类,默认为true
,但是 UILabel 的是false
。 -
accessibilityTraits
帮助标识辅助功能元素的特征。因为这个标签不需要任何交互,所以设置为了none。 - 之后,我们的 VoiceOver 就可以简明地识别出标签的含义了。
Difficulty Level
可以让用户清楚地知道他们在做的食物是什么难度。 - VoiceOver 会把
accessibilityValue
作为标签描述的一部分念出来。在这里设置好难度等级可以让这个元素变得更有帮助。
在真机上构建并运行 App,三击 home 键打开 VoiceOver,轻扫以收听整个食谱列表的播报。
(译者注:原文中此处有YouTube视频,科学上网,自行查看)
当我们扫过不同的辅助功能元素时,VoiceOver 会念出每个单元格的完整描述,包括难度等级。
检查警告
每次暴露一个新的辅助功能元素后,就像我们刚刚对难度等级做的处理,都应该重新运行一下审计 (audit)。
如果 Accessibility Inspector 没启动,先把它打开。在真机或者模拟器运行我们的 App,并设置好相应的检查 target。现在点击 audit 按钮然后点击 Run audit。
警告变少了!仅存的一种警告是标签不支持动态文字(字号)。我们接下来会修复这个问题。
支持动态文字(字号)
警告中明确提示了我们缺少一个让App对每个人都可用的重要步骤:动态文字(dynamic text)。这是一个辅助功能中的重要特性,可以让视力部分受损的用户加大字号,让文字更容易看清。我们现在的App使用了非动态的字体,导致用户不能使用这项功能。
点击 Fix Suggestions 图标,查看修改建议是什么:
建议里面说我们应该考虑用 UIFont 推荐字体 (preferred font),并把
adjustsFontForContentSizeCategory
设为true。现在就来动手吧。
在 RecipeCell.swift 文件的applyAccessibility(_:)
最底部增加下面的代码:
dishNameLabel.font = .preferredFont(forTextStyle: .body)
dishNameLabel.adjustsFontForContentSizeCategory = true
difficultyLabel.font = .preferredFont(forTextStyle: .body)
difficultyLabel.adjustsFontForContentSizeCategory = true
这里把preferredFont
设置为body
风格,也就是说 iOS 会把文字显示为文档主体内容的字体风格。具体字号和字体取决于辅助功能设置。adjustsFontForContentSizeCategory
表示当用户修改了文本内容尺寸时,是否应该自动更新字体。
感谢 Accessibility Inspector,测试 App 如何处理字号变化非常简单。
构建并运行食谱 App,打开 Accessibility Inspector。再次运行审计后所有警告应该都消失了。
测试其他设置项
进入 Accessibility Inspector 的设置页面,尝试一下其他的工具:
- Invert Colors,看一下你的应用是什么样吧。这个对那些有光敏感、弱视和某些情况下色盲的人很有用。
- 你也可以为那些喜欢更大字体的用户测试一下实时修改动态字体大小。
测试我们的App时,看起来应该是这样的:
Accessibility Inspector 使得测试辅助功能变得很容易。至此,我们可以说食谱列表能为那些视力受损的用户很好地工作了。
转换食谱详情页面
现在我们已经处理好了食谱列表,再来看看用户点击某个食谱之后是什么样的吧。在设备上运行 App,打开 VoiceOver,看看详情视图。听起来是这样的:
(译者注:原文中此处有YouTube视频,科学上网,自行查看)
在详情页面用VoiceOver交互有些问题:
- 左箭头按钮对于导航返回按钮来说不是个很好的描述。用户怎么会知道这个按钮是干什么的呢?
- emoji 表情状态是:心型眼的表情和困惑的表情。这种解释会让所有用户都感到懵圈!
- 当用户选择了一个复选框,会听到 icon empty,这个解释性不是很好。
上面的问题中,解释控件状态的含义是很重要的,而不是控件看起来是什么样子。返回按钮比左箭头按钮听起来更明了。喜欢和不喜欢可以很简洁地解释 emoji 表情。下面我们就来修改这两个问题。
修改导航栏返回按钮,打开 RecipeInstructionsViewController.swift 并在viewDidLoad
中的assert(recipe != nil)
后面添加下面的代码:
backButton.accessibilityLabel = "back"
backButton.accessibilityTraits = .button
现在 VoiceOver 不会再说左箭头按钮,而是返回(back)按钮。
再来看看 emoji 表情。还是这个文件中,用下面的代码替换掉isLikedFood(_:)
中的内容:
if liked {
likeButton.setTitle("", for: .normal)
likeButton.accessibilityLabel = "Like"
likeButton.accessibilityTraits = .button
didLikeFood = true
} else {
likeButton.setTitle("", for: .normal)
likeButton.accessibilityLabel = "Dislike"
likeButton.accessibilityTraits = .button
didLikeFood = false
}
为喜欢和不喜欢模式添加了一个accessibilityLabel
,这样按钮的功能就更明确了。我们还把accessibilityTraits
设置为了按钮,这样用户就知道怎么和这个 emoji 表情进行交互了。
在设备上构建运行并启动 VoiceOver。进入食谱详情页面,用 VoiceOver 测试一下我们对视图顶部的按钮所作的修改。
(译者注:原文中此处有YouTube视频,科学上网,自行查看)
现在,每个功能都清晰明确了,简洁的描述可以帮助用户理解这些功能的含义。好多了!
改进复选框
最后的问题是列表清单。对于每个复选框,VoiceOver 现在会念出 icon empty 然后念出食谱的制作方法。这一点都不清晰明了!
来修改一下,打开 InstructionCell.swift 然后找到shouldStrikeThroughText(_:)
。把if strikeThrough
代码块的所有内容替换为下列内容:
// 1
checkmarkButton.isAccessibilityElement = false
if strikeThrough {
// 2
descriptionLabel.accessibilityLabel = "Completed: \(text)"
attributeString.addAttribute(
NSAttributedString.Key.strikethroughStyle,
value: 2,
range: NSRange(text.startIndex..., in: text))
} else {
// 3
descriptionLabel.accessibilityLabel = "Uncompleted: \(text)"
}
这些代码做了什么:
- 把复选框按钮的辅助功能关掉,这样VoiceOver就可以把复选框和后面的文字当成一个整体,而不是两个不同的辅助功能元素。
- 现在
accessibilityLabel
的描述信息使用硬编码的"Completed"
后面接上制作方法文本。这样就把一个带复选框状态和标签内容的所有必要的信息都提供出来了。 - 和已完成的代码一样,如果用户把一个条目标记为未完成,就在标签描述前面增加
Uncomplete
。
再次构建运行 App,然后听一下播报。对用户来说这简直像美妙的音乐一样
(译者注:原文中此处有YouTube视频,科学上网,自行查看)
接下来呢?
你可以在文章开头下载到工程的完整代码。
在这篇 iOS accessibility 教程中,你学到了 VoiceOver 相关的内容。通过挨个元素播报内容的收听和亲自进行的用户体验的测试,我们执行了手动的检查。之后,我们又用 Accessibility Inspector 执行自动审计 (audit),看到了辅助功能元素的值,并操作了实时动态修改反转颜色、修改字号。
现在,你已经学会了如何让你的 App 更加好用。你将对某个人的生活产生积极的影响。
辅助功能还有很多内容。这篇教程只是帮助你入门。下面是更多参考资料:
- Categories of Accessibility
- Accessibility Development Resources
- Applying Accessibility to Custom Views
- Deliver an Exceptional Accessibility Experience
- Accessibility Inspector
如果有任何疑问或指教,在下面留言吧!
[The End]