Designing for visionOS & Accessibility

Mar 27 2024 · Swift 5.10, iOS 17, visionOS 1.1, Xcode 15.3

Lesson 01: Introduction to Accessibility

Demo: Xcode Accessibility Inspector

Episode complete

Play next episode

Next
Transcript

Demo: Xcode Accessibility Inspector

In 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, start up Xcode and open the WaitForIt app in the Starter folder.

Now, select an iOS simulator as Run Destination.

In ContentView, load its preview, make sure you’re in Live mode, 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() }
        }
    }
  }
}

The user interface is very simple, only some text and a button.

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

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

Back in ContentView, there’s no explicit accessibility code, just SwiftUI views ZStack, VStack, Text, Button, and the button’s Text label. Yet, VoiceOver will read out both text values because SwiftUI generates accessibility elements.

Accessibility Inspector

To try this out, switch to Selectable mode.

Select the ZStack in the code editor,

then show the Accessibility Inspector.

If it says No Selection, select another inspector, like Attributes, then click back to Accessibility.

  • Label 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.

VoiceOver on an iOS Device

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

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

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

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

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

Tap 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.

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

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

Then build and run the app on your device:

Turn on VoiceOver.

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

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

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

Screen Curtain

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

This 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.

Double-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.

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

Standard Gesture While Using VoiceOver

Back in Xcode, open RefreshableView.

Change the run destination to a simulator and refresh the preview.

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

Now, switch the canvas to Selectable mode, then select List in the code editor.

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

The 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?

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

Change run destination to your device, then build and run.

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

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

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

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

WaitForIt 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