Detecting VoiceOver Status Changes

This is a follow up to my recent post about adding VoiceOver Accessibility support to an iOS app to cover situations where you need to adjust the user interface when VoiceOver is active.

Gestures and Accessibility

I generally dislike Apps that rely exclusively on gestures such as touches, swipes or shaking the device to perform an operation. The gestures may well have been obvious to the developer but as a user I often struggle to discover what I am supposed to do. This leads to a situation where you end up randomly touching, pinching and swiping the screen to discover “hidden” features. This not only leads to a poor user experience but also presents real challenges to accessibility as the gestures are generally not available when VoiceOver is active.

I like to think of gestures as being the equivalent of a keyboard shortcut for a desktop application. The keyboard shortcut allows power users to perform an operation more efficiently but a toolbar icon or menu option allows a normal user to discover and perform the same operation. Likewise if an app makes use of a gesture it should generally have some visible on-screen control that can be used to perform the same operation. A good example is the common iOS gesture of swiping to delete a row in a table. If you are not aware of the gesture you can still use the edit button in the toolbar and then use the on-screen controls to delete rows. This also removes any issues with VoiceOver as long as you set the accessibility attributes for the on-screen controls.

Relying on a gesture as the only way to access a feature not only makes your app difficult to use with VoiceOver it also makes your app difficult to use for all users. Having said that if you have a situation where you really do not want to have an on-screen control you should at least consider modifying the user interface of the app when VoiceOver is active.

Another example that can cause accessibility issues can occur when you are displaying some information for a limited period of time. For example you might keep some details about a download on-screen for a few seconds after it completes. Using VoiceOver those few seconds might not be enough time for the user to navigate to, select and have VoiceOver read the label. In both cases we need to be able to detect VoiceOver status and change the behaviour and maybe even the user interface when it is active.

Revisiting the Task Timer

I previously created a simple task management app to show the basic techniques involved in adding VoiceOver support. The full details are contained in this previous post but the basic idea was to allow the user to create tasks and then time how long they took to complete. I am going to add a new feature to the app which will allow us to reset the duration of a task by shaking the device. This is just the sort of bad user interface design that I was referring to earlier but one which I hope will illustrate the idea.

Before we take a look at detecting whether VoiceOver is active we need to go on a small diversion to implement the shake to reset feature. Luckily this turns out to be trivial. When a user shakes a device the accelerometer generates motion events. The motion events can be received by any subclass of UIResponder but we need to make sure that our view controller can become the first responder:

- (BOOL)canBecomeFirstResponder
{
  return YES;
}

We do not care in which direction the device is shaken so to detect the shake we implement motionEnded:withEvent: in the task view controller:

- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
  [self taskResetAction];
}

I will not bother showing the details of the taskResetAction here as it is not relevant to this discussion. You can check the code if you are interested. Note that there is no way to test device shaking in the simulator so you will need to deploy to an actual device if you want to verify that the shake event actually works. The shake action will still work even if VoiceOver is activated but to improve the accessibility of the app we will look at how we can provide an alternative user interface when we detect VoiceOver is in use.

Checking the Status of VoiceOver

There are two ways to determine or be informed of the status of VoiceOver. The first way is to call the UIKit function UIAccessibilityIsVoiceOverRunning(). This returns a BOOL indicating if VoiceOver is active. The second way is to register for the UIAccessibilityVoiceOverStatusChanged notification. We will use both of these techniques to show a reset button in our detailed task view whenever VoiceOver is active:

The first thing we will do is add the reset button to both the iPhone and iPad Nib files for the task view controller and connect its outlet and action. The reset button is just a UIButton with a custom image inserted into the task view as shown below:

We can set the accessibility label for the button directly in Interface Builder:

To ensure that the button is not normally visible we also set the hidden property in Interface Builder:

Our view controller implements an IBOutlet property that is wired up to the button.

@property (weak, nonatomic) IBOutlet UIButton *taskResetButton;

The button action is connected to the same taskResetAction we used previously to respond to the shaking motion event:

- (IBAction)taskResetAction;

Now when our view first loads the viewDidLoad method in the view controller is used to configure the view. So we can use a call to UIAccessibilityIsVoiceOverRunning() to see if VoiceOver is already active and if so ensure our reset button is visible:

self.taskResetButton.hidden = ! UIAccessibilityIsVoiceOverRunning();

That takes care of situations where VoiceOver is in use when our view loads but not if it is activated or disabled when our view is already onscreen. For that we need to register an observer for the notification in the viewDidLoad method:

- (void)viewDidLoad
{
  ...
  ...

  [[NSNotificationCenter defaultCenter] addObserver:self 
                         selector:@selector(voiceOverStatusChanged)
                         name:UIAccessibilityVoiceOverStatusChanged
                         object:nil];
}

To be sure we clean up when our view controller goes away we should of course unregister the observer in viewDidUnload and dealloc. Since we are using ARC we have not so far implemented dealloc so we need to add it now just to remove the observer:

- (void)dealloc
{
  [[NSNotificationCenter defaultCenter] removeObserver:self];
}

The notification does not actually provide a parameter indicating the VoiceOver status but that is not an issue as we can simply call UIAccessibilityIsVoiceOverRunning again to get the current status. Depending on the status we can show or hide the reset button:

- (void)voiceOverStatusChanged
{
  self.taskResetButton.hidden = ! UIAccessibilityIsVoiceOverRunning();
}

Now as we enable or disable VoiceOver with the task view visible the reset button is dynamically shown or hidden in response.

Summary

As I discussed at the start of this post if you find yourself needing to modify your app behaviour for VoiceOver it may actually be a warning of a more fundamental problem in your interface design. For those situations where you really do need to do something different I hope this post proves to be useful.

You can find the now archived Xcode project used in this post in my GitHub CodeExamples repository.