On Accessibility in iOs Development, Implementation in TipToe
It’s a wide world
Let’s start with the obvious: it’s a World Wide Web, and we tend to forget about it while being entrenched in one way or another in our lives and work activities. It sounds like I’m going to be apologetic, which is not frequent, and I am. I’ve recently developed and published through the AppStore an app called TipToe, see it here and follow the link to the store — $1, cheap, indispensable (it sure is ;-). So, that’s a cute app that lets you search Bing and Blekko, and aggregates the search results in a nice table. And I’ve put in a minimal in-app browser (minimal, yet complete) which is lean and fast.
The app has been featured on [MinimalMac](http://minimalmac.com], we proudly sponsored the RSS feed the week of the release of TipToe (entry on the 4th of April), and it was picked recently by LifeHacker. Life couldn’t be better (well, actually, we could always sell more, but that’s another story). Couldn’t? Sure it could, and should.
The glitch
A few hours after we got featured on LifeHacker, I received a moving email from a user who purchased the app, regarding the lack of accessibility of TipToe. This user is visually impaired, and complained that VoiceOver in TipToe was inexistent… Well, actually, being a really sweet person, he sent an email noticing that some of the buttons were not customized for VoiceOver access. Indeed, that’s one of those emails that hit you pretty strong in the face, and for two reasons. One, I remember reading a very good blog post by someone visually impaired explaining how the iPhone gave him back a sense of sight by letting him know which colors were in front of him. This post moved me (note to self: gotta find it in my archives). And two, there was an excellent post from Matt Gemmel here that describes how easy and important it is to make your app accessible. So, shame on me for forgetting about it, I read that piece, enjoyed it and bookmarked it for using it later. Only problem is I skipped using it. Now, pretty much like anything I do, I just considered that shortcoming as an engineering problem, ie. a mistake that should be fixed. Off we go, let’s add Accessibility to TipToe. Matt having indicated that it’s easy, I shouldn’t have many problems to implement that. Quite wrong, but here’s what I did in the end.
The elements to be made accessible
I’ve made a quick capture of some screens from TipToe, and here we’re going to address two kinds of tips: 1- making the table cells more readable, and 2- accessing some elements that do not have their Accessibility property reachable from Interface Builder (IB). The latter is probably the most interesting case. In TipToe, I had to trade the Navigation bar at the top of the view for a standard toolbar. Basically, I wanted to define an empty button in the center of the toolbar so that I can make the web address bar disappear on touch. That’s a neat trick to maximize the screen earl estate, and it saves me from a scroll view, which I don’t like when browsing (always need to scroll back up to get the actions, and I wanted the actions button to be always visible, as they open the share dialog). There’s certainly other ways to achieve that, but it was the easiest and quickest to implement. You’ll notice as well that I haven’t circled all the elements on the left hand side of the picture. Basically, the UITextField for search entry and the information button at the bottom right are already addressable in IB, so no need to do a fuss. This case is well documented in both Apple’s Guidelines and Matt Gemmel’s post.
The table
There’s a good hint in Apple’s Accessibility Guidelines, which I shamelessly use here. It consists in concatenating some fields displayed in your cell in order to make it more easy for the robot to read.
Here is the snippet for the table:
1 2 3 4 5 6 7 8 | UILabel * titleLabel = (UILabel *) [cell.contentView viewWithTag:TitleTag]; UILabel * urlLabel = (UILabel *) [cell.contentView viewWithTag:UrlTag]; UILabel * descriptionLabel = (UILabel *) [cell.contentView viewWithTag:DescriptionTag]; titleLabel.text = [displayTitle objectAtIndex:indexPath.row]; urlLabel.text = [displayURL objectAtIndex:indexPath.row]; descriptionLabel.text = [displayDescription objectAtIndex:indexPath.row]; cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; cell.accessibilityLabel = [NSString stringWithFormat:@"%@, %@", titleLabel.text, descriptionLabel.text]; |
So, quite simply, I’ve got a custom cell which displays the title (re-formatted to make it homogeneous in my app, the url (ditto) and the abstracted content (that’s the description label). This is done in a most classic way using three UILabel (Lines 1-3), for which I’m assigning the .text property after, while extracting the search results from the main table of the class (Lines 4-6). Now, the catch is that VoiceOver reads everything in a cell when it is tapped. And clearly, you do not want the url to be read, it will literally read “h t t p column slash slash etc…”. Not really pleasant. Instead, we’re just merging the titleLabel.text and descriptionLabel.text, and assigning the result to the cell.accessibilityLabel (Line 8). This is now the text that will be read when the cell is tapped. That was pretty easy, let’s get to the hairy section now.
Bar buttons
I’m getting away in the Display view with only changing the viewWillAppear method, such that:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | -(void)viewWillAppear:(BOOL)animated { UIToolbar *topbar =(UIToolbar*)[self.view viewWithTag:200]; [topbar setAccessibilityLabel:@"Top bar"]; [topbar setAccessibilityHint:@"Navigate search results"]; // UIView *topbarbutton1 = (UIView*)[topbar.items objectAtIndex:1]; [topbarbutton1 setAccessibilityLabel:@"List"]; [topbarbutton1 setAccessibilityHint:@"Goes back to search results"]; UIView *topbarbutton2 = (UIView*)[topbar.items objectAtIndex:2]; [topbarbutton2 setAccessibilityLabel:@"Share"]; [topbarbutton2 setAccessibilityHint:@"Opens share options"]; // UISegmentedControl *segmentControl = (UISegmentedControl*)[self.view viewWithTag:202]; NSUInteger buttonCount = 0; for (UIView* thisView in [segmentControl subviews]) { switch (buttonCount) { case 0: thisView.accessibilityLabel = NSLocalizedString(@"Down",nil); thisView.accessibilityHint = NSLocalizedString(@"Goes down one entry in results list", nil); break; case 1: thisView.accessibilityLabel = NSLocalizedString(@"Up",nil); thisView.accessibilityHint = NSLocalizedString(@"Goes up one entry in results list", nil); break; } buttonCount++; } } |
Now, there’s plenty to discuss here. The main point is that all those elements do not have their accessibility property accessible from Interface Builder, so we’ve got to be a bit smart. We’re going to use the same trick for all the elements, which consists in accessing the accessibility properties of the view that contains the element.
First up: the top bar. That’s a UIToolbar, I’m accessing it through a tag. Nothing much to say about this… except that we need that UIToolbar to be defined for the next step.
Second: let’s get into the bar buttons. Once you know how to do it, it’s evident. We’re just picking up the UIView that contains the button (Lines 7 & 10), and doing so from the UIToolbar list of items. These items are numbered in the order they are displayed in the IB, starting with 0. In my view I’ve got a space before the first button so my button1 is at index 1 (Line 7). The all shebang is then over, as I can set the accessibility label and hint of my newly defined UIView (Lines 8-9 and 11-12). Simple enough! Unfortunately the UIBarButtonItem accessibility cannot be accessed directly from IB, but this does the trick in a few lines.
Third: well we’re got this damned UISegmentedControl. Let’s face it, life would have been much easier if I had kept the first version of the app (alpha), which had two separate buttons. But no, I had to get it with the look and feel of Apple’s Mail and Safari, which use this UISegmentedControl. Now, the same trick is used, with a twist. We start by picking up the UISegmentedControl (Line 14), and then we’ve got to navigate through its subviews (Lines 15-29). We’re doing so using a for loop (Lines 16-29) over the [segmentControl subviews], and together with a simple switch statement (Line 18) we get our subview accessible. In our case, we’ve just got two segments, but this could be used for more. That’s all there is to it…
Summary
That was a quick rundown, and hopefully this post will be obsolete in a future release of iOS. Quite unfortunately, the doc on this subject is very light, so I hope it may help someone. The take-home message is simple:
- if the element has its accessibility property available in IB, just use that;
- if it does not, I’ve either covered the case here, or you’ll have to dig through your elements inheritance and find the view for which you can set-up the accessibility.
Questions and comments are always welcome, though I’m afraid I don’t have much time to check your code these days… So, please don’t hold it against me if I can’t respond fast enough.
