The Voice Of… iOS

Photo by Jason Rosewell on Unsplash

VoiceOver is often referred to as Apple’s screen-reading technology that gives users control over their device without seeing the screen. It is one of the tools used to make an app more accessible. Most people think it only benefits the visual impaired users and that it is a lot of work to implement… But actually both are not true! Experiencing your app through VoiceOver reveals things like copy, order or navigation that doesn’t sound right and this can be an indication that visually it may not read or navigate well either. So how can you make your app more accessible in a few easy steps?

Where does your app stand right now?

The first thing you can do, is to check how good or bad your app provides information via VoiceOver right now. The easiest way is to check this with the simulator using Xcode’s built in Accessibility Inspector. Launch your app in the simulator and open in the menu bar Xcode > Open Developer Tool > Accessibility Inspector. Select the simulator in the top left corner of the inspector and click the gun-sight button next to it. Hover over the different elements in your app and see what their label, value and traits tell you. Is there information lacking or enough useful for the user?

The Accessibility Inspector

A better way of testing is to actually install the app on a device and turn on VoiceOver via Settings > General > Accessibility > VoiceOver. To quickly turn it on and off you can set a shortcut in Settings > General > Accessibility > Accessibility Shortcut so you can triple click the side-button or home-button to (de)activate VoiceOver. With VoiceOver enabled you can do a 3-finger triple click to activate screen curtain, that makes your screen completely black. Now you can experience your app without seeing anything!

Gestures in VoiceOver and the Rotor

If you turn VoiceOver on it might be handy to know some gestures to navigate around the screens:

Gestures in VoiceOver mode.

If you tap with two fingers and rotate, the Rotor appears that gives some contextual options. For example if you set it to Headings it will only speak the headers to the user when swiping up or down. Like scanning the headers of a newspaper before you dive into the details. Important here is that you give so-called traits of Header to certain labels, otherwise it will all look like plain text to VoiceOver.

Furthermore the rotor can change speech-velocity and the way the text is read, like word by word or character by character. Or you can change the edit or type method of the keyboard when you need to enter text in a text- or searchfield.

The rotor

So you can now navigate around and detect missing information or information that can be improved.

Standard UIKit controls and views

Standard UIKit controls and views are automatically accessible. You can see their settings in the Identity Inspector if you use Storyboards or nib-files.

Identity Inspector contains Accessibility section

You can also set them in code. Here are their meanings:

  • isAccessibilityElement: tells if VoiceOver should be able to detect it.
  • accessibilityLabel: provides descriptive text that VoiceOver reads when the user selects an element. It should be short and informative.
  • accessibilityHint: provides extra information what happens if the user interacts with it. Hints are not always necessary. If you feel like you’re saying too much in an accessibilityLabel, consider moving that text into a hint. VoiceOver uses a small delay between speaking the accessibilityLabel and accessibilityHint. Critical information should never be in the hint, because users can turn hints off.
  • accessibilityTraits: The first kind of traits are the ones that describe the type of element (static text, button). These should be considered mutually exclusive: a button that opens a url in the browser should not have the trait button, but the trait link. The trait image also falls into this category, but is an exception. It can be combined with other traits, like button.
    The other type of traits provide context: 
    UIAccessibilityTraitSelected: is the element selected? Like current selected row in table, selected segment in segmented control or keyboard key.
    UIAccessibilityTraitNotEnabled: can the user interact with it (in Interface Builder userInteractionEnabled)? If it is not enabled, the element is dimmed: the user knows about the existence but can’t interact with it. UIAccessibilityTraitUpdatesFrequently: is the element value updating frequently? indicates that the element updates its label and/or value too often for VoiceOver to read out every single change. VoiceOver will take note of this trait and will instead poll for changes at suitable times. This can be used for progress bars, countdown timers and so forth. UIAccessibilityTraitSummaryElement should be used sparsely and provides a summary of the current situation or state when the application starts. For example in a weather app it can speak today’s forecast for the current location or in a download app the current completed downloads.
    UIAccessibilityTraitStartsMediaSession is handy for example when tapping a button will play a sound that is shorter than the announcement of VoiceOver.
    UIAccessibilityTraitAdjustable should be used with care. It will announce that the user can swipe up or down to adjust the value, but it overrules the VoiceOver and Rotor default gestures. You can override the methods accessibilityIncrement() and accessibilityDecrement() to provide the right implementations.
    UIAccessibilityTraitAllowsDirectInteraction allows the element to be interacted with without having to double tap on that element. For example when the user can draw on the screen.
    If you want to add traits programmatically you can do this like:element.accessibilityTraits = [.button, .selected]
  • accessibilityValue will tell you the current value of a control object like a slider.

A UILabel and accessibilityLabel are different things. By default VoiceOver speaks the text associated with standard UIKit controls as UILabel and UIButton. However these controls can also have corresponding accessibilityLabel properties to add more detail about the label or button.

If your app contains a shop for example and you see a list with items that contain a “Buy”-button, it would not make a lot of sense for the user to just hear “Buy button” when they come across your button. It would make more sense to also mention what they’re going to buy: “Buy AppleWatch” for example:

buyButton.accessibiliyLabel = "Buy \(article.name)"

If an element has a certain trait (e.g. button), you should not repeat this information in the accessibilityLabel (e.g. “Buy AppleWatch button — button”).

Adding a comma in your description can help making the label sound a bit better to your users by adding a subtle pause.

New is accessibilityAttributedLabel where you can set a certain language for VoiceOver to pronounce a word in (e.g. “Bonjour!”) or where you can queue a speech behind any speech that is already there.

let attributedLabel = NSAttributedString(string: "Bonjour", attributes: [UIAccessibilitySpeechAttributeLanguage : "fr-FR"]
imageView.accessibilityAttributedLabel = attributedLabel

Images

In iOS11 and up VoiceOver can detect text in images. It also wil describe the number of faces in the image, the expressions they have, the overall scene and the bluriness level. It will not override a description you provided yourself, but with a 3-finger tap on the image you will hear the Apple description.

Group accessibility information and custom views

VoiceOver speaks from the leading to the trailing edge. It could be that you have labels in a vertical stack view that belong together, but VoiceOver is not speaking them in the right order, because after speaking the first label it first jumps to another element and later comes back to your second label. The user looses context.

Then you want to group these elements by creating a UIAccessibilityElement, initialised with the view that’s containing the elements as a container view, and then adding the information you would like to have grouped together:

var elements = [UIAccessibilityElement]()
let groupedElement = UIAccessibilityElement(accessibilityContainer: self)
groupedElement.accessibilityLabel = "(nameTitle.text!), \(nameValue.text!)"
groupedElement.accessibilityFrameInContainerSpace = nameTitle.frame.union(nameValue.frame)
elements.append(groupedElement)

You can also use this for custom views that contain elements that are not instances of UIView but drawn with CoreGraphics for example.

A view that implements the UIAccessibilityContainer informal protocol uses the UIAccessibilityElement method init(accessibilityContainer:) to create an accessibility element to represent each non-view component that needs to be accessible to users with disabilities. The container view itself is not an accessibility element because users interact with the contents, not with the container. This means that a container view that implements the UIAccessibilityContainer methods must set to false the isAccessibilityElement property of the UIAccessibilityinformal protocol.

The order of accessibility elements within the container view should be the same as the order in which the represented elements are presented to the user, from top-leading to bottom-trailing.

If you want to set the order you can also use the accessibilityElements property on the view. The order of the elements in this array will be the order VoiceOver will speak them.

TableViews and CollectionViews

If your tableView consists of cells that display multiple elements you need to determine if users can interact with each cell as a unit or if the user needs to access individual elements.

Standard elements as a disclosure indicator or delete control are accessible out-of-the-box.

You do not have to identify UITableViewCell as a container view or implement any of the methods of the UIAccessibilityContainer protocol, because the table cell is automatically designated as a container.

If each element has to be accessed individually theisAccessibilityElement is set to true and the container itself to false.

If you want the cell to be spoken out in one swipe of the user you have to make a description about the overall contents of the cell and use this as the label attribute of the cell. Set the isAccessibilityElement of the container to true.

Modal views

If you’re showing a custom modal view, VoiceOver might still allow users to select and activate controls behind it. To stop that happening, set your modal view’s accessibilityViewIsModal property to true.

Make sure that your modal view supports the escape gesture (2-finger Z-gesture) by overriding theaccessibilityPerformEscape() method. The UINavigationController supports this gesture by default.

Magic Tap

Another standard gesture is the 2-finger tap on the screen a.k.a. the magic tap. You can support this gesture by overriding the accessibilityPerformMagicTap() method.

Accessibility notifications

There are three types of UI change notification:

  • screen changes: when the user navigates to a different screen VoiceOver notifies the user with a tone, clears its caches and does preparation to deal with a new set of accessibility data.
UIAccessibility.post(notification: .screenChanged, argument: nil)
  • layout changes: when some part of the UI changes. For example someone presses on a price label and it changes into a “Buy”-button. This notification tells VoiceOver to re-read the current state of all the accessible items that are on screen, it detects the changes and informs the user.
UIAccessibility.post(notification: .layoutChanged, argument: nil)
  • dynamic changes: if a part of the UI is dealing with dynamically changing information like a loading state (“Connected to server”, Loading”, “Loading complete”) or when a significant change in the data of the compass has occured. VoiceOver speaks this information by using the notification:
UIAccessibility.post(notification: .announcement, argument: nil)
  • page scrolled: when the user performs a VoiceOver scroll gesture (see gestures table), this notification can provide information about the contents of the screen. For example it is used in an app with pages like “Page 19 of 23.” or in a tab-based application “Tab 3 of 5.”
UIAccessibility.post(notification: .pageScrolled, argument: nil)

Detect VoiceOver is running

Sometimes it is useful to determine whether VoiceOver is running or not. For example to display a simplified UI or keep short-appearing notes on screen. You can do this with:

if UIAccessibilityIsVoiceOverRunning() {
// VoiceOver is active (do something alternative)
} else {
// VoiceOver is not active (do usual stuff)
}

If the state changes you can be notified by signing up as an observer:

notificationCenter.addObserver(self, selector: #selector(voiceOverStatusChanged), name: UIAccessibility.voiceOverStatusDidChangeNotification, object: nil)

Don’t forget to remove the observer! I hope you will start experiencing your app via VoiceOver and improve the accessibility, so your app becomes better and more people can enjoy it!