iOS Accessibility in SwiftUI: Create Accessible Charts using Audio Graphs

In this iOS accessibility tutorial, learn how to make charts in your app more accessible by using Audio Graphs. By David Piper.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Categorical vs. Numerical Axes

Next, tell the system about the axes of the chart. You can use two types of axes: a numerical axis and a categorical.

A numerical axis uses numbers. This includes every data point that represents a number, e.g., temperature or height of a mountain. Such an axis contains a range of numbers.

A categorical axis shows data divided into groups or categories. It can include names of months, animals or blood types. You define these categories as an array of strings.

For this chart, you'll use both: The x-axis shows month names and thus is a categorical one and the y-axis shows the amount of precipitation as a numerical axis:

Precipitation chart with notes. The x-axis at the bottom is a categorical one. The y-axis is a numerical axis showing the precipitations.

In the next section, you'll see how to create them.

Describing the Axes

It's time to add the axes. Start by creating the x-axis. Add the method below makeChartDescriptor:

private func makeXAxisDescriptor() -> AXCategoricalDataAxisDescriptor {
  AXCategoricalDataAxisDescriptor(
    title: "Months",
    categoryOrder: (0..<12).map(\.monthName)
  )
}

An AXCategoricalDataAxisDescriptor has a title and an ordered list of categories. Here, you are using "Months" as the title and the months' names as the categories.

monthName provides the month name for a given number. You can check the implementation of this in Extensions.swift. By mapping the range of (0..<12), you are creating a list of all month names.

Next, define the y-axis by adding the code below makeXAxisDescriptor:

private func makeYAxisDescriptor() -> AXNumericDataAxisDescriptor {
  // 1
  let maxPrecipitation = (0..<12)
    .map { sumPrecipitation($0).value }
    .max() ?? 0.0
  // 2
  return AXNumericDataAxisDescriptor(
    title: "Precipitation per month",
    range: (0.0 ... maxPrecipitation),
    gridlinePositions: []) {
      // 3
      "Precipitation " +
      Measurement<UnitLength>(
        value: $0, 
        unit: .inches
      ).formatted(.measurement(width: .wide))
  }
}

An AXNumericDataAxisDescriptor represents a numerical axis.

Here’s how this code works:

  1. First, you get the maximum precipitation in a month. You'll need this value in the next step because a numerical axis needs a range. The minimum value of this range is 0, meaning no precipitation, and maxPrecipitation is the maximum value of the range.
  2. In the next step, you create the AXNumericDataAxisDescriptor. Pass in a title for the axis and the range explained above.
  3. An axis also needs a way to transform a value to a text read by VoiceOver. Precipitations are Measurement values with units UnitLength.inches. Because of that, you'll wrap the value back in a Measurement again and use formatted to create a localized text.

You are ready to use the axes. The only element missing is the data the chart contains.

Describing the Data Points

To create an audio representation of the chart, iOS needs to know which data points it shows. That's the next step. Add the method below makeYAxisDescriptor:

private func makeDataSeriesDescriptor() -> [AXDataSeriesDescriptor] {
  // 1
  let dataPoints = (0..<12).map { monthIndex -> AXDataPoint in
    let xValue = monthIndex.monthName
    let yValue = sumPrecipitation(monthIndex).value
    return AXDataPoint(x: xValue, y: yValue)
  }

  // 2
  return [
    AXDataSeriesDescriptor(
      name: "Precipitation",
      // 3
      isContinuous: false,
      // 4
      dataPoints: dataPoints
    )
  ]
}

Lets walk through the code step-by-step:

  1. An AXDataSeriesDescriptor encapsulates an array of AXDataPoint. Each of those points has an x and y value. You create them by mapping over the range 0..<12 to group the measurements by month. As you've seen when defining the x-axis in makeXAxisDescriptor, it's a categorical axis showing the month names. Use monthName for the current monthIndex. The y-axis is a numerical one, which is why the y value must be a number. Here, it's the sum of all precipitations in the given month. You'll combine both to an AXDataPoint for each month.
  2. Now, create the AXDataSeriesDescriptor that bundles the data series and gives it a descriptive name.
  3. Notice that the initializer has a property called isContinuous, which determines how the data points are presented. As discussed above, the precipitation chart shows noncontinuous data where the bars represent the months. Thus, set isContinuous to false. When creating the Audio Graph for the temperature chart in a later section, you'll see how to use this value to create line charts.
  4. Finally, pass in the data points created earlier.

Build the app — everything is compiling again. But a final step remains before you can see the results of your work. PrecipitationChart needs to know about the chart descriptor. Add these two modifiers to HStack in its body property:

.accessibilityChartDescriptor(self)
.accessibilityElement(children: .combine)

The first one sets PrecipitationChart as its own chart descriptor. The second modifier groups all elements of the chart to one item visible to VoiceOver.

Build and run the app. If it isn't already on, activate VoiceOver with a triple press on the side button.

Focus the first weather station and double-tap the screen to select it. Swipe right until VoiceOver highlights the tab Precipitation. Double-tap the screen again to navigate to that tab. Swipe left until you've reached the chart.

Change your rotor setting to Audio Graph by placing two fingers on the screen and rotating them. Repeat this gesture until you set the rotor to Audio Graph.

Next, swipe down until you hear Chart details and select that option by double-tapping the screen. This opens the Audio Graph details view:

A screenshot showing the final Audio Graph for the precipitation bar chart. It shows the bars together with a play button and additional information like a summary and features.

When the screen opens, VoiceOver focuses the title at the top of the page. Swipe right until Play is focused. Start playing the Audio Graph by double-tapping.

A line will appear on the left of the chart above the button and start moving left to right. Every time it touches a bar, you'll hear a sound. The pitch represents the data point, higher pitches meaning higher values and lower pitches mean lower values.

There are more details about the data. Swipe right and explore the next sections. Summary contains the text you've set in makeChartDescriptor. The next section is Features. It shows information about the data points, e.g., about the trend. The last section is Statistics, listing the minimum, maximum and mean values.

Isn't it amazing? Visually impaired users can listen to your chart's data. Audio Graphs makes it easier to understand the data by providing additional information. This is a major improvement in usability!