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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
iOS Accessibility in SwiftUI: Create Accessible Charts using Audio Graphs
25 mins
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.
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:
The app shows weather information for different weather stations. Select the first station and you’ll see three tabs: Temperatures, Snowfall and Precipitation.
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:
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.
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:
- 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.
- Sort the measurements by date and get the first and last measurement. Use them to make the title more specific.
- If a first and last day isn't given, fall back to a simpler title.
- 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:
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:
- 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. - In the next step, you create the
AXNumericDataAxisDescriptor
. Pass in a title for the axis and the range explained above. - An axis also needs a way to transform a value to a text read by VoiceOver. Precipitations are
Measurement
values with unitsUnitLength.inches
. Because of that, you'll wrap the value back in aMeasurement
again and useformatted
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:
- An
AXDataSeriesDescriptor
encapsulates an array ofAXDataPoint
. Each of those points has an x and y value. You create them by mapping over the range0..<12
to group the measurements by month. As you've seen when defining the x-axis inmakeXAxisDescriptor
, it's a categorical axis showing the month names. UsemonthName
for the currentmonthIndex
. 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 anAXDataPoint
for each month. - Now, create the
AXDataSeriesDescriptor
that bundles the data series and gives it a descriptive name. - 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, setisContinuous
tofalse
. 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. - 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:
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
implementsAXChartDescriptorRepresentable
. - There are two computed properties,
temperatureChartTitle
andtemperatureChartSummary
. They are similar toprecipitationChartTitle
andprecipitationChartSummary
you've added forPrecipitationChart
. - The extension contains
makeChartDescriptor
, which is required byAXChartDescriptorRepresentable
. It uses the title and summary and combines them with the axes and data series to create the Audio Graph. -
makeXAxisDescriptor
andmakeYAxisDescriptor
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
)
]
- 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. - Populate the lists by looping over
measurements
. For each item you create a newAXDataPoint
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. - 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 thisAXDataSeriesDescriptor
and the one you implemented before. You setisContinuous
totrue
instead offalse
. This results in a continuous line graph. - 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:
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:
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!
All videos. All books.
One low price.
A Kodeco subscription is the best way to learn and master mobile development — plans start at just $19.99/month! Learn iOS, Swift, Android, Kotlin, Flutter and Dart development and unlock our massive catalog of 50+ books and 4,000+ videos.
Learn more