If you spend even a few minutes with an active VoiceOver user, you’ll learn two things very quickly: they are remarkably adept at navigating around user interfaces, and they also often set reading speed extremely fast – way faster than you or I would use.
It’s important to take both of those things into account when we’re designing our UI: these users aren’t just trying VoiceOver out of curiosity, but are instead VoiceOver power users who rely on it to access your app. As a result, it’s important we ensure our UI removes as much clutter as possible so that users can navigate through it quickly and not have to listen to VoiceOver reading unhelpful descriptions.
Beyond setting labels and hints, there are several ways we can control what VoiceOver reads out. There are three in particular I want to focus on:
All of these are simple changes to make, but they result in a big improvement.
For example, we can tell SwiftUI that a particular image is just there to make the UI look better by using Image(decorative:)
. Whether it’s a simple bullet point or an animation of your app’s mascot character running around, it doesn’t actually convey any information and so Image(decorative:)
tells SwiftUI it should be ignored by VoiceOver.
Use it like this:
Image(decorative: "character")
For all views – images or otherwise – you can get the same result using the .accessibilityHidden()
modifier, which makes any view completely invisible to the accessibility system:
Image(.character)
.accessibilityHidden(true)
Obviously you should only use this if the view in question really does add nothing – if you had placed a view offscreen so that it wasn’t currently visible to users, you should mark it inaccessible to VoiceOver too.
The last way to hide content from VoiceOver is through grouping, which lets us control how the system reads several views that are related. As an example, consider this layout:
VStack {
Text("Your score is")
Text("1000")
.font(.title)
}
VoiceOver sees that as two unrelated text views, and so it will either read “Your score is” or “1000” depending on what the user has selected. Both of those are unhelpful, which is where the .accessibilityElement(children:)
modifier comes in: we can apply it to a parent view, and ask it to combine children into a single accessibility element.
For example, this will cause both text views to be read together, with a short pause between them:
VStack {
Text("Your score is")
Text("1000")
.font(.title)
}
.accessibilityElement(children: .combine)
That works really well when the child views contain separate information, but in our case the children really should be read as a single entity. So, the better solution here is to use .accessibilityElement(children: .ignore)
so the child views are invisible to VoiceOver, then provide a custom label to the parent, like this:
VStack {
Text("Your score is")
Text("1000")
.font(.title)
}
.accessibilityElement(children: .ignore)
.accessibilityLabel("Your score is 1000")
It’s worth trying both of these to see how they differ in practice. Using .combine
adds a pause between the two pieces of text, because they aren’t necessarily designed to be read together. Using .ignore
and a custom label means the text is read all at once, and is much more natural.
Tip: .ignore
is the default parameter for children
, so you can get the same results as .accessibilityElement(children: .ignore)
just by saying .accessibilityElement()
.
SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.
Link copied to your pasteboard.