Supporting VoiceOver on Custom UIViews

Customise the accessibility experience in iOS

It’s paramount more than ever to make your app accessible and inclusive to all of your audience. Not only its a law but it’s also a question of ethics. Making good accessible apps is thus our responsibility.

Apple has been leading and innovating in accessibility. Each year Apple introduces features that enhances the accessibility in their products and software. They extend and even promote accessibility features to developers for their operating systems through their API’s.

This blog post is about how to leverage accessibility API’s on iOS UIKit. In this post we’ll create a progress bar that can be used for uploads. We’ll be drawing the progress bar. This progress bar will be useable for some users, however visually impaired users who rely using the VoiceOver will not be able to consume our progress bar. VoiceOver is an iOS feature that reads the screen to the user. We’ll need to make adjustment to the accessibility properties on our progress bar to make it useable by visually impaired users.

For this blog post I assume you are already familiar with the basics of Swift and iOS development using UIKit. You’ll also need a physical iPhone and an Apple developer account to run the app on the device.

I have used Swift 5.3 and Xcode 12.0.1 whilst writing this article.

Getting Started

In this section we’ll create an app from scratch. We’ll then add a custom progress bar to our app. We’ll be drawing the progress bar instead of using an off the shelf UIView. Then we’ll run VoiceOver on our app and check the experience. Finally we’ll tweak the VoiceOver experience for our custom progress bar by tweaking the accessibility configuration of out custom view.

The steps we’ll take:

  1. Create app from scratch
  2. Add custom progress bar
  3. Test VoiceOver experience
  4. Customise the VoiceOver experience of the progress bar

Let’s dive in!

1. Create app from scratch

Let’s begin by create a new iOS app project. Open Xcode and then from menu select File > New > Project…

Next when prompted “Choose a template for your new project:” select App from the iOS tab. Then click Next.

Name the product AccessibleProgressBar. For Interface select Storyboard. For Life Cycle select UIKit App Delegate. For Language select Swift. Uncheck all check boxes and then click Next.

Finally navigate to an appropriate place to save the project. Finally click Create.

2. Add custom progress bar

Next let’s add a progress bar view into our project. From menu select File > New > File… When prompted Choose a template for your new file select Swift File from the iOS tab. Finally click Next.

Name the file ProgressBarView and finally click Create.

Next copy and post the following code:

We won’t delve into the implementation of the progress bar. However it is important to know that the progress bar is to be drawn on the screen. Views provided by Apple such as UILabel or UIButton already support VoiceOver. However our custom view currently does not.

Next let’s add the progress bar view to the screen. We’ll also add an upload button. When the button gets tapped we’ll fake some upload progress on the progress bar. Open ViewController.swift file and add the following properties to ViewController class:

private weak var progressBarView: ProgressBarView!
private weak var uploadButton: UIButton!

Next let’s write a function to add the progress bar view to the screen:

private func addProgressBarView() {
let progressBarView = ProgressBarView(frame: CGRect.zero)
progressBarView.translatesAutoresizingMaskIntoConstraints = false
progressBarView.backgroundColor = UIColor.systemTeal
progressBarView.progressColor = UIColor.systemBlue
self.progressBarView = progressBarView
self.progressBarView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
self.progressBarView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
self.progressBarView.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.8),
self.progressBarView.heightAnchor.constraint(equalTo: self.view.heightAnchor, multiplier: 0.05)

Now let’s write a function to add the upload button. We’ll also need to include an function which will be notified when the button has been tapped:

private func addUploadButton() {
let uploadButton = UIButton()
uploadButton.translatesAutoresizingMaskIntoConstraints = false
uploadButton.setTitle("Upload", for: .normal)
uploadButton.addTarget(self, action: #selector(self.didTapUploadButton), for: .touchUpInside)
uploadButton.setTitleColor(.blue, for: .normal)
uploadButton.setTitleColor(.gray, for: .disabled)
self.uploadButton = uploadButton
self.uploadButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
self.uploadButton.topAnchor.constraint(equalTo: self.progressBarView.bottomAnchor, constant: 5)
@objc private func didTapUploadButton() {
// upload tapped fill in upload action here

We’ll fill in didTapUploadButton later.

And finally let’s call our functions from within viewDidLoad:

override func viewDidLoad() {

If you run the app and tap the upload button you’ll notice that nothing happens. Let’s change that. When the user taps on the upload button we’ll fake a network request. Copy and past the following code:

private var totalProgress: CGFloat = 0private func scheduleTimer(progress: CGFloat, onCompletion: @escaping () -> ()) {
withTimeInterval: TimeInterval.random(in: 1...2),
repeats: false,
block: { _ in
.progressBarView.progress = progress
private func updateProgress(onProgressCompleted: @escaping () -> ()) {
guard totalProgress < 1 else {
self.scheduleTimer(progress: self.totalProgress, onCompletion: { self.updateProgress(onProgressCompleted: onProgressCompleted) })
private func addToProgress() {
let rand = CGFloat.random(in: 0...1)
self.totalProgress += rand
if self.totalProgress > 1 {
self.totalProgress = 1.0

The functions above basically create some random progress and schedule a timer between 1 and 2 seconds then update the progress bar view. This is to fake the illusion of a network request. We won’t delve in the code as this is out of scope of this post.

Next let’s use the functions to fake progress when the user taps the upload button. Add the following lines of code to the didTapUploadButton function:

self.uploadButton.isEnabled = false
.totalProgress = 0
self.progressBarView.progress = 0.0
self.updateProgress(onProgressCompleted: { self.uploadButton.isEnabled = true })

That’s it. Run the app and see the progress bar in action!

3. Test VoiceOver experience

Next let’s test the VoiceOver experience. For this you will need to run the app on a device and thus need an Apple developer account. I assume you already have this and able to run on device.

To enable VoiceOver navigate on the device to Settings > Accessibility > VoiceOver and then turn on the switch.

Then you’ll need to navigate back to the AccessibleProgressBar app using the VoiceOver gestures. You can also just tell Siri to turn on VoiceOver.

Notice that the progress bar is not visible to VoiceOver. That is you can’t focus on the element. Furthermore when we tap (or double tap when VoiceOver is on) on the upload button then the user is not informed that an upload is happening. For the visually impaired user this could feel like the app is unresponsive or has crashed.

4. Customise the VoiceOver experience of the progress bar

In this step we’ll tweak the accessibility configuration of our custom view so the visually impaired user is included in the progress update experience. To fix that we first have to tell UIKit that our custom view is an accessible element. To do that open ProgressBarView.swift and add the following line to the commonInit function:

self.isAccessibilityElement = true

The progress bar is now focusable by VoiceOver. However VoiceOver still does not tell us anything about the view or its status. Let’s tell VoiceOver what tell the user when the progress bar view is focused. Add the following line to the commonInit:

self.accessibilityLabel = "progress bar"

Run it again and focus on the progress bar. VoiceOver now tells the user the focused element is a progress bar!

Most users can visually see the progress made when the user taps the upload button. However the visually impaired user is not informed about the progress made. Let’s fix that. Inside the didSet closure of the progress property add the following line:

let progressString = "\(Int(self.progress * 100))%"
self.accessibilityValue = progressString
UIAccessibility.post(notification: .announcement, argument: progressString)

The above will set the current progress as the value of the view element. Thus when the user focuses on the progress bar VoiceOver will read “Progress bar — {progress value}%”. However the system will not re-read the value once the user is focused on it. To keep the user posted on the progress we have added:

UIAccessibility.post(notification: .announcement, argument: progressString)

This line basically tells VoiceOver to read a string. Here we simply notify the user the new progress value. Note this will only be announced to VoiceOver users.

Run the app and test the VoiceOver experience. You’ll notice that when the user double taps on the upload button the focus stays on the focus button. Ideally we want to move the focus from the upload button to the progress bar. To fix that open ViewController.swift file and at the beginning of didTapUploadButton add the following line:

UIAccessibility.post(notification: .screenChanged, argument: self.progressBarView)

Run the app again and notice that on double tap on the upload button the focus moves from the button to the progress bar view.

And that’s it! The VoiceOver user has now a better user experience on your app 🙌🏽


In this post you have learnt:

  • how to support VoiceOver on custom view
  • how to customise the VoiceOver experience using UIAccessibility.post
  • enhanced your skills in building more inclusive apps 👏🏽

Final Notes

You can find the full source code in the link below:

When I became a software engineer accessibility was not in the forefront of my mind. However I have been fortunate enough to experience user testing from those who require these features. I have seen their struggle but I have also seen some smiles when they have been able to use apps and service without any help. Helping people become independent is empowering for the society as a whole.

If you’d like to keep expanding your user audience as well as making your app even more inclusive checkout my previous post on accessibility where I show you how to support large text size.

For more on iOS development follow me on Twitter or Medium!

Senior iOS Engineer @ Onfido. Writing weekly blogs on iOS and programming. Follow me to stay tuned!

Get the Medium app