You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

兼容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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.24 10:36:05