How do you make a custom-drawn control (a canvas, a chart or a hand-built segmented control) accessible with UIAccessibilityElement?
Predict first
What does a container of synthetic elements report?
import UIKit
final class Segmented: UIView {
private var items: [UIAccessibilityElement] = []
override var accessibilityElements: [Any]? {
get {
if items.isEmpty {
for (i, name) in ["Low", "Medium", "High"].enumerated() {
let e = UIAccessibilityElement(accessibilityContainer: self)
e.accessibilityLabel = name
e.accessibilityTraits = i == 1 ? [.button, .selected] : .button
e.accessibilityFrameInContainerSpace = CGRect(x: CGFloat(i) * 100, y: 0, width: 100, height: 44)
items.append(e)
}
}
return items
}
set {}
}
}
@MainActor func run() {
let v = Segmented(frame: CGRect(x: 20, y: 100, width: 300, height: 44))
print("container itself is an element:", v.isAccessibilityElement)
for e in v.accessibilityElements as! [UIAccessibilityElement] {
print(e.accessibilityLabel!, "x in container:", e.accessibilityFrameInContainerSpace.origin.x, "selected:", e.accessibilityTraits.contains(.selected), "container is view:", e.accessibilityContainer === v)
}
}
MainActor.assumeIsolated { run() }
- container itself is an element: false / Low x in container: 0.0 selected: false container is view: true / Medium … 100.0 … true … / High … 200.0 …
- container itself is an element: true / three lines of 0.0
- The array is empty
Mental model. A painting in a gallery has no labelled parts. The audio guide only works because someone wrote a script that says “top left: the ship; centre: the storm”, with a position for each point.
A view that draws its own parts with Core Graphics has one view and no sub-views, so the accessibility tree has nothing to expose. The pattern is a container with synthetic elements. The container view itself must not be an element (isAccessibilityElement stays false, as the check prints), and instead it returns an array of UIAccessibilityElement objects from accessibilityElements (or implements the older UIAccessibilityContainer methods). Each element is created with init(accessibilityContainer:), gets a label, value and traits (the middle segment carries .selected in the check), and a frame. Give the frame in the container’s own coordinates with accessibilityFrameInContainerSpace (x = 0, 100 and 200 below), and the system converts it to screen space for you, which is why you should prefer it to accessibilityFrame (screen coordinates that go stale when the view moves or scrolls). Each element reports the container as its accessibilityContainer.
Build the elements lazily and rebuild them when the content or layout changes, because the elements are your responsibility: a stale element reading an old value is worse than none. Keep the order logical (the array order is the reading order) and the count small; for a chart expose a summary element and, if the data matters, a way to explore it (an audio graph, a table) rather than one element per pixel (Q467). Elements that are actionable need the right trait (.button) and must respond to activation, which UIKit sends to the element’s accessibilityActivate() (override it on a subclass) or, as a fallback, as a tap at the element’s activation point. In SwiftUI the same job is done with .accessibilityRepresentation or by composing real child views with .accessibilityElement(children:) (Q466). The values here run in a simulator without an assistive technology, so the screen frames and the element count the system reports are not shown: they were not asserted.
The container is not an element; each synthetic element has its own container-space frame, traits and a back reference to the container.
Common traps
- Making the container an element too. Then its children are skipped; the container must be
isAccessibilityElement = false. - Setting
accessibilityFrameonce in screen coordinates. It breaks when the view scrolls or rotates; use the container-space frame.
They might follow up with
- How do elements react to a double tap? Override
accessibilityActivate()on an element subclass, or handle the activation-point tap. - Can I avoid all this? Often yes: build the control from real
UIButtons or SwiftUI views so the system exposes them.
Related questions
- 24.1.1 How does VoiceOver read your UI: accessibility elements, labels, values, hints and traits, and how do you fix…
- 24.1.2 How do custom actions, adjustable controls, rotors and announcements make complex UI usable with VoiceOver?
- 24.6.1 What can a unit test assert about accessibility properties, and why does a standard UIButton report no label…
- 24.6.3 How do you make a custom modal screen work with VoiceOver: modal views, the escape gesture and the magic tap?
← What can a unit test assert about accessibility properties, and why… How do you make a custom modal screen work with VoiceOver: modal… →