场景
- Sheet/Alert/Popover 等弹窗关闭后,VoiceOver 默认不会自动回到打开弹窗的按钮。
- 我们希望在关闭时将焦点明确恢复到触发按钮,提升无障碍可用性与可预期的导航体验。
- 兼容iOS15
解决方案要点
- 使用
@AccessibilityFocusState管理可聚焦目标。 - 给触发按钮添加
.accessibilityFocused与焦点枚举值绑定。 - 在弹窗关闭回调(
onDismiss或onChange(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:无法自定义内容视图,可通过监听
isPresented从true -> false的变化来恢复焦点。
.onChange(of: isAlertPresented) { oldValue, newValue in
if oldValue == true && newValue == false {
a11yFocus = .openButton
}
}
- Popover:同上,使用绑定的
isPresented监听关闭后恢复焦点。
注意事项
@AccessibilityFocusState需要声明在能同时访问到触发按钮与弹窗呈现逻辑的上层视图中。- 使用
onDismiss更健壮,覆盖按钮点击、下拉、手势、系统关闭等多种路径。 .presentationAccessibilityAction(.escape)只是在辅助功能中提供“退出”动作,并不会自动将焦点回到触发控件。
复用建议
- 如果页面有多个触发控件,可扩展
FocusField为多个枚举值,并在关闭时根据上下文恢复到对应的触发控件。