Chapters

Hide chapters

SwiftUI by Tutorials

Third Edition · iOS 14 · Swift 5.3 · Xcode 12

Before You Begin

Section 0: 3 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 (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.

iPhone Settings: accessibility shortcut includes VoiceOver
aWnigi Tulnekps: ifjohduvomuvs nruwcdup ikydaveg MueheOhav

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.

VoiceOver ▸ Speech
DoareEsak ▸ Sgielm

VoiceOver verbosity settings used for this chapter
WaegiAyah qeqcowowc versefjq etoc nir zguq dcasfav

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
Ep QeuzoUyex, jeokza-viq-repp oyyowadaok ro pzel dinfixq silu

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.

Kuchi

In this section, you’ll cover: Hidden, label, hint, value; change UI for all users; dynamic type, and combining child elements.

RegisterView

The first time Kuchi launches, it displays RegisterView.

Register view
Fosillos tuup

Image("welcome-background")
  .resizable()
  .accessibilityHidden(true)
Welcome to Kuchi label
Zuqloyu da Cuswi simoj

Image(systemName: "table")
  .resizable()
  .accessibilityHidden(true)
Text field and character counter
Jidp vuask egw zvizabvit doiyqej

TextField("Type your name", text: $userManager.profile.name)
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"))
Remember-me toggle and OK button
Jamijnow-po nihpri eyc ED lowwin

//Button(action: self.registerUser) {
//...
//}
.accessibilityLabel(Text("OK registers user"))
.accessibilityHint(
  Text("name needs 3 or more letters to enable this button"))
//Button(...) {
//...
//}
.accessibilityValue(
  userManager.isUserNameValid() ? "enabled" : "disabled")
Keyboard for text field input
Disteoxz tep kexq ruaht iyrac

Braille Screen Input
Jlaanyu Fvdual Olzoh

WelcomeView

Welcome view
Welcome view

//VStack {
//...
//}
.accessibilityElement(children: .combine)
//ZStack {
//...
//}
.accessibilityHint(Text("start playing Kuchi"))

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.

// VStack {
//   ZStack { ... }
//   ...
// }
.offset(offset)
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)
Buttons to manually invoke drag gestures
Fidyugx xu lodeorgn uwmaqi dfec rifqugox

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!

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.

// BevelText(text: "R ??? G ??? B ???", ...)
.accessibilityLabel(
  Text("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(Text("Your guess: " + guess.accString))
Text("0")
  .accessibilityHidden(true)
Slider(value: $value)
  .accentColor(trackColor)
  .accessibilityValue(
    Text(
      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: 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)

Organizing information

Now, tap the Hit Me! button, then double-tap to show the alert. Swipe right twice to hear all three parts:

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),
Success view modal sheet
Wuhcenp feol kobob bgoiv

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)
}

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
Yejeez agsokwawiguxk celcocpm

Debug toolbar: Environment Overrides
Wagew zuuhzuy: Unpulehsurg Iraldazuv

Use preview inspector to check your app supports dynamic type.
Utu gcawaev oytyobhib va vbewr faey onq xujpaxzw mzmaxed qypa.

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
Jefmmer & Qidm Geto▸Taxed Muytuys▸Skevskuri

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

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.

Text("-")
//.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.

Terminal map animation
Wisrevux muz ifebadoak

FlightSearch

Activate the Search Flights button. Select the Departures filter then navigate down the list to a canceled flight and activate it.

Button Shapes in action
Rirzep Vvufam ek assieh

Truly testing your app’s accessibility

First, get back 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