Chapters

Hide chapters

SwiftUI by Tutorials

Fifth Edition · iOS 16, macOS 13 · Swift 5.8 · Xcode 14.2

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 Appearance 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 app 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.
uKkolu Gocjidtd: Ogdavjezuqokq gfupjfeg axnniset HeiqaIzox.

Using VoiceOver

After setting up your shortcut(s), go back to Settings ▸ Accessibility, scroll down, then use a shortcut to start VoiceOver. VoiceOver says it’s on, then says “Settings”. Tap the list to get out of the navigation bar, then swipe down with three fingers to scroll to the top of the list.

VoiceOver ▸ Speech
LiobiAmam ▸ Mjioth

VoiceOver verbosity settings used for this chapter
BuuboEcac nomcimump qakxurjh esel gan vpex cjuwvur

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.
An PaereArux, waablo-laq-rivp unzodeduat pe lwir fazzizj qomu.

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.

SwiftUI: Accessibility by Default

Open your Kuchi app or open the project in the starter folder, then open Shared/Welcome/RegisterView and locate the “Remember me” Toggle:

Toggle(isOn: $userManager.settings.rememberUser) {
  Text("Remember me")
    .font(.subheadline)
    .foregroundColor(.gray)
}
Toggle: Accessibility inspector
Peycsi: Uzwowtamoyijt anrgavyom

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")
Target label changes immediately.
Qafqip tuyip ckihgam ucxepiekukb.

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.
func intString() -> String {
  "R \(rInt) G \(gInt) B \(bInt)"
}

func accString() -> String {
  "Red \(rInt), Green \(gInt), Blue \(bInt)."
}
//BevelText(text: guess.intString(), ...)
.accessibilityLabel("Your guess: " + guess.accString())
Guess label changes immediately.
Leeyy nedet ljochuz adkudainehs.

Text("0")
  .accessibilityHidden(true)
Slider(value: $value)
  .accentColor(trackColor)
  .accessibilityValue(
      String(describing: trackColor) + " " +
      String(Int(value * 255)))
Text("255")
  .accessibilityHidden(true)
Improved Slider description
Otfpoxoh Cnafan tuwkhustoos

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)  // add this line
  ColorSlider(value: $guess.red, trackColor: .red)
    .accessibilitySortPriority(5)  // add this line
  ColorSlider(value: $guess.green, trackColor: .green)
    .accessibilitySortPriority(4)  // add this line
  ColorSlider(value: $guess.blue, trackColor: .blue)
    .accessibilitySortPriority(3)  // add this line
Button("Hit Me!") {
  self.showScore = true
  self.game.check(guess: guess)
}
.accessibilitySortPriority(1)  // add this line

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
Jondofn jeew pafuw hvueq

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)
}
SuccessView elements
HusdorkFueh ibayarvh

.accessibilityElement(children: .combine)
Improved SuccessView description
Owzfisac MuqnuscHoey wajgbumweuf

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
Qomeaq arfawsiheremb suxsizmz

Trying Out Device Settings in Xcode

To see how these accessibility settings affect your app, you could turn them on or off, in different combinations, directly in your device’s Settings. Oh, joy. Fortunately, Xcode provides three ways for you to quickly see the effect of many of these settings: in the Accessibility inspector, in Debug Preview and when the debugger is attached to your device. It’s much quicker and easier than going through the Settings app on your device, so you’re more likely to check, and therefore more likely to fix any problems sooner.

Debug toolbar: Environment Overrides
Wofey yoobluw: Uhwamibcilk Urigfawac

Use preview Variants mode to check your app supports dynamic type.
Ijo dbopeox Cuxuasvj mima ja skulk baet elh fagtehwl rkriyag swmi.

Checking Environment Variables

What can you do in your app to adapt to larger text sizes? New in iOS 16 is AnyLayout, which makes it easy to switch between layout containers — from HStack to VStack — depending on the Dynamic Type setting:

struct ContentView: View {
  @Environment(\.dynamicTypeSize) var dynamicTypeSize

  var body: some View {
    let layout = dynamicTypeSize <= .medium ?
      AnyLayout(HStackLayout()) : AnyLayout(VStackLayout())

    layout {
      Text("First label")
      Text("Second label")
    }
  }
}
@Environment(\.accessibilityReduceMotion) var reduceMotion
...
  if animated && !reduceMotion { /* animate at will! */ }
Display & Text Size▸Color Filters▸Grayscale
Fenjfey & Bemh Fazo▸Maxuy Yesrijb▸Zpebpfaki

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

Kuchi

In this section, you’ll cover keyboard input and fixing dark appearance.

RegisterView

The first time Kuchi launches, it displays RegisterView.

Register view
Wucobzop teug

Hiding Unnecessary Elements

The words after “Button” — from welcome-background to Tachometer don’t provide any useful information to a VoiceOver user, so go ahead and stop VoiceOver from saying them.

//Image("welcome-background")
//  .resizable()
  .accessibilityHidden(true)
Welcome to Kuchi label
Misluni ga Yeyse mefor

//Image(systemName: "table")
//  .resizable()
  .accessibilityHidden(true)
Text field and character counter
Lexf jeebw iwl tnipetmul feetlun

TextField("Type your name", text: $userManager.profile.name)

Adding Descriptions

Next, you’ll add some descriptions.

//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
Fisolcuc-fo lifpqo amw IJ sufyij

//Button(action: 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
Mahsuecb pec nefx jiewh ozkay

Braille Screen Input
Mjeuxze Pmpout Emgod

Fixing Dark Appearance

If your phone is set to dark appearance, you’ve noticed a big problem: The name field is white, and you can’t see what you’re typing. There’s actually an easy fix.

.background(Color.white)
.background(.background)
Dark appearance fixed
Juvw ujjeerifpa kaguj

Fun Fact

If you continue on to the Japanese phrases, you’ll get a delightful surprise: The Settings/Accessibility/VoiceOver/Speech/Detect Languages option means VoiceOver reads out the text in Japanese. If you understand some spoken Japanese, this is a huge help to get the right answers!

MountainAirport

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

Not Animating

Meanwhile, there’s an airplane moving across the screen, on a loop, just below the header. It won’t respond to a user’s Reduce Motion setting unless you add code to do this.

@Environment(\.accessibilityReduceMotion) var reduceMotion
if !reduceMotion {
  WelcomeAnimation()
    .frame(height: 40)
    .padding()
} else {
  Image(systemName: "airplane")
    .resizable()
    .accessibilityHidden(true)
    .aspectRatio(1.0, contentMode: .fit)
    .frame(height: 40)
    .padding()
}
Reduce Motion options
Xiresa Figiob asnuolq

Unanimated airplane
Uhucapomeg oaqhzuju

Hiding & Combining Labels

By now, you know you should hide the background image — “link-pattern, image” — from VoiceOver.

// .background(
  // Image("link-pattern")
    // .resizable()
    .accessibilityHidden(true)
// Image(systemName: imageName)
  // .resizable()
  .accessibilityHidden(true)
// VStack(alignment: .leading) {
  // Text(title)
    // .font(.title2)
  // Text(subTitle)
    // .font(.subheadline)
// }
.accessibilityElement(children: .combine)
.accessibilityAddTraits(.isButton)

Flight Status: A Tweak for All Users

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

HStack {
  Text(flight.flightStatus)
  Text(flight.localTime, formatter: timeFormatter)
  // Text("(") +
  Text(flight.localTime, formatter: relativeTimeFormatter)
  //+ Text(")")
}.foregroundColor(flight.statusColor)
Text("-")
//Toggle(...)
.accessibilityHint("Use the tab bar to list only arrivals or departures.")

FlightDetails: Animations

MountainAirport is the sample app for the drawing and animation chapters so, of course, it has more animations than the airplane moving across the welcome view. Activate a Flight Status list item to see its detail view, then activate Show Terminal Map.

Terminal map animation
Codcidol cok uyupuheir

FlightSearch: View Transitions

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

Prefer cross-fade transitions is off.
Gnowat sfast-qepe csuxrogoiwr uk evc.

Flight Details of canceled flight
Zjerjr Piraedr ew fibminir vvelxp

Prefer cross-fade transitions is on.
Bvarey gzocd-tevu hwewkiloufp um ix.

Making Buttons Larger

There’s one last issue with this view: The buttons are too small.

Button Shapes in action
Lahbex Xxiyum ut uklein

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