Learn how to compose Accessibility Identifiers with the same ergonomics as modifying View with padding, background or foreground colors
What Are Accessibility Identifiers, and What Are They For?
Accessibility identifiers can be used to reliably distinguish an element (XCUIElement) in UI automated tests (XCUITests). The alternatives either have low performance (XPath) or require specific selectors that may randomly fail. Still, it may be useful to know them. To learn about other options, check BrowserStack’s guide on finding elements.
For more information about accessibility identifiers, please visit Apple’s documentation.
How To Verify Them
The best way is to preview XCUIElements by using inspector tools. XCUIElements are tree-structured, the same as views. There are two well-known ways to preview them.
Accessibility Inspector
The Accessibility Inspector is a tool provided with Xcode. It can be opened from Xcode’s menu: Xcode -> Open Developer Tool -> Accessibility Inspector or cmd+space and typing “accessibility inspector.”
While the app is running in the simulator, go to “All processes” in “Accessibility Inspector,” choose “Simulator,” and then click on the “Target an element” button (1). The “selection mode” will be enabled.
Click on the element you want to inspect (green overlay will assist with choosing the right one). The accessibility identifier is under “Identifier” in the “Advanced” section (2).
Here’s an example of Health.app:
Accessibility Inspector comes with amazing tools that help you improve the accessibility of your app (e.g., contrast ratio scans). To learn more about Accessibility Inspector and its features, please watch a WWDC session from 2019.
Appium Inspector
Appium Inspector requires more work to set up, but in the end, it is more convenient for accessibility identifier preview and provides data in a readable form.
Installation
Several tutorials will help you install Appium Inspector, but if you want to save some time, feel free to use the one below:
Appium Inspector requires npm and Appium Server. The simplest way is to type these commands in the terminal and install it through homebrew:
brew install node # installs npm
npm install -g appium@next # installs appium server through npm
You can check the Appium installation by printing its version. You can do it by typing the following in the terminal:
appium -v
To run XCUITests or scan the tree structure, Appium needs to install an additional driver. To install the XCUITest driver, type the following in the terminal:
appium driver install xcuitest # installs Appium driver which allows to run XCUITest
To check if the driver was successfully installed, you can create a list of the installed drivers using the terminal:
appium driver list --installed
Next, download Appium Inspector’s installation package from Appium’s GitHub. In case of errors, please follow Appium’s installation guide.
Usage
First, run the Appium server by typing this command in the terminal:
appium server
Then, open the Appium Inspector app and define properties (1). At a minimum, you have to define platformName and appium:automationName as follows:
- platformName: use iOS
- appium:automationName: use XCUITest
You can save that configuration (it is not saved by default). To start a screen scanning process, click the “Start Session” button (2).
To learn more about available options (like running on a physical device), please visit Appium’s guide.
Click on the element you want to inspect (an overlay will assist with choosing the right one). Here’s an example of Health.app (with the same screen and identifier used in the previous example above):
Use the “Refresh” button from the top list to refresh the screen and see the updated view data.
Composing IDs
Now, as you know how to verify the identifiers, it’s time to compose them. We are going to use a tree structure.
A tree is constructed with branches (1, 2) and leaves (3, 4, 5). Like regular trees, a leaf cannot exist without at least one branch.
Take a look at the identifier from Health.app: UIA.Health.ReviewHealthChecklistTile.Header.Title. A dot separates each of its parts and defines the next level of nesting, building a tree structure. In that case, the following parts: UIA, Health, ReviewHealthChecklistTile, and Header are branches and Title is a leaf.
As you may notice, a tree structure has the following key advantages:
- it allows better identifier organization
- each nesting level provides more detail about the view placement, so it is easier to find it in a view hierarchy
The example of Apple’s Health app is constructed by using the following format: Purpose.App.ViewGroup.Component.Element, but feel free to define your own (e.g., it is absolutely fine to use ScreenName.ViewGroup.Component.Element instead).
Composing identifiers can be done by simply passing the identifier through a constructor like this:
struct UserDetails: View {
let name: String
private let parentIdentifier: String
init(name: String, parentIdentifier: String) {
self.name = name
self.parentIdentifier = parentIdentifier
}
var body: some View {
Text(name)
.accessibilityIdentifier("(parentIdentifier).Label")
}
}
However, it is not flexible enough. First, it introduces an additional dependency passed through the constructor, polluting its API with UI-related dependency. Second, you lose an option to set an “identifier branch” to regular SwiftUI view containers, like VStack. The best option would be to allow an identifier to be created in the same way that sets up a background color or adds padding to a View or a group of them.
To achieve that, we are going to use custom EnvironmentKey and ViewModifier. Let’s start by creating a dedicated EnvironmentKey:
import SwiftUI
struct ParentAccessibilityBranchKey: EnvironmentKey {
static let defaultValue: String? = nil
}
extension EnvironmentValues {
var parentAccessibilityBranch: String? {
get { self[ParentAccessibilityBranchKey.self] }
set { self[ParentAccessibilityBranchKey.self] = newValue }
}
}
That environment value will be used to pass a branch name from the parent View, to its children. Once set to a parent, it is visible to all child views.
Next, let’s create a dedicated ViewModifier to construct a branch and leaf.
Branch modifier
import SwiftUI
public struct AccessibilityIdentifierBranchModifier: ViewModifier {
@Environment(.parentAccessibilityBranch) private var parentBranch
private let branch: String
public init(branch: String) {
self.branch = branch
}
public func body(content: Content) -> some View {
content
.environment(.parentAccessibilityBranch, makeGroupPath())
}
private func makeGroupPath() -> String {
guard let parentBranch = parentBranch else { return branch }
return "(parentBranch).(branch)"
}
}
public extension View {
func accessibilityIdentifierBranch(_ branch: String) -> ModifiedContent<Self, AccessibilityIdentifierBranchModifier> {
modifier(AccessibilityIdentifierBranchModifier(branch: branch))
}
}
The branch modifier takes a branch from a parent View and appends to it the branch provided in the constructor. The modifier only constructs the identifier but does not apply it to a View.
Leaf modifier
public struct AccessibilityIdentifierLeafModifier: ViewModifier {
@Environment(.parentAccessibilityBranch) private var branch
private let leaf: String
public init(leaf: String) {
self.leaf = leaf
}
public func body(content: Content) -> some View {
if let branch = branch {
content
.accessibilityIdentifier("(branch).(leaf)")
.environment(.parentAccessibilityBranch, nil)
} else {
content
}
}
}
public extension View {
func accessibilityIdentifierLeaf(_ leaf: String) -> ModifiedContent<Self, AccessibilityIdentifierLeafModifier> {
modifier(AccessibilityIdentifierLeafModifier(leaf: leaf))
}
}
The leaf modifier applies the identifier to a View. You may have noticed, that the parentAccessibilityBranch environment value is nullified for the leaf modifier. It prevents further branching creation because growing a branch from a leaf is not feasible. The leaf modifier closes the identifier creation.
Once the implementation is done, let’s create an example of a reusable View:
struct UserDetails: View {
let name: String
init(name: String) {
self.name = name
}
var body: some View {
HStack {
Image(systemName: "person.circle")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 40.0)
VStack(alignment: .leading) {
Text("Name")
.foregroundColor(.secondary)
.font(.caption)
.accessibilityIdentifierLeaf("Label")
Text(name)
.accessibilityIdentifierLeaf("Value")
}
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(.thinMaterial)
.cornerRadius(10.0)
}
}
and use it by another View with identifier modifiers:
struct ContentView: View {
var body: some View {
VStack {
UserDetails(name: "Bugs Bunny")
.accessibilityIdentifierBranch("UserDetails")
}
.padding()
.accessibilityIdentifierBranch("Users")
}
}
As a result of the above, let’s take a look at the preview to verify the identifier:
Conclusion
Setting up accessibility identifiers with custom SwiftUI components can be flexible without introducing an additional element to the Views constructor. SwiftUI provides an API that allows composing identifiers with the same ergonomics as modifying a View with padding, background, and foreground color.
Although the solution above is flexible, consider building the identifier tree with caution to prevent introducing frequent changes that may break the UI tests.
You may find the complete code in my GitHub repository.
Composing Accessibility Identifiers for SwiftUI Components was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.