大多数浏览器和
Developer App 均支持流媒体播放。
-
在 SwiftUI 和 UIKit 中创建易于访问的 App
了解如何利用 UI 框架的优化来更轻松地创建丰富、易于访问的体验。了解 VoiceOver 等技术是如何通过辅助功能特征及操作来更好地与 App 界面进行交互的。我们将分享 SwiftUI 的最新更新来帮助你完善辅助功能体验,并向你展示如何在 UIKit App 中保持辅助功能信息同步更新。
章节
- 0:00 - Welcome
- 1:30 - Explore the toggle trait
- 2:46 - Discover multi-platform accessibility announcements
- 3:58 - Assign priority to announcements
- 6:36 - Meet the zoom action
- 8:00 - Refine VoiceOver direct touch experiences
- 11:08 - Customize accessibility content shapes in SwiftUI
- 12:48 - Keep accessibility attributes up-to-date in UIKit using block-based setters
资源
相关视频
WWDC23
-
下载
♪ ♪
Allison:大家好 我是 Allison 一名辅助功能工程师 今天我们会介绍一些 激动人心的新方法 使你的 App 更易访问 在 Apple 辅助功能是我们 所构建内容的重要组成部分 因为我们坚信 每个人都有使用科技的权利 我们希望你能轻松自如地 使你的 App 易于访问 过去几年里 我们一直在做各种优化 来确保每个人都能在 你的 App 中拥有最佳用户体验 这一章节 我们将探索使人们 以激动人心的新方式 与你的 App 交互的 API 然后 我们将探讨 如何在 SwiftUI App 提升你内容的辅助功能视觉 最后 我们来学习一个 在 UIKit 中使你的辅助功能属性 同步更新的理想方法 我们首先来讲解辅助功能优化 我一直在开发一个照片编辑 App 它使我可以给 来自图库或相机的照片 添加酷炫的修图效果 我可以使用不同的滤镜 改变照片的色调 或使用 App 内置的琴键 创建自定义音效 以匹配图片 我们来介绍几个可以整合到 App 上的辅助功能优化 我的照片 App 的滤镜页面中 存在一个有开关状态的自定义按钮
“Filter” 开关按钮使我们 切换图像滤镜的开启或关闭 对于这一自定义 UI 系统无法识别正确的 辅助功能提示或标题 我们想确保提供一个适配于 其它系统切换的辅助功能体验 这正是全新辅助功能特征 isToggle 派上用场的时候 我们有一个代表滤镜按钮的 struct 在 struct 的 body 中 我们创建一个按钮 当按下时滤镜会切换 按钮的颜色随滤镜状态变量改变 我们把 isToggle 特征 加入 accessibilityAddTraits 修饰符中的滤镜按钮 isToggle 提供了恰当的 辅助功能提示 及 “开关按钮”描述 旁白:滤镜 开关按钮 连按两下以切换设置
Allison:UIKit 中也存在 全新切换特征 在 viewDidLoad 方法中 我们设定按钮视图 然后 我们将按钮上的 accessibilityTraits 属性设置为 包含 .toggleButton 在照片滤镜 App 中 我想给照片导航栏按钮 添加一个新通知来使人们 知道照片视图正在加载 辅助功能通知是一个全新 API 可以帮我们实现这一功能 辅助功能通知提供了一个 统一的多平台方法 来创建通知 并将信息传递到 使用你 App 辅助技术的用户那里 在 SwiftUI、UIKit 及 AppKit 运行的 App 可以创建辅助功能通知 借助 AccessibilityNotification 你可以使用 Swift 的原生态方法 发送公告、更改布局、更改屏幕及 接受屏幕滚动通知 按下照片工具栏按钮 意味着我想发出一个公告 旁白:照片 按钮 照片 载入照片视图 Allison:我们可以在工具栏 按钮的操作中发布该公告 我们可使用 AccessibilityNotification.Announcement 并以“载入照片视图” 作为字符串参数来创建公告 在 App 中 当相机导航栏按钮按下时 意味着我还想创建三个公告 第一个公告 “开启相机” 及第三个公告 “相机激活” 至关重要 我们来看看旁白 现在的公告语音模式 请注意第二个公告 “相机载入” 是如何打断“开启相机”的 旁白:相机 按钮 完成 开启 相机 相机激活 Allison:现在 在 SwiftUI 和 UIKit 中 你还可以设置每个公告的优先等级 使你可以对等待被辅助技术说出的 公告的重要性进行设置 这可以使你更好地控制 哪些是人们需要听到的公告 哪些是即便没有被 及时说出也可忽略的公告 你可以使用高、默认和低 三种公告优先等级中的一种 来详述这一信息的重要性 高优先级公告可以打断其它语音 而且一旦开始就不会被打断 默认优先级公告可以打断现有语音 但当新的语音开始时 可以被打断 低优先级公告在队列中等候 如果没有新的公告开始 它就会 在其它语音结束后被说出 在照片 App 中 我们可以用公告优先等级 处理打断的字符串 我们用属性字符串创建了三个公告 在 SwiftUI 中 我们把优先等级设定到 accessibilitySpeechAnnouncementPriority 字符串属性上 我们的第二个公告 “相机载入” 最不重要 所以我们将它设为低优先级 最后一个公告 “相机激活” 是最重要的 所以我们将它设为高优先级 然后 我们把属性字符串 加入 AccessibilityNotification 首先 我们将发送默认优先级公告 然后是低优先级 最后是高优先级 现在注意低优先级公告不再打断 默认优先级公告 而高优先级公告 打断了默认和低优先级公告 旁白:相机 按钮 完成 开启相机 相机激活 Allison:我们可以 在 UIKit 中实现同样的公告顺序 我们把公告优先等级设为 NSAttributedString 的 key 和 value 我们使用 key UIAccessibilitySpeechAttribute AnnouncementPriority 并把 value 设为 相应的 UIAccessibilityPriority 然后我们给属性字符串 初始化程序赋予这些属性 回到 App 有一个人们可以 通过实际触控或捏住 来放大缩小图片的视图 当辅助技术开启时 实体触控或捏住的动作很难完成 现在 通过辅助功能缩放操作 当辅助技术开启时 人们可以对 UI 元素放大或缩小 我们来给图片添加缩放操作 图片位于 ZoomingImageView struct 的 body 之中 首先我们加入 accessibilityZoomAction 修饰符 然后 基于缩放操作的方向 我们会把内容缩小或放大 并发送一个辅助功能通知公告 现在 让我们通过这些更改 来探索旁白的缩放功能 旁白:缩放图片视图 图片 缩放 2x 放大 3x 放大 4x 放大 3x 放大 Allison:我们也可以 在 UIKit 中添加缩放特征及操作 我们首先创建一个 包含图像视图的缩放视图 然后 我们将 supportsZoom 特征 同图像特征一起加到缩放视图 然后 我们通过使用 accessibilityZoomInAtPoint 和 accessibilityZoomOutAtPoint 返回的 boolean 来表示缩放成功或失败 在这些方法中 我们会更新缩放大小 并发送一个公告 来表示缩放大小的改变 在图片滤镜 App 中 我们也可以通过敲击小琴键 来给图片添加一个短音频 人们可以用琴键 为图片创建自定义音调 我们来检验一下 当我想创建一个音调时 现有的 伴有音效的旁白体验感如何
每当元素被触碰到 旁白都会读出 key 的标签 并播放其触发音 这使得快速连续敲击琴键很难 一般来说 旁白提供 一个安全的探索体验 但有时 为了合理使用你的 App 人们需要直接与之交互 对于这一 App 最好是 人们可以直接触碰琴键 而不播放额外的语音和音效 这是在我们的视图上 使用直接触控特征 allowsDirectInteraction 的绝佳时机 辅助功能直接触控区域 将使你可以具体规定 旁白手势直接传递给 App 的 屏幕区域 在默认状态下 旁白既会读出 又会激活直接触控元素的内容 然而 对于这一 App 最好是当人们触碰某个琴键时 旁白能够静音 这样人们就能立刻听到音调 而无需先激活琴键元素 除了 allowsDirectInteraction 特征 现在我们支持 两个新的直接触碰选项 首先 你可以明确规定 silentOnTouch 来确保当触碰到直接触碰区域时 旁白保持静音 以便你的 App 可以给出自己的声音反馈 其次 你可以明确规定 requiresActivation 使直接触碰区域在触控传递发生前 需要旁白激活相应元素 这是 KeyboardKeyView 的代码片段 每个琴键都是一个 能播放特定声音的长方形 为了解决旁白 总在播放声音时讲话的问题 我们把按钮的 直接触控选项设为触碰时静音 现在 一旦旁白来到琴键按钮 正确的音调就会播放出来 而不受旁白语音的影响 我们也可以把 新的直接触控选项加到 UIKit 中 我们可以把琴键按钮 创建为 UIButton 然后 我们加入 allowsDirectInteraction 辅助功能特征 在 UIKit 中设定 辅助功能直接触控选项时 需要添加这一特征 最后 我们给 accessibilityDirectTouchOptions 添加 silentOnTouch 选项 借助辅助功能切换特征、 公告优先等级、缩放特征 以及直接触控选项 你对辅助技术与你的 SwiftUI 及 UIKit App 交互的方式有了更多控制权 接下来 我们讲解在 SwiftUI 中的 辅助功能内容形状种类 这一种类设定辅助功能元素的路径 并控制它们在屏幕上的形态 之前 交互内容形状种类 改变了辅助功能形状 和点击测试形状 现在有一个辅助功能内容形状的种类 不会影响点击测试形状 只会影响辅助功能内容的形状 当某一元素需要特定的形状 比如圆形 计算机辅助功能光标视图 可能会阻挡屏幕上的其它项目 在这里 辅助功能路径是一个方块 和红色圆形内容不匹配 当辅助功能内容形状种类 应用于一个视图时 它会按修饰符提供的形状 更新元素基本的辅助功能几何图形 这使你借助现有 SwiftUI 形状 快速更新元素路径 我用一个圆形图片 创建了一个圆形按钮 我们可以设置 frame 及辅助功能标签 以匹配红色 最后 我们可以给视图添加 类型为辅助功能及形状为圆形的 内容形状修饰符
现在 辅助功能路径正确地匹配了 红色按钮的圆形形状 最后 我们来介绍 UIKit 辅助功能的一个附加项 基于 block 的属性设置器 在照片编辑 App 中 我希望图片视图的辅助功能值 可以表示照片是否添加滤镜 现在 有一个简单的方法 使我视图的基本辅助功能属性 总是与呈现出的 UI 保持一致 这个方法就是 使用辅助功能基于 block 的设置器 新的辅助功能 block API 使你提供一个闭包 该闭包不直接存储值 而是每当属性被调用时接受评估 每当辅助技术引用或访问视图时 闭包都被重新评估 利用在视图控制器的 viewDidLoad 方法中 创建的闭包 我们可以化繁为简 我们把 accessibilityValueBlock 属性设定到 zoomView 上 使值基于图片 是否添加滤镜进行同步更新 闭包必须返回该属性的正确类型 即一个可选的字符串 注意我们给 self 用了弱引用 来避免循环引用 最好在一个类循环周期的开始 添加分块 以恰当的 辅助功能属性信息开启一个类 现在 辅助功能属性更加易于维护 每当有人把旁白光标 移到一个新的元素时 旁白首先会寻找闭包设定的属性 并重新评估闭包
当你在创建自定义 UI 时 考虑加入辅助功能特性 比如切换 以及像直接触控交互等特质 来提升用户的使用感
其次 考虑在你的视图中 使用 SwiftUI 的自定义形状 如果辅助功能形状与 UI 不匹配 考虑引入一个自定义辅助功能形状 最后 我建议你评估 你设置辅助功能属性的方式 并且辨别基于 block 的设置器 是否更有助于你的 App 在 Apple 我们坚信 使用辅助功能是人们的权利 在你的帮助下 我们可以创造技术 使每个用户的生活得到改善 且更加自主 这些新增加的 API 对于那些依赖辅助功能的用户 是提升你 App 可用性的绝佳途径 我建议你使用所有这些附件 来构建出色且易于访问的 App 感谢你的观看
-
-
1:54 - Add the accessibility toggle trait
import SwiftUI struct FilterButton: View { @State var filter: Bool = false var body: some View { Button(action: { filter.toggle() }) { Text("Filter") } .background(filter ? darkGreen : lightGreen) .accessibilityAddTraits(.isToggle) } }
-
2:31 - Add the accessibility toggle trait with UIKit
import UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let filterButton = UIButton(type: .custom) setupButtonView() filterButton.accessibilityTraits = [.toggleButton] view.addSubview(filterButton) } }
-
3:43 - Post an accessibility notification
import SwiftUI struct ContentView: View { var body: some View { NavigationView { PhotoFilterView .toolbar { Button(action: { AccessibilityNotification.Announcement("Loading Photos View") .post() }) { Text("Photos") } } } } }
-
5:13 - Assign announcement priority
import SwiftUI struct ZoomingImageView: View { var defaultPriorityAnnouncement = AttributedString("Opening Camera") var lowPriorityAnnouncement: AttributedString { var lowPriorityString = AttributedString("Camera Loading") lowPriorityString.accessibilitySpeechAnnouncementPriority = .low return lowPriorityString } var highPriorityAnnouncement: AttributedString { var highPriorityString = AttributedString("Camera Active") highPriorityString.accessibilitySpeechAnnouncementPriority = .high return highPriorityString } // ... }
-
5:46 - Post announcements with priority set
import SwiftUI struct CameraButton: View { // ... var body: some View { Button(action: { // Open Camera Code AccessibilityNotification.Announcement(defaultPriorityAnnouncement).post() // Camera Loading Code AccessibilityNotification.Announcement(lowPriorityAnnouncement).post() // Camera Loaded Code AccessibilityNotification.Announcement(highPriorityAnnouncement).post() }) { Image("Camera") } } } }
-
6:15 - Assign announcement priority with UIKit
class ViewController: UIViewController { let defaultAnnouncement = NSAttributedString(string: "Opening Camera", attributes: [NSAttributedString.Key.UIAccessibilitySpeechAttributeAnnouncementPriority: UIAccessibilityPriority.default] ) let lowPriorityAnnouncement = NSAttributedString(string: "Camera Loading", attributes: [NSAttributedString.Key.UIAccessibilitySpeechAttributeAnnouncementPriority: UIAccessibilityPriority.low] ) let highPriorityAnnouncement = NSAttributedString(string: "Camera Active", attributes: [NSAttributedString.Key.UIAccessibilitySpeechAttributeAnnouncementPriority: UIAccessibilityPriority.high] ) // ... }
-
6:56 - Add the accessibility zoom action
struct ZoomingImageView: View { @State private var zoomValue = 1.0 @State var imageName: String? var body: some View { Image(imageName ?? "") .scaleEffect(zoomValue) .accessibilityZoomAction { action in let zoomQuantity = "\(Int(zoomValue)) x zoom" switch action.direction { case .zoomIn: zoomValue += 1.0 AccessibilityNotification.Announcement(zoomQuantity).post() case .zoomOut: zoomValue -= 1.0 AccessibilityNotification.Announcement(zoomQuantity).post() } } } }
-
7:18 - Add the accessibility zoom action with UIKit
import UIKit class ViewController: UIViewController { let zoomView = ZoomingImageView(frame: .zero) let imageView = UIImageView(image: UIImage(named: "tree")) override func viewDidLoad() { super.viewDidLoad() zoomView.isAccessibilityElement = true zoomView.accessibilityLabel = "Zooming Image View" zoomView.accessibilityTraits = [.image, .supportsZoom] zoomView.addSubview(imageView) view.addSubview(zoomView) } }
-
7:43 - Respond to accessibility zoom actions with UIKit
import UIKit class ZoomingImageView: UIScrollView { override func accessibilityZoomIn(at point: CGPoint) -> Bool { zoomScale += 1.0 let zoomQuantity = "\(Int(zoomValue)) x zoom" UIAccessibility.post(notification: .announcement, argument: zoomQuantity) return true } override func accessibilityZoomOut(at point: CGPoint) -> Bool { zoomScale -= 1.0 let zoomQuantity = "\(Int(zoomValue)) x zoom" UIAccessibility.post(notification: .announcement, argument: zoomQuantity) return true } }
-
10:10 - Use accessibility direct touch options
import SwiftUI struct KeyboardKeyView: View { var soundFile: String var body: some View { Rectangle() .fill(.white) .frame(width: 35, height: 80) .onTapGesture(count: 1) { playSound(sound: soundFile, type: "mp3") } .accessibilityDirectTouch(options: .silentOnTouch) } }
-
10:46 - Use accessibility direct touch options with UIKit
import UIKit class ViewController: UIViewController { let waveformButton = UIButton(type: .custom) override func viewDidLoad() { super.viewDidLoad() waveformButton.accessibilityTraits = .allowsDirectInteraction waveformButton.accessibilityDirectTouchOptions = .silentOnTouch waveformButton.addTarget(self, action: #selector(playTone), for: .touchUpInside) view.addSubview(waveformButton) } }
-
12:21 - Set the accessibility content shape
import SwiftUI struct ImageView: View { var body: some View { Image("circle-red") .resizable() .frame(width: 200, height: 200) .accessibilityLabel("Red") .contentShape(.accessibility, Circle()) } }
-
13:35 - Update accessibility values using block-based setters with UIKit
import UIKit class ViewController: UIViewController { var isFiltered = false override func viewDidLoad() { super.viewDidLoad() // Set up views zoomView.accessibilityValueBlock = { [weak self] in guard let self else { return nil } return isFiltered ? "Filtered" : "Not Filtered" } } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。