NEW: Learn to build the incredible iOS 15 Weather app today! >>

< 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


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.

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.

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.

Understanding assertions

27:33

INTERMEDIATE SWIFT

FREE: Understanding assertions

Assertions allow us to have Swift silently check the state of our program at runtime, but if you want to get them right you need to understand some intricacies. In this article I’ll walk you through the five ways we can make assertions in Swift, and provide clear advice on which to use and when.

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.

Challenge 1: Converter

38:12

SOLUTIONS

FREE: Challenge 1: Converter

This early challenge day asks you to build a converter app that’s able to move between any two similar units, such as kilometers and miles. Let’s solve it now, then we’ll take it further, and then we’ll take it even further

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.

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.

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.

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.

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 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.

Transforming data with map()

42:32

FUNCTIONAL PROGRAMMING

FREE: Transforming data with map()

In this article we’re going to look at the map() function, which transforms one thing into another thing. Along the way we’ll also be exploring some core concepts of functional programming, so if you read no other articles in this course at least read this one!

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 – part 2

20:24

ADVANCED SWIFT

Making the most of optionals – part 2

I already introduced how the internals of optionals work, including how they use conditional conformance and how to avoid infinitely sized structs. In this video I’m going to go further as we look at how our knowledge of Optional can be translated to Result, why it’s so important that optionals are functors and monads, and more.

Sorted arrays

15:34

DATA STRUCTURES

Sorted arrays

A sorted array is one that retains a correct sort order no matter how and when you add items. Although this sounds simple enough to implement, in this article you’ll see that it’s actually quite fun to explore because there are a number of interesting challenges we’ll face.

Existentials and type erasure – part 2

41:03

ADVANCED SWIFT

Existentials and type erasure – part 2

In the first part of this tutorial we looked at the underlying problem that type erasure is trying to solve, and tried out Swift’s approach using AnySequence. In this second part we’re going to adapt Swift’s own solution to get real type erasure for our own code.

Posting comments through CloudKit

23:21

ULTIMATE PORTFOLIO APP

Posting comments through CloudKit

The last major piece of CloudKit work we’re going to add will let users post comments on shared projects – hopefully encouraging ones! This will combine querying and writing CloudKit data in a single part of our app, and also demonstrate how to write single records rather than several at once.

Core Data delete rules and predicates

14:20

ULTIMATE PORTFOLIO APP

Core Data delete rules and predicates

One of the least obvious but most important clean ups lies in our use of Core Data, because right now we’re leaking data and also showing flat out wrong data. To fix these we need to use some more advanced Core Data, so let’s get into it…

Bringing MVVM into our SwiftUI project, part 1

43:39

ULTIMATE PORTFOLIO APP

Bringing MVVM into our SwiftUI project, part 1

The final major change we’re going to make to our project is to look at how it fits in with the MVVM design pattern. I left this one to last because it’s quite a jump from our previous work and in some respects SwiftUI even fights against it, but I do think it’s worth exploring so you can be sure your code is sound.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.