iOS Accessibility in SwiftUI Tutorial Part 3: Adapting

In this accessibility tutorial, you’ll improve the accessibility of a SwiftUI app by making it responsive to some common user accessibility settings. By Audrey Tam.

Login to leave a rating/review
Download materials
Save for later

In this accessibility tutorial, you’ll improve the accessibility of a SwiftUI app by making it responsive to some common user accessibility settings.

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!

If you want more people using your app more often, explore all the ways they can adapt their iOS devices to their own needs and preferences, then think about how you might adapt your app to these situations.

Note: The new iOS13 Voice Control isn’t part of this tutorial, but it’s a lot of fun to use, and you should try it out on your app. Just go to Settings▸Accessibility▸Voice Control, and turn on Voice Control. The only Language option is English (United States). Tap Customize Commands to explore what’s available, then have a play. If nothing else, you might decide to shorten the titles of some items. Tell Voice Control to turn off Voice Control, and confirm you want it to execute this command. In future, now that you know your way around, you can just ask Siri to turn on Voice Control. Although Siri needs an internet connection to work, while Voice Control doesn’t.

In Part 1 of this tutorial, you fixed the accessibility of a simple master-detail SwiftUI app by creating informative labels for various types of images. In Part 2, you fixed the accessibility of a more interactive app by restructuring its accessibility tree. In this third and final part, you’ll move beyond VoiceOver and make the app responsive to some common user accessibility settings.

In Part 3 of this tutorial, you’ll learn how to:

  • Use the speech synthesizer to read out text in your app.
  • Adapt your app to user accessibility settings, especially Dark Mode, Smart Invert and larger sizes of Dynamic Type.

Apple wants to help you improve the accessibility of your apps. With SwiftUI, it’s easier than ever before. The future is accessible, and you can help make it happen!

Note: This tutorial assumes you’re comfortable with using Xcode to develop iOS apps. You need Xcode 11 to use SwiftUI. To see the SwiftUI preview, you need macOS 10.15. Some familiarity with SwiftUI will be helpful. Check out our article SwiftUI: Getting Started if you need a refresher. You’ll also need an iOS device to hear the effect of some of your work. Part 1 of this tutorial includes detailed instructions for using VoiceOver on a device and in the Accessibility Inspector.

Getting Started

Get started by downloading the materials for this article; you can find the link at the top or bottom of this article. The begin folder contains the PublicArt project from Part 1 and the ContrastPicker project from Part 2.

Reading Text Out Loud Without VoiceOver

Some of your users might not use VoiceOver, but would still prefer to hear text instead of reading it. Or your app could make announcements when the user isn’t looking at the device, for example, during a workout.

Apple has made big improvements to its text-to-speech technology, using machine learning models to generate speech that sounds remarkably natural. It does a pretty good job of turning an e-book into an audiobook.

Hearing is believing! On your iOS device, open Settings▸Accessibility▸Spoken Content, and turn on Speak Screen.

Turn on Speak Screen accessibility setting.

Next, open a book in the Books app or this article in Safari, then swipe down with two fingers from the top of the screen. Even long sentences sound pretty good! To turn off speech, reopen the control window and tap X.

Now, it’s time to learn how to use the AVSpeechSynthesizer class to read out text in your app.

Open the PublicArt project in the begin folder, then open DetailView.swift. Down in struct DetailView_Previews, edit the DetailView initializer:

DetailView(artwork: artData[1])

You’re setting the preview to use the second list item. Start Live Preview:

Detail View Live Preview.

The description of the Makahiki Festival Mauka Mural is quite long, so it’s a convenient example for trying out AVSpeechSynthesizer.

Scroll back up in DetailView.swift, and add this import statement:

import AVFoundation

AVSpeechSynthesizer is in the AVFoundation framework.

Next, add these two properties to DetailView:

let speaker = AVSpeechSynthesizer()
var utterance: AVSpeechUtterance {
  AVSpeechUtterance(string: artwork.description)

You’re creating a speech synthesizer to speak an utterance. The utterance is the artwork’s description. You can set properties of utterance, including voice, rate and volume.

Now, scroll down to the ScrollView, and add this code before its Text element:

HStack {
  Button("Speak") {
    if self.speaker.isPaused {
    } else {
  Button("Pause") {
    if self.speaker.isSpeaking {
      self.speaker.pauseSpeaking(at: .word)
  Button("Stop") {
    self.speaker.stopSpeaking(at: .word)

These are basic, no-frills buttons to speak, pause and stop the speech synthesizer, arranged in a row above the artwork description text. Pausing and stopping should happen at the end of a word.

Here, you’re checking the synthesizer’s isSpeaking and isPaused properties, but you can implement AVSpeechSynthesizerDelegate for closer monitoring. One way to do this in a SwiftUI app is to create a SpeechSynthesizing class that conforms to AVSpeechSynthesizerDelegate and ObservableObject. A delegate gives you greater control: For example, it could implement speechSynthesizer(_:willSpeakRangeOfSpeechString:utterance:) to highlight each word as it’s spoken.

Refresh the Live Preview (Option-Command-P):

Detail View with speech buttons.

Now, tap the Speak button to start the speech, then tap Pause to pause. Tap Speak to resume, then tap Stop to stop — a few words are spoken, then it stops. Tap Speak to start the speech from the beginning. Tapping Stop after Pause also resets the speech to the beginning.

And that’s all there is to synthesizing speech. Next, you’ll learn about some iOS accessibility settings.

Stop Live Preview, but leave the PublicArt project open.

Adapting to User Settings

Open the ContrastPicker project in the begin folder. Build and run in a simulator, then open Accessibility Inspector and set its target to your simulator:

Accessibility Inspector target is ContrastPicker in Simulator.

Your users can access a multitude of options for customizing their iOS devices. Take a look: On your iOS device, open Settings▸Accessibility▸Display & Text Size.

iOS accessibility display and text size 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 two ways for you to quickly see the effect of many of these settings: in Accessibility Inspector and in Debug Preview. 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.

Accessibility Inspector and Debug Preview are most useful for checking how your app looks in dark mode or inverted colors, or with different font sizes of dynamic type elements. Some options only work on a device, and some don’t work at all (yet?).

Using Accessibility Inspector Settings

Click the third tab of the accessibility inspector — the button with the Settings icon.

Accessibility Inspector Settings tab.

It lists these accessibility options:

  • Invert colors @Environment(\.accessibilityInvertColors): The Smart Invert accessibility option reverses the colors of the display, except for images, media, and some apps that use dark color styles. Classic Invert reverses all colors. At the time of writing this article, this Accessibility Inspector Settings tool only works on an iOS or macOS device: It inverts all colors, including images (classic invert). The @Environment variable actually reports on Smart Invert. To make your app automatically adapt, use standard system color objects like label instead of specific color values like blue.
  • Increase contrast @Environment(\.colorSchemeContrast): This accessibility option alters color and text styling, and adjusts dynamic type to the user’s preferred text size. 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.
  • Reduce transparency @Environment(\.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 @Environment(\.accessibilityReduceMotion): This accessibility option slows down, reduces or removes some animations, like the spinning Activity app awards. Your app should run animations only if this option isn’t enabled:
@Environment(\.accessibilityReduceMotion) var reduceMotion
  if animated && !reduceMotion { // animate as much as you want }
  • Font size @Environment(\.sizeCategory): This accessibility option sets the user’s preferred text size in apps that support Dynamic Type. This Accessibility Inspector Settings tool is very useful for checking how all your dynamic type looks at larger sizes. Consider adapting the layout of UI elements so important information is still legible at large font sizes.

Font size is the only Accessibility Inspector Settings tool that works in the simulator for this app. Try it out — move the slider to the fourth size from the right:

Accessibility Inspector Setting: very large font size.

This is the Accessibility Large font size — at this font size, the text is jumbled, making it hard to read. Later in this article, you’ll learn how to fix this problem.

Close Accessibility Inspector.

Using Debug Preview Environment Overrides

You can try out even more accessibility options in Xcode’s Debug Preview.

Switch back to the PublicArt project, open ContentView.swift, open its canvas, and refresh its preview (Option-Command-P).

To start Debug Preview, Control-click the Live Preview button, then select Debug Preview:

Starting Debug Preview

The first time you start Debug Preview, it takes a while to load. Eventually you’ll see the usual debug toolbar below your code editor:

Debug toolbar with View Debugger and Environment Overrides buttons

Click the Environment Overrides button to see what’s available:

Environment Overrides window

You can very easily see how your UI looks in Dark Mode.

The Text▸Dynamic Type slider is the same as Accessibility Inspector’s Font size slider, but the sizes are labeled, from Extra Small to Accessibility XXXL.

The Accessibility options include those in the Accessibility Inspector Settings tab, but also these:

  • Bold Text @Environment(\.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.

Environment Overrides: On/Off Labels

  • Button Shapes (There’s no @Environment variable or UIAccessibility property.): This accessibility option shows enabled buttons as underlined blue text. If this messes up your custom button, consider redesigning it for all users.

Environment Overrides: Button Shapes

  • Grayscale UIAccessibility.isGrayscaleEnabled: This accessibility option turns on a color filter that shows only the relative luminance of colors. This Debug Preview option doesn’t work, so you’ll have to check it on a device: Enable Settings▸Accessibility▸Display & Text Size▸Color Filters▸Color Filters▸Grayscale. Consider using higher contrast colors for elements so they’re still distinct in grayscale. lets you check how specific foreground and background colors look in grayscale.

Environment Overrides: Grayscale

Note: Color filters don’t show up in screenshots. I had to use another phone’s camera to take a photo of my phone.
  • Smart Invert @Environment(\.accessibilityInvertColors): The same as Accessibility Inspector▸Settings▸Invert colors. In both Debug Preview and debugging on a device, this actually inverts image colors in PublicArt; the real on-device Settings▸Accessibility▸Display & Text Size▸Smart Invert setting doesn’t.
  • Differentiate Without Color @Environment(\.accessibilityDifferentiateWithoutColor): This accessibility option replaces UI items that rely on color to convey information with alternatives. I couldn’t find any example in an app to check whether this Debug Preview option works. You should always try to use shapes or additional text in addition to color.

There is also @Environment(\.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 might prefer to use Voice Control in quiet environments.

There are many other UIAccessibility properties on this page, listed under Getting Capabilities. Check these values whenever you need them, to ensure you’re getting their current status.

Close the PublicArt project.

Adapting to Dark Mode

Now you get to try out some of these settings. Dark screens are really popular and play an important role in accessibility, so you definitely must ensure your apps look good in Dark Mode and Smart Invert.

You’ll use Debug Preview for these exercises because it has more settings, Smart Invert mostly works and, after its initial startup, it refreshes quickly.

First, see what happens in Dark Mode.

Open ContrastListView.swift in the ContrastPicker project, and run Debug Preview. Then turn on Environment Overrides▸Interface Style▸Dark, and turn off everything else.

Environment Overrides: Dark Mode

The color descriptions disappear! This is because the HStack background color is set to white, the same as the dark mode label color.

Now see what happens in Smart Invert, in Light Mode: Turn off Interface Style to revert to Light Mode, then turn on Environment Overrides▸Accessibility▸Smart Invert:

Environment Overrides: Smart Invert

The color descriptions remain visible, but the actual text and background colors are all inverted.

Note: For an RGB color with values r, g and b between 0 and 1, the inverse color has values 1-r, 1-g and 1-b.

On a device, if Dark Mode is set to Automatic, enabling Smart Invert also turns on Dark Mode, so the color descriptions disappear and the text and background colors are inverted:

iPhone with automatic Dark Mode and Smart Invert

The Dark Mode problem is easy to fix: UIKit provides standard color objects for foreground and background colors of UI elements. The names of these color objects describe their intended use — they’re not specific color values. And standard color objects adapt automatically to Dark Mode, Smart Invert and Increase Contrast.

To ensure your app adapts automatically to both light and dark modes, locate the .background(Color(.white)) modifier of the ListCellView HStack, and replace it with this:


You’re setting the HStack background color to the standard color object .systemBackground: This is white for light mode and black for dark mode. The color description Text elements already use .label as their foreground color, so they automatically switch between dark and light colors.

Turn off Smart Invert, and turn on Environment Overrides▸Interface Style▸Dark again to see the HStack background is now black, so the color descriptions are visible:

Fixed: Environment Overrides: Dark Mode

Adapting to Smart Invert

So that’s fixed, but there’s a harder problem to solve for this app: Smart Invert actually changes all the background and text colors, so the RGB values and contrast ratios are all wrong.

It’s a can of worms to display the corrected color values and also keep track of the color sliders while iOS is busy inverting all these values for the display. So it’s best to ask the user to turn off Smart Invert.

First, at the top of ContrastListView, create a local var for this environment variable:

@Environment(\.accessibilityInvertColors) var invertColors

The value of invertColors will stay in sync with the @Environment variable accessibilityInvertColors.

Then add this modifier to the List:

.alert(isPresented: .constant(invertColors)) {
    title: Text("Please Turn Off Smart Invert"),
    message: Text("This app doesn't work with Smart Invert."
      + " Dark mode is OK."))

If \.accessibilityInvertColors is true, you display an alert asking the user to turn it off; they can use dark mode instead. When they turn off Smart Invert, invertColors becomes false.

You have to pass a binding as the argument of isPresented, and .constant() is how you create a binding from any value.

Build and run the app in Simulator, then turn on Environment Overrides▸Accessibility▸Smart Invert:

Simulator with Smart Invert alert

All the colors change to the inverse of the color descriptions, and the alert appears.

Turn off Smart Invert, and the colors revert to match their RGB values:

Simulator after Smart Invert disabled

Stop the simulator.

Congratulations! You’ve taken care of Dark Mode and Smart Invert. Next, you’ll fix another really common problem.

Adapting to Large Font Sizes

Dynamic Type lets your users set the font size they feel comfortable reading — smaller, to fit more content on the page, or larger, for less than perfect vision or to compensate for low color contrast.

Apple’s Text Size and Weight guidelines recommend that you:

  1. Use Dynamic Type.
  2. Test that your app’s design can scale and is legible at all accessibility font sizes.

The first is easy: All SwiftUI text elements support multi-line dynamic type by default. And Apple provides several text styles: the default body, larger styles like title and headline, and smaller styles like caption and footnote.

The second is much less of a chore because both Accessibility Inspector▸Settings and Debug Preview▸Text enable you to quickly and easily check your UI at different font sizes.

Debug Preview lets you turn on Bold Text, as well. And anyway, you’re already using it.

Note: To match my screenshots, set the simulator scheme to iPhone 8.

In Debug Preview▸Environment Overrides, turn on Bold Text and Text: Dynamic Type. Move the Dynamic Type slider to Accessibility Large:

Environment Overrides: Bold Accessibility Large Text size

It looks pretty awful! At least each Text element is multi-line, so all the text appears, but it’s hard to read. It might look better if you stack the Text elements vertically instead of horizontally?

As it happens, Supporting Files/AdaptingStack.swift contains the relevant code from WWDC 2019 Session 412: Debugging in Xcode 11:

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,
        return AnyView(VStack(content: self.content).padding(.top, 10))
        return AnyView(HStack(alignment: .top, content: self.content))

AdaptingStack takes some content and displays it in an HStack, if the user’s preferred text size is smaller than accessibilityLarge, or in a VStack, if the user’s preferred text size is accessibilityLarge or larger.

So all you have to do is replace the HStack in ListCellView with the following AdaptingStack:

AdaptingStack {
  Text("Text \(self.contrast.text.description)")
    .accessibility(label: Text("for Text color "
      + self.contrast.text.accDescription))
  Text("Bkgd \(self.contrast.bkgd.description)")
    .accessibility(label: Text("on Background color "
      + self.contrast.bkgd.accDescription))
  Text("Ratio " + self.contrast.ratio())

You’re changing HStack to AdaptingStack, and inserting self. wherever you use a ListCellView property, because AdaptingStack wraps everything in another closure.

If necessary, refresh the debug preview (Option-Command-P), and set the Dynamic Type size to Accessibility Large. For good measure, check Bold Text, too:

Environment Overrides: VStack for Bold Accessibility Large Text size

Hey presto! The color description HStack is now a VStack! Set Dynamic Type size to Large, and you’re back to HStack:

Environment Overrides: HStack for Bold Large Text size

This is a great trick to have in your repertoire! Where else can you use it? I’m glad you asked ;].

Tap a list item to edit its colors, then increase Dynamic Type size to Accessibility XL

Environment Overrides: Bold Accessibility XL Text size

Uh oh, some truncation is happening in the slider labels. At larger sizes, even the numbers get truncated:

Environment Overrides: Bold Accessibility XXL Text size

Challenge: Use AdaptingStack to fix this problem.

Environment Overrides: VStack for Bold Accessibility XL Text size

Hint: You’ll need to break up the single Text element into two.

Click Reveal to see the solution.

[spoiler title=”Solution”]
In ColorPicker.swift, down in struct SliderBlock, replace Text(colorString + ": " + colorInt) with this AdaptingStack, attaching the two modifiers to the AdaptingStack :

AdaptingStack {
  Text(self.colorString + ": ")
.accessibility(hidden: true)


Where to Go From Here?

You can download the completed version of the project using the Download Materials button at the top or bottom of this tutorial.

You’ve learned a lot more about SwiftUI accessibility in this article. You’ve added a speech synthesizer to read out text in your app and adapted your app to user accessibility settings like Dark Mode, Smart Invert and larger font sizes.

There’s more you might want to do. These two topics are beyond the scope of this article:

Here are some links for further exploration:

We hope you enjoyed this article, and if you have any questions or comments, please join the forum discussion below!