I have difficulties understanding what is the right way to make a custom UIView accessible on iOS. As a case study, here is a custom UIControl
subclass wrapping a UILabel
and a UITextField
subviews, that I want to test via a UI test.
View hierarchy:
→ UIControl
↪︎ UILabel
↪︎ UIView
Intended behavior:
1️⃣ The default behavior
By default, the behavior is not great:
- The accessibility inspector inspects the two subviews as unrelated;
- The accessibility inspector’s audit raises a “hit area is too small” warning since it is ignoring the whole
UIControl
can be tapped and will give focus to theUITextField
.
But this testing code works fine:
let emailField = app.textFields["Email"]
emailField.tap()
emailField.typeText("toto@tata.com")
2️⃣ Becoming an accessibility element
If the UIControl
becomes flagged as an accessibility element as following, the accessibility inspector will show only 1 pointer for the whole UIControl
but the UI test will fail.
Here is the code:
isAccessibilityElement = true
accessibilityLabel = innerLabel.text
accessibilityValue = innerTextField.text
accessibilityTraits = innerTextField.accessibilityTraits
And the test failure stack, that seems to have an Element subtree limited to the UIControl
(aka TextField
) itself
UI Test Activity:
Assertion Failure: SampleUITests.swift:21: Failed to synthesize event: Neither element nor any descendant has keyboard focus. Event dispatch snapshot: TextField, label: 'Email'
Element debug description:
Attributes: TextField, {{32.0, 229.0}, {350.0, 52.0}}, label: 'Email'
Element subtree:
→TextField, 0x600000925340, {{32.0, 229.0}, {350.0, 52.0}}, label: 'Email'
3️⃣ Simplifying accessibility information
Inspired by Apple’s documentation on how to simplify your accessibility information , I used the following code:
var elements = [UIAccessibilityElement]()
let groupedElements = UIAccessibilityElement(accessibilityContainer: self)
groupedElements.accessibilityLabel = innerLabel.text
groupedElements.accessibilityTraits = innerTextField.accessibilityTraits
elements.append(groupedElements)
Which seems to do nothing: I’m back to the default behavior.
Wrapping things up
I’d like to have the accessibility structure from 2️⃣ and still be able to run UI tests in the most expressive way ie using the same code as 1️⃣, how can I do this? What did I do wrong in 3️⃣ that made it behave the same way as 1️⃣?
Edits
As hinted by @Defragged, I tried to improve the
UIControl
s compliance toUIResponder
but it didn't really help:override var canBecomeFirstResponder: Bool { return innerTextField.canBecomeFirstResponder } override func becomeFirstResponder() -> Bool { return innerTextField.becomeFirstResponder() } override var isFirstResponder: Bool { return innerTextField.isFirstResponder }
Scrutinizing the UI test error logs a little deeper, I read this:
t = 9.65s Synthesize event t = 9.68s Get number of matches for: Elements containing elements matching predicate 'hasKeyboardFocus == 1' t = 9.72s Requesting snapshot of accessibility hierarchy for app with pid 24879 t = 9.74s Find: Descendants matching type TextField t = 9.74s Find: Elements matching predicate '"Email" IN identifiers' t = 9.74s Find: Elements containing elements matching predicate 'hasKeyboardFocus == 1'
It seems that
XCUIElement
instances have a undocumentedhasKeyboardFocus
attribute that can be inspected with a debugger but that I have no clue how to control ♂️.