Grouping elements for better accessibility on iOS

On iOS, VoiceOver users can navigate a screen through a swipe right gesture. Doing so causes iOS to change accessibility focus to the next accessible element in your views’ accessibility hierarchy. By default, many UIKit elements declare themselves as accessibility elements, e.g. UILabel, UIButton, &c.

The by-default accessibility of many iOS components is wonderful, but sometimes we need to do a bit more work. As a concrete example, let’s say that we have a UICollectionView composed of cells like the following:

collection view cell example

A straightforward implementation of the above view might look like this:

class ProductCard: UICollectionViewCell {
    @IBOutlet var image: UIImageView!
    @IBOutlet var brandName: UILabel!
    @IBOutlet var productName: UILabel!
    @IBOutlet var salePrice: UILabel!
    @IBOutlet var originalPrice: UILabel!
    @IBOutlet var discountLabel: UILabel!
    @IBOutlet var starView: UIImageView!
    @IBOutlet var reviewCount: UILabel!
    @IBOutlet var addToCartBtn: UIButton!

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

By default each UI element is its own accessibility element, which means this card would take 9 swipes to navigate through! That can be exhausting, particularly in a collection with many cells (especially if you have motor control concerns). Moreover, it makes more sense for some of these elements to be grouped and read together; e.g. instead of VoiceOver reading the sale price -> swipe right -> reading the original price, grouping the price labels into a single accessibility element better explains the intention and reduces the number of gestures required to navigate the card.

We can make this a bit more sensible by grouping the brand name & product name; the sale price, original price, & discount label; and the star view & review count. I might do so as follows:

class ProductCard: UICollectionViewCell {
  /* ... */

  func setupAccessibility() {
    accessibilityElements = [image, brandName, salePrice, starView, addToCartBtn]

    let brandAndProductLabel = "\(productName.text) by \(brandName.text)"
    brandName.accessibilityLabel = brandAndProductLabel

    let priceLabel = "Sale price: \(salePrice.text); originally \(originalPrice.text); \(discountLabel.text)"
    salePrice.accessibilityLabel = priceLabel

    let starCount = /* code to find current rating */
    let numRatings = /* code to find the number of ratings */
    let ratingsLabel = "Rating: \(starCount) out of 5; based on \(numRatings) reviews"
    starView.accessibilityLabel = ratingsLabel

In doing so we’ve done two things: one, reduce the number of navigation swipes per cell from 9 to 5; two, logically group related labels to give our VoiceOver users more context.

Before our changes, VoiceOver would read our card like this (a semicolon indicates a swipe gesture):

Image; thoughtbot; Product design and development; Five dollars; Ten dollars; You save fifty percent; Image; Nine thousand seven hundred eighty four; Add to cart button

After our changes:

Image; Product design and development by thoughtbot; Five dollars, originally ten dollars, you save fifty percent; Rating: five out of five, based on nine thousand seven hundred eighty four reviews; Add to cart button

Apple has done a great job of building accessibility into the UIKit framework, which allows many apps to be reasonably accessible by default. As we’ve seen, with just a small amount of attention and code, we as developers can provide an even better experience to app users with accessibility needs.