iOS Accessibility Tutorial: Making Custom Controls Accessible

In this iOS accessibility tutorial, you’ll learn to make custom controls accessible using VoiceOver, elements group, custom action, traits, frame and more. By Andrew Tetlaw.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Working with Custom Accessibility Actions

Custom accessibility actions have been available since iOS 8. They’re shortcuts to common operations.

VoiceOver will announce that actions are available when a container has custom accessibility actions. You’ll swipe up or down to iterate actions selection. A double-tap will activate the action.

Navigating Next and Previous in VoiceOver mode will be even easier if you add custom actions. The actions relieve the user of the need to swipe through all the elements to find the buttons.

First, open AmpDetailsViewController.swift. Then, add these methods to the class:

@objc func nextAmpAction() -> Bool {
  guard selectedIndex != dataSource.endIndex - 1 else { return false }
  selectedIndex = min(selectedIndex + 1, dataSource.endIndex - 1)
  return true
}

@objc func previousAmpAction() -> Bool {
  guard selectedIndex != 0 else { return false }
  selectedIndex = max(selectedIndex - 1, dataSource.startIndex)
  return true
}

These are the methods your custom actions will call. They simply increment and decrement the selected index respectively, while making sure it’s never out of bounds. The accessibility API requires the methods to return a Bool and have the @objc attribute.

Now, in viewDidLoad(), add the following code to the bottom:

let next = UIAccessibilityCustomAction(
  name: "Next amp", 
  target: self, 
  selector: #selector(nextAmpAction))

let previous = UIAccessibilityCustomAction(
  name: "Previous amp", 
  target: self, 
  selector: #selector(previousAmpAction))

accessibilityCustomActions = [next, previous]

You can see that adding a custom action is a lot like setting the action for a UIButton. The difference is that VoiceOver reads out the custom action name from the accessibility custom actions selection instead of having a user look, find and tap the button.

Build and run.

Wait a moment after the first announcement. You’ll hear an announcement about your custom actions being available.

Swipe up and down to select a custom action. Then, double-tap to activate it.

They work like buttons, but you interact with them in a different way.

Navigate to either end of the list and you’ll see the grayed out previous or next buttons. Notice how nextAmpAction() and previousAmpAction() return false in this scenario. This causes the bonk sound to play, indicating the action is not possible.

Adding Your Rating Action

There’s one more custom action you should add. Since this is all about rating classic amps, you should make the rating action as easy as possible with a custom rating action.

Still inside AmpDetailsViewController.swift, add this method:

@objc func rateAmpAction() -> Bool {
  performSegue(withIdentifier: "AddRating", sender: ratingButton)
  return true
}

This method triggers the existing storyboard segue for going to the AddRatingViewController. All that’s left is to add the custom action to the list of accessibility actions.

To do so, first add the following below previous in viewDidLoad():

let rate = UIAccessibilityCustomAction(
  name: "Add rating", 
  target: self, 
  selector: #selector(rateAmpAction))

Then, replace:

accessibilityCustomActions = [next, previous]

With:

accessibilityCustomActions = [rate, next, previous]

Now, build and run. Test out your new custom actions.

Time to face the music. The Add your rating experience is plain broken when using VoiceOver. You can’t even find the rating knob! But don’t fret, there is a solution.

Making the Dial Control Accessible

This calls for a better approach: subclassing UIAccessibilityElement. You can create your own accessibility element to represent a custom element for the accessibility system.

First, open AddRatingViewController.swift. Underneath the class, add the following new class:

class RatingKnobAccessibilityElement: UIAccessibilityElement {
  override init(accessibilityContainer container: Any) {
    super.init(accessibilityContainer: container)
    accessibilityLabel = "Rating selector"
    accessibilityHint = "Adjust your rating of this amp"
  }
}

The initializer sets the accessibility label and hint properties with appropriate VoiceOver values.

Now, in AddRatingViewController‘s viewDidLoad(), add the following at the bottom:

let ratingElement = RatingKnobAccessibilityElement(accessibilityContainer: self)
accessibilityElements = [cancelButton, ratingElement, addRatingButton]
  .compactMap { $0 }
UIAccessibility.post(notification: .screenChanged, argument: ratingElement)

Here, you set the accessibilityElements array as you did for AmpDetailsViewController. The only difference is that one of the elements is an instance of your RatingKnobAccessibilityElement. You also post .screenChanged notification so that element for changing the rating gets focus first.

Notice that you pass the view controller as the accessibilityContainer? That’s important as you’ll see in a moment.

Now, open Main.storyboard. In Add Rating View Controller Scene, uncheck the Accessibility Enabled checkbox of Rating Label, Highest Value and Lowest Value. You don’t need them to be read out by VoiceOver.

Build and run. Activate the Add your rating action.

Once you’re at the Add Rating View Controller you can now swipe between Cancel, Rating Selector and Add your rating. The rating knob is now visible to VoiceOver thanks to your RatingKnobAccessibilityElement instance. It’s not very useful without being able to adjust the rating value though.

Hold tight, because the rating knob is about to get seriously cool.

Setting Accessibility Traits For Custom Controls

Open AddRatingViewController.swift, locate RatingKnobAccessibilityElement and add the following to init(accessibilityContainer:):

accessibilityTraits = [.adjustable]

When you add adjustable, you indicate that this custom element has a value you can increment or decrement. This is perfect for your rating knob.

Note: There are many more interesting accessibility traits available. You can find a complete list in Apple’s documentation. You only have to worry about them for your custom elements. All UIKit elements already have the appropriate traits set.

Build and run. Then activate Add rating. Swipe to the rating selector. You’ll hear a lot of new information:

Rating selector, adjustable, adjust your rating of this amp, swipe up or down with one finger to adjust the value.

However, if you swipe up or down, the rating value doesn’t get adjusted.

Supporting Accessibility Gestures

This is where you unleash the secret power of adjustable. All you need to do is override accessibilityIncrement() and accessibilityDecrement().

First, in AddRatingViewController, add these methods:

func incrementAction() {
  ratingKnob.setValue(ratingKnob.value + 1)
  setRating(ratingKnob.value)
}

func decrementAction() {
  ratingKnob.setValue(ratingKnob.value - 1)
  setRating(ratingKnob.value)
}

These are the methods the adjustable gestures will call. They simply adjust the rating value and update the label.

Then, in RatingKnobAccessibilityElement, add this helper property:

var ratingViewController: AddRatingViewController? {
  return accessibilityContainer as? AddRatingViewController
}

You’ll call ratingViewController often in the rest of the code, so this will save a lot of typing.

Next, add the following method overrides to RatingKnobAccessibilityElement:

override func accessibilityIncrement() {
  ratingViewController?.incrementAction()
}

override func accessibilityDecrement() {
  ratingViewController?.decrementAction()
}

Build and run. If you have Screen Curtain on, disable it for now so you can see if the screen is updating. You should find that when you swipe up and down to adjust the value, the knob is actually updating correctly.

However, there’s no VoiceOver announcement.