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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
iOS Accessibility Tutorial: Making Custom Controls Accessible
20 mins
- Getting Started
- Experiencing VoiceOver
- Grouping Accessibility Elements
- Making Screen Updates
- Working with Custom Accessibility Actions
- Adding Your Rating Action
- Making the Dial Control Accessible
- Setting Accessibility Traits For Custom Controls
- Supporting Accessibility Gestures
- Overriding Accessibility Value
- Accessibility Frame in Container Space
- Supporting Reduced Motion
- Taking Accessibility to the Next Level
- Where to Go From Here?
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.
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.