Inclusivity with Voice & Language

May 30 2025 · Swift 5.9, iOS 17, Xcode 15.3

Lesson 01: Accessibility & VoiceOver

Demo: Xcode Accessibility Inspector

Episode complete

Play next episode

Next
Transcript

00:02In this demo, you’ll take a quick look at Xcode’s Accessibility Inspector and learn a few more VoiceOver tricks. If you’re following along, download the course materials, start Xcode and open the WaitForIt app in the 01-accessibility-voiceover/Starter folder.

00:20Now, select an iOS simulator as Run Destination.

00:32In ContentView, load its preview, make sure you’re in Live mode, and then tap Fetch a joke.

ZStack {
  Text(jokeService.joke)
    .multilineTextAlignment(.center)
    .padding(.horizontal)
  VStack {
    Spacer()
    Button(action: {
      Task {
        try? await jokeService.fetchJoke()
      }
    }) {
      Text("Fetch a joke")
        .padding(.bottom)
        .opacity(jokeService.isFetching ? 0 : 1)
        .overlay {
          if jokeService.isFetching { ProgressView() }
        }
    }
  }
}

00:39The user interface is very simple — only some text and a button.

00:44Tapping the button sends a request to an API that returns a random Chuck Norris joke.

00:55The query item specifies the dev category, so all the jokes have a techie flavor. Warning: Some of these jokes are a little violent.

01:06Back in ContentView, there’s no explicit accessibility code, just SwiftUI views ZStack, VStack, Text, and Button, and the button’s Text label. But VoiceOver reads out both text values because SwiftUI generates accessibility elements.

01:24To try this out, switch to Selectable mode.

01:29Select the ZStack in the code editor,

01:31and then show the Accessibility Inspector.

01:34If it says “No Selection”, select another inspector such as Attributes, then click back to Accessibility.

01:42Label defaults to the element’s label: Joke appears here or Fetch a joke. — Value is none because neither element has a value. — Traits defaults to isStaticText or .isButton. — Actions defaults to activate for the Button.

01:59Now, to find out how this sounds on your device, you’ll connect your iOS device to your Mac. First, change the target’s Bundle Identifier and set a Team.

02:14If necessary, adjust the project’s iOS Deployment Target to match your device.

02:24If you haven’t used this device as a run destination before, turn on Developer mode:

02:35You’ll see an alert warning you that Developer Mode reduces the security of your device. Tap the alert’s Restart button.

02:43After your device restarts and you unlock it, you’ll see an alert asking you to confirm that you want to turn on Developer Mode.

02:51Tap Turn On to acknowledge the reduction in security protection in exchange for allowing Xcode and other tools to execute code, then enter your device passcode when prompted.

03:07Now, connect your device to your Mac with a cable. Use an Apple cable, because other-brand cables might not work for this purpose.

03:14Select your device from the run-destination menu. It appears near the top, above the simulators.

03:23Then, build and run the app on your device:

03:29Turn on VoiceOver.

03:35Swipe up with two fingers to hear: “Wait for it. Joke appears here. Fetch a joke. Button.”

01:16With the button selected, double-tap to activate it.

03:51When the joke appears, swipe up with two fingers to hear VoiceOver read the joke, then return to the button.

04:04VoiceOver has an on-screen tool you can use to adjust some settings. It’s the VoiceOver rotor, and you use it by rotating two fingers on the screen as if you’re turning a dial. Lift your fingers to choose an option, then swipe up or down to change that option’s value.

05:25Change the Speaking Rate to a higher or lower value, then get VoiceOver to speak the joke again.

04:53To really test whether a VoiceOver user can use your app, triple-tap with three fingers to turn on the screen curtain:

05:00This turns off the display while keeping the screen contents active. VoiceOver users can use this for privacy or if the screen light would disturb other people, like in a dark theater.

05:25Double-tap anywhere on the screen to activate the button, wait a bit, then swipe up with two fingers to hear the joke and return to the button. You can’t see the joke and button, so you must rely entirely on VoiceOver information and gestures.

05:42Triple-tap with three fingers to turn off the screen curtain and show the display again.

01:05Back in Xcode, open RefreshableView.

05:54Change the run destination to a simulator and refresh the preview.

06:00This also fetches a joke, but there’s no button. This view fetches a joke when it loads, and the user can pull down to refresh the view, which fetches a new joke.

List {
  Text("Pull to refresh")
    .font(.largeTitle)
    .listRowSeparator(.hidden)
  Text(jokeService.joke)
    .multilineTextAlignment(.center)
    .lineLimit(nil)
    .lineSpacing(5.0)
    .padding()
    .font(.title)
}

06:15Now, switch the canvas to Selectable mode, and then select List in the code editor.

06:20The Accessibility Inspector shows an Accessibility Container with no Label! But each of the Text views has a Label and Traits.

06:32The refreshable modifier works with List, not with a stack, and List isn’t a “real” SwiftUI element, so the Accessibility Inspector can’t really parse it. What happens if you try to use VoiceOver with this view?

06:46In WaitForItApp, comment out ContentView() and uncomment RefreshableView():

06:52Change run destination to your device, then build and run.

07:16A joke loads right away. Swipe up with two fingers:

07:33VoiceOver says: “Wait for it. Pull to refresh.” and the joke. But now what? Swiping down doesn’t register as a pull-down action.

07:42VoiceOver lets you use a standard gesture: Double-tap and hold your finger on the screen until you hear three rising tones, and then make the gesture.

07:53VoiceOver gestures resume when you lift your finger after making the standard gesture.

08:03WaitForIt is a very simple app, and the default SwiftUI accessibility is sufficient. Most apps are much more complex. In the next lesson, you’ll learn what you can do if the default accessibility isn’t enough.

See forum comments
Cinema mode Download course materials from Github
Previous: SwiftUI: Default Accessibility Next: Conclusion