UIStackView 辅助功能 - 在默认的可访问元素中插入辅助功能视图?

时间:2021-04-15 21:03:04

标签: ios uistackview voiceover uiaccessibility

现在了解到 UIStackView 的可访问性行为默认情况下会在打开画外音时按顺序读取排列的子视图。假设我有这个层次结构:

UIView
   - UIStackView
       -view1
       -view2
       -view3
       -view4
   - collapsibleSpecialView

collapsibleSpecialView 锚定在 view4 的顶部:

        etc
|------------------|
|      view3       |
|------------------|
|    collapsible   |     <- when collapsed overlaps with vw3 to vw1
|------------------|
|      view4       |
|------------------|

我需要配音才能按上述顺序阅读我的元素。但是由于 stackView 需要首先完成其所有可访问的子视图,所以我最后读取了可折叠视图,但我需要在 view4 之前读取它。

我无法将其添加到 stackview,因为当 collapsibleView 折叠时,它会将其上方的所有内容向上推。我不想为了解决这个问题而绕过自动布局。

我认为可行的是在 view4 之前插入一个不可见的代理 UIView,并在该 ProxyView 中覆盖其 methods 以返回可访问元素。

public class ProxyView: UIView {
    private var view: UIView?

    /// view is the collapsibleView
    @objc public convenience init(view: UIView) {
        self.init()
        self.isAccessibilityElement = true
        self.view = view
    }

    public override func accessibilityElementCount() -> Int {
        return 1
    }

    public override func accessibilityElement(at index: Int) -> Any? {
        guard let menu = view else { return nil }
        return menu
    }

    public override func index(ofAccessibilityElement element: Any) -> Int {
        return 0
    }
}

使用此画外音可以在 view4 之前读取我的 ProxyView,但由于某种原因它没有进入可折叠视图,向右滑动会将焦点移至 view4。

如果我对 UIAccessibilityContainer 的理解是正确的,这理论上应该可行,但事实并非如此。我错过了什么?谢谢

1 个答案:

答案 0 :(得分:0)

我能够解决这个问题。我的解决方案几乎就是我上面的解决方案,并做了一些修复。

class CollapsibleView: UIView {
     /// set isAccessibilityElement = true in init
    
     /// set it accessibilityElements in order you want

     /// this is to tell the  VoiceOver not to capture 
     /// our subviews because we are going to pass them to our proxy
     /// setting this to false will cause VO to loop back to 
     /// this view's subviews.    
}

class ProxyView: UIView {
    var lastFocusedElement: Any?
    
    private var view: CollapsibleView!
    
    /// Collapsible view is accessible element and so VO will still focus on it. We pass the focus to last focused element since we want view before collapsibleView be the last focuseable element.

    /// If you have other views to focus after your view, you have to figure out how to tell VO to skip collapsibleView without setting its isAccessibilityElement to false
    @objc func voFocused(notification: Notification) {
        guard let focused = notification.userInfo?[UIAccessibility.focusedElementUserInfoKey] else {
            return
        }

        if let asView = focused as? UIView, asView == view {
            ///change focus to last
            UIAccessibility.post(notification: .screenChanged, argument: lastFocusedElement)
        } else {
            lastFocusedElement = focused
        }
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder) 
        /// This is an invisible container-only view. We
        /// It needs to be set to false to allow VO
        /// Recognize it as a container. 
        self.isAccessibilityElement = false
        NotificationCenter.default.addObserver(self, selector: #selector(voFocused(notification:)),
                                               name: UIAccessibility.elementFocusedNotification,
                                               object: nil)

    }
    
    func setView(vw: CollapsibleView){
        self.view = vw
        self.accessibilityElements = view.accessibilityElements
    }
}

剩下的就是将 ProxyView() 添加到我们想要宣布的任何顺序的堆栈