Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,164 @@ extension Steps {
}
}
}

// MARK: SwiftUI + Swift 6 concurrency support for accessibility traversal

@MainActor
public extension Steps {
/// Prints all accessibility labels reachable from the top-most view controller
func IPrintAllSwiftUILabels() {
self.performStep {
self.viewTester.wait(forTimeInterval: 2)
let labels = self.topController()?.view.collectSwiftUIAccessibilityLabels() ?? []
print("========================")
print(labels)
print("========================")
}
}

/// Prints all accessibility identifiers reachable from the top-most view controller
func IPrintAllSwiftUIIdentifiers() {
self.performStep {
self.viewTester.wait(forTimeInterval: 2)
let ids = self.topController()?.view.collectSwiftUIAccessibilityIdentifiers() ?? []
print("========================")
print(ids)
print("========================")
}
}

/// Prints all accessibility (label, identifier) pairs reachable from the top-most view controller
func IPrintAllSwiftUILabelsAndIdentifiers() {
self.performStep {
self.viewTester.wait(forTimeInterval: 2)
let pairs = self.topController()?.view.collectSwiftUIAccessibilityElements() ?? []
print("========================")
print(pairs)
print("========================")
}
}

func topController() -> UIViewController? {
guard let keyWindow = UIApplication.shared.connectedSceneActiveWindow else {
return nil
}
if let root = keyWindow.rootViewController as? UINavigationController {
if let topController = root.visibleViewController,
topController as? UIAlertController == nil {
return topController
}
if let topController = root.topViewController {
return topController
}
return nil
} else {
return nil
}
}
}

public extension UIApplication {
fileprivate var connectedSceneActiveWindow: UIWindow? {
return self.connectedScenes
// Keep only active scenes, onscreen and visible to the user
.first(where: { $0 is UIWindowScene })
// Get its associated windows
.flatMap({ $0 as? UIWindowScene })?.windows
// Finally, keep only the key window
.first(where: \.isKeyWindow)
}
}

// MARK: - SwiftUI-aware accessibility traversal

@MainActor
private extension UIView {
func collectSwiftUIAccessibilityLabels() -> [String] {
var labels: [String] = []
SwiftUIAccessibilityWalker.walk(self) { node in
guard let label = node.accessibilityLabel, !label.isEmpty else { return }
labels.append("".appendingFormat("%@", label))
}
return labels
}

func collectSwiftUIAccessibilityIdentifiers() -> [String] {
var ids: [String] = []
SwiftUIAccessibilityWalker.walk(self) { node in
guard let id = node.accessibilityIdentifier, !id.isEmpty else { return }
ids.append(id)
}
return ids
}

func collectSwiftUIAccessibilityElements() -> [AccessibilityPair] {
var pairs: [AccessibilityPair] = []
SwiftUIAccessibilityWalker.walk(self) { node in
guard let id = node.accessibilityIdentifier, !id.isEmpty,
let label = node.accessibilityLabel, !label.isEmpty else { return }
pairs.append(AccessibilityPair(label: "".appendingFormat("%@", label),
identifier: id))
}
return pairs
}
}

/// A single node in the accessibility traversal - either a real `UIView` or a
/// `UIAccessibilityElement` produced by SwiftUI.
@MainActor
private struct AccessibilityNode {
let accessibilityLabel: String?
let accessibilityIdentifier: String?

init(view: UIView) {
self.accessibilityLabel = view.accessibilityLabel
self.accessibilityIdentifier = view.accessibilityIdentifier
}

init(element: NSObject) {
self.accessibilityLabel = element.value(forKey: "accessibilityLabel") as? String
self.accessibilityIdentifier = (element as? UIAccessibilityIdentification)?.accessibilityIdentifier
?? element.value(forKey: "accessibilityIdentifier") as? String
}
}

private enum SwiftUIAccessibilityWalker {
/// Depth-first traversal that visits every visible `UIView` in the
/// hierarchy plus every entry in each view's `accessibilityElements`
/// array (the mechanism SwiftUI uses to publish its a11y tree).
@MainActor
static func walk(_ root: UIView, visit: (AccessibilityNode) -> Void) {
guard !root.isHidden else { return }

visit(AccessibilityNode(view: root))

if let elements = root.accessibilityElements as? [NSObject] {
for element in elements {
visitElement(element, visit: visit)
}
}

for subview in root.subviews {
walk(subview, visit: visit)
}
}

@MainActor
private static func visitElement(_ element: NSObject,
visit: (AccessibilityNode) -> Void) {
if let view = element as? UIView {
walk(view, visit: visit)
return
}

visit(AccessibilityNode(element: element))

// Some SwiftUI a11y elements are themselves containers.
if let nested = element.value(forKey: "accessibilityElements") as? [NSObject] {
for child in nested {
visitElement(child, visit: visit)
}
}
}
}
Loading