Streamlined Accessibility in SwiftUI
Making software accessible involves designing it so that it can be used by everyone, including people with disabilities. This means ensuring that the software can be operated, understood, and navigated by users with various physical or cognitive challenges. Different platform-provided accessibility tools enable people to access variety of software features with ease.
Today, we’re focusing on how people with visual impairments can read the content displayed on their screens.
Reading what’s on the screen is made possible through features like VoiceOver on iOS devices. VoiceOver is a screen reader tool that audibly describes what is happening on the screen, from text content to button labels and even complex visual elements like graphs and images.
When a user with vision impairment interacts with an app, VoiceOver reads the elements on the screen in a systematic way. The user can swipe to navigate through items and tap to select options. VoiceOver provides feedback in a synthesized voice, describing everything from basic navigation cues to detailed descriptions of visual content.
Developers need to ensure that their apps work seamlessly with VoiceOver by properly labeling all interactive and non-interactive elements. This requires a thoughtful approach to UI design, where navigation is predictable and feedback is clear. For example, buttons should be clearly labeled not just visually but in a way that makes sense when read aloud by VoiceOver.
Integrating VoiceOver support isn’t just about compliance with accessibility standards. It’s about creating an inclusive user experience that allows users with vision impairment to use apps with the same level of independence as sighted users. This approach benefits everyone and leads to software that’s more robust, flexible, and user-friendly.
🇪🇺 Talking about standards, in European Union, lots of software publishers will need to comply with new European Accessibility Act from 2025 where it will require some everyday products and services to be accessible for people with disabilities, so this topic will get even more important in the world of software development.
In this article, we are exploring a way of simplifying labeling the different elements in iOS application written in SwiftUI so it does not become a hassle in any way.
Accessibility parameters
There are five commonly used accessibility options for different UI elements in SwiftUI. For our streamlined accessibility use we need to declare each of them that are going to be used only when needed.
Let’s see what parameters we’ll be able to use after the implementation:
- Accessibility Traits — these are the traits (characteristics) that define the element’s type and state, such as
button
,selected
, orlink
. Adding appropriate traits helps assistive technology describe UI components and their behaviour more accurately.
var body: some View {
Text("Submit")
.onTapGesture {
print("Submitted")
}
.accessibilityAddTraits(.isButton)
}
- Label — provides a vocal description of the UI element that does not display text. Like an icon. Just make sure not to include text in the label that repeats information that users already have. For example, you could use this method to label a button that plays music with the text
Play
. But don’t use the labelPlay button
because a button already has a trait that identifies it as a button.
var body: some View {
Button("Submit") {
print("Login form details submitted")
}
.accessibilityLabel("Submit details")
}
- Value — This parameter allows describing the value represented by a view, but only if that’s different than the view’s label. As an example, for a slider that you would label as
Volume
, you can provide the current volume setting as a value, like60 %
.
var body: some View {
Slider(value: $volumeValue, in: 0...100)
.accessibilityLabel("Volume")
.accessibilityValue("\(Int(sliderValue)) percent")
}
- Hint — For providing additional context or directions on what the element does offer hints. It communicates to the user what happens after performing the interactive element’s action. which is particularly useful for interactive elements like buttons or sliders. A hint could be in the form of a brief phrase, like
Purchases the item
orDownloads the attachment
.
var body: some View {
Button("Submit") {
print("Login form details submitted")
}
.accessibilityHint("Submits login form details")
}
- Accessibility Hidden — Sometimes, certain UI elements might be irrelevant for users interacting through assistive technologies. Hiding the accessibility features capability for the element can declutter the experience. This is essential when chaining multiple views into a single parent element with its own accessibility declaration and omitting child elements.
var body: some View {
VStack(alignment: .leading) {
Text("Cities")
.font(.title)
.foregroundStyle(.primary)
.accessibilityLabel("Primary title")
.accessibilityHidden(true)
Text("Some city")
.font(.title3)
.foregroundStyle(.secondary)
.accessibilityLabel("Secondary title")
.accessibilityHidden(true)
}
.accessibilityLabel("This will be read to the user, child elements not")
}
- Behaviour — This determines how children of the UI element should be treated by accessibility tools, providing finer control over the accessibility tree.
There are three accessibility behaviour types: contain
, combine
and ignore
(default).
.contain
behavior treats each child of a specified container as a separate accessible element.
Use .contain
when you want users to interact with or receive information about each component individually. It's particularly useful in complex UIs where different elements have distinct functions or information, such as a form with multiple input fields or a layout with various interactive components.
var body: some View {
VStack {
Text("Settings")
.accessibilityAddTraits(.isHeader)
Slider(value: $sliderValue, in: 0...100)
.accessibilityLabel("This will be read to the user")
.accessibilityValue("\(Int(sliderValue)) percent")
Toggle("Enable Feature", isOn: $isToggleOn)
}
.accessibilityElement(children: .contain)
.accessibilityLabel("This will be read to the user too")
}
.combine
behavior merges all child elements of the container into a single accessible element.
Use .combine
when the elements of a container are closely related and should be considered as a single interactive entity. This is often used in simpler or smaller UI components where grouping elements can make understanding and interacting with the content easier, such as a single button with both icon and text that should be read as one element.
var body: some View {
VStack {
Text("Main Content")
Divider()
Text("Additional Information")
}
.accessibilityElement(children: .combine)
.accessibilityLabel("This will be read to the user for all child elements")
}
.ignore
behavior specifies that the accessibility system should ignore the children of a view and treat the entire group as if it doesn’t have any accessible elements. This is generally used to simplify the user interface for accessibility tools like VoiceOver by omitting unnecessary or redundant information that could clutter the experience.
The difference between using .accessibilityHidden(true)
and .ignore
with accessibilityElement(children:)
lies in their use cases and granularity:
.accessibilityHidden(true)
— Applied directly to individual elements. It’s best used when you want to hide specific components within a view, such as a decorative image or a piece of text that is redundant or irrelevant for users who are utilizing assistive technology. Each element needs to be marked hidden explicitly..ignore
— Applied to a container, instructing the accessibility system to completely ignore all child elements within that container. This means the children will not be individually accessible, nor will the container itself be presented as a single accessible element unless you also define how the container should be treated as a whole. It’s useful for simplifying complex UIs where multiple elements together do not provide additional value to the accessibility system.
Now that we know the key acessibility options, we need to have them declared in an enumeration to serve as a unified access point:
import SwiftUI
public enum AccessibilityOption {
/// Adds the given traits to the view.
case traits([AccessibilityTraits])
/// Adds a label to the view that describes its contents.
///
/// Use this method to provide an accessibility label for a view that doesn’t display text, like an icon. For example, you could use this method to label a button that plays music with the text “Play”.
///
/// Don’t include text in the label that repeats information that users already have.
///
/// For example, don’t use the label "Play button" because a button already has a trait that identifies it as a button.
case label(_ label: String)
/// Describes the value represented by a view, but only if that’s different than the view’s label.
///
/// Use this method to describe the value represented by a view, but only if that’s different than the view’s label.
///
/// For example, for a slider that you label as “Volume” using `accessibilityLabel()`, you can provide the current volume setting, like “60%”, using `accessibilityValue()`.
case value(_ value: String)
/// Communicates to the user what happens after performing the view’s action.
///
/// Communicates to the user what happens after performing the view’s action.
///
/// Provide a hint in the form of a brief phrase, like “Purchases the item” or “Downloads the attachment”.
case hint(_ hint: String)
/// Specifies whether to hide this view from system accessibility features.
case accessibilityHidden
/// Determines how child items should be treated by accessibility tools, providing better control over the accessibility tree. The default is ignore.
///
/// Determines how child items should be treated by accessibility tools, providing better control over the accessibility tree.
///
/// The default is `.ignore` behavior.
///
/// - `.contain` behavior treats each child of a specified container as a separate accessible element.
///
/// Use when you want users to interact with or receive information about each component individually. It's particularly useful in complex UIs where different elements have distinct functions or information, such as a form with multiple input fields or a layout with various interactive components.
///
/// - `.combine` behavior merges all child elements of the container into a single accessible element.
///
/// Use when the elements of a container are closely related and should be considered as a single interactive entity. This is often used in simpler or smaller UI components where grouping elements can make understanding and interacting with the content easier, such as a single button with both icon and text that should be read as one element.
case behaviour(_ behaviour: AccessibilityChildBehavior)
}
Single use View Modifier
Okay, so we’ve come to the main part of creating a streamlined accessibility solution.
Let’s start by adding a ViewModifier
that would apply the appropriate accessibility options to any view in our project code.
The view modifier contains a variable that would be initialized with the call of our AccessibilityViewModifier
— an array of accessibility options (that we have declared previously).
View modifier’s body will contain the logic of applying different modifications (accessibilityAddTraits
, accessibilityLabel
, accessibilityValue
, accessibilityHint
, accessibilityHidden
, accessibilityElement
) for a single element (view).
import SwiftUI
struct AccessibilityViewModifier: ViewModifier {
private let label: String?
private let value: String?
private let hint: String?
private let traits: AccessibilityTraits?
private let accessibilityHidden: Bool
private let behaviour: AccessibilityChildBehavior?
public init(options: [AccessibilityOption]) {
var label: String? = nil
var value: String? = nil
var hint: String? = nil
var combinedTraits = AccessibilityTraits()
var traitsSet = false
var accessibilityHidden = false
var behaviour: AccessibilityChildBehavior? = nil
for option in options {
switch option {
case .label(let labelValue):
label = labelValue
case .value(let valueValue):
value = valueValue
case .hint(let hintValue):
hint = hintValue
case .traits(let traitsValue):
traitsSet = true
let traitsToAdd = traitsValue.reduce(AccessibilityTraits()) { $0.union($1) }
combinedTraits.formUnion(traitsToAdd)
case .accessibilityHidden:
accessibilityHidden = true
case .behaviour(let behaviourValue):
behaviour = behaviourValue
}
}
self.label = label
self.value = value
self.hint = hint
self.traits = traitsSet ? combinedTraits : nil
self.accessibilityHidden = accessibilityHidden
self.behaviour = behaviour
}
func body(content: Content) -> some View {
content
.modifierIf(behaviour != nil) { $0.accessibilityElement(children: behaviour!) }
.modifierIf(traits != nil) { $0.accessibilityAddTraits(traits!) }
.modifierIf(label != nil) { $0.accessibilityLabel(Text(label!)) }
.modifierIf(value != nil) { $0.accessibilityValue(Text(value!)) }
.modifierIf(hint != nil) { $0.accessibilityHint(Text(hint!)) }
.modifierIf(accessibilityHidden) { $0.accessibilityHidden(true) }
}
}
We also create an extension for View
type for previously declared modifierIf
attaching the modifier within any view:
extension View {
@ViewBuilder
public func modifierIf<ModifiedContent: View>(
_ condition: Bool,
modifier: (Self) -> ModifiedContent
) -> some View {
if condition {
modifier(self)
} else {
self
}
}
public func accessibility(options: [AccessibilityOption]) -> some View {
self.modifier(AccessibilityViewModifier(options: options))
}
}
Using the stereamlined solution
Finally, it becomes super easy to attach accessibility features to different views via .accessibility(priority:options:)
in different scenes:
var body: some View {
Text("Submit")
.onTapGesture {
print("Submitted")
}
.accessibility(
options: [
.traits([.isButton])
]
)
}
var body: some View {
Slider(value: $volumeValue, in: 0...100)
.accessibility(
options: [
.label("Volume"),
.value("\(Int(sliderValue)) percent"),
.hint("Changes the volume")
]
)
}
var body: some View {
VStack {
Text("Settings")
.accessibility(
options: [
.traits([.isHeader])
]
)
Slider(value: $sliderValue, in: 0...100)
.accessibility(
options: [.accessibilityHidden]
)
Toggle("Enable Feature", isOn: $isToggleOn)
.accessibility(
options: [
.label("This will be read to the user"),
.hint("Enables the feature")
]
)
}
.accessibility(
options: [
.behaviour(.contain),
.label("This will be read to the user too")
]
)
}
What an easy way to apply accessibility when having a single point of accessibility management!
Thank you for reading and good luck improving your application’s ease of use! :)
More about me and my experience here.