swiftswiftuiaccessibility

Have Accessibility Read from Top to Bottom


I have code something like the below. When it's read via VoiceOver, it reads "A text", "D text", "B text", "E text", "C button", "F button".

I want it to read, "A text", "B text", "C button", "D text", "E text", "F button".

How would I accomplish that?

ScrollView(.horizontal) {
    HStack {
        VStack {
            Text("A")
            Text("B")
            Button(action: {}, label: { Text("C") })
        }
        VStack {
            Text("D")
            Text("E")
            Button(action: {}, label: { Text("F") })
        }
    }
}

I've tried .accessibilityElement(children: .combine) but that gets rid of all the traits and just reads the buttons as "button" ignoring the text in them. I've tried a few other ideas as well, but nothing seems to work.


Solution

  • If you launch the Accessibility Inspector (version 5.0) and then use the target button (with the crosshairs) to go to the example running in the simulator, it reads horizontally as you described and also reads the text as "A text".

    However, the Accessibility Inspector behaves differently if you select the simulator process in the dropdown of the Accessibility Inspector:

    Screenshot

    Once the process has been selected like this, it still reads horizontally, but it now reads "Capital A" instead of "A text".


    One way to get it to read vertically is to group the views as an accessibility element. You mentioned in the question that you had tried this already, but you said you used children: .combine. This ignores the traits from the children and combines them into one single element.

    It maintains the traits of the children and navigates to them individually when you use children: .contain instead:

    HStack {
        VStack {
            Text("A")
            Text("B")
            Button(action: {}, label: { Text("C") })
        }
        .accessibilityElement(children: .contain)
    
        VStack {
            Text("D")
            Text("E")
            Button(action: {}, label: { Text("F") })
        }
        .accessibilityElement(children: .contain)
    }
    

    Animation


    You can also get it to read vertically by setting .accessibilitySortPriority, as described in another answer. I found it was sufficient to set a sort priority on each of the VStack, you don't need to set a priority value on each underlying child item if they are already arranged in the order that you want them to be read. However, the highest priority will be read first, so you have to make sure to decrease the sort order value with each subsequent group.

    The good news is, it seems to work with negative values, so it may be easiest to start with -1 for the first group:

    HStack {
        VStack {
            Text("A")
            Text("B")
            Button(action: {}, label: { Text("C") })
        }
        .accessibilitySortPriority(-1)
    
        VStack {
            Text("D")
            Text("E")
            Button(action: {}, label: { Text("F") })
        }
        .accessibilitySortPriority(-2)
    }
    

    The documentation for accessibilitySortPriority(_:) states:

    Sets the sort priority order for this view’s accessibility element, relative to other elements at the same level

    I think "same level" means "same accessibility element". So once child elements have been grouped together as an accessibility element, this restricts the scope of the sort priority. However, depending on how the sort priority is applied, the scope may still be quite wide. This might therefore cause the reader to jump around between sections when reading all the content from top to bottom.

    Conclusion: I would suggest using accessibilityElement as the preferred way to group content. Setting accessibilitySortPriority should only be necessary when the order of the child elements, or the elements themselves, needs to be different from the order in which they are arranged in the layout.