第75天 项目 15 第二部分
今天,我们将回顾之前的三个项目,重点找出其中的无障碍问题并进行修复。这听起来可能有些枯燥,但请大家重新思考我们的目标:我们是否希望开发出能让所有人受益的软件?
我希望答案是“是的”。纽约有一位名叫格雷戈里·曼斯菲尔德(Gregory Mansfield)的律师,他一直为残疾人权益奔走,他曾写道:“无障碍设计不是慈善,不是慷慨之举,不是附加福利,也不是额外恩惠。你不是在‘给予’使用权——你是在‘确保’使用权。”
在学习今天的内容时,希望大家能惊喜地发现两点:一是这些无障碍优化其实非常简单,二是它们对代码其他部分的影响微乎其微。一旦意识到这一点,你可能会疑惑,为什么那么多应用开发者在让应用具备无障碍功能方面做得如此糟糕。
今天你需要学习四个主题,包括修复本课程中之前制作的三个项目。
- 在 SwiftUI 中处理语音输入
- 修复“猜国旗”(Guess the Flag)项目
- 修复“单词拼写”(Word Scramble)项目
- 修复“书虫”(Bookworm)项目
在 SwiftUI 中处理语音输入
作者:Paul Hudson 2024年1月19日
确保你的应用能很好地支持“语音控制”(Voiceover)后,接下来一个重要步骤是确保它能处理语音输入——即与苹果的“语音控制”(Voice Control)技术良好兼容,让用户可以通过说话来操作你的应用。
语音输入允许用户通过名称或数字激活控件,其中名称会根据你展示的内容自动生成。以下是一个简单示例:
Button("点击我") {
print("按钮已点击")
}由于按钮上明确显示了“点击我”,用户可以通过说“按下点击我”来激活它。这很方便,但实际情况往往更复杂。
例如,假设你有一些以总统名字命名的按钮,代码如下:
Button("约翰·菲茨杰拉德·肯尼迪") {
print("按钮已点击")
}用户说“点击约翰·菲茨杰拉德·肯尼迪”时按钮能正常激活,但如果还能识别“点击肯尼迪”甚至“点击JFK”岂不是更好?要是这三种说法都能识别呢?
这时,SwiftUI 需要我们提供一点额外帮助——使用 accessibilityInputLabels() 修饰符。该修饰符接受一个字符串数组,可将这些字符串附加到按钮上,这样用户就能通过多种方式触发按钮。因此,要让按钮支持三种不同说法触发,代码如下:
Button("约翰·菲茨杰拉德·肯尼迪") {
print("按钮已点击")
}
.accessibilityInputLabels(["约翰·菲茨杰拉德·肯尼迪", "肯尼迪", "JFK"])我们的目标是帮助用户通过他们觉得自然的方式激活控件——你可以提供任意数量的字符串,iOS 会监听所有这些字符串。
修复“猜国旗”项目
作者:Paul Hudson 2024年1月19日
早在第二个项目中,我们制作了“猜国旗”应用,它会显示三面国旗图片,让用户猜测对应的国家。基于你现在对“语音控制”(Voiceover)的了解,能发现这个游戏中的致命问题吗?
没错:SwiftUI 的默认行为是将图片名称作为“语音控制”的标签,这意味着使用“语音控制”的用户只需将焦点移到三面国旗上,系统就会播报哪一面是正确答案。
要修复这个问题,我们需要为每面国旗添加文字描述,描述要足够详细,让了解这些国旗特征的用户能正确猜测,但显然不能直接透露国家名称。
如果你打开这个项目的代码副本,会发现它使用了一个存储国家名称的数组,如下所示:
@State private var countries = ["Estonia", "France", "Germany", "Ireland", "Italy", "Nigeria", "Poland", "Spain", "UK", "Ukraine", "US"].shuffled()因此,最简单的添加标签的方式(无需修改现有代码结构)是创建一个字典,以国家名称为键,以无障碍标签为值。请将以下代码添加到 ContentView 中:
let labels = [
"Estonia": "旗帜有三条水平条纹,顶部为蓝色,中间为黑色,底部为白色。",
"France": "旗帜有三条垂直条纹,左侧为蓝色,中间为白色,右侧为红色。",
"Germany": "旗帜有三条水平条纹,顶部为黑色,中间为红色,底部为金色。",
"Ireland": "旗帜有三条垂直条纹,左侧为绿色,中间为白色,右侧为橙色。",
"Italy": "旗帜有三条垂直条纹,左侧为绿色,中间为白色,右侧为红色。",
"Nigeria": "旗帜有三条垂直条纹,左侧为绿色,中间为白色,右侧为绿色。",
"Poland": "旗帜有两条水平条纹,顶部为白色,底部为红色。",
"Spain": "旗帜有三条水平条纹,顶部为细红色条纹,中间为宽金色条纹(左侧带有纹章),底部为细红色条纹。",
"UK": "蓝色背景上有交错的红色和白色十字图案,既有正十字也有斜十字。",
"Ukraine": "旗帜有两条水平条纹,顶部为蓝色,底部为黄色。",
"US": "旗帜上有许多红白相间的条纹,左上角蓝色区域内有白色五角星。"
]接下来,我们只需为国旗图片添加 accessibilityLabel() 修饰符。听起来简单,但代码需要完成三件事:
- 使用
countries[number]获取当前国旗对应的国家名称。 - 将该名称作为
labels字典的键。 - 提供一个默认字符串,以防国家名称未在字典中找到(这种情况理论上不会发生,但做个安全保障总没错!)
综合以上三点,在国旗图片的其他修饰符下方直接添加以下修饰符:
.accessibilityLabel(labels[countries[number], default: "未知旗帜"])现在重新运行游戏,你会发现无论是否使用“语音控制”,它都能正常作为一款游戏运行。这正是无障碍设计的核心:如今,无论用户有怎样的使用需求,所有人都能享受这款游戏的乐趣。
修复“单词拼写”项目
作者:Paul Hudson 2024年1月19日
在第五个项目中,我们制作了“单词拼写”应用——系统会给出一个随机的8字母单词,用户需要用其中的字母组成新单词。这款应用在“语音控制”下大部分功能都能正常使用,没有完全“无法访问”的部分,但我们仍有改进空间。
要发现一个明显的痛点,只需尝试添加一个单词。你会看到单词会滑入提示下方的表格中,但如果用“语音控制”点击它,会发现播报效果很差:字母数量会被读成“五个圆形,图片”,而文本则是一个单独的元素。
有多种改进方法,但最佳方案可能是将这两个元素组合成一个组,让“语音控制”忽略其子元素,然后为整个组添加一个更自然的描述标签。
我们当前的代码如下:
Section {
ForEach(usedWords, id: \.self) { word in
HStack {
Image(systemName: "\(word.count).circle")
Text(word)
}
}
}要修复这个问题,我们需要将 HStack 内的元素组合在一起,以便自定义“语音控制”的表现:
Section {
ForEach(usedWords, id: \.self) { word in
HStack {
Image(systemName: "\(word.count).circle")
Text(word)
}
.accessibilityElement()
.accessibilityLabel("\(word),\(word.count)个字母")
}
}或者,你也可以将文本拆分,分别设置提示和标签,如下所示:
HStack {
Image(systemName: "\(word.count).circle")
Text(word)
}
.accessibilityElement()
.accessibilityLabel(word)
.accessibilityHint("\(word.count)个字母")无论选择哪种方式,重新尝试游戏时,你会听到播报变成了“spill,五个字母”,效果比之前好得多。
修复“书虫”项目
作者:Paul Hudson 2024年4月25日
在第十一个项目中,我们制作了“书虫”应用,用户可以存储自己读过的书籍的评分和评论,同时我们还引入了一个自定义的 RatingView UI 组件,用于显示1到5星的评分。
同样,应用的大部分功能在“语音控制”下表现良好,但评分控件却是一个明显的短板——它使用了多个独立的按钮来实现功能,无法传达“这些按钮共同代表评分”这一核心信息。例如,如果我点击其中一颗星星,“语音控制”会播报:“收藏,按钮,收藏按钮,收藏按钮”等等——这样的播报完全没有实际意义。
这本身就是个问题,而更严重的是,我们的 RatingView 设计初衷是可复用的——这类组件你可以从这个项目中提取出来,用到十几个其他项目中,这就意味着会有大量应用因它而存在无障碍缺陷。
我们将通过一种特别的方式来修复这个问题:首先使用一组相对简单的修饰符实现基础优化,然后再看看如何通过 accessibilityAdjustableAction() 获得更理想的效果。
我们的初步方案会用到两个修饰符,都添加到星星按钮上。首先,需要添加一个修饰符,为每颗星星提供有意义的标签,代码如下:
.accessibilityLabel("\(number == 1 ? "1颗星" : "\(number)颗星")")接下来,我们可以让“语音控制”在星星已高亮时,额外添加一个 .isSelected 特征。
因此,在之前添加的修饰符下方,再添加以下修饰符:
.accessibilityAddTraits(number > rating ? [] : [.isSelected])只需这两处微小的修改,这个组件就比之前改进了很多。
这种初步方案已经能满足基本需求,而且实现起来最简单,因为它基于你之前已经掌握的技能。不过,还有第二种方案我想介绍给大家,因为它能带来更实用的效果——对于依赖“语音控制”等工具的用户来说,操作会更高效。
首先,移除我们刚刚添加的两个修饰符,然后在 HStack 上添加以下四个修饰符:
.accessibilityElement()
.accessibilityLabel(label)
.accessibilityValue(rating == 1 ? "1颗星" : "\(rating)颗星")
.accessibilityAdjustableAction { direction in
switch direction {
case .increment:
if rating < maximumRating { rating += 1 }
case .decrement:
if rating > 1 { rating -= 1 }
default:
break
}
}这样会将所有子元素组合在一起,为其添加“评分”(Rating)标签,然后根据当前星星数量设置对应的值。同时,用户还可以通过滑动手势增减评分值,这比逐一操作多个独立图片要便捷得多。