iOS可访问性教程。实现自定义控件的可访问性¶
在这个iOS无障碍教程中,你将学习使用VoiceOver
、元素组、自定义动作、特质、框架等使自定义控件无障碍。
Version¶
- Swift 5, iOS 13, Xcode 11
苹果的可访问性是最好的。iOS
充满了可访问性功能,可以使你的应用程序超越其他应用程序。所以,当你能把它提升到11分时,为什么还要满足于10
分呢?
在这个iOS可访问性教程中,你将学会:
- 对自定义控件应用可访问性增强。
- 组织可访问性元素,使其更加清晰和可用。
- 通知用户用户界面的重大变化。
为了充分利用本教程,你需要一个iOS
设备来使用VoiceOver
。
Note
这个中级教程假定您能够使用Xcode
、UIKit
和编写Swift
来构建一个iOS
应用程序,并且对iOS
无障碍功能的运作有基本的了解,特别是VoiceOver
。如果您需要了解无障碍功能的入门知识,我建议您在开始入门之前先阅读iOS无障碍教程:入门。
本教程使用了[How To Make a Custom Control Tutorial: A Reusable Knob. ] (https://www.raywenderlich.com/5294-how-to-make-a-custom-control-tutorial-a-reusable-knob)中的可重复使用的旋钮控件。
开始使用¶
你的应用程序,Goes to 11
,几乎已经准备好向经典吉他音箱爱好者的热切观众推出,他们都准备好分享他们对经典吉他音箱的评价。每个放大器爱好者都可以提交一个从0到11的评级。然而,你的目标是接触广泛的受众,为此,你的应用程序应该是可访问的。
要开始,请使用本教程顶部或底部的下载材料按钮下载启动项目。
然后,打开启动项目。构建并运行:
你会看到该应用程序功能齐全。你可以在每个放大器之间向前和向后导航。你可以看到每个放大器的品牌、型号、描述和平均评分。此外,你可以点击添加你的评价按钮来评价一个放大器。当你点击添加你的评价时,屏幕会简单地展开到前一个屏幕。
这个样本项目看起来相当简单明了,对吗?
体验VoiceOver¶
要测试VoiceOver
,你需要一个实际的iOS
设备。在模拟器上,你可以使用辅助功能检查器,但它不会说出辅助功能信息,所以你无法听到你的元素描述将如何听起来。这是很不方便的!
你可以配置你的设备的辅助功能快捷键,以方便地切换VoiceOver
的开启或关闭。
请按以下步骤设置您的辅助功能快捷方式:
- 首先,打开
Settings
应用程序。 - 然后,导航到
Accessibility ▸ Accessibility Shortcut
。 - 最后,选择
VoiceOver
。
现在,你可以通过三连击主页或侧边按钮来激活或停用VoiceOver
。
在你开始之前,在激活VoiceOver
的情况下,用三根手指敲击屏幕以启用屏幕幕。在屏幕关闭的情况下,你必须依靠VoiceOver
的公告来浏览应用程序。如果你需要,可以再次三击屏幕来禁用屏幕幕。
Note
如果你对你的设备进行了配置,使三指三点的作用不是控制屏幕幕布,你必须使用三指四点来代替。
建立并运行。
现在,在你的设备上激活VoiceOver
和Screen Curtain
。
使用VoiceOver
,尝试在放大器之间导航,调整评级值并提交你的放大器评级。
当你在屏幕上的元素之间滑动时,你会听到什么?
这将是与此相近的东西,连字符代表刷卡。
放大器的详细信息,标题 - 品牌 - Vox - 型号 - AC30 - 9 - 0 - 11 - 添加您的评分 - 描述...。
目前,到达添加你的评级按钮需要从标题处刷8次。此外,你必须在所有的元素中来回滑动,才能得到当前屏幕的要点。用户可能永远也搞不清楚9-0-11
是什么。
你的应用程序可以做得比这更好!
将可访问性元素分组¶
在使用VoiceOver
时,有许多标签没有增加任何信息,使人们更难获得真正的信息。首先,打开Main.storyboard
。然后,在Amp Details Scene
下,选择以下标签。
- Brand.
- Model.
- Description.
- Highest Value.
- Lowest Value.
在选择每个标签时,取消选中Identity inspector
中的Accessibility Enabled
复选框。这将从VoiceOver
的公告中隐藏这些标签。
接下来,打开AmpDetailsViewController.swift
。
然后,在onSelectedIndexUpdate()
中的以下代码的正下方。
ratingLabel.text = "\(amp.rating)"
加上这个:
ratingLabel.accessibilityLabel = "Average rating is \(amp.rating) out of 11"
而且,就在下面:
brandLabel.text = amp.brand
加上这个:
brandLabel.accessibilityLabel = "\(amp.brand) \(amp.model)"
有了上面添加的代码,VoiceOver
将根据accessibilityLabel
进行阅读。
现在,打开Main.storyboard
。
然后,取消Model Label
的Accessibility Enabled
复选框。这一改变确保VoiceOver
不会将品牌读出两次。
现在,建立并运行该项目。保持VoiceOver
激活和Screen Curtain
开启。听听改进后的效果。
放大器的细节,标题 - Vox AC30 - 平均评分是9/11 - 添加你的评分...。
在三次滑动中,你可以听到所有的相关信息,在滑动之间找到添加你的评级按钮。这是一个巨大的改进,只需很少的工作,你不同意吗?不过,继续吧!
你可以设置视图控制器的accessibilityElements
数组来设置VoiceOver
读出视图元素的顺序。
首先,打开AmpDetailsViewController.swift
。然后,在viewDidLoad()
的开头添加这一行:
accessibilityElements = [
brandLabel,
ratingLabel,
descriptionLabel,
ratingButton,
nextButton,
previousButton
].compactMap { $0 }
accessibilityElements
是一个可选的Any
的数组。因为你把它设置为一个隐式解包的可选UIView
的数组,它们不能被隐式地投到Any
。
你通过调用.compactMap { $0 }
对所有的元素进行安全和简洁的解包来处理这个问题。
现在,构建并运行。试试VoiceOver
,注意它读出元素的顺序。它首先读出内容,然后是互动元素。你现在真的找到了你的节奏。
现在,用VoiceOver
选择Next
和Previous
按钮。请注意所发生的事情。即使屏幕上更新了新的放大器的细节,但按钮仍然是集中的。
这并不理想。
进行屏幕更新¶
还是在AmpDetailsViewController.swift
中,找到onSelectedIndexUpdate()
并在最后添加这一行。
UIAccessibility.post(notification: .screenChanged, argument: brandLabel)
screenChanged
是一个无障碍通知,当屏幕的一个主要部分发生变化时,你可以发布。它会发出小的boop
声。
你传递的参数可以是一个无障碍元素,一个String
,或nil
。传递一个元素,该元素就会在通知后获得焦点,VoiceOver
就会读出其标签。
如果你传递nil
,容器内的第一个可访问性元素就会获得焦点。如果你传递一个字符串,VoiceOver会读出该文本,然后是容器中的第一个可访问性元素。
建立和运行。听听你取得的好成绩。你仍然启用了屏幕幕布,对吗?
顺利! 它现在更流畅了。你可以轻松地浏览整个放大器的集合。
使用自定义无障碍操作的工作¶
自定义辅助功能动作从iOS 8
开始可用。它们是常见操作的快捷方式。
当一个容器有自定义无障碍操作时,VoiceOver
会宣布该操作是可用的。你可以向上或向下滑动来迭代动作选择。双击将激活该动作。
如果你添加了自定义动作,在VoiceOver
模式下浏览下一页和上一页将变得更加容易。这些动作使用户不必在所有的元素中滑动来寻找按钮。
首先,打开AmpDetailsViewController.swift
。然后,将这些方法添加到该类中:
@objc func nextAmpAction() -> Bool {
guard selectedIndex != dataSource.endIndex - 1 else { return false }
selectedIndex = min(selectedIndex + 1, dataSource.endIndex - 1)
return true
}
@objc func previousAmpAction() -> Bool {
guard selectedIndex != 0 else { return false }
selectedIndex = max(selectedIndex - 1, dataSource.startIndex)
return true
}
这些是你的自定义动作将调用的方法。它们分别简单地递增和递减所选的索引,同时确保它不会超出范围。可访问性API
要求这些方法返回Bool
并具有@objc
属性。
现在,在viewDidLoad()
中,在底部添加以下代码:
let next = UIAccessibilityCustomAction(
name: "Next amp",
target: self,
selector: #selector(nextAmpAction))
let previous = UIAccessibilityCustomAction(
name: "Previous amp",
target: self,
selector: #selector(previousAmpAction))
accessibilityCustomActions = [next, previous]
你可以看到,添加一个自定义动作很像为UIButton
设置动作。不同的是,VoiceOver
从无障碍自定义动作选择中读出自定义动作的名称,而不是让用户寻找,找到并点击按钮。
建立并运行。
在第一个公告后等待片刻。你会听到一个关于你的自定义动作可用的公告。
向上和向下滑动,选择一个自定义动作。然后,双击来激活它。
它们像按钮一样工作,但你以不同的方式与它们互动。
导航到列表的任何一端,你会看到灰色的上一个或下一个按钮。注意nextAmpAction()
和previousAmpAction()
在这种情况下如何返回false
。这导致了bonk
声音的播放,表明该动作是不可能的。
添加你的评级动作¶
还有一个自定义动作你应该添加。因为这都是关于经典放大器的评级,你应该用一个自定义的评级动作使评级动作尽可能的简单。
还是在AmpDetailsViewController.swift
里面,添加这个方法:
@objc func rateAmpAction() -> Bool {
performSegue(withIdentifier: "AddRating", sender: ratingButton)
return true
}
这个方法触发了现有的故事板分离,以进入AddRatingViewController
。剩下的就是将自定义动作添加到可访问性动作的列表中。
要做到这一点,首先在viewDidLoad()
中的previous
下面添加以下内容:
let rate = UIAccessibilityCustomAction(
name: "Add rating",
target: self,
selector: #selector(rateAmpAction))
然后,替换:
accessibilityCustomActions = [next, previous]
为:
accessibilityCustomActions = [rate, next, previous]
现在,构建并运行。测试一下你的新的自定义动作。
是时候面对音乐了。在使用VoiceOver
时,"添加你的评价"的体验是完全错误的。你甚至找不到评级旋钮! 但不要着急,有一个解决方案。
使拨号控制无障碍¶
这需要一个更好的方法:子类化UIAccessibilityElement
。你可以创建你自己的可访问性元素来代表可访问性系统的自定义元素。
首先,打开AddRatingViewController.swift
。在该类下面,添加以下新类:
class RatingKnobAccessibilityElement: UIAccessibilityElement {
override init(accessibilityContainer container: Any) {
super.init(accessibilityContainer: container)
accessibilityLabel = "Rating selector"
accessibilityHint = "Adjust your rating of this amp"
}
}
初始化器用适当的VoiceOver
值设置了可访问性标签和提示属性。
现在,在AddRatingViewController
的viewDidLoad()
中,在底部添加以下内容:
let ratingElement = RatingKnobAccessibilityElement(accessibilityContainer: self)
accessibilityElements = [cancelButton, ratingElement, addRatingButton]
.compactMap { $0 }
UIAccessibility.post(notification: .screenChanged, argument: ratingElement)
在这里,你设置accessibilityElements
数组,就像你对AmpDetailsViewController
所做的那样。唯一的区别是,其中一个元素是RatingKnobAccessibilityElement
的实例。你还发布了.screenChanged
通知,所以改变评级的元素首先得到关注。
注意到你把视图控制器作为accessibilityContainer
来传递?这很重要,你马上就会看到。
现在,打开Main.storyboard
。在Add Rating View Controller Scene
中,取消对Rating Label
、Highest Value
和Lowest Value
的Accessibility Enabled
复选框。你不需要它们被VoiceOver
读出来。
建立并运行。激活添加你的评级动作。
一旦你到了添加评级视图控制器,你现在可以在取消、评级选择器和添加你的评级之间滑动。由于你的RatingKnobAccessibilityElement
实例,VoiceOver
现在可以看到评级旋钮。但如果不能调整评级值,它就不太有用。
抓紧了,因为评级旋钮即将变得非常酷。
为自定义控件设置可访问性特征¶
打开AddRatingViewController.swift
,找到RatingKnobAccessibilityElement
并在init(accessibilityContainer:)
中添加以下内容:
accessibilityTraits = [.adjustable]
当你添加adjustable
时,你表示这个自定义元素有一个你可以递增或递减的值。这对你的评级旋钮来说是完美的。
Note
还有很多有趣的可访问性特征可用。你可以在苹果的文档中找到一个完整的列表。你只需要为你的自定义元素担心这些。所有的UIKit元素都已经设置了相应的特性。
构建并运行。然后激活添加评级。扫到评级选择器。你会听到很多新的信息。
*评级选择器,可调整,调整你对这个放大器的评级,用一个手指向上或向下滑动来调整数值。
然而,如果你向上或向下滑动,评级值就不会被调整。
支持无障碍手势¶
这就是你释放adjustable
的秘密力量的地方。你所需要做的就是覆盖accessibilityIncrement()
和accessibilityDecrement()
。
首先,在AddRatingViewController
中,添加这些方法:
func incrementAction() {
ratingKnob.setValue(ratingKnob.value + 1)
setRating(ratingKnob.value)
}
func decrementAction() {
ratingKnob.setValue(ratingKnob.value - 1)
setRating(ratingKnob.value)
}
这些是"可调整"手势将调用的方法。它们只是调整评级值和更新标签。
然后,在RatingKnobAccessibilityElement
中,添加这个辅助属性:
var ratingViewController: AddRatingViewController? {
return accessibilityContainer as? AddRatingViewController
}
在接下来的代码中,你会经常调用ratingViewController
,所以这将节省大量的输入。
接下来,给RatingKnobAccessibilityElement
添加以下方法重写:
override func accessibilityIncrement() {
ratingViewController?.incrementAction()
}
override func accessibilityDecrement() {
ratingViewController?.decrementAction()
}
建立并运行。如果你有屏幕幕布,请暂时关闭它,以便你能看到屏幕是否在更新。你应该发现,当你向上和向下滑动以调整数值时,旋钮实际上正在正确地更新。
然而,没有VoiceOver
公告。
重写无障碍值¶
对于旋钮评级的值,VoiceOver
的公告实现很简单。你需要重写accessibilityValue
属性。你可以在你的子类中重写这个属性。
在RatingKnobAccessibilityElement
中添加以下代码:
override var accessibilityValue: String? {
get {
guard let rating = ratingViewController?.rating else {
return super.accessibilityValue
}
return "\(rating)"
}
set {
super.accessibilityValue = newValue
}
}
建立并运行。VoiceOver
现在应该在你调整时宣布评级值。
容器空间中的可访问框架¶
在禁用屏幕幕布的情况下,你可能会注意到,当你将焦点切换到你的自定义元素时,它周围并没有画出框架。你需要覆盖accessibilityFrameInContainerSpace
来解决这个问题。
首先,在AddRatingViewController
中,添加以下代码:
var frameForAccessibilityElement: CGRect {
return ratingKnobContainer.convert(ratingKnob.frame, to: nil)
}
上面的代码将旋钮的框架转换为可访问性容器或视图控制器的视图的坐标空间。你必须这样做,因为这个旋钮是嵌套在另一个视图中。这将允许可访问性焦点框架在视图控制器的视图中正确定位。
然后,在RatingKnobAccessibilityElement
中覆盖以下属性:
override var accessibilityFrameInContainerSpace: CGRect {
get {
guard let frame = ratingViewController?.frameForAccessibilityElement
else {
return super.accessibilityFrameInContainerSpace
}
return frame
}
set {
super.accessibilityFrameInContainerSpace = newValue
}
}
建立和运行。然后,添加一个评级。轻扫选择旋钮控制。框架应该正确地环绕控制。
支持减少运动¶
由于旋钮控制可以以动画方式进入下一个位置,你也可以添加对无障碍的减少运动功能的支持。如果你的终端用户对运动效果或屏幕移动敏感,你通常会使用这个功能。
首先,打开Knob.swift
。
在类KnobRenderer
中找到setPointerAngle(_:animated:)
,然后替换:
if animated {
为:
if animated && !UIAccessibility.isReduceMotionEnabled {
在if
语句中,你决定是否根据设备的减少运动可访问性设置来对旋钮的旋转制作动画。
为了测试这一点,你需要进入你的设备的可访问性设置,并将Reduce Motion
设置为On
。你可以在Accessibility ▸ Motion
下找到这个设置。
建立并运行。现在,在安培之间导航。你应该看到旋钮值的变化而没有旋钮旋转的动画。
将无障碍设施提升到新的水平¶
现在,添加一个安培等级,使其成为11级。似乎有点反常吧?
如果一个扩音器应该得到最终的评价,应用程序应该以某种方式承认这一点。
在AddRatingViewController.swift
中,进入RatingKnobAccessibilityElement
,在accessibilityIncrement()
的末尾添加以下内容:
if let rating = ratingViewController?.rating, rating == 11 {
let message = NSAttributedString(
string: "Whoa! This one goes to 11!",
attributes: [.accessibilitySpeechPitch: 0.1,
.accessibilitySpeechQueueAnnouncement: true])
UIAccessibility.post(notification: .announcement,
argument: message)
}
UIAccessibility.Notification.announce
是一个无障碍通知,你可以用来让VoiceOver
宣布一些事情。我想这个名字已经很明显了。你可以传递一个NSAttributedString
,并为VoiceOver
注释特殊属性。
accessibilitySpeechPitch
控制声音的音调。它是一个在0.0
和2.0
之间的值。1.0
是正常的,0.0
是最低音,2.0
是最高音。
accessibilitySpeechQueueAnnouncement
控制字符串何时被宣布。指定true
以确保VoiceOver
在完成其他当前公告后排队公告。
如果是false
,当应用程序发布这个通知时,通知可以打断VoiceOver
正在宣布的事情。你不想打断原来的公告,而是在你的用户在评级旋钮上选择11
的时候,把它作为一个有趣的VoiceOver
复活节彩蛋。
建立并运行。试试吧。希望这将是一个有趣的惊喜:)
Note
还有更多的属性字符串可访问性属性可以探索。请看一下苹果文档中的所有内容。
就这样了! 你现在有了一个无障碍的应用程序。浏览这个应用程序是一个简单而清晰的体验,它甚至包括一个复活节彩蛋,供那些使用VoiceOver
的人使用!
干得好! :]
从这里开始去哪里?¶
你可以使用本教程顶部或底部的下载材料按钮下载最终项目。
当然,这并不是你利用iOS
的可访问性功能所能实现的极限。下面是几个你可能想尝试的挑战。
实现accessibilityPerformMagicTap()
。
魔术点击是一个特殊的无障碍手势,应该作为最可能的行动的快捷方式。例如,如果有一个来电,魔力敲击将接听电话,如果你再次执行该手势,就会挂断电话。也许对于你的应用程序,你可能会用它来触发添加评级或查看下一个安培。
实现accessibilityPerformEscape()
。
这是另一个标准的手势,用于解散屏幕上的当前模态视图。这对于解散AmpDetailsViewController
可能很有用。
最后,开始探索。iOS
的可访问性功能非常广泛。实现其中的任何一个,都会使你的应用程序立即超越所有没有考虑这些的应用程序。
苹果公司的文档是一座金矿,可以为你的应用程序改善可访问性提供思路。而且,[苹果可及性文档](https://developer.apple.com/documentation/uikit/accessibility)是一个很好的开始。
我希望你喜欢这个iOS
可及性教程。如果你有任何问题或意见,请加入下面的论坛讨论。