Create accessible experiences for watchOS

Description: Discover how you can build a top-notch accessibility experience for watchOS when you support features like larger text sizes, VoiceOver, and AssistiveTouch. We’ll take you through adding visual and motor accessibility support to a SwiftUI app built for watchOS, including best practices around API integration, experience, and more.

Accessibility on watchOS

Assistive technologies:

  • VoiceOver - allows people with visual impairments full use of their Apple Watch by navigating a screen using a series of gestures and taps while content is read back to them
  • AssistiveTouch (new with watchOS 8) - allows those with motor impairments to use their Apple Watch without the need to touch the screen at all

Display accommodations:

  • Reduce Motion
  • Bold Text
  • Accessibility large text (new with watchOS 8)

Visual accessibility

  • Use Font.TextStyles to size your fonts - by using a text style, the system will automatically adjust the font size with the system text size settings
  • Wrap text, don't truncate - avoid restricting texts with lineLimit(_:)
  • Prefer vertical layouts for larger text styles - use sizeCategory environment value to change layout based on the user preferred Dynamic Type:
struct PlantContainerView: View {
  @Environment(\.sizeCategory) var sizeCategory // 👈🏻
  @Binding var plant: Plant
  
  var body: some View {
    if sizeCategory < .extraExtraLarge {
      PlantViewHorizontal(plant: $plant)
    } else {
      PlantViewVertical(plant: $plant)
    }
  }
}
struct PlantTaskFrequency: View {
  let task: PlantTask
  @Binding var plant: Plant
  let increment: () -> Void
  let decrement: () -> Void
  
  var value: Int {
    switch task {
    case .water:
      return plant.wateringFrequency
    case .fertilize:
      return plant.fertilizingFrequency
    default:
      return 0
    }
  }
  
  var body: some View {
    Section(header: Text("\(task.name) frequency in days"), content: {
      CustomCounter(value: value, increment: increment, decrement: decrement)
        .accessibilityElement()
        .accessibilityAdjustableAction { direction in
          switch direction {
          case .increment:
            increment()
          case .decrement:
            decrement()
          default:
            break
          }
        }
        .accessibilityLabel("\(task.name) frequency")
        .accessibilityValue("\(value) days")
    })
  }
}

Complication tips

  • if your complication text contains abbreviations, make sure to add accessibility labels with the non-abbreviated versions
  • use accessibilityLabel(_:) on images and symbols

AssistiveTouch (Motor accessibility)

  • Full use of Apple Watch without touch
  • Controlled by hand gestures or hand motions
  • Access to functionality on screen content

Two main features:

  • the cursor
  • the action menu

When you turn on AssistiveTouch, you see a cursor appear on the screen. The cursor will focus on each element on the screen one at a time, in order from top left to bottom right.

Gestures

Hand gestures:

  • Clench 🤜 ➡️ Tap
  • Double-clench 🤜🤜 ➡️ Action Menu
  • Pinch 🤏 ➡️ Next element
  • Double-pinch 🤏🤏 ➡️ Previous element

Hand motion (alternative to hand gestures, leverages wrist motion):
By tilting their wrist, people are able to move the onscreen pointer and interact with the UI elements. Similar to AssistiveTouch on iOS, with Dwell Control you can perform an action by resting the pointer over an element for a set amount of time.

Improve AssistiveTouch usage in your app

Focusable elements

  • Built-in control elements - only interactive elements that respond to user interaction are focusable (e.g., Button, Toggle and NavigationLink are focusable by default, when not disabled)
  • Actionable elements - all elements with some gesture (e.g., onTapGesture, accessibilityaction(_:_:))
  • Increase the frame size of small interactive elements by using contentShape(_:eoFill:)
struct DrinkView: View {
  var currentDrink:DrinkInfo
  
  var body: some View {
    HStack(alignment: .firstTextBaseline) {
      DrinkInfoView(drink:currentDrink)
      
      Spacer()
      
      NavigationLink(destination: EditView()) {
        Image(systemName: "ellipsis")
          .symbolVariant(.circle)
      }
      .contentShape(Circle().scale(1.5)) // 👈🏻
    }
  }
}

Missing anything? Corrections? Contributions are welcome 😃

Related

Written by

Federico Zanetello

Federico Zanetello

Software engineer with a strong passion for well-written code, thought-out composable architectures, automation, tests, and more.