deepestAccessibilityHitPath

fun deepestAccessibilityHitPath(node: Any, view: UIView, positionInWindowPx: AxPoint, scale: Float, preferClickableBranches: Boolean = true): List<Any>?

Returns the path from node down to the deepest descendant whose accessibility frame contains positionInWindowPx, or null if node itself doesn't contain it.

The full root-to-leaf path is returned, not just the leaf, so callers can inspect ancestry — pick the nearest clickable ancestor of the hit node, or detect that the path crosses a view owned by another capture pipeline.

Overlap tie-break, and its limit. Children are searched in reverse order, so among subviews a later sibling — the one drawn on top — wins an overlap. That is only a true z-order tie-break within a single group: accessibilityChildren returns accessibilityElements + subviews, a concatenation whose across-group order has no relation to what is drawn on top, and reversing it searches every subview before every accessibility element. So a node that exposes an on-top overlay through accessibilityElements while the covered content is a plain subview resolves a tap to the covered subview instead of the overlay. This is long-standing behavior, unchanged by the extraction — documented rather than fixed, since this walk is now shared API and its callers should know the edge of the contract they depend on.

Clickable branches win over the tie-break. Before z-order is consulted at all, a branch that yields a clickable (isAccessibilityButton) is preferred over one that yields none; the reverse order above decides only among branches that tie on that. Without this the walk commits to the first branch geometrically containing the point and never reconsiders, so a single empty view covering the content swallows every tap. That is not hypothetical — measured on a real SwiftUI List, where a full-screen _UITouchPassthroughView sits on top of the cells and, as its name says, passes touches straight through to them. UIKit's own hitTest gets this right because such views decline the hit; the accessibility tree carries no equivalent signal, so "did this branch lead anywhere a tap can be attributed to" is the closest available stand-in.

The trade this accepts: an opaque non-interactive overlay that genuinely does block touches — a modal scrim over a button, say — is likewise invisible to this walk, and a tap on it now resolves to the button beneath instead of to nothing. Both directions are wrong for some tree; this one is wrong for the rarer one, and it fails toward reporting an event rather than toward the pipeline being silently inert on every SwiftUI screen built out of List.

Note this is decided per branch, not globally: a subtree with no clickable anywhere still resolves exactly as before, so callers that don't care about clickability see no change.

preferClickableBranches turns the preference off, restoring the plain topmost-branch descent. That is not a compatibility shim: the preference answers "which element should this tap be attributed to", and a caller asking "where did this tap visually land" needs the other answer. resolveNativeTapTarget needs both — it decides Compose ownership from the topmost path, because a preference for clickables would otherwise route the path around a Compose host and let the native pipeline claim a tap that landed on Compose-owned content — and resolves the target from the preferred one. Conflating the two is exactly the bug that motivated splitting them.

view supplies the coordinate space and scale the point-to-pixel ratio — both are handed to accessibilityBoundsInWindowPx unchanged, so its precondition on scale applies here too.

Threading. Main thread only: every property this reads (accessibilityChildren, accessibilityFrame, subviews) is main-thread-only UIKit API.

Termination. The tree this walks is supplied by the host app, not by this module, and nothing in the UIAccessibility contract forbids an element from listing an ancestor among its accessibilityElements. Such a link makes the graph cyclic, and a naive descent would recurse until the stack overflows. Three bounds prevent that: a branch that revisits a node already on the path being built is abandoned (the parent then tries its next sibling), descent stops at MAX_ACCESSIBILITY_TREE_DEPTH, and the walk as a whole stops after MAX_ACCESSIBILITY_NODE_VISITS nodes. The last is what bounds breadth rather than depth, and it is load-bearing precisely because of the clickable preference above: exploring every branch of a clickable-free subtree compounds per level whenever a node is reachable by more than one route. All three degrade to resolving a shallower element rather than crashing — a missed leaf is a dropped event, an overflow or a multi-second walk on the main thread is a wedged app.