© Daniel Devesa Derksen-Staats 2019
D. D. Derksen-StaatsDeveloping Accessible iOS Appshttps://doi.org/10.1007/978-1-4842-5308-3_4

4. VoiceOver – Up to 11

Daniel Derksen-Staats1 
(1)
London, UK
 

Now we know the foundations for creating accessible apps for VoiceOver users and even Voice Control users. But we don’t want to stop there, we don’t want our apps to be just usable, we want to go one step further and make them exceptional and delightful, for all our users. Let’s bring our VoiceOver support to the next level.

Did you know that a Magic Tap double tap with two fingers should execute the main action of the current view? Or that a “Z” gesture with two fingers means Perform Escape and should close the current view? In this chapter we’ll tune your VoiceOver skills up to 11.
../images/473721_1_En_4_Chapter/473721_1_En_4_Fig1_HTML.jpg
Figure 4-1

Accessibility ➤ Up to 11

Advanced VoiceOver gestures

These custom actions are less known by non-VoiceOver users, but if you want to go beyond offering an acceptable experience and offer an excellent one instead, this is a good place to start by implementing both the Performe Escape action and Magic Tap where it makes sense.

Perform Escape

This is one of the most useful VoiceOver actions that developers forget to implement. But imagine for one second that, when presenting a view controller in a navigation controller, the back button was not there or didn’t respond. Or that a modal view was presented with a tiny close or done button that had a very small hit area, so it made the view very difficult to dismiss. You would consider all those serious bugs and you would probably get frustrated with the app and stop using it, or in the case that it was your own app, you would probably prioritize some bug tickets to be able to fix them in the next release.

This is exactly the kind of experience you might be offering to your VoiceOver users if you are not implementing the Perform Escape action. The Perform Escape action allows VoiceOver users to go back or get out of a section of your app with a two-finger scrub back and forth gesture like drawing a “Z” on the screen.

If you are using a navigation controller, this gesture should already work for you out of the box. But if you are presenting view controllers with a custom mechanism or implementation, you may want to test it and make sure it works. It is a good idea to check any modal presentation too, including modal view controllers, popovers, custom alerts, custom action sheets, etc.

The good news again is that the implementation is really easy. You will just need to override the accessibilityPerformEscape() method to capture that gesture in the view or view controller and dismiss it appropriately. In the case of a modal view controller, it would be as easy as this:
// Dismiss modal view with an escape gesture when using VoiceOver
override func accessibilityPerformEscape() -> Bool {
    self.dismiss(animated: true, completion: nil)
    return true
}

Magic Tap

There are also lots of situations where there is a main action in a particular section of your app. That action is probably very well defined, maybe the button that triggers it is bigger, it has a predominant position in the UI, etc. It is easy to find and action.

Some examples would be a start/stop action in a timer app, the button to take a picture in a photography app, a player button in a music or video on demand app, etc.

Now imagine that you open the player and the play icon is somewhere you wouldn’t expect like the top left corner, or that it is very small or not noticeable at all. That would be strange as it is probably the button that all your users are going to look for as soon as they open the app.

This is a similar example as the Perform Escape action; it is called Magic Tap. A VoiceOver user will expect that main action to be triggered when performing a double tap with two fingers on the screen. For example, double tap with two fingers to play and repeat the action to pause. In this case we just need to override the accessibilityPerformMagicTap() function. The code would look something similar to this:
// Perform play/pause actions with Magic Tap
override func accessibilityPerformMagicTap() -> Bool {
        if player.isPlaying {
                player.pause()
        } else {
                player.play()
        }
        return true
}

Managing the accessibility focus

Most of times you’ll find that VoiceOver’s focus goes where you would expect. If you swipe right, it moves to the next element; if you swipe left, it goes to the previous one. However, sometimes the UI changes as the user interacts with the app, and it is important to tell the accessibility framework how to behave in those situations. Again, this is something you will find more often when developing custom UI components. It will be easier to understand with some examples.

Is the accessibility view a modal view?

Imagine that you have to implement a little pop-up, maybe to show some more detailed information. Something like the one in the screenshot (Figure 4-2). You would find that VoiceOver can still “see” the UI elements that are covered by the custom pop-up, and it creates a very confusing experience. This happens a lot when implementing things like slide-over hamburger menus, where VoiceOver starts jumping in a way that feels random between elements of the menu and the main content.
../images/473721_1_En_4_Chapter/473721_1_En_4_Fig2_HTML.jpg
Figure 4-2

Before presenting a custom modal view with VoiceOver (left). Incorrect implementation (center). Expected result (right)

Fortunately, the accessibility framework has a property to define that a view should behave as a modal, “hiding” to VoiceOver any accessible elements that are siblings of the currently one on focus.
// Specify that a view behaves as a modal view
customModalView.accessibilityViewIsModal = true

Post notifications

Following previous example, when presenting the “modal” view, you would like the focus to move to the newly presented view, instead of staying in the button that has just been actioned. When major UI changes happen on screen, you may need to notify it. In this case, you would probably want to notify that the screen changed and pass the custom modal view as an argument so that it gets the focus. When dismissing it, you want to notify that the screen changed again and return the focus to the button where you were before presenting the view.
// Post notification for a screen change on modal presentation
UIAccessibility.post(notification: .screenChanged, argument: customModalView)
The .screenChanged notification is then used when a major portion of the screen changes, but there are some other notifications you may want to post when your screen changes slightly. For example, if what changes is a new element appearing or disappearing from the screen, you would use .layoutChanged instead. This is very useful when an information is collapsed and you can expand it, collapse it again, etc. Like the example in the image (Figure 4-3). On the other hand, if that element that appeared on screen is going to stay there briefly and then disappear again, like custom toast/snack bar-like component, custom in-app notifications, etc., you can use an .announcement notification instead.
../images/473721_1_En_4_Chapter/473721_1_En_4_Fig3_HTML.jpg
Figure 4-3

Example of VoiceOver usage when some information in the screen has been hidden and a “Layout Changed” notification is sent

Accessibility labels can be attributed labels, as we have seen before. And one of the attributes available lets you specify if an announcement should be queued or if it is important enough to interrupt any ongoing ones.
// Send a queued announcement
let queueAnnouncementAttributes = [NSAttributedString.Key.accessibilitySpeechQueueAnnouncement: true]
let announcementAttributedString = NSAttributedString(string: customToastView.accessibilityLabel!, attributes: queueAnnouncementAttributes)
UIAccessibility.post(notification: .announcement, argument: announcementAttributedString)

Another important notification you may want to post is when scrolling a view on behalf of the user: .pageScrolled.

If you would like to know more about it, you can find all the details in Apple’s documentation: https://developer.apple.com/documentation/uikit/uiaccessibility/notification.

Control the order of elements on screen

As we have seen, VoiceOver generally moves its focus to the next element, going from left to right and from top to bottom, when swiping right. However, sometimes we tend to distribute things visually in a way that makes this default behavior not the best one for our UI to make sense when vocalized by VoiceOver. A common pattern in lots of apps, for example, is to place some key information in columns. Each column may have a title label and another label with the value of that info. Something like the example in the screenshot (Figure 4-4). Sounds familiar? I’m sure you have seen it a ton of times.
../images/473721_1_En_4_Chapter/473721_1_En_4_Fig4_HTML.jpg
Figure 4-4

Example of a profile-like UI

In this case, VoiceOver will naturally move from the user’s name to “Followers,” then “Following,” “Posts,” “550,” “340,” and “750” (Figure 4-5). As you can see, that doesn’t make much sense. “Followers” and “550” are two labels that work as one piece of information, and the same applies to “Following” with “340” and “Posts” with “750.”
../images/473721_1_En_4_Chapter/473721_1_En_4_Fig5_HTML.jpg
Figure 4-5

Sequence of the default behavior when navigating the UI in Figure 4-4 using VoiceOver

Fortunately, there are ways to fix this. Views can define their own accessibility elements as an array, and the order will follow the order in how the elements are placed in the array.

If we consider a view like the one in the image (Figure 4-4), which could be built with UIStackViews, the code could look something like the following code snippet, in which we define each one of the blocks of information as a single accessibility element. To achieve this, we can combine both labels and even change the accessibility frame to be the union of both labels too. Then we place them in the order we want and return them as the new array of accessibility elements for the view:
// Define a new array of accessibility elements
override var accessibilityElements: [Any]? {
        set {}
        get {
                var elements = [UIAccessibilityElement]()
                let followersAccessibilityElement = UIAccessibilityElement(accessibilityContainer: self)
                let followingAccessibilityElement = UIAccessibilityElement(accessibilityContainer: self)
                let postsAccessibilityElement = UIAccessibilityElement(accessibilityContainer: self)
                followersAccessibilityElement.accessibilityLabel = "(followerHeadingLabel.accessibilityLabel!), (followersNumberLabel.accessibilityLabel!)"
                followersAccessibilityElement.accessibilityFrameInContainerSpace = followersStackView.convert(followerHeadingLabel.frame, to: self).union(followersStackView.convert(followersNumberLabel.frame, to: self))
                elements.append(followersAccessibilityElement)
                followingAccessibilityElement.accessibilityLabel = "(followingHeadingLabel.accessibilityLabel!), (followingNumberLabel.accessibilityLabel!)"
                followingAccessibilityElement.accessibilityFrameInContainerSpace = followingStackView.convert(followingHeadingLabel.frame, to: self).union(followingStackView.convert(followingNumberLabel.frame, to: self))
                elements.append(followingAccessibilityElement)
                postsAccessibilityElement.accessibilityLabel = "(postsHeadingLabel.accessibilityLabel!), (postsNumberLabel.accessibilityLabel!)"
                postsAccessibilityElement.accessibilityFrameInContainerSpace = postsStackView.convert(postsHeadingLabel.frame, to: self).union(postsStackView.convert(postsNumberLabel.frame, to: self))
                elements.append(postsAccessibilityElement)
                return elements
        }
}
Now when using VoiceOver, the focus will behave in a much more logical way, and you will need less swipes to navigate the app, as you can see in Figure 4-6:
../images/473721_1_En_4_Chapter/473721_1_En_4_Fig6_HTML.jpg
Figure 4-6

Sequence of the behavior when navigating the UI in Figure 4-4 after grouping some UI elements creating your own array of accessibility elements

Custom actions

There should always exist an easy way for performing any available action in the app. But sometimes there are features for power users that allow you to perform some of those actions with shortcuts or gestures. An example of this could be the possibility of marking as read, flag, or archive an email by swiping left or right. But it turns out that actually VoiceOver overrides those gestures for navigating to the previous or next accessible element.

Some other times there are lists with rows of information with actions available for each one of those items that you may want to avoid reading every time when using VoiceOver because it could get very repetitive for the user. An example of this scenario would be a Twitter client app, or similar, where you can always reply, retweet, like, share, etc. Reading all those options for every single tweet would be very redundant. You would probably have to swipe right or left many times to get to the next or previous tweet too. Plus, while visually it is clear that those actions belong to a particular tweet, because they are inside one single cell, with VoiceOver it could be unclear if those actions belong to the current tweet or the following one if we break that encapsulation, especially if the user is exploring the screen instead of swiping from one accessibility element to the other.

A possible solution for those two use cases, and others, is the use of custom actions. You can change the buttons so they are no longer accessible to VoiceOver and then you can create an array of accessibility custom actions for all those options, with a name and a selector to perform that action, and either add it to the cell/view or override that property.
// Configure custom actions
accessibilityCustomActions = [UIAccessibilityCustomAction(name: like, target: self, selector: #selector(likeButtonPressed(_:))),
UIAccessibilityCustomAction(name: share, target: self, selector: #selector(shareButtonPressed(_:)))]
The selector just needs to get the custom action as a parameter and return a boolean as whether that action was or not successful:
@objc func likeButtonPressed(_ action: UIAccessibilityCustomAction) -> Bool {
        var success = false
        // Perform action
        return success
}
And new in iOS 13, you can create actions that take an action handler instead of a target and selector. So you could write something like this:
var userCustomAction = UIAccessibilityCustomAction(name: viewModel.userName) { (customAction) -> Bool in
        var success = false
        // Perform action
        return success
}
accessibilityCustomActions?.append(userCustomAction)

When doing this, a hint saying “Actions available” gets vocalized by VoiceOver. When swiping up and down, those configured actions get read and can be actioned with a double tap. With the code example, a swipe up would say “Share,” another swipe would say “Like,” and if double tapping it would execute likeButtonPressed(_:).

And because it is an array of actions, you can always append more of them if the configuration changes:
// Append more custom actions
accessibilityCustomActions?.append(UIAccessibilityCustomAction(name: viewModel.userName, target: self, selector: #selector(userButtonPressed(_:))))

And the best thing, your app will now be much easier to navigate with VoiceOver because with just one swipe the user will be able to get to the next logical piece of content, and it will now be clear what content the user is performing those actions with.

This has also the advantage of making these custom actions easily available for Switch Control users and, new in iOS 13, to Full Keyboard Access and Voice Control users too.

In the case of actions hidden behind custom gestures, like swipes or long taps, the Switch Control menu is probably powerful enough to allow the user to perform such actions. However, using custom actions can first improve discoverability and second make your app more usable (Figure 4-7). It can sometimes reduce the wait time or number of interactions needed by more than 60%, as the custom actions become available in the first page of the Switch Control menu.
../images/473721_1_En_4_Chapter/473721_1_En_4_Fig7_HTML.jpg
Figure 4-7

Example of the Switch Control menu when adding custom actions

Is VoiceOver running?

It is possible to check if some of the assistive technologies are running or even get notifications when the configuration changes. So, for example, you can check if VoiceOver is running or get notified when it gets enabled or disabled. This is useful in the case your UI needs some sort of adaptation for VoiceOver users. Apple suggests in the documentation that you may want to check if VoiceOver is running, for example, if you display UI elements that briefly overlay other parts of your UI and that you could make persistent for VoiceOver users. It is probably a better approach for UI elements not to be ephemeral. This approach can make your UI more usable for any of your users. However, you have the possibility of providing a different experience for VoiceOver users, if you need to.
// Check if VoiceOver is running
if UIAccessibility.isVoiceOverRunning {
        // Do something here
}

I actually think this can be useful for doing some optimizations. For example, imagine that you have a long list of elements in a table view or collection view and you have to build some accessibility labels, maybe using some number or date formatters, and also create an array of custom actions, etc. It can get a bit expensive, especially if you are building them as the user scrolls.

When doing this, though, you have to consider a couple things. The first one is to listen to the accessibility notifications to see if the user’s preferences around accessibility change during the use of the app.
// Add observer to get notified when VoiceOver gets enabled
NotificationCenter.default.addObserver(self, selector: #selector(configureAccessibility), name: UIAccessibility.voiceOverStatusDidChangeNotification, object: nil)
The second one is to think if whatever you are putting inside the “if” statement could be useful for other users. For example, if you are creating custom actions for a cell, these are extremely useful for Switch Control users too, and you could also check if Switch Control is running.
// Check if Voice Control is on
if UIAccessibility.isSwitchControlRunning {
        // Do something here
}

UISwitch

A very common UI pattern in apps, especially in the settings sections, is to have UITableViewCells with a UISwitch (Figure 4-8). When using VoiceOver, the ideal behavior is for the whole cell to be one accessible element. The accessibility label could be the text in the cell. The accessibility value, the value of the UISwitch. And you should be able to toggle the switch by double tapping the screen when the cell is focused. You can also hint the user that you can double tap to toggle the value of the cell.
../images/473721_1_En_4_Chapter/473721_1_En_4_Fig8_HTML.jpg
Figure 4-8

Example of a UI with a UISwitch

This is exactly the behavior you will find in the General Settings of the system and in any Apple app. It is actually a common mistake not to do so, and you’ll find lots of apps that first read the text in the cell, and when swiping right, VoiceOver focuses on the switch and reads the labels again. For a cell that looks like the one in the image, you could create a subclass of UITableViewCell that reproduces the expected behavior like this:
// Create Settings Cell
class SwitchTableViewCell: UITableViewCell {
    @IBOutlet weak var settingTitleLabel: UILabel!
    @IBOutlet weak var settingSubtitleLabel: UILabel!
    @IBOutlet weak var settingSwitch: UISwitch!
    override func awakeFromNib() {
        super.awakeFromNib()
        isAccessibilityElement = true
    }
    override var accessibilityLabel: String? {
        get { "(settingTitleLabel.accessibilityLabel ?? ""). (settingSubtitleLabel.accessibilityLabel ?? "")" }
        set {}
    }
    override var accessibilityTraits: UIAccessibilityTraits {
        get { settingSwitch.accessibilityTraits }
        set {}
    }
    override var accessibilityValue: String? {
        get { settingSwitch.accessibilityValue }
        set {}
    }
    override func accessibilityActivate() -> Bool {
        settingSwitch.isOn = !settingSwitch.isOn
        return true
    }
}

Making the cell itself accessible prevents VoiceOver from focusing the labels and switch separately. Overriding the traits and the value helps VoiceOver vocalize the whole cell as if it was a switch. Overriding the accessibility label lets you concatenate both the title and subtitle accessibility labels. And overriding accessibilityActivate() allows you to capture the double tap event and manually toggle the switch value.

Summary

  • The Perform Escape action lets users go back with a “scrub” on the screen with two fingers.

  • The Magic Tap is a double tap with two fingers that triggers the most important action of the current section of the app.

  • You can specify a view as modal in terms of accessibility, so VoiceOver ignores any siblings of that view.

  • You can post notifications to let VoiceOver know that the screen or layout changed, or even to make announcements.

  • It is possible to group several elements into a new accessibility element.

  • Custom actions will make VoiceOver experiences less verbose and more structured, and they will help Switch Control users perform those actions with less effort.

With this information we have all we need to create amazing experiences for our VoiceOver users. In the next chapter, we will start exploring how to create apps that let the users configure large font sizes and how to tweak the layout to adapt accordingly.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.6.75