@@ -163,6 +163,41 @@ public final class AccessibilityHierarchyParser {
163163 userInterfaceLayoutDirectionProvider: UserInterfaceLayoutDirectionProviding = UIApplication . shared,
164164 userInterfaceIdiomProvider: UserInterfaceIdiomProviding = UIDevice . current
165165 ) -> [ AccessibilityHierarchy ] {
166+ parseAccessibilityHierarchy (
167+ in: root,
168+ rotorResultLimit: rotorResultLimit,
169+ userInterfaceLayoutDirectionProvider: userInterfaceLayoutDirectionProvider,
170+ userInterfaceIdiomProvider: userInterfaceIdiomProvider,
171+ makeElement: { element, traversalIndex, _ in . element( element, traversalIndex: traversalIndex) } ,
172+ makeContainer: { container, children, _ in . container( container, children: children) }
173+ )
174+ }
175+
176+ /// Parses the accessibility hierarchy and folds it into a caller-defined node type.
177+ ///
178+ /// Walks the accessibility tree once in VoiceOver traversal order and builds a tree of `Node`
179+ /// values bottom-up. `makeElement` constructs a leaf from its parsed `AccessibilityElement`, its
180+ /// `traversalIndex`, and the live accessibility object that produced it. `makeContainer`
181+ /// constructs an interior node from its `AccessibilityContainer`, its already-built child nodes
182+ /// in traversal order, and the source object backing the container.
183+ ///
184+ /// The `source` parameters expose the originating accessibility object so callers can correlate
185+ /// parsed markers back to their source views — useful for test harnesses, debugging overlays,
186+ /// and custom renderers. `parseAccessibilityHierarchy(in:)` is the default instantiation,
187+ /// producing `[AccessibilityHierarchy]` and ignoring the source.
188+ ///
189+ /// Both closures are invoked synchronously during the walk, on the caller's thread.
190+ ///
191+ /// - parameter makeElement: Builds a leaf node. Called once per element, in traversal order.
192+ /// - parameter makeContainer: Builds an interior node from its children. Called once per container.
193+ public func parseAccessibilityHierarchy< Node> (
194+ in root: UIView ,
195+ rotorResultLimit: Int = AccessibilityElement . defaultRotorResultLimit,
196+ userInterfaceLayoutDirectionProvider: UserInterfaceLayoutDirectionProviding = UIApplication . shared,
197+ userInterfaceIdiomProvider: UserInterfaceIdiomProviding = UIDevice . current,
198+ makeElement: ( AccessibilityElement , _ traversalIndex: Int , _ source: NSObject ) -> Node ,
199+ makeContainer: ( AccessibilityContainer , _ children: [ Node ] , _ source: NSObject ) -> Node
200+ ) -> [ Node ] {
166201 let userInterfaceLayoutDirection = userInterfaceLayoutDirectionProvider. userInterfaceLayoutDirection
167202 let userInterfaceIdiom = userInterfaceIdiomProvider. userInterfaceIdiom
168203
@@ -194,7 +229,14 @@ public final class AccessibilityHierarchyParser {
194229 buildElement ( from: element. object, context: element. context, in: root, rotorResultLimit: rotorResultLimit)
195230 }
196231
197- return mapNodesToHierarchy ( accessibilityNodes, sortedElements: uncontextualizedElements, elements: elements, in: root)
232+ return foldNodes (
233+ accessibilityNodes,
234+ sortedElements: uncontextualizedElements,
235+ elements: elements,
236+ in: root,
237+ makeElement: makeElement,
238+ makeContainer: makeContainer
239+ )
198240 }
199241
200242 // MARK: - Private Types
@@ -531,23 +573,31 @@ public final class AccessibilityHierarchyParser {
531573
532574 // MARK: - Private Hierarchy Methods
533575
534- private func mapNodesToHierarchy(
576+ /// Folds the structural accessibility tree into a caller-defined `Node` type, bottom-up.
577+ ///
578+ /// Each produced node is paired internally with a traversal-derived sort key so that a
579+ /// container can order its children without inspecting the opaque `Node`. This mirrors
580+ /// `AccessibilityHierarchy.sortIndex`: an element sorts by its `traversalIndex`, a container by
581+ /// the minimum sort key among its children.
582+ private func foldNodes< Node> (
535583 _ nodes: [ AccessibilityNode ] ,
536584 sortedElements: [ ( object: NSObject , contextProvider: ContextProvider ? ) ] ,
537585 elements: [ AccessibilityElement ] ,
538- in root: UIView
539- ) -> [ AccessibilityHierarchy ] {
586+ in root: UIView ,
587+ makeElement: ( AccessibilityElement , _ traversalIndex: Int , _ source: NSObject ) -> Node ,
588+ makeContainer: ( AccessibilityContainer , _ children: [ Node ] , _ source: NSObject ) -> Node
589+ ) -> [ Node ] {
540590 var indexLookup : [ ObjectIdentifier : Int ] = [ : ]
541591 for (index, element) in sortedElements. enumerated ( ) {
542592 indexLookup [ ObjectIdentifier ( element. object) ] = index
543593 }
544594
545- func mapNode( _ node: AccessibilityNode ) -> [ AccessibilityHierarchy ] {
595+ func mapNode( _ node: AccessibilityNode ) -> [ ( node : Node , sortIndex : Int ) ] {
546596 switch node {
547597 case let . element( object, _) :
548598 guard let index = indexLookup [ ObjectIdentifier ( object) ] ,
549599 index < elements. count else { return [ ] }
550- return [ . element ( elements [ index] , traversalIndex : index) ]
600+ return [ ( makeElement ( elements [ index] , index , object ) , index) ]
551601
552602 case let . group( children, _, _, containerInfo) :
553603 let mappedChildren = children. flatMap { mapNode ( $0) } . sorted { lhs, rhs in
@@ -581,14 +631,15 @@ public final class AccessibilityHierarchyParser {
581631 type: containerType,
582632 frame: frame
583633 )
584- return [ . container( container, children: mappedChildren) ]
634+ let sortIndex = mappedChildren. map { $0. sortIndex } . min ( ) ?? Int . max
635+ return [ ( makeContainer ( container, mappedChildren. map { $0. node } , info. view) , sortIndex) ]
585636 }
586637
587638 return mappedChildren
588639 }
589640 }
590641
591- return nodes. flatMap { mapNode ( $0) }
642+ return nodes. flatMap { mapNode ( $0) } . map { $0 . node }
592643 }
593644}
594645
0 commit comments