Skip to content

Commit 3733a88

Browse files
Add container visualization support to AccessibilitySnapshotView
- Add showContainers option to Overlay configuration - Add internal initializer for sub-struct configuration - Add static factory method `withContainers()` for easy container config - Add container overlay rendering with dashed borders (12pt dash, 6pt gap) - Containers are rendered first (below elements) when showContainers enabled 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent fd625d2 commit 3733a88

2 files changed

Lines changed: 123 additions & 10 deletions

File tree

Sources/AccessibilitySnapshot/Core/AccessibilitySnapshotConfiguration.swift

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,22 @@ public struct AccessibilitySnapshotConfiguration {
3434
/// order, repeating through the array as necessary.
3535
/// Defaults to `MarkerColors.defaultColors`.
3636
public let colors: [UIColor]
37-
37+
3838
/// Controls when to show indicators for elements' accessibility activation points.
3939
/// Defaults to `.whenOverridden`.
4040
public let activationPointDisplay: AccessibilityContentDisplayMode
41-
41+
42+
/// Controls whether containers are rendered in the overlay (as dashed borders).
43+
/// Defaults to `false` for backward compatibility.
44+
public let showContainers: Bool
45+
4246
init(colors: [UIColor] = MarkerColors.defaultColors,
43-
activationPointDisplay: AccessibilityContentDisplayMode = .whenOverridden
47+
activationPointDisplay: AccessibilityContentDisplayMode = .whenOverridden,
48+
showContainers: Bool = false
4449
) {
4550
self.colors = colors.isEmpty ? MarkerColors.defaultColors : colors
4651
self.activationPointDisplay = activationPointDisplay
52+
self.showContainers = showContainers
4753
}
4854
}
4955

@@ -73,7 +79,14 @@ public struct AccessibilitySnapshotConfiguration {
7379
public let snapshot: Snapshot
7480
public let overlay: Overlay
7581
public let legend: Legend
76-
82+
83+
/// Internal initializer for creating configuration from sub-structs
84+
internal init(snapshot: Snapshot, overlay: Overlay, legend: Legend) {
85+
self.snapshot = snapshot
86+
self.overlay = overlay
87+
self.legend = legend
88+
}
89+
7790
/// Creates a new accessibility snapshot configuration.
7891
///
7992
/// - Parameters:
@@ -129,3 +142,38 @@ public enum AccessibilityContentDisplayMode {
129142
/// Never show the accessibility content.
130143
case never
131144
}
145+
146+
extension AccessibilitySnapshotConfiguration {
147+
148+
/// Creates a configuration with container visualization enabled using default settings.
149+
///
150+
/// Containers will be rendered as dashed borders (12pt dash, 6pt gap, 4pt line width at 0.8 alpha) and will
151+
/// appear in the legend if they have labels.
152+
///
153+
/// - parameter viewRenderingMode: The view rendering mode for snapshot generation. Defaults to `.drawHierarchyInRect`.
154+
/// - parameter markerColors: Colors for overlays (used for both elements and containers). Defaults to `MarkerColors.defaultColors`.
155+
/// - parameter activationPointDisplayMode: When to show activation point indicators. Defaults to `.whenOverridden`.
156+
/// - parameter showUserInputLabels: When to show user input labels. Defaults to `.whenOverridden`.
157+
/// - returns: Configuration with container visualization enabled
158+
public static func withContainers(
159+
viewRenderingMode: ViewRenderingMode = .drawHierarchyInRect,
160+
markerColors: [UIColor] = MarkerColors.defaultColors,
161+
activationPointDisplayMode: AccessibilityContentDisplayMode = .whenOverridden,
162+
showUserInputLabels: AccessibilityContentDisplayMode = .whenOverridden
163+
) -> AccessibilitySnapshotConfiguration {
164+
return AccessibilitySnapshotConfiguration(
165+
snapshot: Snapshot(
166+
viewRenderingMode: viewRenderingMode,
167+
colorMode: .fullColor
168+
),
169+
overlay: Overlay(
170+
colors: markerColors,
171+
activationPointDisplay: activationPointDisplayMode,
172+
showContainers: true
173+
),
174+
legend: Legend(
175+
includesUserInputLabels: showUserInputLabels
176+
)
177+
)
178+
}
179+
}

Sources/AccessibilitySnapshot/Core/AccessibilitySnapshotView.swift

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ public final class AccessibilitySnapshotView: SnapshotAndLegendView {
103103

104104
private var displayMarkers: [DisplayMarker] = []
105105

106+
private var containerOverlays: [UIView] = []
107+
106108
// MARK: - Public Methods
107109

108110
/// Parse the `containedView`'s accessibility and add appropriate visual elements to represent it.
@@ -117,6 +119,7 @@ public final class AccessibilitySnapshotView: SnapshotAndLegendView {
117119
$0.overlayView.removeFromSuperview()
118120
$0.activationPointView?.removeFromSuperview()
119121
}
122+
self.containerOverlays.forEach { $0.removeFromSuperview() }
120123

121124
let viewController = containedView.next as? UIViewController
122125
let originalParent = viewController?.parent
@@ -153,26 +156,36 @@ public final class AccessibilitySnapshotView: SnapshotAndLegendView {
153156
containedView.layoutIfNeeded()
154157

155158
let parser = AccessibilityHierarchyParser()
159+
160+
// Render containers first if enabled (so they appear below elements in z-order)
161+
if snapshotConfiguration.overlay.showContainers {
162+
let hierarchy = parser.parseAccessibilityHierarchy(
163+
in: containedView,
164+
rotorResultLimit: snapshotConfiguration.legend.rotorResultLimit
165+
)
166+
renderContainers(from: hierarchy)
167+
}
168+
156169
let markers = parser.parseAccessibilityElements(in: containedView, rotorResultLimit: snapshotConfiguration.legend.rotorResultLimit)
157-
158-
170+
171+
159172
var displayMarkers: [DisplayMarker] = []
160173
for (index, marker) in markers.enumerated() {
161174
let color = snapshotConfiguration.overlay.colors[index % snapshotConfiguration.overlay.colors.count]
162175

163176
let legendView = LegendView(marker: marker, color: color, configuration: snapshotConfiguration.legend)
164177
addSubview(legendView)
165-
178+
166179
let rotorResultsShapes = marker.displayRotors(snapshotConfiguration.legend.includesCustomRotors).flatMap(\.resultMarkers).compactMap(\.shape)
167-
180+
168181
let overlayView = OverlayView(
169182
frame: snapshotView.bounds,
170183
elementShape: marker.shape,
171184
includedShapes: rotorResultsShapes,
172185
color: color)
173-
186+
174187
snapshotView.addSubview(overlayView)
175-
188+
176189
var displayMarker = DisplayMarker(
177190
marker: marker,
178191
legendView: legendView,
@@ -209,6 +222,58 @@ public final class AccessibilitySnapshotView: SnapshotAndLegendView {
209222
self.displayMarkers = displayMarkers
210223
}
211224

225+
// MARK: - Private Methods
226+
227+
/// Renders container overlays as dashed borders
228+
private func renderContainers(from nodes: [AccessibilityHierarchyNode]) {
229+
var containerIndex = 0
230+
var newOverlays: [UIView] = []
231+
232+
func collectAndRenderContainers(from nodes: [AccessibilityHierarchyNode]) {
233+
for node in nodes {
234+
switch node {
235+
case .element:
236+
break // Elements are handled separately
237+
case .container(let containerInfo, let children):
238+
let color = snapshotConfiguration.overlay.colors[
239+
containerIndex % snapshotConfiguration.overlay.colors.count
240+
]
241+
containerIndex += 1
242+
243+
let overlayView = createContainerOverlay(for: containerInfo, color: color)
244+
snapshotView.addSubview(overlayView)
245+
newOverlays.append(overlayView)
246+
247+
// Recurse into children
248+
collectAndRenderContainers(from: children)
249+
}
250+
}
251+
}
252+
253+
collectAndRenderContainers(from: nodes)
254+
self.containerOverlays = newOverlays
255+
}
256+
257+
/// Creates a visual overlay for a container as a dashed border
258+
private func createContainerOverlay(for containerInfo: ContainerInfo, color: UIColor) -> UIView {
259+
let overlayView = UIView()
260+
overlayView.frame = containerInfo.frame
261+
overlayView.backgroundColor = .clear
262+
overlayView.isUserInteractionEnabled = false
263+
264+
// Create dashed border layer
265+
let borderLayer = CAShapeLayer()
266+
borderLayer.path = UIBezierPath(rect: overlayView.bounds).cgPath
267+
borderLayer.fillColor = nil
268+
borderLayer.strokeColor = color.withAlphaComponent(0.8).cgColor
269+
borderLayer.lineWidth = 4
270+
borderLayer.lineDashPattern = [12, 6] // 12pt dash, 6pt gap
271+
272+
overlayView.layer.addSublayer(borderLayer)
273+
274+
return overlayView
275+
}
276+
212277
// MARK: - Private Types
213278

214279
private struct DisplayMarker {

0 commit comments

Comments
 (0)