SwiftUI 辅助功能:弹窗关闭后将 VoiceOver 焦点恢复到触发按钮

57 阅读2分钟

场景

  • Sheet/Alert/Popover 等弹窗关闭后,VoiceOver 默认不会自动回到打开弹窗的按钮。
  • 我们希望在关闭时将焦点明确恢复到触发按钮,提升无障碍可用性与可预期的导航体验。
  • 兼容iOS15

解决方案要点

  • 使用 @AccessibilityFocusState 管理可聚焦目标。
  • 给触发按钮添加 .accessibilityFocused 与焦点枚举值绑定。
  • 在弹窗关闭回调(onDismissonChange(of: isPresented))里,将焦点设置回按钮。
  • 相比 UIAccessibility.post(.layoutChanged, ...),该方式更稳健、可维护,且更符合 SwiftUI 风格。

最小实现

下面示例基于文件 AccessibilityTest.swift 的实现,展示如何在 Sheet 关闭后把 VoiceOver 焦点回到“打开 Sheet”按钮。

import SwiftUI

struct AccessibilityContentView: View {
    @State private var showSheet = false
    @AccessibilityFocusState private var a11yFocus: FocusField?
    private enum FocusField: Hashable { case openButton }
        
    var body: some View {
        NavigationStack {
            VStack {
                Button("打开 Sheet") {
                    showSheet = true
                }
                .accessibilityIdentifier("openBtn")
                .accessibilityFocused($a11yFocus, equals: .openButton)
            }
            .navigationTitle("Demo")
        }
        .sheet(isPresented: $showSheet, onDismiss: {
            // 弹窗关闭后,把 VoiceOver 焦点移回触发按钮
            a11yFocus = .openButton
        }) {
            // 作为 sheet 的根视图
            NavigationStack {
                Text("Sheet 内容")
                    .toolbar {
                        ToolbarItem(placement: .confirmationAction) {
                            Button("完成") {
                                showSheet = false
                            }
                        }
                    }
            }
        }
    }
}

其它弹窗类型参考

  • Alert/ConfirmationDialog:无法自定义内容视图,可通过监听 isPresentedtrue -> false 的变化来恢复焦点。
.onChange(of: isAlertPresented) { oldValue, newValue in
    if oldValue == true && newValue == false {
        a11yFocus = .openButton
    }
}
  • Popover:同上,使用绑定的 isPresented 监听关闭后恢复焦点。

注意事项

  • @AccessibilityFocusState 需要声明在能同时访问到触发按钮与弹窗呈现逻辑的上层视图中。
  • 使用 onDismiss 更健壮,覆盖按钮点击、下拉、手势、系统关闭等多种路径。
  • .presentationAccessibilityAction(.escape) 只是在辅助功能中提供“退出”动作,并不会自动将焦点回到触发控件。

复用建议

  • 如果页面有多个触发控件,可扩展 FocusField 为多个枚举值,并在关闭时根据上下文恢复到对应的触发控件。