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

00:01In 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.

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

00:32In 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() }
        }
    }
  }
}

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:05Back 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.

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

01:29Select the ZStack in the code editor,

01:31then show the Accessibility Inspector.

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

  • 01:42Label defaults to the element’s label Joke appears here or Fetch a joke.
  • 01:47Value is none because neither element has a value.
  • 01:50Traits defaults to isStaticText or .isButton.
  • 01:56Actions 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, as 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:06To really test whether a VoiceOver user can use your app, triple-tap with three fingers to turn on the screen curtain:

04:13This 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.

04:38Double-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.

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

01:05Back in Xcode, open RefreshableView.

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

05:13This 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)
}

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

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

05:44The 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?

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

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

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

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

06:55VoiceOver 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.

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

07:16WaitForIt 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