12.
Accessibility
Written by Audrey Tam
Accessibility matters, even if you’re not in the 15-20% of people who live with some form of disability or the 5% who experience short-term disability. Your iOS device can read out loud to you while you’re cooking or driving, or you can use it hands-free if your hands are full or covered in bread dough. Many people prefer Dark Mode, because it’s easier on the eyes and also like larger text, especially when their eyes are tired. And there are growing concerns about smartphone addiction. A popular tip is to set your iPhone to grayscale (bit.ly/3hoVUPm)! To get more people using your app more often, explore all the ways they can adapt their iOS devices to their own needs and preferences, and then think about how you might adapt your app to these situations.
Most app UIs are very visual experiences, so most accessibility work focuses on VoiceOver — a screen reader that lets people with low to no vision use Apple devices without needing to see their screens. VoiceOver reads out information to users about your app’s UI elements. It’s up to you to make sure this information helps users interact efficiently with your app.
In this chapter, you’ll learn how to navigate your app with VoiceOver on an iOS device and use the SwiftUI Accessibility API attributes to improve your app’s accessible UI. You’ll add labels that provide context for UI elements and improve VoiceOver information by reordering, combining or ignoring child elements.
Apple’s investing a lot of effort in helping you improve the accessibility of your apps. With SwiftUI, it’s easier than ever before. The future is accessible (bit.ly/3htnuLe), and you can help make it happen!
Using VoiceOver on a device
Xcode has an Accessibility Inspector you can open with Xcode ▸ Open Developer Tool ▸ Accessibility Inspector. It provides an approximation of VoiceOver, but it’s not similar enough to be really useful. Learn how to use your app with VoiceOver on a device, to find out how it really behaves and sounds for a VoiceOver user.
Setting up VoiceOver shortcut
On your device, open Settings ▸ Accessibility ▸ Accessibility Shortcut, and select VoiceOver. This enables you to switch VoiceOver on and off by triple-clicking the device’s side button.
Note: I check more than one shortcut option to avoid accidentally turning on VoiceOver when I’m trying to use Apple Pay. You could turn on Settings ▸ Accessibility ▸ Touch ▸ Back Tap and set Double-tap or Triple Tap to VoiceOver. Or just ask Siri to turn VoiceOver on or off.
Using VoiceOver
Still in Settings▸Accessibility, start VoiceOver. Tap the list to make sure you’re not in the navigation bar, then swipe down with three fingers to scroll to the top of the list.
Note: If your screen locks on an iPhone with Face ID: Wake iPhone and glance at it, then drag up from the bottom edge of the screen until you feel a vibration or hear two rising tones.
Now practice some basic navigation in VoiceOver.
First, Swipe right until you reach VoiceOver. Double-tap anywhere to activate this item.
A split-tap gesture is another way to activate an item: Touch and hold Speech with one finger, then tap the screen with another.
Make sure the Detect Languages option is on. You’ll soon hear it in action.
Next, With two fingers, do a Z gesture. This takes you back to the previous screen. You can also use it to dismiss an alert.
Now, activate Verbosity: VoiceOver users can customize what VoiceOver reads out. These are my settings:
If your device’s settings are different, your VoiceOver might say slightly different things.
If you’re on an iPhone with Face ID, drag up from the bottom edge of the screen until you feel a vibration or hear two rising tones. This takes you back to the home screen.
There are many more gestures, and links to more information about VoiceOver, at Learn VoiceOver Gestures on iPhone (apple.co/38PipsI).
Accessibility in SwiftUI
With SwiftUI, it’s easy to ensure your apps are accessible because SwiftUI does a lot of the work for you. SwiftUI elements support Dynamic Type and are accessible by default. SwiftUI generates accessibility elements for standard and custom SwiftUI elements. Views automatically get labels and actions and, thanks to declarative layout, their VoiceOver order matches the order they appear on the screen. SwiftUI tracks changes to views and sends notifications to keep VoiceOver up to date on visual changes in your app.
When the accessibility built into SwiftUI doesn’t provide the right information in the right order, you’ll use the SwiftUI Accessibility API to make your accessible elements understandable, interactable and navigable:
-
Understandable: In this chapter, you’ll learn what SwiftUI generates for VoiceOver from SwiftUI element initializers. Then, to clarify or add context to accessible elements, you’ll override the generated default labels. You’ll customize the accessibility labels and values, hiding elements that provide redundant information, and moving some information to hints that the user doesn’t have to listen to.
-
Interactable: Aim to give your accessible elements appropriate default actions and create custom actions to simplify interaction for users of assistive technology. When your app has custom actions like context menus, double-tap-hold can display them. For example, in Maps with VoiceOver on, double-tap-hold an annotation to see the usual context menu.
- Navigable: You’ll change the order that VoiceOver visits elements, and you’ll group elements to reduce the number of steps and speed up navigation for VoiceOver users.
The amount of work for each accessible element could be as little as a few words or lines of code. Or you might need to refactor or add code, or even change a navigation link into a modal sheet.
Most of the time, you’ll add accessibility to your app without changing its appearance and behavior for users who aren’t using VoiceOver. But sometimes, something you do for VoiceOver will inspire an improvement to your visual UI.
SwiftUI: Accessibility by default
Look at this typical Toggle code:
Toggle(isOn: $rememberUser) { Text("Remember me") }
There’s no explicit accessibility code here, just the type of element — Toggle — and its label. VoiceOver reads this as Remember me, switch button, off, double-tap to toggle setting because SwiftUI generates an accessibility element:
-
The accessibility label defaults to the element’s label Remember me.
-
The accessibility value defaults to match the element’s value: 0, because the initial value of
Remember meisfalse. -
The accessibility trait defaults to Button because this element is a Toggle.
Accessibility API
Now that you’ve gotten comfortable splashing around in the deep end of accessibility, it’s time to dive into some details about the SwiftUI Accessibility API.
When a user of your app turns on an iOS assistive technology like VoiceOver, they’re actually interacting with an accessible user interface that iOS creates for your app. This accessible UI tells a VoiceOver user about the accessible elements of your UI — what they are and how to use them.
Every accessible UI element has these two attributes:
-
Frame: The element’s location and size in its
CGRectstructure. -
Label: The default value of an element’s label — the label, name or text used to create the element. Apple’s programming guide (apple.co/2WWrKtq) provides guidelines for creating labels and hints.
Depending on its nature, a UI element might have one or more of these three attributes:
-
Traits: An element can have one or more traits, describing its type or state. The list of traits (apple.co/34VNb1X) includes
isButton,isModal,isSelectedandupdatesFrequently. SwiftUI views have default traits. For example, a Trait ofToggleis Button. You can add traits withaccessibility(addTraits:)or remove them withaccessibility(removeTraits:). VoiceOver reads out an element’s traits, so never include them in its label. -
Value: A UI element has a value if its content can change. If the default value isn’t meaningful to VoiceOver users, use
accessibilityValue(_:)to create a more useful value. For example,Slidervalues often need context to mean anything to your users. -
Hint: This attribute is optional. If the user doesn’t do anything after VoiceOver reads a label, VoiceOver reads the hint. Use
accessibilityHint(_:)to describe what happens if the user interacts with the element.
The accessible UI doesn’t change anything in your app’s visible UI, so you can add more information, in a different order, than what your other users see.
Note: There is one more accessibility attribute:
identifier. This is only used in UITests. You would set an identifier for an element that doesn’t have an accessibility label, or if an element’s accessibility label is too long or ambiguous.
Kuchi
In this section, you’ll cover: Hidden, label, hint, value; change UI for all users; dynamic type, and combining child elements.
First, open the Kuchi project in the starter folder. Customize it to run on your device: In the project window, select the iOS target. In the General tab, customize the bundle ID. Then, in the Signing & Capabilities tab, select a team.
Now, connect your iOS device to your Mac and select the Kuchi (iOS) run target and your device. If necessary, adjust the iOS Deployment target in the target window, then build and run.
RegisterView
The first time Kuchi launches, it displays RegisterView.
Start VoiceOver, then swipe up with two fingers to hear something like this:
welcome-background, Image, Diagram. Welcome to, Kuchi. Type your name, ellipsis, Text field, Double-tap to edit. 0. Remember me, Switch button, off; Double-tap to toggle setting. OK, dimmed; Button
The words welcome-background, Image, Diagram don’t provide any useful information to a VoiceOver user, so we can stop VoiceOver from saying them.
Find the file WelcomeBackgroundImage.swift (in Shared, Welcome, Components groups), and hide the Image from VoiceOver:
Image("welcome-background")
.resizable()
.accessibilityHidden(true)
You add accessibilityHidden(_:) after resizable() because resizable() is an Image modifier and accessibilityHidden(_:) doesn’t return an Image.
In the Welcome to Kuchi label, sometimes VoiceOver reads the icon as “Logo other”.
So go to LogoImage.swift, to hide this image:
Image(systemName: "table")
.resizable()
.accessibilityHidden(true)
In the text field’s placeholder text, VoiceOver reads “…” as “ellipsis”.
This provides no useful information to anyone, so just delete it for all users.
Next, in RegisterView.swift (up one level, in the Welcome group), delete “…” from the first argument of TextField(_:text:):
TextField("Type your name", text: $userManager.profile.name)
VoiceOver reads out the number “0”, with no context. This Text view keeps count of the number of characters the user types in the TextField. The OK button is disabled while this number is less than 3.
In the first HStack, modify the Text view:
Text("\(userManager.profile.name.count)")
.accessibilityLabel(
Text("name has \(userManager.profile.name.count) letters"))
.accessibilityHint(
Text("name needs 3 or more letters to enable OK button"))
You provide a more descriptive label for VoiceOver to read. If the user doesn’t immediately move on to another interface element, VoiceOver reads the hint.
The accessibility of the Remember me toggle is fine.
By default, VoiceOver reads its on/off state and tells the user what to do with it. Its label is a standard login option, so doesn’t need more explanation.
But the OK button needs work. VoiceOver should tell users what happens when they tap it. Tapping a button is a command for the app to act, and its accessibility label should tell the user what this command is. In this case, tapping OK “registers user”.
Now, add an accessibility label to the OK Button:
//Button(action: self.registerUser) {
//...
//}
.accessibilityLabel(Text("OK registers user"))
Note: I’ve included commented-out lines in code blocks to show you exactly where you need to add code. You can copy and paste the whole code block into your code without breaking anything. Don’t comment out the corresponding lines in your project.
Adding an accessibility label means VoiceOver won’t read the Text string, so you include “OK” in the accessibility label.
Just in case the user skipped listening to the character counter hint, also add it to the OK Button:
.accessibilityHint(
Text("name needs 3 or more letters to enable this button"))
Finally, VoiceOver says the OK button is “dimmed”, but “disabled” would be more informative.
➤ Add an accessibility value to the OK Button:
//Button(...) {
//...
//}
.accessibilityValue(
userManager.isUserNameValid() ? "enabled" : "disabled")
Build and run the app on your device, then swipe up with two fingers to listen to VoiceOver read something like this:
Welcome to Kuchi. Type your name, Text field, Double-tap to edit. Name has 0 letters. Remember me, Switch button, off; Double-tap to toggle setting. OK registers user, disabled, dimmed; Button, name needs 3 or more letters to enable this button.
Now, tap the text field.
VoiceOver says something like this:
Type your name, Text field. Double-tap to edit
If you double-tap, then you get this:
Text field, is editing. Type your name, Character mode, insertion point at start. Use the rotor to access misspelled words
Type your name: Select then double-tap each key to enter it in the text field.
When you tap a key, VoiceOver repeats it and uses the NATO phonetic alphabet (Alpha, Bravo, Charlie, Delta, Echo, etc.) to made sure the user knows which letter they selected.
Note: Blind users would use Braille Screen Input.
Activate the keyboard’s return key to dismiss it, then tap OK to hear VoiceOver say:
OK registers user, enabled, Button, name needs 3 or more letters to enable this button.
Double-tap to navigate to WelcomeView.
WelcomeView
Tap the first line, then swipe right twice to hear something like this:
Hi, Audrey. Welcome to, Kuchi. Start, button.
The three parts of the WelcomeView are separate elements. VoiceOver stops after each one. To make VoiceOver read them as a single unit, the easiest solution is to combine them into a single accessibility element.
In WelcomeView.swift, modify the VStack to combine its children:
//VStack {
//...
//}
.accessibilityElement(children: .combine)
Each of the three children elements is useful, so you just combine them to make VoiceOver read them without stopping after each one.
Build and run to hear this:
Hi, Audrey, Welcome to, Kuchi, button. Actions available.
Unfortunately, now VoiceOver doesn’t read the button’s label “Start”.
Next, add a hint to the ZStack to tell the user what the button does:
//ZStack {
//...
//}
.accessibilityHint(Text("start playing Kuchi"))
VoiceOver reads this only if the user doesn’t immediately double-tap the button.
Build and run on your device to hear the hint:
Hi, Audrey, Welcome to, Kuchi, button. Start playing Kuchi. Actions available.
Finally, activate the Start button.
You’ll now continue on with the Learn tab.
Learn tab
Kuchi starts you in the Learn tab, which involves a lot of fancy gesture recognition. Unfortunately, with VoiceOver on, none of them work.
One solution is to provide buttons to implement the swipe-left and swipe-right gestures.
Note: This chapter’s starter Kuchi project encapsulates the
DragGestureonEndedaction indiscardCard(to:).
In the body of CardView.swift (in the Learn group), embed the ZStack in a VStack, then move offset(offset) to modify your new top-level VStack:
// VStack {
// ZStack { ... }
// ...
// }
.offset(offset)
offset(offset) moves the flash card off the screen. The buttons you’re about to add also need to move off the screen, to make way for the next flash card’s buttons.
Now add this button stack just above the closing } of the VStack, below the modified ZStack:
HStack {
Button { discardCard(to: .left) } label: {
Image(systemName: "arrowshape.turn.up.left.circle")
.accessibilityLabel(Text("Swipes left"))
}
Spacer()
Button { discardCard(to: .right) } label: {
Image(systemName: "arrowshape.turn.up.right.circle")
.accessibilityLabel(Text("Swipes right"))
}
}
.padding(45)
.font(.largeTitle)
You set up two buttons to manually invoke the DragGesture action. And you provide accessibility labels that tell the user what the buttons do.
Build and run on your device. Turn off VoiceOver to check both the buttons and the gestures work without VoiceOver. Before you run out of cards, turn on VoiceOver and test the buttons.
Thanks to the Detect Languages option, VoiceOver speaks a Japanese phrase when you tap the card, but it’s not necessarily the phrase on the top card.
Activating the card button shows the English translation for the top card. Tapping on this reads it out, but the tap target area is too small to be accessible.
The main problem is, the buttons don’t work in VoiceOver. The left arrow increments Remembered but neither arrow removes the card. This is probably a VoiceOver bug. Until it’s fixed, you’d have to create a separate LearnView for VoiceOver users.
Challenge tab
The Challenge tab is a much better user experience. VoiceOver speaks the Japanese phrase when you tap it. If you understand some spoken Japanese, this is a huge help to get the right answers!
The only issue is the Alert title and message are separate. VoiceOver doesn’t read message unless you swipe to it. But in this case, hearing the title — Congratulations or Oh no! — tells you all you need to know. So you don’t need to do anything.
Finally, close Kuchi.
RGBullsEye
In this section, you’ll cover: label, value, hidden; sort priority; and change UI for all users.
Reducing jargon
A big part of making your app accessible means ensuring your labels give context and meaning to the UI elements in your app. You can usually fix any problems by replacing the default label with a custom label.
➤ Open, build and run the starter RGBullsEye project on your device with VoiceOver on (don’t forget to customize the bundle id and team). Swipe up with two fingers to hear:
R 3 question marks G 3 question marks B 3 question marks
R 127 grams 127 B 127 …
Pretty meaningless. Your first task is obvious.
Set accessibility labels for the color value Text views.
First, in ContentView.swift, add a meaningful accessibility label to the target Text view:
// BevelText(text: "R ??? G ??? B ???", ...)
.accessibilityLabel(
Text("Target red, green, blue values you must guess"))
You translate “???” to something that makes sense.
For the guess color, you’ll need a few computed variables to enable VoiceOver to say “Red”, “Green” and “Blue” instead of “R”, “G” (or “grams”) and “B”.
Now, in RGB.swift (in the Model group), replace var intString with the following code:
var rInt: Int {
Int(red * 255.0)
}
var gInt: Int {
Int(green * 255.0)
}
var bInt: Int {
Int(blue * 255.0)
}
/// A String representing the integer values of an RGB instance.
var intString: String {
"R \(rInt) G \(gInt) B \(bInt)"
}
var accString: String {
"Red \(rInt), Green \(gInt), Blue \(bInt)."
}
You create computed variables for the red, green and blue integer values, then use these in the strings you display on screen (intString) and read out in the accessibility label (accString).
Now go back to ContentView.swift and add this label to the guess Text view:
//BevelText(text: guess.intString, ...)
.accessibilityLabel(Text("Your guess: " + guess.accString))
Build and run on your device to hear VoiceOver say exactly what you told it to say.
Next, listen to VoiceOver read out a slider. You must swipe right twice to hear all three components:
0. 0.5, adjustable, swipe up or down with one finger to adjust the value. 255
Note: The swipe up/down slider increments are too large to get a high score. To control the slider more accurately, tap a slider to select it, then double-tap and hold the slider thumb until you hear three rising tones. Now you can drag the slider in the usual way.
The issues here are:
- Users don’t need to hear “0” and “255”.
- The slider value is between 0 and 1, but the interface displays values between 0 and 255.
Don’t read out “0” and “255”. And translate the slider value into an integer.
In ContentView.swift, scroll down to struct ColorSlider and replace the contents of the HStack with the following code:
Text("0")
.accessibilityHidden(true)
Slider(value: $value)
.accentColor(trackColor)
.accessibilityValue(
Text(
String(describing: trackColor) +
String(Int(value * 255))))
Text("255")
.accessibilityHidden(true)
You hide the “0” and “255” Text views from VoiceOver and tell VoiceOver to read the slider color and integer slider value.
Note:
Colorconforms to theCustomStringConvertibleprotocol, soString(describing: trackColor)is “red”, “green” or “blue”.
Reordering navigation
When the app launches, VoiceOver starts reading from the top of the screen. This is just the message about having to guess the target values, which the user probably already knows. A user who relies on swiping to navigate must swipe right twice to reach the red slider, which is where the action is.
Change the navigation order so VoiceOver starts with the sliders, then moves to the guess string, and then to the button.
Next, in ContentView.swift, replace the guess BevelText, sliders and button with the following:
BevelText(
text: guess.intString,
width: geometry.size.width * labelWidth,
height: geometry.size.height * labelHeight)
.accessibilityLabel(Text("Your guess: " + guess.accString))
.accessibilitySortPriority(2)
ColorSlider(value: $guess.red, trackColor: .red)
.accessibilitySortPriority(5)
ColorSlider(value: $guess.green, trackColor: .green)
.accessibilitySortPriority(4)
ColorSlider(value: $guess.blue, trackColor: .blue)
.accessibilitySortPriority(3)
Button("Hit Me!") {
self.showScore = true
self.game.check(guess: guess)
}
.accessibilitySortPriority(1)
You change the sort priority of these five elements. VoiceOver starts reading from the element with the highest sort value (5). The color Text views have the default sort priority 0.
This sort order lets the user immediately start moving the sliders. Then they listen to the full RGB values of their guess. And then they activate Hit Me!.
Build and run on your device. VoiceOver reads “Red 127”. Swiping right moves to “Green 127” then “Blue 127” then “Your guess: …” then “Hit me, button”.
Organizing information
Now, tap the Hit Me! button, then double-tap to show the alert. Swipe right twice to hear all three parts:
Alert, Your Score. 92. OK, Button.
There are two problems:
- You must swipe right to hear your score, which is the most important information, then again to select the OK button.
- The target
Textview now shows the target’s color values, but there’s no way to get VoiceOver to read them.
It would be nice if you could combine the three parts of the alert into a single accessibility label, then add the target color values as an accessibility value or hint. Unfortunately, you can’t use accessibility modifiers with the SwiftUI Alert view.
Note:
UIAlertControllercan set itsview.accessibilityLabelandview.accessibilityValue, so one solution would be to use this instead ofAlert. You’ll learn about integrating UIKit in “Complex Interfaces”.
Modify the alert for all users.
In ContentView.swift, in the body of ContentView, replace the first two arguments of Alert with these:
title: Text("You scored \(game.scoreRound)"),
message: Text("Target values: " + game.target.accString),
You include the score in title and present the target color values in message. You must use the accessible string so VoiceOver can read “Red”, “Green” and “Blue” instead of “R”, “G” and “B”.
OK, here’s a confession: The Figma design for RGBullsEye actually has a full-screen SuccessView modal sheet instead of the Alert. I didn’t implement it, back in “Diving Deeper into SwiftUI”, because it would have covered the guess and target color values, and the design didn’t include this information in the modal. But now that you’re including the target color values in the alert, you might as well do the same in SuccessView. And you can also show the user’s guess color values.
SuccessView.swift is already in the starter project. It displays the target and guess color values on the backgrounds of those colors. The text colors use the nifty computed variable accessibleFontColor from Apple’s Scrumdinger app apple.co/3mXdqeL.
Refactor to use SuccessView modal sheet.
In ContentView.swift, replace .alert(...) { ... } with the following:
.sheet(isPresented: $showScore) {
SuccessView(
game: $game,
score: game.scoreRound,
target: game.target,
guess: $guess)
}
➤ Build and run on your device, then activate the Hit Me! button. Swipe right enough times to hear VoiceOver read something like this:
wand, Image. Congratulations! You scored 77 points on this color. Target: R 157 G 219 B 163. Guess: R 127 G127 B127. Try another one, Button.
The advantage of using a modal sheet instead of an Alert is you can now combine the Text views into a single readout. You can also attach accessibility modifiers to each component.
Exercise: In SuccessView.swift, fix the accessibility issues.
First, hide the “wand” image from VoiceOver.
Next, tell VoiceOver to read out the accessible strings for the target and guess colors.
Finally, combine the
Textelements so VoiceOver reads them all at once instead of separately.The solution is in the challenge folder.
Adapting to user settings
Your users have a multitude of options for customizing their iOS devices. Most of the ones that could affect their experiences with your app are Vision settings:
For some of these options, your app can check if it’s enabled, then adapt itself. But for some options, there isn’t (yet?) an @Environment or UIAccessibility variable, so you might have to tweak your design to work for all your users.
To see how these accessibility settings affect your app, you could turn them on or off, in different combinations, directly in your device’s Settings. Oh, joy. Fortunately, Xcode provides three ways for you to quickly see the effect of many of these settings: in Accessibility Inspector, in Debug Preview and when the debugger is attached to your device. It’s much quicker and easier than going through the Settings app on your device, so you’re more likely to check, and therefore more likely to fix any problems sooner.
Build and run on your device. When it’s running, open Environment Overrides in the debug toolbar:
You can use this tool to check Dark Mode, Text size, Increase Contrast, Bold Text, On/Off Labels and Button Shapes. You must change the actual settings on your device to check Reduce Motion and Grayscale. The Smart Invert environment override inverts most colors, but it’s safer to use the actual setting on your device, just to be sure.
Dark screens are really popular and play an important role in eye health as well as with accessibility, so you definitely must ensure your apps look good in Dark Mode. You’ve already seen in “Diving Deeper Into SwiftUI” how to automatically adapt to Dark Mode by setting a Dark Appearance for your custom colors. You can also set light appearance and high-contrast versions. UIColor has system colors like systemBlue, but also semantic colors like label, systemFill, systemBackground and placeholderText that automatically adapt to Dark Mode.
Note: You can also check Dark Mode and text size with the preview inspector.
In “Intro to Controls: Text & Image”, you switched from using a specific font size like font(.system(size: 30)) to using standard text styles like font(.headline) and .font(.largeTitle). These respond to a user’s accessibility setting for Display & Text Size ▸ Larger Text, so your app automatically supports Dynamic Type. The preview inspector makes it easy to check this, and Apple’s Typography documentation apple.co/37SCpvt shows size and weight for standard text styles at different Dynamic Type sizes.
Finally, close RGBullsEye.
What can you do in your app to adapt to larger text sizes? One trick is to change an HStack to a VStack when the device uses accessibility text sizes. WWDC 2019 Session 412: Debugging in Xcode 11 (apple.co/2KHXMa6) provided this AdaptingStack:
struct AdaptingStack<Content>: View where Content: View {
init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
var content: () -> Content
@Environment(\.sizeCategory) var sizeCategory
var body: some View {
switch sizeCategory {
case .accessibilityLarge,
.accessibilityExtraLarge,
.accessibilityExtraExtraLarge,
.accessibilityExtraExtraExtraLarge:
return AnyView(VStack(
content: self.content)
.padding(.top, 10))
default:
return AnyView(
HStack(alignment: .top,
content: self.content))
}
}
}
This code uses the environment value sizeCategory. This is the font size you can set in Settings ▸ Accessibility ▸ Display & Text Size ▸ Larger Text. Some of the other environment values are:
-
Invert colors
accessibilityInvertColors: The Smart Invert accessibility option reverses colors of the display and shouldn’t invert colors of images, media and some apps that use dark color styles. But it’s currently behaving more like Classic Invert, which reverses all colors. For elements you don’t want inverted, useaccessibilityIgnoresInvertColors(true). -
Increase contrast
colorSchemeContrast: This accessibility option alters color and text styling, and adjusts dynamic type to the user’s preferred text size. In RGBullsEye, it darkens the slider track colors. If your app detects this option is enabled, it should ensure color contrast ratios are 7:1 or higher. Or consider designing your UI so color contrast ratios are 7:1 or higher for all users. You can check the contrast ratio of specific foreground and background colors at contrastchecker.com. -
Reduce transparency
accessibilityReduceTransparency: This accessibility option reduces the transparency and blurs on some backgrounds. If your app detects this option is enabled, it should ensure all alpha values are set to 1.0. -
Reduce motion
accessibilityReduceMotion: This accessibility option slows down, reduces or removes some animations, like the spinning Activity app awards. Check it on your device with Settings ▸ Accessibility ▸ Motion. Your app should run animations only if this option isn’t enabled:
@Environment(\.accessibilityReduceMotion) var reduceMotion
...
if animated && !reduceMotion { /* animate at will! */ }
-
Bold Text
legibilityWeight: This accessibility option displays all text in boldface characters, so large font text uses even more space. -
On/Off Labels
UIAccessibility.isOnOffSwitchLabelsEnabled: This accessibility option shows 1 or 0 in a toggle that is on or off. If this messes up your custom toggle, consider redesigning it. Or replace it with a standard toggle if this option is enabled. -
Button Shapes (There’s no
@Environmentvariable orUIAccessibilityproperty.): This accessibility option shows enabled buttons as underlined blue text. If this messes up your custom button, consider redesigning it for all users. -
Grayscale
UIAccessibility.isGrayscaleEnabled: This accessibility option turns on a color filter that shows only the relative luminance of colors. Check it on your device with Settings ▸ Accessibility ▸ Display & Text Size ▸ Color Filters ▸ Grayscale. Consider using higher contrast colors for elements so they’re still distinct in grayscale. You can check how specific foreground and background colors look in grayscale at contrastchecker.com.
Note: Color filters don’t show up in screenshots. I had to use another phone’s camera to take this photo of my phone.
-
Differentiate Without Color
accessibilityDifferentiateWithoutColor: This accessibility option replaces UI items that rely on color to convey information with alternatives. You should always try to use shapes or additional text in addition to color.
There is also accessibilityEnabled: This is true if VoiceOver, Voice Control or Switch Control is enabled. Check UIAccessibility.isVoiceOverRunning or UIAccessibility.isSwitchControlRunning. There’s no way to check for Voice Control, unless the user has not enabled VoiceOver or Switch Control:
accessibilityEnabled == true
&& !UIAccessibility.isVoiceOverRunning
&& !UIAccessibility.isSwitchControlRunning
Note: This seems to be a reasonable test for VoiceControl, as VoiceOver and VoiceControl don’t work well together, and Switch Control users like Ian Mackay in this Apple video (bit.ly/2KEVcld) might prefer to use Voice Control in quiet environments.
There are many other UIAccessibility properties in Apple’s documentation (apple.co/3pA2fKC), listed under Getting Capabilities. Check these values whenever you need them, to ensure you’re getting their current status.
MountainAirport
In this section, you’ll cover: Motion, cross-fade, hidden, and increase button’s tappable area
The third starter project is a peek into the future. MountainAirport is the sample app for the next section of this book, where you’ll learn to draw and animate custom graphics in SwiftUI.
Open, build and run MountainAirport on your device with VoiceOver on. Swipe up with two fingers to hear:
Mountain Airport, Heading. welcome-background, image. Flight Status … button. Search Flights … button. Your Awards … button
By now, you know you should hide the background image from VoiceOver. It isn’t just unnecessary information. It actually prevents you from tapping the Flight Status and Search Flights buttons.
First, tap Flight Status. VoiceOver says:
welcome-background, image. search flights, flight status, departure and search upcoming arrival information flights
Now, in WelcomeView.swift, add this accessibility modifier to the Image:
.accessibilityHidden(true)
Remember to put it after resizable().
Build and run on your device again. Now you’re able to tap Flight Status to select it.
Flight Status
This view has a list of arrivals and departures.
To start, tap a flight to listen to VoiceOver.
VoiceOver reads out all three lines at once, and it doesn’t read out the icons. Great! Just one small issue: It reads out the “middle dot” punctuation between the airport and the gate number.
Next, in FlightRow.swift (in the FlightStatusBoard group), replace it with a hyphen:
Text("-")
A hyphen is more commonly used than middle-dot, so VoiceOver doesn’t read it out. It just treats it like a comma.
One more issue: There’s a tab bar at the bottom. If you didn’t know about it and just kept swiping right, VoiceOver wouldn’t read it until you’d gone through every item in the list!
You might think sort priority will help you here, but there’s nowhere to attach it.
About all you can do is provide a hint on the Hide Past Toggle in FlightStatusBoard.swift:
//.navigationBarItems(...)
.accessibilityHint(Text("Use the tab bar to show only arrivals or departures."))
FlightDetails
First, activate a list item to see its detail view, then activate Show Terminal Map.
You’ll create this animation in “Animations”. When you do, consider checking the environment value accessibilityReduceMotion. If the user’s device has this setting, your app should replace the map animation with a drawing and stop the airplane icons from swinging around.
Now, return to the welcome view: Use the Back button or do a Z gesture with two fingers.
FlightSearch
Activate the Search Flights button. Select the Departures filter then navigate down the list to a canceled flight and activate it.
This view has several view transitions, starting with the details view itself.
Next, tap each button to see its transition:
-
FlightSearchDetailsis a modal sheet. - Rebook Flight displays an alert.
- Check In for Flight displays an action sheet.
- On-Time History displays a popover.
Now open Settings ▸ Accessibility ▸ Motion and turn on Reduce Motion. Then turn on Prefer Cross-Fade Transitions.
Reopen MountainAirport. Close the flight search details view then select it again. Tap each button.
The FlightSearchDetails modal sheet and On-Time History popover now cross-fade instead of sliding up. So you don’t have to do anything special to your app if you use standard SwiftUI elements.
The alert and action sheet transitions don’t change, but they’re much smaller views.
There is one issue with this view: The buttons are too small. Here are two options:
- Increase each button’s frame height: In FlightSearchDetails.swift (in the SearchFlights group), modify each button with
.frame(height: 44.0) - Rewrite each button so its label is a trailing closure, then add padding to the
Textview.
Also, it’s not obvious they’re buttons, but that’s an easy one.
Finally, open Settings ▸ Accessibility ▸ Display & Text Size and turn on Button Shapes. Then return to MountainAirport and reopen the flight details view.
This setting underlines standard button labels. Too easy!
Truly testing your app’s accessibility
First, get back to the welcome view of MountainAirport.
To truly test whether a VoiceOver user can use your app, turn on the screen curtain: Triple-tap with three fingers.
Note: If you have the zoom accessibility feature enabled, you’ll need to quadruple-tap with three fingers.
This turns off the display while keeping the screen contents active. VoiceOver users can use this for privacy.
Now, navigate to the Flight Status list, turn on Hide Past, list only departures, then find the next (not canceled) departure that’s more than one hour away.
Note: Sometimes swipe-right/left doesn’t work. Swipe up with two fingers to get VoiceOver to read continuously from the top.
Did you succeed? Good for you!
Lastly, Triple-tap with three fingers to show the display, then triple-click the side button to turn off VoiceOver.
Congratulations, you’re well on your way to becoming an accessibility ninja!
Key points
- Use VoiceOver on a device to hear how your app really sounds.
- Accessibility is built into SwiftUI. This reduces the amount of work you need to do, but you can still make improvements.
- Use semantic font sizes to support Dynamic Type. Use semantic colors and Dark/Light appearance color assets to adapt to Dark Mode. Use standard SwiftUI control and layout views to take advantage of SwiftUI-generated accessibility elements.
- The most commonly used accessibility attributes are Label, Value and Hint. Use Hidden to hide UI elements from VoiceOver. Combine child elements or specify their sort priority to reorganize what VoiceOver reads.
- Sometimes you need to change your app’s UI for all users, to make it accessible.
- Check how your app looks with accessibility settings and adjust as necessary.
- Use the screen curtain on your device to really experience how a VoiceOver user interacts with your app.
Where to go from here?
There are lots of resources to help you make your apps accessible. Here are just a few:
-
Apple HIG for accessibility apple.co/34ZS76a
-
WWDC 2020 Accessibility sessions apple.co/3rQNQMa
-
WWDC 2019 Accessibility sessions apple.co/2KFu5Xe
-
iOS Accessibility in SwiftUI Tutorials: Three-part tutorial, starting at bit.ly/2WYD9sI includes accessibility inspector, color contrast ratio, headings for faster navigation and AVSpeechSynthesizer.