Repository navigation
Fall back to UIAccessibilityContainer index APIs when accessibilityElements is nil - #344
RoyalPineapple wants to merge 1 commit into
Conversation
969bf79 to
535b5fd
Compare
0176ab5 to
51851ab
Compare
…ements is nil When `accessibilityElements` is nil, fall back to the `accessibilityElementCount()` / `accessibilityElement(at:)` container APIs so that objects whose children are exposed only through those APIs are captured rather than treated as leaves. This applies to all objects including UIViews, matching the behavior of UIKit's own accessibility tree walker. Guards against the NSNotFound sentinel from `accessibilityElementCount()` to avoid iterating ~NSIntegerMax times, and tolerates nil returns from `accessibilityElement(at:)` for individual indices.
51851ab to
64659c0
Compare
robmaceachern
left a comment
There was a problem hiding this comment.
The bot generated some feedback that might be worth considering but lgtm.
| elements.append(element) | ||
| } | ||
| } | ||
| return elements.isEmpty ? nil : elements |
There was a problem hiding this comment.
blocker: Preserve explicit-container precedence when all indexed slots are invalid
A positive count selects the dynamic container path, but filtering every slot to an empty array turns it back into nil. For a UIView, that switches to subview traversal at line 854, so native subviews leak into the snapshot even though the index APIs should take precedence; it also differs from an explicit accessibilityElements = []. Return the possibly empty elements array after a valid positive count, and cover count = 1 with a nil slot plus an accessible real subview to lock this precedence.
This response was drafted with AI assistance.
| // _UIInheritedView inside _UIFloatingBarContainerView) use zero-frame wrappers whose children overflow and | ||
| // are visible. Pruning those hides real accessible content such as the UISearchBarTextField rendered by | ||
| // .searchable(). | ||
| let explicitAccessibilityElements = resolvedAccessibilityElements() |
There was a problem hiding this comment.
suggestion: Prune hidden and transparent views before resolving dynamic elements
This call now runs before the existing isHidden / alpha short-circuit. A hidden custom view with nil accessibilityElements and a finite count will enumerate every index even though line 824 immediately discards it; the base implementation never evaluated its elements because the visibility condition short-circuited first. Split the visibility/alpha guard ahead of resolution, leaving the zero-frame decision after it, and add a call-count regression so hidden reusable views cannot introduce unexpected work or side effects.
This response was drafted with AI assistance.
| } | ||
|
|
||
| var elements: [NSObject] = [] | ||
| elements.reserveCapacity(count) |
There was a problem hiding this comment.
suggestion: Bound implausible positive counts before reserving and iterating
NSNotFound is only one invalid or stale value. A container returning Int.max - 1, or another implausibly large finite count, still reaches reserveCapacity and can terminate the host or spend unbounded time dispatching accessibilityElement(at:). Since this parser runs in the client process and already treats the sentinel defensively, reject and log an implementation-defined safety limit before allocation/iteration. A fake container can cover this by asserting that indexed lookup is never called.
This response was drafted with AI assistance.
| // `accessibilityElement(at:)` may return `nil` (or a non-`NSObject`) for individual indices even | ||
| // when `accessibilityElementCount()` reports a larger count; those are skipped rather than treated | ||
| // as a hard error, mirroring how a consumer iterating the container would tolerate gaps. | ||
| if let element = accessibilityElement(at: index) as? NSObject { |
There was a problem hiding this comment.
blocker: Detect cycles on the active traversal path
An index container can vend itself or an ancestor; once appended here, line 840 recursively enters that object without termination. This is newly reachable for dynamic-only containers, which were not traversed while accessibilityElements was nil before this change. Thread active ancestor identities through recursiveAccessibilityHierarchy and skip/log back-edges only (rather than globally deduplicating, so legitimate repeated elements remain). Please cover both a self-reference and a two-container cycle.
This response was drafted with AI assistance.
| XCTAssertTrue(elements.isEmpty, "Expected the NSNotFound count to be treated as no elements, got: \(elements)") | ||
| } | ||
|
|
||
| func testContainerWithZeroCountIsTreatedAsEmpty() { |
There was a problem hiding this comment.
suggestion: Make the zero-count test distinguish the new traversal behavior
This test also passes on the base revision: a non-UIView object with no explicit elements produced no nodes before the fallback. To guard the intended compatibility branch, use an index-backed UIView reporting zero with an accessible subview and assert that the subview is still traversed. Otherwise resolvedAccessibilityElements() could incorrectly return [] for zero and this test would remain green.
This response was drafted with AI assistance.
Summary
Falls back to the
UIAccessibilityContainerindex APIs (accessibilityElementCount()/accessibilityElement(at:)) whenaccessibilityElementsreturns nil.Some containers vend their children only through the index-based container APIs, returning nil from
accessibilityElements. The parser previously treated these as having no explicit elements and fell through to subview traversal, missing the container-vended children. This change mirrors the behavior of UIKit's own accessibility tree walker, which uses the index APIs for any object that reports a valid count — including UIViews.How it works
resolvedAccessibilityElements()checksaccessibilityElementsfirst. If nil, it iteratesaccessibilityElement(at:)up toaccessibilityElementCount(). Objects that reportNSNotFoundfromaccessibilityElementCount()(the default) fall through unchanged — their existing traversal path (subview iteration with grouping, sorting, and pruning) is preserved.In practice, no standard UIKit UIView uses the index API without also setting
accessibilityElements— UITableView, UICollectionView, UIScrollView, etc. all report eitherNSNotFoundor count=0 with nil elements. The fallback primarily captures non-UIView containers (e.g.UIAccessibilityElementsubclasses) that implement the dynamic container methods.Validation
Validated against
_accessibilityLeafDescendantsWithOptions:(UIKit's internal tree walker) with default options, VoiceOver options, and scanner groups mode. All three confirm the index API takes precedence over subview traversal when implemented.Changes
resolvedAccessibilityElements()onNSObject— triesaccessibilityElements, falls back to index APIs, filtersNSNotFoundand non-positive count sentinelsNSNotFoundsentinel, zero-count,UISegmentedControlend-to-end, andUIViewcontainer resolution