Home iOS & Swift Tutorials

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.

Version

  • Swift 5.5, iOS 15, Xcode 13

Charts and graphs are integral elements of many apps because they provide concise views of large sets of data. Whether it’s stocks, fitness, news or weather, charts will improve your users’ comprehension.

However, charts can be inaccessible to visually impaired users. As Apple says in its session Bring accessibility to charts in your app: “There’s no value in a chart you can’t see.”

In this tutorial, you’ll learn how to use Apple’s Audio Graphs to improve the accessibility of a chart. Audio Graphs provides an audible representation of a chart’s data by playing a pitch for each data point. This pitch modulates up or down, depending on the y value of a data point. Developers can add descriptions of the data and axes for VoiceOver to read out.

You’ll learn how to:

  • Navigate to and use Audio Graphs.
  • Create Audio Graphs for continuous and noncontinuous data sets.
  • Show many data sets in one Audio Graph.
Note: Audio Graphs, and VoiceOver in general, are not supported on the iOS simulator. To follow along with this tutorial you will need a device and a developer account.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of this tutorial.

You might already know this app from SwiftUI Tutorial for iOS: Creating Charts. You’ll build on your knowledge to make the charts more accessible.

Open the starter project in Xcode, then build and run the app:

Screenshot of the first view of the example app WeatherChart. It shows a list of weather stations to show data for.

The app shows weather information for different weather stations. Select the first station and you’ll see three tabs: Temperatures, Snowfall and Precipitation.

Three screenshots showing the three tabs for a weather station in the example app WeatherChart. The first chart is a line chart and shows the temperature for 2018. The second screenshot shows a horizontal bar chart showing the snowfall in 2018. The last screenshot shows the precipitation as a bar chart.

Each of these tabs uses a different type of chart. You’ll start by making the Precipitation bar chart accessible and learn about noncontinuous data sets.

After that, you’ll add an Audio Graph for the Temperature line chart showing two continuous data sets. One represents each day’s minimum temperature and the other the maximum temperature.

Check out the original tutorial SwiftUI Tutorial for iOS: Creating Charts to learn how to create these charts in SwiftUI.

Continuous vs. Noncontinuous Data

There is an important difference you need to consider. Audio Graphs can show continuous and noncontinuous data sets.

In continuous data sets, each data point connects with its predecessor and follower. Audio Graphs presents that kind of data as a line chart.

Typical examples of continuous data are bodyweight or temperature over time. Continuous data allows you to interpolate the y value for an x value, which has no measurement. If it’s 68 degrees Fahrenheit at 8 a.m. and 70 degrees at 8:10 a.m., you can assume it was around 69 degrees at 8:05 a.m., even though that time isn’t presented.

In noncontinuous data sets, also called discrete data sets, each data point is separated. The Audio Graph looks like a bar chart for categorical axes or shows circles for numerical axes.

As opposed to continuous data, it’s not possible to say anything about values between two data points. Examples of discrete data include the number of apps you use daily or the precipitation per month in inches.

Take a look at the charts in the example app WeatherChart. On the left is the temperature chart. Because the temperature data is continuous, its chart is a line chart. On the right side, you see the precipitation chart. The precipitation per month is noncontinuous and thus shown as a bar chart:

Two charts side-by-side. The left one shows temperature data as a continuous line chart. The right one shows precipitation per month. Its data is non-continuous and shown as a bar chart.

Navigating With VoiceOver

Your users will access Audio Graphs via VoiceOver. VoiceOver reads out what’s visible on the screen and allows visually impaired users to move focus around and interact using gestures.

For this tutorial, you will find it helpful to set up an accessibility shortcut to activate and deactivate voiceover. Open Settings ▸ Accessibility ▸ Accessibility Shortcut and select VoiceOver. Now you can turn VoiceOver on or off by triple-clicking the power button.

When VoiceOver is active, you don’t use the screen in the same way. You swipe left and right to move between focusable areas of the screen, and double-tap to activate the focused element. Have a play around in the settings app to get used to how it works.

Swiping up or down offers information about the focused element. What these up or down gestures do is controlled by the VoiceOver rotor. You bring up the rotor by placing two fingers on the screen then rotating them, like turning a key in a lock. Each rotation switches to a different way of working through the details of the focused element.

When a view supports Audio Graphs, this appears as an option on the VoiceOver rotor. It lets up and down swipes offer the following options for a graph:

  • Describe chart: Reads a description of the chart.
  • Play Audio Graph: Plays sounds based on the data in the chart.
  • Chart Details: Opens a separate screen so the user can navigate through more details.

This is how the VoiceOver rotor looks. It shows the option Audio Graph once a view supports this feature.

A screenshot of the example app WeatherChart. When rotating two fingers over the screen, the rotor opens and show options to select. In the screenshot the setting Audio Graph is selected.

To learn about VoiceOver, go to iOS Accessibility: Getting Started or Apples iPhone User Guide Learn VoiceOver gestures on iPhone.

Creating Accessible Charts

Build and run the app on your device. Activate VoiceOver by pressing the side button three times. Swipe right to select the first weather station. Navigate to this weather station with a double tap. Again, swipe right until you have selected the label Temperatures for 2018. Swipe right one more time — you have selected the tab bar.

You can’t move focus onto the chart because it’s not visible to VoiceOver yet. This is the first thing you will change.

Open TemperatureTab.swift. Add this modifier to TemperatureChart:

.accessibilityLabel("A chart showing Temperatures for 2018")

Build and run the app again. Follow the same steps as described above, but now you are able to focus the temperature chart with VoiceOver. By adding a accessibilityLabel, VoiceOver knows what to read to the user.

This is a major improvement, but it’s only a first step toward fully accessible charts.

Creating Explorable Charts

The explanation you’ve added in the previous step isn’t providing a lot of information, is it? In this section, you’ll change that.

Open PrecipitationChart.swift. This chart shows the precipitation per month as a bar chart.

Start by making this View conform to the new protocol, AXChartDescriptorRepresentable. Add this extension above PrecipitationChart_Previews:

extension PrecipitationChart: AXChartDescriptorRepresentable {

}

This protocol requires a method, makeChartDescriptor() so add this code inside the extension:

func makeChartDescriptor() -> AXChartDescriptor {
  AXChartDescriptor(
    title: precipitationChartTitle,
    summary: precipitationChartSummary,
    xAxis: makeXAxisDescriptor(),
    yAxis: makeYAxisDescriptor(),
    series: makeDataSeriesDescriptor()
  )
}

Here, you create a AXChartDescriptor. This class encapsulates all data iOS needs to show the Audio Graph. It includes a title and a summary, information about the axes and the data the chart presents.

In the following sections, you’ll take a detailed look at how to describe axes and data series. But first, let’s provide a reasonable title and a more detailed summary.

Add two computed properties inside the extension before makeChartDescriptor ():

// 1
private var precipitationChartTitle: String {
  // 2
  let sortedMeasurements = measurements.sorted { 
    $0.date < $1.date 
  }
  if 
    let firstDay = sortedMeasurements.first?.date
      .formatted(date: .long, time: .omitted), 
    let lastDay = sortedMeasurements.last?.date
      .formatted(date: .long, time: .omitted) {
    return "Precipitation from \(firstDay) to \(lastDay)"
  }

  // 3
  return "Precipitation"
}

// 4
private var precipitationChartSummary: String {
  "\(measurements.count) measurements of precipitation, grouped by month"
}

Here's what's happening:

  1. Each Audio Graph needs a title. To give your users more information, you'll use the first and last day with measurements to create a better title.
  2. Sort the measurements by date and get the first and last measurement. Use them to make the title more specific.
  3. If a first and last day isn't given, fall back to a simpler title.
  4. Finally, provide a summary for the chart. This summary should give the user more information about what the chart is about. To give a good overview, name the number of measurements and that they are grouped by month. With that information, it'll be easier for users to understand the Audio Graph.

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!

Showing Many Data Series

Now that the precipitation chart is more accessible, it's time for the temperature chart.

By adding an Audio Graph for this chart, you'll learn two important differences to the first chart: Presenting a line chart and using many data series in one Audio Graph.

Open TemperatureChart.swift. A lot of what you've implemented in the previous sections already is created:

  • TemperatureChart implements AXChartDescriptorRepresentable.
  • There are two computed properties, temperatureChartTitle and temperatureChartSummary. They are similar to precipitationChartTitle and precipitationChartSummary you've added for PrecipitationChart.
  • The extension contains makeChartDescriptor, which is required by AXChartDescriptorRepresentable. It uses the title and summary and combines them with the axes and data series to create the Audio Graph.
  • makeXAxisDescriptor and makeYAxisDescriptor define the x- and y-axes. Again, this works similar to how you created axes for the previous chart. But instead of using a categorical and a numerical axes, this chart uses two numerical ones. This is because the x-axis shows the number of days instead of the names of months.

Make the chart visible as an audio graph by adding the following modifiers to the Canvas:

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

The interesting part for you to implement is the data series descriptor, which you're going to add now. Replace the contents of makeDataSeriesDescriptor at the bottom of the extension with the following code:

// 1
var lowTemperatures: [AXDataPoint] = []
var highTemperatures: [AXDataPoint] = []

// 2
for measurement in measurements {
  let numberOfDay = Calendar.current.ordinality(
    of: .day,
    in: .year, 
    for: measurement.date
  ) ?? 30
  lowTemperatures.append(
    AXDataPoint(x: Double(numberOfDay), y: measurement.low.value)
  )
  highTemperatures.append(
    AXDataPoint(x: Double(numberOfDay), y: measurement.high.value)
  )
}

return [
  // 3
  AXDataSeriesDescriptor(
    name: "Low Temperatures",
    isContinuous: true,
    dataPoints: lowTemperatures
  ),

  // 4
  AXDataSeriesDescriptor(
    name: "High Temperatures",
    isContinuous: true,
    dataPoints: highTemperatures
  )
]
  1. To create an Audio Graph with more than one data series, you need to return an array of AXDataPoint for each data series. Here, you create two arrays, one for low temperatures per day and one for high ones. The user can then choose to either see and hear all data series at once or each one separately.
  2. Populate the lists by looping over measurements. For each item you create a new AXDataPoint for each data series. The x value represents the number of the day in the year. The y value is either the low or the high temperature, depending on the list you add the data point to.
  3. For PrecipitationChart you return one single array of data points. But, this time you return two. The first one is the data series for low temperatures. There is an important difference between this AXDataSeriesDescriptor and the one you implemented before. You set isContinuous to true instead of false. This results in a continuous line graph.
  4. Finally, you create the second AXDataSeriesDescriptor. This one represents the high temperatures.

Listening to many Data Series

Build and run the app. Select a weather station and open the temperature tab. If not active yet, turn on VoiceOver and navigate to the Audio Graph details page by focusing on the chart and swiping down. Once you hear Chart details, double-tap the screen.

This is what the Audio Graph looks like for two continuous data series:

A screenshot of the line chart for temperatures. It shows two lines, one for high and one for low temperatures. Below the chart is a play button and more information about the chart, e.g., a summary and features.

Swipe right until VoiceOver focuses Play and double-tap to play the Audio Graph. Again, a vertical line moves from left to right. Both the high and low temperature data sets play at the same time. This already provides some insights — you can hear the low and high temperatures follow a similar path.

But, it can get confusing to hear simultaneously more than one data series. Imagine having three, four or even five data series, perhaps with different paths. No one could understand it.

It's possible to select a specific data series. Above the chart is a button All Series. This means that the graph shows all data series at once and that VoiceOver plays all sounds together. You can change the selection to listen to specific data series.

Swipe left to select the button via VoiceOver. Double-tap the screen to open a context menu, where you can choose which data series to play:

A screenshot showing the same view as before. This time the context menu to select a series is open. It shows three options: All Series, Low Temperatures and High Temperatures.

Try it out and select Low Temperatures. Navigate to Play and listen again. This time it's only one tone and it's easier to follow.

Where to Go From Here?

You can download the completed version of the project using the Download Materials button at the top or bottom of this tutorial.

Accessibility is a critical, but often overlooked, element of apps. Audio Graph is an amazing feature that helps people who are blind or have reduced vision. They make charts more accessible, and it's super easy to use. All you need to do is help iOS create an audible representation of your charts data by defining axes and data sets.

If you want to learn more about accessibility and SwiftUI, check out iOS Accessibility in SwiftUI Tutorial Part 1, Part 2 and Part 3.

If you have any questions or comments, please join the forum discussion below!

Contributors

Comments

Reviews

More like this