Skip to content

Commit 379a4f7

Browse files
Merge pull request #173 from cashapp/alex/AXCustomContent
Support Accessibility Custom Content
2 parents f039d27 + 2f24559 commit 379a4f7

26 files changed

+453
-58
lines changed

Example/AccessibilitySnapshot.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
66E2CD14CD63946657E17B15 /* Pods_SnapshotTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A3192D7B9B16BD10FB517A2 /* Pods_SnapshotTests.framework */; };
5858
83A295842AC22D9D00DFBE4F /* UserInputLabelsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A295832AC22D9D00DFBE4F /* UserInputLabelsViewController.swift */; };
5959
83A295862AC22EEE00DFBE4F /* UserInputLabelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A295852AC22EEE00DFBE4F /* UserInputLabelsTests.swift */; };
60+
AC725B842B06D07E009AD59B /* AccessibilityCustomContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC725B832B06D07E009AD59B /* AccessibilityCustomContentViewController.swift */; };
6061
C47F6C5316FB0C043BEB59F3 /* Pods_AccessibilitySnapshotDemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A886964D2E787399E137105 /* Pods_AccessibilitySnapshotDemo.framework */; };
6162
D2F76EED2945C879000A453F /* HitTargetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F76EEC2945C879000A453F /* HitTargetTests.swift */; };
6263
D38F6F4E508A3D067D677F69 /* Pods_UnitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BFCB4FD6BC17AB232B26E72 /* Pods_UnitTests.framework */; };
@@ -148,6 +149,7 @@
148149
83A295832AC22D9D00DFBE4F /* UserInputLabelsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserInputLabelsViewController.swift; sourceTree = "<group>"; };
149150
83A295852AC22EEE00DFBE4F /* UserInputLabelsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserInputLabelsTests.swift; sourceTree = "<group>"; };
150151
88C33CBF672C290CE1EE86AF /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
152+
AC725B832B06D07E009AD59B /* AccessibilityCustomContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityCustomContentViewController.swift; sourceTree = "<group>"; };
151153
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>"; };
152154
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>"; };
153155
D2F76EEC2945C879000A453F /* HitTargetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitTargetTests.swift; sourceTree = "<group>"; };
@@ -211,6 +213,7 @@
211213
3DDE7FF524C6D6BF00999ABA /* AccessibilityCustomActionsViewController.swift */,
212214
1104A8D02B595FF600B6715F /* TextFieldViewController.swift */,
213215
112909BB2B63E57B00B4EBEB /* TextViewViewController.swift */,
216+
AC725B832B06D07E009AD59B /* AccessibilityCustomContentViewController.swift */,
214217
);
215218
name = "Accessibility Screens";
216219
sourceTree = "<group>";
@@ -643,6 +646,7 @@
643646
112909BC2B63E57B00B4EBEB /* TextViewViewController.swift in Sources */,
644647
3DDE7FF624C6D6BF00999ABA /* AccessibilityCustomActionsViewController.swift in Sources */,
645648
3DBAC28F2242E7C700EF4D0A /* ListContainerViewController.swift in Sources */,
649+
AC725B842B06D07E009AD59B /* AccessibilityCustomContentViewController.swift in Sources */,
646650
3DBEAA5B2222953E00FAE61D /* SwitchControlViewController.swift in Sources */,
647651
3DBAC2912242F9B200EF4D0A /* LandmarkContainerViewController.swift in Sources */,
648652
1104A8CF2B580AC500B6715F /* SwiftUITextEntry.swift in Sources */,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
//
2+
// Copyright 2024 Block 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 Accessibility
18+
import Paralayout
19+
import UIKit
20+
21+
@available(iOS 14.0, *)
22+
final class AccessibilityCustomContentViewController: AccessibilityViewController {
23+
24+
// MARK: - UIViewController
25+
26+
override func loadView() {
27+
view = View(
28+
views: [
29+
.init(includeLabel: true, includeHint: true),
30+
.init(includeLabel: true, includeHint: false),
31+
.init(includeLabel: false, includeHint: true),
32+
.init(includeLabel: false, includeHint: false),
33+
]
34+
)
35+
}
36+
37+
}
38+
39+
// MARK: -
40+
@available(iOS 14.0, *)
41+
private extension AccessibilityCustomContentViewController {
42+
43+
final class View: UIView {
44+
45+
// MARK: - Life Cycle
46+
47+
init(views: [CustomContentView], frame: CGRect = .zero) {
48+
self.views = views
49+
50+
super.init(frame: frame)
51+
52+
views.forEach(addSubview)
53+
}
54+
55+
@available(*, unavailable)
56+
required init?(coder: NSCoder) {
57+
fatalError("init(coder:) has not been implemented")
58+
}
59+
60+
// MARK: - Private Properties
61+
62+
private let views: [CustomContentView]
63+
64+
// MARK: - UIView
65+
66+
override func layoutSubviews() {
67+
views.forEach { $0.bounds.size = .init(width: bounds.width / 2, height: 50) }
68+
69+
let statusBarHeight = window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
70+
71+
var distributionSpecifiers: [ViewDistributionSpecifying] = [ statusBarHeight.fixed, 1.flexible ]
72+
for subview in views {
73+
distributionSpecifiers.append(subview)
74+
distributionSpecifiers.append(1.flexible)
75+
}
76+
applyVerticalSubviewDistribution(distributionSpecifiers)
77+
}
78+
79+
}
80+
81+
}
82+
83+
// MARK: -
84+
@available(iOS 14.0, *)
85+
private extension AccessibilityCustomContentViewController {
86+
87+
final class CustomContentView: UIView, AXCustomContentProvider {
88+
// MARK: - Life Cycle
89+
90+
init(includeLabel: Bool, includeHint: Bool) {
91+
super.init(frame: .zero)
92+
93+
backgroundColor = .gray
94+
95+
isAccessibilityElement = true
96+
97+
accessibilityLabel = includeLabel ? "Label" : nil
98+
accessibilityHint = includeHint ? "Hint" : nil
99+
}
100+
101+
@available(*, unavailable)
102+
required init?(coder: NSCoder) {
103+
fatalError("init(coder:) has not been implemented")
104+
}
105+
106+
// MARK: - UIAccessibility
107+
var accessibilityCustomContent: [AXCustomContent]! = {
108+
let customContent = AXCustomContent(label: "Custom Content Label", value: "Custom Content Value")
109+
110+
let highImportance = AXCustomContent(label: "High Importance Label", value: "High Importance Value")
111+
highImportance.importance = .high
112+
113+
return [customContent, highImportance]
114+
}()
115+
}
116+
}

Example/AccessibilitySnapshot/RootViewController.swift

+6-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ final class RootViewController: UITableViewController {
2626
// MARK: - Life Cycle
2727

2828
init() {
29-
self.accessibilityScreens = [
29+
var accessibilityScreens = [
3030
("View Accessibility Properties", { _ in return ViewAccessibilityPropertiesViewController() }),
3131
("Label Accessibility Properties", { _ in return LabelAccessibilityPropertiesViewController() }),
3232
("Button Accessibility Traits", { _ in return ButtonAccessibilityTraitsViewController() }),
@@ -66,7 +66,11 @@ final class RootViewController: UITableViewController {
6666
("Text View", { _ in return TextViewViewController() }),
6767
("SwiftUI Text Entry", { _ in return UIHostingController(rootView: SwiftUITextEntry()) }),
6868
]
69-
69+
if #available(iOS 14.0, *) {
70+
accessibilityScreens.append( ("Accessibility Custom Content", { _ in return AccessibilityCustomContentViewController() }))
71+
}
72+
self.accessibilityScreens = accessibilityScreens
73+
7074
super.init(nibName: nil, bundle: nil)
7175
}
7276

Example/AccessibilitySnapshot/SwiftUIView.swift

+69-48
Original file line numberDiff line numberDiff line change
@@ -28,54 +28,75 @@ fileprivate struct Circle: View {
2828
struct SwiftUIView: View {
2929
var body: some View {
3030
VStack(spacing: 30) {
31-
// View with nothing.
32-
Circle()
33-
.accessibility(label: Text(""))
34-
.accessibility(value: Text(""))
35-
.accessibility(hint: Text(""))
36-
37-
// View with label.
38-
Circle()
39-
.accessibility(label: Text("Label"))
40-
.accessibility(value: Text(""))
41-
.accessibility(hint: Text(""))
42-
43-
// View with value.
44-
Circle()
45-
.accessibility(label: Text(""))
46-
.accessibility(value: Text("Value"))
47-
.accessibility(hint: Text(""))
48-
49-
// View with hint.
50-
Circle()
51-
.accessibility(label: Text(""))
52-
.accessibility(value: Text(""))
53-
.accessibility(hint: Text("Hint"))
54-
55-
// View with label and value.
56-
Circle()
57-
.accessibility(label: Text("Label"))
58-
.accessibility(value: Text("Value"))
59-
.accessibility(hint: Text(""))
60-
61-
// View with label and hint.
62-
Circle()
63-
.accessibility(label: Text("Label"))
64-
.accessibility(value: Text(""))
65-
.accessibility(hint: Text("Hint"))
66-
67-
// View with value and hint.
68-
Circle()
69-
.accessibility(label: Text(""))
70-
.accessibility(value: Text("Value"))
71-
.accessibility(hint: Text("Hint"))
72-
73-
// View with label, value, and hint.
74-
Circle()
75-
.accessibility(label: Text("Label"))
76-
.accessibility(value: Text("Value"))
77-
.accessibility(hint: Text("Hint"))
78-
31+
Group {
32+
// View with nothing.
33+
Circle()
34+
.accessibility(label: Text(""))
35+
.accessibility(value: Text(""))
36+
.accessibility(hint: Text(""))
37+
38+
// View with label.
39+
Circle()
40+
.accessibility(label: Text("Label"))
41+
.accessibility(value: Text(""))
42+
.accessibility(hint: Text(""))
43+
44+
// View with value.
45+
Circle()
46+
.accessibility(label: Text(""))
47+
.accessibility(value: Text("Value"))
48+
.accessibility(hint: Text(""))
49+
50+
// View with hint.
51+
Circle()
52+
.accessibility(label: Text(""))
53+
.accessibility(value: Text(""))
54+
.accessibility(hint: Text("Hint"))
55+
56+
// View with label and value.
57+
Circle()
58+
.accessibility(label: Text("Label"))
59+
.accessibility(value: Text("Value"))
60+
.accessibility(hint: Text(""))
61+
62+
// View with label and hint.
63+
Circle()
64+
.accessibility(label: Text("Label"))
65+
.accessibility(value: Text(""))
66+
.accessibility(hint: Text("Hint"))
67+
68+
// View with value and hint.
69+
Circle()
70+
.accessibility(label: Text(""))
71+
.accessibility(value: Text("Value"))
72+
.accessibility(hint: Text("Hint"))
73+
74+
// View with label, value, and hint.
75+
Circle()
76+
.accessibility(label: Text("Label"))
77+
.accessibility(value: Text("Value"))
78+
.accessibility(hint: Text("Hint"))
79+
80+
if #available(iOS 14.0, *) {
81+
// View with label, value, hint, and Custom Actions.
82+
Circle()
83+
.accessibility(label: Text("Label"))
84+
.accessibility(value: Text("Value"))
85+
.accessibility(hint: Text("Hint"))
86+
.accessibilityAction(named: "Custom") {}
87+
}
88+
89+
if #available(iOS 15.0, *) {
90+
// View with label, value, hint, and Custom Content.
91+
Circle()
92+
.accessibility(label: Text("Label"))
93+
.accessibility(value: Text("Value"))
94+
.accessibility(hint: Text("Hint"))
95+
.accessibilityCustomContent("Key", "Value")
96+
.accessibilityCustomContent("Important Key", "Important Value", importance: .high)
97+
}
98+
}
99+
79100
Spacer()
80101
}
81102
}

Example/SnapshotTests/AccessibilityPropertiesTests.swift

+12
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,18 @@ final class AccessibilitySnapshotTests: SnapshotTestCase {
6969
customActionsViewController.view.frame = UIScreen.main.bounds
7070
SnapshotVerifyAccessibility(customActionsViewController.view)
7171
}
72+
73+
@available(iOS 14.0, *)
74+
func testCustomContent() throws {
75+
try XCTSkipUnless(
76+
ProcessInfo().operatingSystemVersion.majorVersion >= 14,
77+
"This test only supports iOS 14 and later"
78+
)
79+
80+
let customContentViewController = AccessibilityCustomContentViewController()
81+
customContentViewController.view.frame = UIScreen.main.bounds
82+
SnapshotVerifyAccessibility(customContentViewController.view)
83+
}
7284

7385
func testLargeView() throws {
7486
let view = UIView(frame: CGRect(x: 0, y: 0, width: 1400, height: 1400))

Scripts/ValidateLocalizedStrings.swift

+10-2
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,18 @@ let translationRegex = Regex {
6666
var allValid = true
6767

6868
for filePath in stringsFiles {
69-
let translationKeys = try String(contentsOfFile: filePath).matches(of: translationRegex).map { $0.1 }
69+
let translationKeys = try Set(String(contentsOfFile: filePath).matches(of: translationRegex).map { $0.1 })
7070

71-
if Set(translationKeys) != localizedKeysSet {
71+
if translationKeys != localizedKeysSet {
7272
print("\(filePath) does not match expected set of localized string keys")
73+
let expected = translationKeys.subtracting(localizedKeysSet)
74+
if expected.count != 0 {
75+
print("Expected Keys not found: \(expected)")
76+
}
77+
let found = localizedKeysSet.subtracting(translationKeys)
78+
if found.count != 0 {
79+
print("No translation found for keys: \(found)")
80+
}
7381
allValid = false
7482
}
7583
}

Sources/AccessibilitySnapshot/Core/Swift/Assets/de.lproj/Localizable.strings

+3
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@
8282
/* Description for an accessibility element indicating that it has custom actions available */
8383
"custom_actions.description" = "Aktionen verfügbar";
8484

85+
/* Description for an accessibility element indicating that it has additional custom content available */
86+
"custom_content.description" = "Weiterhin inhalte verfügbar";
87+
8588
/* Description for the 'text entry' accessibility trait */
8689
"trait.text_field.description" = "Textfeld.";
8790

Sources/AccessibilitySnapshot/Core/Swift/Assets/en.lproj/Localizable.strings

+3
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@
8282
/* Description for an accessibility element indicating that it has custom actions available */
8383
"custom_actions.description" = "Actions Available";
8484

85+
/* Description for an accessibility element indicating that it has additional custom content available */
86+
"custom_content.description" = "More Content Available";
87+
8588
/* Description for the 'text entry' accessibility trait */
8689
"trait.text_field.description" = "Text Field.";
8790

Sources/AccessibilitySnapshot/Core/Swift/Assets/ru.lproj/Localizable.strings

+3
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@
8282
/* Description for an accessibility element indicating that it has custom actions available */
8383
"custom_actions.description" = "Доступны действия";
8484

85+
/* Description for an accessibility element indicating that it has additional custom content available */
86+
"custom_content.description" = "Доступно больше контента";
87+
8588
/* Description for the 'text entry' accessibility trait */
8689
"trait.text_field.description" = "текстовое поле.";
8790

0 commit comments

Comments
 (0)