FREE TRIAL: Accelerate your app development career with Hacking with Swift+! >>

< Back to Latest Articles

Making your app accessible

It is my firm belief that every iOS app should be usable to everyone, and putting in the work to make your app function well no matter who is using it says a lot about the kind of developer you are.

Watch the video here, or read the article below

Quick links

A brief intro to VoiceOver

iOS has all sorts of assistive technologies, but at the core is VoiceOver – Apple’s screen reader system. This obviously primarily benefits blind and other vision-impaired users, but it does so much more: VoiceOver tells you how the system interprets your UI, in terms of what’s visible and also how it’s ordered.

For example, it’s really useful to find parts of your UI that are visually hidden (you can’t see them on screen), but still visible to the assistive technologies – UI you thought was off screen is often completely navigable using assistive technologies, which makes them a nightmare to use.

And so, testing your app thoroughly with VoiceOver enabled is one of the most important steps towards building a great app.

If you haven’t used VoiceOver before, I suggest you enable it now – you’ll need to use a physical device, because it doesn’t work in the simulator. You can find it under Settings > Accessibility > VoiceOver. Once enabled you’ll see a VoiceOver Practice button appear, but here’s the least you need to know:

  • You can tap on a specific item to highlight it, then double-tap to select it. This goes for buttons, tabs, navigation links, and more.
  • You can swipe left or right with one finger to move between the previous and next items.
  • You can swipe left, right, up, or down with three fingers to scroll.
  • If you swipe up from the bottom edge with one finger, then swipe up a little further, you’ll go to the Home Screen. Listen for the little sounds as a cue.
  • If you swipe even further, you’ll go to multitasking. You can then swipe left and right with one finger to move between windows, then double-tap to select.

When VoiceOver works, you’ll hear the immediate title of something first, then a description of what kind of control is selected (a button, an image etc), then after a short pause any further text the app developer added to give context to what the object does – known as the hint.

Last, but not least, it’s worth adding that most VoiceOver users set the voice pitch extremely fast – significantly faster than I can follow. Remember, for many people it’s the primary way of using your app, so they rely on the information being fast and easy to find.

If you get stuck: Listen, I’ve used VoiceOver a lot and feed comfortable with it, but I remember when I first tried it out I found it hard to use and struggled to turn it off. If you hit a similar problem, relax: tell Siri “turn off VoiceOver” and you’re done.

Grouping our data

Once you activate VoiceOver and try navigating around, chances are the first problem you hit lies on the Home view: our little horizontal grid of projects navigates really strangely, with “10 items” being read in the first project, then “10 items” being read in the second, then both titles, then both progress views.

The easy fix here is to use a modifier called .accessibilityElement() modifier, which allows us to combine the elements into one for assistive technologies. Add this to the projects’ VStack, below the shadow:

.accessibilityElement(children: .combine)

If you try that now, you’ll see VoiceOver does a pretty good job already – it will say “10 items, Project 3, 50%” or similar. That’s quite good, but in this case the vertical reading isn’t ideal: it’s clear visually that the project name is the most important thing on that card, but that doesn’t translate to VoiceOver.

A better idea is to make a group that ignores the children, and provide your own label, like this:

.accessibilityElement(children: .ignore)
.accessibilityLabel("\(project.projectTitle), \(project.projectItems.count) items, \(project.completionAmount * 100, specifier: "%g")% complete.")

You’ll need to modify your Localizable.strings files too. Add this for English:

"%@, %lld items, %g%% complete." = "%@, %lld items, %g%% complete.";

And this for Hungarian:

"%@, %lld items, %g%% complete." = "%@, %lld tétel, %g%% kész.";

This ensures users get the correct language for VoiceOver, as well as on-screen.

Over in ProjectsView you’ll see a similar problem exists with the project headers, but this can be fixed by combining children – add this to the end of the HStack in ProjectHeaderView:

.accessibilityElement(children: .combine)

That will automatically read the progress view as a percentage, which will work great. Even better, it will make the editing button work across the whole group.

Adding traits

Almost all of our app uses basic iOS controls such as buttons, text, and navigation views, but one part does not and as a result it’s suffering. I encourage you to explore the app and try to spot the problem yourself – it’s good practice!

I’ll give you a moment, because I know it’s easy to read ahead by accident.

Still here? Okay, the problem is in our project editing view: we have lots of colors inside one row, and this becomes completely unavailable to iOS navigation – it relies exclusively on taps, and iOS won’t even recognize that our color squares should be selectable.

To fix this, we need to add some extra descriptive information to our UI: we need to make it clear that these are buttons and should be treated as such. Furthermore, we also need to make it clear which color is selected – we don’t want VoiceOver to read out the check mark image.

So, first open EditProjectView and add this after the onTapGesture() modifier so that SwiftUI doesn’t try to read inside the ZStack:

.accessibilityElement(children: .ignore)

Next, we want to add either one or two traits - we always want this to be a button, but only the currently selected button should have the extra isSelected trait. Add this modifier below:

.accessibilityAddTraits(
    item == color
        ? [.isButton, .isSelected]
        : .isButton
)

Of course, now that VoiceOver is reading out the names of each of our colors we need to localize the colors by adding another modifier:

.accessibilityLabel(LocalizedStringKey(item))

That means the string that gets displayed will be localized, while the underlying piece of data will remain in English.

With that in place, we can add the extra entries to Localizable.strings. Here’s the English:

"Select a project color" = "Select a project color";
"Pink" = "Pink";
"Purple" = "Purple";
"Red" = "Red";
"Orange" = "Orange";
"Gold" = "Gold";
"Green" = "Green";
"Teal" = "Teal";
"Light Blue" = "Light Blue";
"Dark Blue" = "Dark Blue";
"Midnight" = "Midnight";
"Dark Gray" = "Dark Gray";
"Gray" = "Gray";

And here’s the Hungarian:

"Select a color" = "Válassza ki a színt";
"Pink" = "Rózsaszín";
"Purple" = "Lila";
"Red" = "Piros";
"Orange" = "Narancs sárga";
"Gold" = "Arany";
"Green" = "Zöld";
"Teal" = "Zöldeskék";
"Light Blue" = "Világos kék";
"Dark Blue" = "Sötét kék";
"Midnight" = "Éjfél";
"Dark Gray" = "Sötét szürke";
"Gray" = "Szürke";

And that’s another piece of our app ready for everyone to use!

Adding extra labels

Now that we’ve fixed the navigation problems, let’s look at the labeling problems. There are three in particular I want to address:

  • Highlighting an award icon reads the image name, which is usually meaningless.
  • When swiping through ProjectsView, there’s no audio indicator when an item is high priority or completed.
  • The Add Project button in ProjectsView just says “Add” – it’s not clear what it actually does.

None of those are terribly hard to fix, so let’s just dive in and see where we get to.

First, highlighting an award name should really read the award title, whether it’s locked or unlocked, and ideally also the description after a pause. We can do that with a label and a hint, and if users want the hint immediately they can just trigger the button’s action to show our existing alert.

Add these two below the award buttons in AwardsView:

.accessibilityLabel(
    Text(dataController.hasEarned(award: award) ? "Unlocked: \(award.name)" : "Locked")
)
.accessibilityHint(Text(award.description))

We already have those two strings localized, so we don’t need any further work here.

Second, we need to make ItemRowView describe whether an item is completed or high priority as part of its item label, which we can do with another computed property:

var label: Text {
    if item.completed {
        return Text("\(item.itemTitle), completed.")
    } else if item.priority == 3 {
        return Text("\(item.itemTitle), high priority.")
    } else {
        return Text(item.itemTitle)
    }
}

We can then attach it to our existing NavigationLink like this:

NavigationLink(destination: EditItemView(item: item)) {
    Label {
        Text(item.itemTitle)
    } icon: {
        icon
    }
    .accessibilityLabel(label)
}

We don’t have those strings localized, so please add this to Localizable.strings for English:

"%@, completed." = "%@, completed.";
"%@, high priority." = "%@, high priority.";

The “%@“ part is the name of the award, which will be replaced at runtime.

You’ll also need to add this to the Hungarian strings:

"%@, completed." = "%@, elkészült.";
"%@, high priority." = "%@, elsőbbségi.";

Finally, we need to look at that + button in ProjectsView. This is good enough for sighted users because it’s not attached to a project, but when mixed up amongst many other buttons it’s going to be confusing for VoiceOver.

What’s particularly confusing here is that VoiceOver reads “Add”, which isn’t actually used in our UI – we have a label with the text “Add Project” and the image “plus”, and the string “Add” doesn’t appear anywhere.

This is SwiftUI trying to be helpful: if that button was placed directly into a view, rather than in a toolbar, then it would be read as “Add Project”, exactly as you would expect. However, SwiftUI spots that it’s in a toolbar and takes matters into its own hand: it realizes the + button here must mean “add something new”, so it uses its own title.

Worse – at least as far as I can tell – it won’t honor any custom label or even a hint. Having spoken to the team at Apple it seems clear this is a bug, and perhaps it will even be resolved by the time you follow along in which case this problem won’t even exist for you. But right now it does exist for me, so until I find a better solution we’re just going to adapt our UI a little by replacing the button label with a condition:

if UIAccessibility.isVoiceOverRunning {
    Text("Add Project")
} else {
    Label("Add Project", systemImage: "plus")
}

If you find a better solution to this, or if you find a newer SwiftUI release solves this problem, let me know and I’ll update this article.

Anyway, we’ve covered quite a lot: the basics of using VoiceOver, grouping data, ignoring a view’s children, adding custom labels and hints, and even building custom UI to deliver a comparable experience when needed.

The end result is a user interface that anyone can navigate using assistive technology, that VoiceOver can read well, and that ultimately makes our app available to everyone.

If you liked this, you'd love Hacking with Swift+…

Here's just a sample of the other tutorials, with each one coming as an article to read and as a 4K Ultra HD video.

Find out more and subscribe here


User-friendly network access

14:26

NETWORKING

FREE: User-friendly network access

Anyone can write Swift code to fetch network data, but much harder is knowing how to write code to do it respectfully. In this article we’ll look at building a considerate network stack, taking into account the user’s connection, preferences, and more.

Shadows and glows

19:50

SWIFTUI SPECIAL EFFECTS

FREE: Shadows and glows

SwiftUI gives us a modifier to make simple shadows, but if you want something more advanced such as inner shadows or glows, you need to do extra work. In this article I’ll show you how to get both those effects and more in a customizable, flexible way.

Using memoization to speed up slow functions

36:18

HIGH-PERFORMANCE APPS

FREE: Using memoization to speed up slow functions

In this article you’ll learn how memoization can dramatically boost the performance of slow functions, and how easy Swift makes it thanks to its generics and closures.

Creating a WaveView to draw smooth waveforms

32:08

CUSTOM SWIFTUI COMPONENTS

FREE: Creating a WaveView to draw smooth waveforms

In this article I’m going to walk you through building a WaveView with SwiftUI, allowing us to create beautiful waveform-like effects to bring your user interface to life.

Understanding generics – part 1

20:01

INTERMEDIATE SWIFT

FREE: Understanding generics – part 1

Generics are one of the most powerful features of Swift, allowing us to write code once and reuse it in many ways. In this article we’ll explore how they work, why adding constraints actually helps us write more code, and how generics help solve one of the biggest problems in Swift.

Functional programming in Swift: Introduction

6:52

FUNCTIONAL PROGRAMMING

FREE: Functional programming in Swift: Introduction

Before you dive in to the first article in this course, I want to give you a brief overview of our goals, how the content is structured, as well as a rough idea of what you can expect to find.

Trees

31:55

DATA STRUCTURES

FREE: Trees

Trees are an extraordinarily simple, extraordinarily useful data type, and in this article we’ll make a complete tree data type using Swift in just a few minutes. But rather than just stop there, we’re going to do something quite beautiful that I hope will blow your mind while teaching you something useful.

How to use phantom types in Swift

24:11

ADVANCED SWIFT

FREE: How to use phantom types in Swift

Phantom types are a powerful way to give the Swift compiler extra information about our code so that it can stop us from making mistakes. In this article I’m going to explain how they work and why you’d want them, as well as providing lots of hands-on examples you can try.

Making the most of optionals

23:07

ADVANCED SWIFT

FREE: Making the most of optionals

Swift’s optionals are implemented as simple enums, with just a little compiler magic sprinkled around as syntactic sugar. However, they do much more than people realize, and in this article I’m going to demonstrate some of their power features that can really help you write better code – and blow your mind along the way.

Ultimate Portfolio App: Introduction

14:17

ULTIMATE PORTFOLIO APP

FREE: Ultimate Portfolio App: Introduction

While I’m sure you’re keen to get started programming immediately, please give me a few minutes to outline the goals of this course and explain why it’s different from other courses I’ve written.

Creating a custom property wrapper using DynamicProperty

14:20

INTERMEDIATE SWIFTUI

FREE: Creating a custom property wrapper using DynamicProperty

It’s not hard to make a basic property wrapper, but if you want one that automatically updates the body property like @State you need to do some extra work. In this article I’ll show you exactly how it’s done, as we build a property wrapper capable of reading and writing documents from our app’s container.

WeSplit

7:17

SOLUTIONS

WeSplit

There are three challenges for WeSplit, including adding section headers and showing a grand total. Let’s solve them now…

Stacks

24:20

DATA STRUCTURES

Stacks

There are many data structures in computing, but stacks are one of the most fundamental – they get used in so many places, often without us even realizing. Helpfully, they are also one of the easiest types to learn, which makes them a great starting point for this new series on data structures.

Advanced string interpolation, part one

24:23

ADVANCED SWIFT

Advanced string interpolation, part one

String interpolation is easy, right? Wrong! String interpolation is actually a huge power feature in Swift, and we have a massive array of functionality on hand to help us customize it. In this article I’ll show you just how much control we have, and how to use that control to make your code easier to read.

Why opaque return types are so important

12:19

ADVANCED SWIFT

Why opaque return types are so important

Opaque return types are a powerful feature in Swift, and are also critically important for writing SwiftUI. In this article I’ll be explaining how they work, and why they give us more power than returning a simple protocol.

Creating an AccessibleStack that flips stack axis based on Dynamic Type

11:12

CUSTOM SWIFTUI COMPONENTS

Creating an AccessibleStack that flips stack axis based on Dynamic Type

There are several times when you might want to flip between a HStack and VStack, but one useful option is to look at the Dynamic Type size. Apple uses this itself to switch list rows to a vertical layout when using larger fonts, and in this tutorial I’ll show you how it’s done.

Existentials and type erasure – part 1

17:14

ADVANCED SWIFT

Existentials and type erasure – part 1

Type erasure helps us solve difficult type system problems by purposefully discarding some information. In this article we’ll look at what the underlying problem is and how Swift solves it, and in the second part we’ll continue on to look at how we can build type erasure ourselves.

Uploading Codable data

27:13

NETWORKING

Uploading Codable data

In a previous article we already looked at a great way to download data using Combine, but in this article we’re going to examine the other side of the problem: uploading Codable data. Apple’s API here is a little gnarly, so I’m going to show you how to wrap it in a neat container using generics and Result.

Testing extensions

26:04

ULTIMATE PORTFOLIO APP

Testing extensions

Our project has three extensions that add functionality to Apple’s own code, and we need to treat these carefully – we don’t own them, so their behavior might change at any point in the future. So, in this article we’re going to write tests for the extensions we created, making sure they work correctly every time.

Handling names correctly

18:50

MAKING THE MOST OF FOUNDATION

Handling names correctly

There are lots of UI mistakes we can make in programming, but unless our bugs actually get in the way of functionality most users don’t care that much. But there is one exception, and we’re going to look at it here: in this article I’ll show you how to handle names correctly – the most personal data of all.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.