兼容iOS13+的SwiftUI如何实现代码切换VoiceOver无障碍焦点
解决方案
核心问题:UIAccessibility.post 的 argument 参数仅支持传入 UIKit 体系的 UIView 或 UIViewController 实例,直接传入 SwiftUI 视图对象无法触发焦点跳转,这是此前尝试失败的根本原因。
实现逻辑
通过 UIViewRepresentable 封装捕获 SwiftUI 目标视图对应的底层 UIKit 视图实例,再将该实例作为参数传入焦点切换通知即可实现需求。
步骤1:封装焦点捕获组件
struct AccessibilityFocusCapture: UIViewRepresentable { @Binding var targetView: UIView? func makeUIView(context: Context) -> UIView { let view = UIView(frame: .zero) view.isAccessibilityElement = false view.backgroundColor = .clear return view } func updateUIView(_ uiView: UIView, context: Context) { DispatchQueue.main.async { // 捕获目标视图所在的父视图,对应SwiftUI视图的底层渲染实例 targetView = uiView.superview } } }
步骤2:改造原有视图逻辑
struct ExpandingMenu: View { @State var expanded: Bool = false // 存储需要聚焦的目标UIKit视图实例 @State private var focusTarget: UIView? var body: some View { ScrollView { VStack(spacing: 12) { Text("Some views above the menu") .accessibility(addTraits: .isStaticText) .accessibility(identifier: "Some view identifier") if self.expanded { VStack(spacing: 12) { Text("Menu Item 1") Text("Menu Item 2") Text("Menu Item 3") .background( // 附加捕获组件,获取当前Text对应的底层UIView AccessibilityFocusCapture(targetView: $focusTarget) ) .onAppear(perform: { DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { guard let target = focusTarget else { return } // 局部布局变更使用layoutChanged通知,优先级更高无额外延迟 UIAccessibility.post(notification: .layoutChanged, argument: target) } }) Image(systemName: "chevron.up") FullDivider() Spacer() } .accessibility(addTraits: .isButton) .accessibility(identifier: "Menu Expanded") .accessibility(removeTraits: .isStaticText) .onTapGesture { self.expanded.toggle() } } else { VStack(spacing: 12) { FullDivider() Image(systemName: "chevron.down") FullDivider() Spacer() } .accessibility(addTraits: .isButton) .accessibility(identifier: "Menu Collapsed") .accessibility(removeTraits: .isStaticText) .onTapGesture { self.expanded.toggle() } } Text("Some views below the menu") .accessibility(addTraits: .isStaticText) .accessibility(identifier: "Some Text 3") } } } } // 补齐原有代码中缺失的FullDivider实现,不影响逻辑可直接使用 struct FullDivider: View { var body: some View { Rectangle() .fill(.gray) .frame(height: 1) .frame(maxWidth: .infinity) } }
注意事项
- 优先使用
.layoutChanged通知:仅局部布局变更时使用该类型,比.screenChanged优先级更高,不会出现超长延迟问题。 - 延迟设置建议调整为 0.2~0.5 秒:只需预留 SwiftUI 视图渲染到底层 UIKit 视图生成的时间即可,无需设置过长。
- 若目标视图在滚动容器内,需确保触发焦点切换前目标视图已经滚动到可视区域内,否则焦点切换会失效。
内容的提问来源于stack exchange,提问作者Lee Graham
相关产品推荐
相关产品推荐

