Short note on labelling text fields in native iOS applications

VoiceOver users interact with the web and native applications on their iOS device using a number of gestures. The simplest gesture involves swiping the screen from left to right by using a finger to navigate through page content. While there are many other gestures to support speedy navigation, a useful rule of thumb is for developers to try to reduce the number of swipes required on a page (without missing out on necessary content). In theory, this should improve the experience for those people who use VoiceOver.

Reducing the number of required swipes is particularly helpful when supporting form navigation. In HTML, assuming a developer has applied the for attribute to the label element, and its value matches the id attribute of the related input, only the input will fall into VoiceOver swipe focus order, and VoiceOver will read out the label text – for example, “First name, text field”. Unfortunately, there is no concept of providing matching for and id attributes when developing native iOS applications, hence I sometimes encounter cases where navigating through forms, and particularly large forms, can be a bewildering process.

To illustrate, let’s take an example. Imagine you have a form that contains three simple text fields:

  • Name
  • Nationality
  • Age

For each of the above three components, you may decide to drop these onto the storyboard in Xcode as follows: a Label to represent the visible label, and a Text Field to represent the related input, as in the following screenshot:

A screenshot from the storyboard in Xcode showing labels and visually associated form fields representing Name, Nationality and Age

Unfortunately, many beginners’ guides to Xcode development I encounter stop there. While it is visually obvious how the labels and associated text fields match up, this isn’t so obvious to VoiceOver users. For example, if I run the above example on an iOS device, the label is announced first. When I swipe to the related text field, VoiceOver simply announces “text field”. The problem with this approach is twofold:

  • If I navigate forwards through the form (i.e. swiping from left to right), it is reasonably obvious to identify how the text fields relate to their associated labels; that is, the label is announced by VoiceOver, followed by the text field – which is announced as “text field”. However, if I decide to swipe backwards through the form (because, say, I have forgotten to fill in a field), or I am using explore-by-touch (holding the tip of the finger on the page and moving it around until I locate the content I require), “text field” on its own does not clearly indicate which label the text field relates to. Consequently, I may enter the wrong data into the wrong text field.
  • Both the label and the text field fall into VoiceOver swipe focus order as separate elements. This means that, in the above example, a VoiceOver user must swipe six times (once for each component) in order to reach a Submit button.

To deal with the above issues, here are my recommendations.

Make sure that all form controls are labelled programmatically

The first step is to ensure that you provide an accessible label for each of the text fields. You can do this in one of two ways:

  • Provide a label for the text field in the Identity Inspector. Make sure the text matches up exactly with the visible label. Or:
  • Provide a label for the text field at code level, by applying the accessibilityLabel property to each text field. Again, make sure this text matches up exactly with the visible label.

A screenshot from the Identity Inspector in Xcode showing the text Nationality added to the Label field

The rest of this post assumes the second approach is taken, but either method is acceptable.

Example code for labelling form fields (note: for illustrative purposes only)

@IBOutlet weak var nameTextField: UITextField!
@IBOutlet weak var nationalityTextField: UITextField!
@IBOutlet weak var ageTextField: UITextField!

class ViewController: UIViewController {
…
	override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
	
	self.nameTextField.accessibilityLabel = "Name";
	self.nationalityTextField.accessibilityLabel = "Nationality";
	self.ageTextField.accessibilityLabel = "Age";
		
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
…
}

Now, whenever any of the text fields receive focus, the label will be announced. For example, VoiceOver will announce “Nationality, text field” when the Nationality text field receives focus.

Hide visible text labels from VoiceOver users

However, the above technique only partially resolves the issue. If a VoiceOver user swipes down from the top of the page, they will hear the visible label, and then by swiping right they will hear the label text again as part of the text field. This obviously results in duplication.

To resolve this issue, apply the isAccessibilityElement method to each of the visible labels, and set each to a value of false. This removes each of the visible text labels from VoiceOver swipe focus order. Consequently, only the text fields themselves are focusable, which reduces the number of swipes required through the form from six down to three.

Example code for removing visible text labels from VoiceOver swipe focus order (note: for illustrative purposes only)

@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var nationalityLabel: UILabel!
@IBOutlet weak var ageLabel: UILabel!

@IBOutlet weak var nameTextField: UITextField!
@IBOutlet weak var nationalityTextField: UITextField!
@IBOutlet weak var ageTextField: UITextField!

class ViewController: UIViewController {
…
	override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
	
	self.nameTextField.accessibilityLabel = "Name";
	self.nationalityTextField.accessibilityLabel = "Nationality";
	self.ageTextField.accessibilityLabel = "Age";
	
	self.nameLabel.isAccessibilityElement = false;
	self.nationalityLabel.isAccessibilityElement = false;
	self.ageLabel.isAccessibilityElement = false;
		
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
…
}

Be  aware that the above is a very simple example, but it highlights how strategic use of the accessibilityLabel and isAccessibilityElement properties can both ensure components are labelled correctly, and can help to reduce the number of swipes per page.

Postscript (31st July 2017)

Thank you to Ashton Williams who commented that hiding elements from VoiceOver that would otherwise appear to be focusable may be confusing. Indeed, I agree this may confuse sighted users who use VoiceOver. Ashton suggests the following alternative approaches:

  • Expand the accessibilityFrame of the text field to encapsulate both the label and the text field, which makes them appear as a single focusable element.
  • Group form controls in a TableView, whereby each cell contains a label and a text field that is treated as a single focusable element.

 

Categories: Technical

Comments

Ashton Williams says:

Graeme, I respectfully disagree about making the labels hidden to VoiceOver. I think it can cause some confusion to have elements that look like they should be able to be focused that aren’t. Instead I think there are two good options: expand the accessibilityFrame of the textField to encapsulate the label – making them appear as a single element when focused or focusing; or use another view that groups accessibility children, this is what you see in Apple apps and across the iOS system apps for forms in tableViews, the cell is a single element that contains a label and an field, but a single accessibility element.

Graeme Coleman says:

Ashton, thank you for your comment. I agree that your approaches are more robust. I’ll add a postscript to my post to reflect your suggestions.

Thanks again,

Graeme

Gijs Veyfeyken says:

The images aren’t shown (error 404 Not Found).

Patrick H. Lauke says:

Thanks for the heads-up. Seems we had a few stragglers from when we transitioned the blog to its new subdomain. Should all be fixed now.