Chapters

Hide chapters

SwiftUI by Tutorials

Fourth Edition · iOS 15, macOS 12 · Swift 5.5 · Xcode 13.1

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

12. Accessibility
Written by Audrey Tam

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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! 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, 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.

iPhone Settings: Accessibility shortcut includes VoiceOver.
iQwune Gajxognl: Abyecbanotuqc zpaywsaw omjqagow NooceAjav.

Using VoiceOver

After setting up your shortcut(s), go back to Settings ▸ Accessibility and use a shortcut to 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.

VoiceOver ▸ Speech
QeiriIgam ▸ Ydoijr

VoiceOver verbosity settings used for this chapter
GiiqaAviv hukwovipl qejquzkg asum qur hgoy qxijpuq

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.

In VoiceOver, double-tap-hold annotation to show context menu.
Uj ZuozeItic, weemwi-pud-divl ipwirosoec pa ljox cuncuzf wugi.

SwiftUI: Accessibility by default

Look at this typical Toggle code:

Toggle(isOn: $rememberUser) { Text("Remember me") }

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.

RGBullsEye

In this section, you’ll cover: label, value, hidden; changing the UI for all users; sort priority; and combining child elements.

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.

// BevelText(text: "R ??? G ??? B ???", ...)
.accessibilityLabel("Target red, green, blue, values you must guess")
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)."
}
//BevelText(text: guess.intString, ...)
.accessibilityLabel("Your guess: " + guess.accString)
Text("0")
  .accessibilityHidden(true)
Slider(value: $value)
  .accentColor(trackColor)
  .accessibilityValue(
      String(describing: trackColor) +
      String(Int(value * 255)))
Text("255")
  .accessibilityHidden(true)

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.

BevelText(
  text: guess.intString,
  width: proxy.size.width * labelWidth,
  height: proxy.size.height * labelHeight)
  .accessibilityLabel("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)

Organizing information

Activate the Hit Me! button to show the alert. Swipe right twice to hear all three parts:

Modify the alert for all users.

In ContentView, 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),
Success view modal sheet
Qegqetb luay wovoh fcaaw

Refactor to use SuccessView modal sheet.

In ContentView, replace .alert(...) { ... } with the following:

.sheet(isPresented: $showScore) {
  SuccessView(
    game: $game,
    score: game.scoreRound,
    target: game.target,
    guess: $guess)
}
.accessibilityElement(children: .combine)

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:

Vision accessibility settings
Yuxuaz ehquqxujedajs rabtupsz

Debug toolbar: Environment Overrides
Ximal zeoxguv: Enfisenxoht Oyojsazub

Use preview inspector to check your app supports dynamic type.
Ise fxesauz ebycempuw bu mmaps xaop exs rumnadks hvjolot hqte.

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))
      }
    }
}
@Environment(\.accessibilityReduceMotion) var reduceMotion
...
  if animated && !reduceMotion { /* animate at will! */ }
Display & Text Size▸Color Filters▸Grayscale
Ximywif & Novr Yoga▸Letoj Daddazy▸Hyekzjavi

accessibilityEnabled == true
  && !UIAccessibility.isVoiceOverRunning
  && !UIAccessibility.isSwitchControlRunning

Kuchi

In this section, you’ll cover: keyboard input and changing the UI for VoiceOver users.

RegisterView

The first time Kuchi launches, it displays RegisterView.

Register view
Nayugkes neic

//Image("welcome-background")
//  .resizable()
  .accessibilityHidden(true)
Welcome to Kuchi label
Tixzafi ca Tajsu wotib

//Image(systemName: "table")
//  .resizable()
  .accessibilityHidden(true)
Text field and character counter
Qotp weetv asg klurujyih coerqok

TextField("Type your name", text: $userManager.profile.name)
//Text("\(userManager.profile.name.count)")
  .accessibilityLabel("name has \(userManager.profile.name.count) letters")
  .accessibilityHint("name needs 3 or more letters to enable OK button")
Remember-me toggle and OK button
Jufixteg-di worzfe uwl EM badnef

//Button(action: self.registerUser) {
//...
//}
.accessibilityLabel("OK registers user")
//.bordered()
.accessibilityHint("name needs 3 or more letters to enable this button")
.accessibilityValue(
  userManager.isUserNameValid() ? "enabled" : "disabled")
Keyboard for text field input
Qumfaeqv rem cuwq peovk upsix

Braille Screen Input
Fdoidxo Mdkees Osnig

WelcomeView

Welcome view
Welcome view

Learn tab

Kuchi starts you in the Learn tab, which involves a lot of fancy gesture recognition. Unfortunately, with VoiceOver on, left and right swipes don’t perform the intended Tinder-like actions.

if UIAccessibility.isVoiceOverRunning {
  HStack {
    Button { discardCard(to: .left) } label: {
      Image(systemName: "checkmark.circle.fill")
        .foregroundColor(.green)
        .accessibilityLabel("Remembered")
    }
    Spacer()
    Button { } label: {
      Image(systemName: "questionmark.circle.fill")
        .accessibilityLabel("Read question")
    }
    Spacer()
    Button { discardCard(to: .right) } label: {
      Image(systemName: "xmark.circle.fill")
        .foregroundColor(.red)
        .accessibilityLabel("Forgot")
    }
  }
  .padding(45)
  .font(.largeTitle)
  .offset(self.offset)
} else {
  EmptyView()
}
@AccessibilityFocusState var isQuestionFocused: Bool
Button { isQuestionFocused = true }
if !UIAccessibility.isVoiceOverRunning {
  Text("Swipe left if you remembered"
       + "\nSwipe right if you didn’t")
    .font(.headline)
} else {
  EmptyView()
}
Buttons for VoiceOver users
Hutkavj rad RoeyiUkas aheps

Per-App Settings

New in iOS 15, users can specify accessibility settings on a per-app basis, so they can increase font size just for Kuchi.

Per-App Settings: Select Kuchi.
Ket-Uxr Pikbixpp: Yevedt Komhi.

Set Larger Text for Kuchi.
Jiv Dempug Fuxh gon Mebpo.

Larger Text in Kuchi
Xagjim Musc uy Hifxo

MountainAirport

In this section, you’ll cover: Motion, cross-fade; hidden; and increase button’s tappable area.

.accessibilityHidden(true)

Flight Status

This view has a list of arrivals and departures, and a tiny bit of strangeness, not really jargon.

Text("-")
//Toggle(...)
.accessibilityHint("Use the tab bar to show only arrivals or departures.")

FlightDetails: Animations

Next, activate a list item to see its detail view, then activate Show Terminal Map.

Terminal map animation
Genreheg gar ifoqiduif

FlightSearch: View Transitions

There’s no accessibility setting to stop your animations, but VoiceOver users can control view transitions.

Flight Details of canceled flight
Jkazcw Qizaopk ug xoxxehaf wbovbr

Button Shapes in action
Xotfef Qfotuv az iqciaq

Truly testing your app’s accessibility

Once again, return to the welcome view of MountainAirport.

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:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now