SwiftUI Accessibility: Beyond the basics
Accessibility 进阶 22m

SwiftUI 无障碍:超越基础

SwiftUI Accessibility: Beyond the basics

2021年6月11日

在 Apple 官方观看视频

一句话判断

SwiftUI 的无障碍 API 在 iOS 15 中终于补齐了和 UIKit 的差距 —— 自定义 VoiceOver 手势、排序和分组、动态字体响应式布局这些”高级”需求现在都有原生方案了,但你得主动用,框架不会帮你自动处理。

这场 Session 讲了什么

Session 深入讲解了 SwiftUI 中超越基础无障碍支持的高级技术。基础无障碍包括 accessibilityLabelaccessibilityHintaccessibilityElement 等,这集聚焦在更复杂的场景。

核心内容:

  1. 自定义手势与 VoiceOver:当你的 app 使用自定义手势(如双指滑动、长按拖动)时,如何让 VoiceOver 用户也能触发这些操作。
  2. 无障碍元素的排序(Sort Priority)accessibilitySortPriority 控制 VoiceOver 的阅读顺序,当视觉布局和逻辑顺序不一致时必须手动设置。
  3. 元素分组(Grouping):把多个相关元素组合成一个 VoiceOver 单元,避免用户逐个听取零散信息。
  4. 动态字体(Dynamic Type)的响应式布局:不只是文字放大,整个布局要在超大字体下仍然可用。
  5. 自定义控件的无障碍化:给用 UIViewRepresentable 或完全 SwiftUI 自定义的控件添加完整的无障碍支持。

值得深挖的点

自定义手势的 VoiceOver 适配。VoiceOver 模式下,用户的触摸行为被 VoiceOver 拦截(单击选择、双击激活、左右滑动切换元素)。你的自定义手势在 VoiceOver 模式下完全失效。解决方案是用 accessibilityAction 系列修饰符为每个手势提供 VoiceOver 等效操作:.accessibilityAction(named: "删除") { ... } 让 VoiceOver 用户通过”上下滑动选择操作 -> 双击执行”的方式触发。

Sort Priority 的陷阱。VoiceOver 默认按照视觉布局从左到右、从上到下阅读。如果你的布局用了 ZStackoverlay,VoiceOver 可能按照 View 声明顺序(代码顺序)而不是视觉层次阅读。accessibilitySortPriority 值越大越先被读,默认值是 0。给关键元素设更高的值确保它们先被读到。

代码片段

自定义手势的 VoiceOver 适配

import SwiftUI

struct SwipeableCardView: View {
    let card: Card
    let onSwipeLeft: () -> Void
    let onSwipeRight: () -> Void

    var body: some View {
        CardContent(card: card)
            // 视觉用户的手势
            .gesture(DragGesture()
                .onEnded { value in
                    if value.translation.width < -50 {
                        onSwipeLeft()
                    } else if value.translation.width > 50 {
                        onSwipeRight()
                    }
                }
            )
            // VoiceOver 用户的等效操作
            .accessibilityElement(children: .combine)
            .accessibilityLabel("\(card.title), \(card.description)")
            .accessibilityAction(named: "左滑删除") {
                onSwipeLeft()
            }
            .accessibilityAction(named: "右滑收藏") {
                onSwipeRight()
            }
    }
}

排序优先级和分组

import SwiftUI

struct WeatherRowView: View {
    let city: String
    let temperature: String
    let condition: String

    var body: some View {
        HStack {
            Text(condition)  // 天气图标
            VStack(alignment: .leading) {
                Text(city)
                Text(temperature)
            }
        }
        // 无障碍分组:三个元素合并为一个
        .accessibilityElement(children: .combine)
        .accessibilityLabel("\(city), \(temperature), \(condition)")
        // 确保城市名先被读到
        .accessibilitySortPriority(1)
    }
}

动态字体的响应式布局

import SwiftUI

struct ResponsiveArticleView: View {
    let article: Article
    @ScaledMetric(relativeTo: .body) var imageSize: CGFloat = 80

    var body: some View {
        VStack {
            // 图片大小随字体缩放
            AsyncImage(url: article.imageURL)
                .frame(width: imageSize, height: imageSize)
                .clipShape(RoundedRectangle(cornerRadius: 8))

            // 文字使用 .layoutPriority 确保在大字体下不被挤压
            VStack(alignment: .leading) {
                Text(article.title)
                    .font(.headline)
                    .layoutPriority(1)
                Text(article.summary)
                    .font(.body)
                    .lineLimit(3...5)  // 允许更多行
            }
        }
        // 大字体时切换为垂直布局
        .modifier(DynamicLayoutModifier())
    }
}

// 根据字体大小动态调整布局
struct DynamicLayoutModifier: ViewModifier {
    @Environment(\.sizeCategory) var sizeCategory

    func body(content: Content) -> some View {
        if sizeCategory >= .accessibilityLarge {
            // 超大字体:纯垂直布局
            content
                .frame(maxWidth: .infinity)
        } else {
            // 正常字体:水平布局
            content
        }
    }
}

最佳实践

  1. 每个自定义手势都必须有 accessibilityAction 等效。没有适配的手势对 VoiceOver 用户就是”这个功能不存在”。
  2. .accessibilityElement(children: .combine) 合并相关元素。把”城市名 + 温度 + 天气”合为一句话,比逐个朗读效率高得多。
  3. @ScaledMetric 不只是给字体用。图标、间距、图片大小都应该随 Dynamic Type 缩放,否则在大字体下布局会失衡。

还有什么值得关注

  • AccessibilityFocusState 可以编程控制 VoiceOver 的焦点位置,适合表单验证错误时自动聚焦到错误字段。
  • SwiftUI 的 ToggleStepper 等控件默认支持 VoiceOver,不需要额外适配。
  • Xcode 13 的 Accessibility Inspector 新增了 VoiceOver 模拟器,可以在不开启设备 VoiceOver 的情况下测试阅读顺序。
WWDC 2021