Skip to content

Commit 3d8c515

Browse files
adding container visualizations
1 parent 930b45e commit 3d8c515

10 files changed

Lines changed: 726 additions & 95 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,6 @@ Pods/
3232
generated/
3333
.build/
3434
.swiftpm/
35+
36+
# Private thoughts/notes
37+
thoughts/

Example/AccessibilitySnapshot.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
AC725B842B06D07E009AD59B /* AccessibilityCustomContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC725B832B06D07E009AD59B /* AccessibilityCustomContentViewController.swift */; };
6666
AC969EC52E4B6F1E007D8B7A /* AccessibilityAccessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC969EC42E4B6F1E007D8B7A /* AccessibilityAccessorTests.swift */; };
6767
ACAB81272E44F23100FB54FE /* BlockBasedAccessibilityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACAB81262E44F23100FB54FE /* BlockBasedAccessibilityViewController.swift */; };
68+
ACE3B9492EC3B1B3002D0052 /* ContainerVisualizationDemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACE3B9482EC3B1B3002D0052 /* ContainerVisualizationDemoViewController.swift */; };
6869
C47F6C5316FB0C043BEB59F3 /* Pods_AccessibilitySnapshotDemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A886964D2E787399E137105 /* Pods_AccessibilitySnapshotDemo.framework */; };
6970
D2F76EED2945C879000A453F /* HitTargetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F76EEC2945C879000A453F /* HitTargetTests.swift */; };
7071
D38F6F4E508A3D067D677F69 /* Pods_UnitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BFCB4FD6BC17AB232B26E72 /* Pods_UnitTests.framework */; };
@@ -164,6 +165,7 @@
164165
AC725B832B06D07E009AD59B /* AccessibilityCustomContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityCustomContentViewController.swift; sourceTree = "<group>"; };
165166
AC969EC42E4B6F1E007D8B7A /* AccessibilityAccessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityAccessorTests.swift; sourceTree = "<group>"; };
166167
ACAB81262E44F23100FB54FE /* BlockBasedAccessibilityViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockBasedAccessibilityViewController.swift; sourceTree = "<group>"; };
168+
ACE3B9482EC3B1B3002D0052 /* ContainerVisualizationDemoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerVisualizationDemoViewController.swift; sourceTree = "<group>"; };
167169
C78F90CE7A2A315AADF80144 /* Pods-SnapshotTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SnapshotTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SnapshotTests/Pods-SnapshotTests.debug.xcconfig"; sourceTree = "<group>"; };
168170
CCFF2A604706B71DC0CBD38B /* Pods-AccessibilitySnapshotDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AccessibilitySnapshotDemo.release.xcconfig"; path = "Pods/Target Support Files/Pods-AccessibilitySnapshotDemo/Pods-AccessibilitySnapshotDemo.release.xcconfig"; sourceTree = "<group>"; };
169171
D2F76EEC2945C879000A453F /* HitTargetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitTargetTests.swift; sourceTree = "<group>"; };
@@ -231,6 +233,7 @@
231233
AC3C4C792E3B7A0C00D16410 /* AccessibilityCustomRotorsViewController.swift */,
232234
1104A8D02B595FF600B6715F /* TextFieldViewController.swift */,
233235
112909BB2B63E57B00B4EBEB /* TextViewViewController.swift */,
236+
ACE3B9482EC3B1B3002D0052 /* ContainerVisualizationDemoViewController.swift */,
234237
AC725B832B06D07E009AD59B /* AccessibilityCustomContentViewController.swift */,
235238
);
236239
name = "Accessibility Screens";
@@ -649,6 +652,7 @@
649652
files = (
650653
607FACD81AFB9204008FA782 /* RootViewController.swift in Sources */,
651654
3F8E5DB92535B25000276B32 /* SwiftUIView.swift in Sources */,
655+
ACE3B9492EC3B1B3002D0052 /* ContainerVisualizationDemoViewController.swift in Sources */,
652656
3DBEAA5F2223D0DA00FAE61D /* AccessibilityPathViewController.swift in Sources */,
653657
3D4674682116A3F100278B57 /* ViewAccessibilityPropertiesViewController.swift in Sources */,
654658
3D39BFB22239BEA6009C3EF4 /* ActivationPointViewController.swift in Sources */,
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
//
2+
// Copyright 2025 Square Inc.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
import UIKit
18+
19+
final class ContainerVisualizationDemoViewController: AccessibilityViewController {
20+
21+
// MARK: - UIViewController
22+
23+
override func loadView() {
24+
self.view = ContainerView()
25+
}
26+
27+
}
28+
29+
// MARK: -
30+
31+
private extension ContainerVisualizationDemoViewController {
32+
33+
final class ContainerView: UIView {
34+
35+
// MARK: - Life Cycle
36+
37+
override init(frame: CGRect) {
38+
// Create a container view that's inset from the edges
39+
containerBox = UIView()
40+
containerBox.backgroundColor = UIColor.systemGray6
41+
containerBox.layer.cornerRadius = 8
42+
43+
// Create labels inside the container
44+
labels = (1...3).map {
45+
let label = UILabel()
46+
label.text = "Item \($0)"
47+
label.font = .preferredFont(forTextStyle: .body)
48+
label.textAlignment = .center
49+
return label
50+
}
51+
52+
super.init(frame: frame)
53+
backgroundColor = .white
54+
55+
addSubview(containerBox)
56+
labels.forEach { containerBox.addSubview($0) }
57+
58+
// Set up accessibility: the container box is a list container
59+
containerBox.accessibilityContainerType = .list
60+
containerBox.accessibilityLabel = "Shopping List"
61+
containerBox.accessibilityValue = "3 items"
62+
containerBox.accessibilityElements = labels
63+
64+
// Make the parent view expose the container box in its accessibility hierarchy
65+
self.accessibilityElements = [containerBox]
66+
}
67+
68+
@available(*, unavailable)
69+
required init?(coder: NSCoder) {
70+
fatalError("init(coder:) has not been implemented")
71+
}
72+
73+
// MARK: - Private Properties
74+
75+
private let containerBox: UIView
76+
private let labels: [UILabel]
77+
78+
// MARK: - UIView
79+
80+
override func layoutSubviews() {
81+
super.layoutSubviews()
82+
83+
// Inset the container box by 40 points on all sides
84+
let inset: CGFloat = 40
85+
containerBox.frame = bounds.insetBy(dx: inset, dy: inset)
86+
87+
// Layout labels vertically inside the container
88+
labels.forEach { $0.sizeToFit() }
89+
90+
let containerHeight = containerBox.bounds.height
91+
let totalHeight = labels.reduce(0) { $0 + $1.bounds.height }
92+
let spacing = (containerHeight - totalHeight) / CGFloat(labels.count + 1)
93+
94+
var yOffset = spacing
95+
for label in labels {
96+
label.center = CGPoint(
97+
x: containerBox.bounds.midX,
98+
y: yOffset + label.bounds.height / 2
99+
)
100+
yOffset += label.bounds.height + spacing
101+
}
102+
}
103+
104+
}
105+
106+
}

Example/SnapshotTests/AccessibilityContainersTests.swift

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ import FBSnapshotTestCase
2020
@testable import AccessibilitySnapshotDemo
2121

2222
final class AccessibilityContainersTests: SnapshotTestCase {
23+
24+
override func setUp() {
25+
super.setUp()
26+
recordMode = false
27+
}
2328

2429
// This test technically doesn't match VoiceOver behavior. VoiceOver says the
2530
// last cell is element "5 of 1," which seems like a bug in VoiceOver that isn't
@@ -71,4 +76,61 @@ final class AccessibilityContainersTests: SnapshotTestCase {
7176
SnapshotVerifyAccessibility(viewController.view)
7277
}
7378

79+
// MARK: - Container Visualization Tests
80+
81+
func testListWithContainerVisualization() {
82+
let viewController = ListContainerViewController()
83+
viewController.view.frame = UIScreen.main.bounds
84+
85+
let configuration = AccessibilitySnapshotConfiguration.withContainers()
86+
SnapshotVerifyAccessibilityWithHierarchy(
87+
viewController.view,
88+
snapshotConfiguration: configuration
89+
)
90+
}
91+
92+
func testLandmarkWithContainerVisualization() {
93+
let viewController = LandmarkContainerViewController()
94+
viewController.view.frame = UIScreen.main.bounds
95+
96+
let configuration = AccessibilitySnapshotConfiguration.withContainers()
97+
SnapshotVerifyAccessibilityWithHierarchy(
98+
viewController.view,
99+
snapshotConfiguration: configuration
100+
)
101+
}
102+
103+
func testDataTableWithContainerVisualization() {
104+
let viewController = DataTableViewController(configuration: .basic)
105+
viewController.view.frame = UIScreen.main.bounds
106+
107+
let configuration = AccessibilitySnapshotConfiguration.withContainers()
108+
SnapshotVerifyAccessibilityWithHierarchy(
109+
viewController.view,
110+
snapshotConfiguration: configuration
111+
)
112+
}
113+
114+
func testDataTableWithHeadersAndContainerVisualization() {
115+
let viewController = DataTableViewController(configuration: .withHeaders)
116+
viewController.view.frame = UIScreen.main.bounds
117+
118+
let configuration = AccessibilitySnapshotConfiguration.withContainers()
119+
SnapshotVerifyAccessibilityWithHierarchy(
120+
viewController.view,
121+
snapshotConfiguration: configuration
122+
)
123+
}
124+
125+
func testContainerVisualizationDemo() {
126+
let viewController = ContainerVisualizationDemoViewController()
127+
viewController.view.frame = UIScreen.main.bounds
128+
129+
let configuration = AccessibilitySnapshotConfiguration.withContainers()
130+
SnapshotVerifyAccessibilityWithHierarchy(
131+
viewController.view,
132+
snapshotConfiguration: configuration
133+
)
134+
}
135+
74136
}

Example/SnapshotTests/SnapshotTestCase.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ class SnapshotTestCase: FBSnapshotTestCase {
6868

6969
fileNameOptions = [.OS, .screenSize, .screenScale]
7070

71-
recordMode = false
71+
recordMode = true
7272
}
7373

7474
}

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+
}

0 commit comments

Comments
 (0)