Skip to content

Commit 921272e

Browse files
authored
✨ Add support for accessibility modifiers on TextState (#863)
* wip * wip2 * fix unsupported builds * handle custom dump * alphabetize * use TextState for .accessibilityLabel associated value
1 parent edfe198 commit 921272e

File tree

1 file changed

+125
-0
lines changed

1 file changed

+125
-0
lines changed

Sources/ComposableArchitecture/SwiftUI/TextState.swift

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ public struct TextState: Equatable, Hashable {
5858
fileprivate let storage: Storage
5959

6060
fileprivate enum Modifier: Equatable, Hashable {
61+
case accessibilityHeading(AccessibilityHeadingLevel)
62+
case accessibilityLabel(TextState)
63+
case accessibilityTextContentType(AccessibilityTextContentType)
6164
case baselineOffset(CGFloat)
6265
case bold
6366
case font(Font?)
@@ -122,6 +125,8 @@ public struct TextState: Equatable, Hashable {
122125
}
123126
}
124127

128+
// MARK: - API
129+
125130
extension TextState {
126131
public init(verbatim content: String) {
127132
self.storage = .verbatim(content)
@@ -206,6 +211,82 @@ extension TextState {
206211
}
207212
}
208213

214+
// MARK: Accessibility
215+
216+
extension TextState {
217+
public enum AccessibilityTextContentType: String, Equatable, Hashable {
218+
case console, fileSystem, messaging, narrative, plain, sourceCode, spreadsheet, wordProcessing
219+
220+
#if compiler(>=5.5.1)
221+
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
222+
var toSwiftUI: SwiftUI.AccessibilityTextContentType {
223+
switch self {
224+
case .console: return .console
225+
case .fileSystem: return .fileSystem
226+
case .messaging: return .messaging
227+
case .narrative: return .narrative
228+
case .plain: return .plain
229+
case .sourceCode: return .sourceCode
230+
case .spreadsheet: return .spreadsheet
231+
case .wordProcessing: return .wordProcessing
232+
}
233+
}
234+
#endif
235+
}
236+
237+
public enum AccessibilityHeadingLevel: String, Equatable, Hashable {
238+
case h1, h2, h3, h4, h5, h6, unspecified
239+
240+
#if compiler(>=5.5.1)
241+
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
242+
var toSwiftUI: SwiftUI.AccessibilityHeadingLevel {
243+
switch self {
244+
case .h1: return .h1
245+
case .h2: return .h2
246+
case .h3: return .h3
247+
case .h4: return .h4
248+
case .h5: return .h5
249+
case .h6: return .h6
250+
case .unspecified: return .unspecified
251+
}
252+
}
253+
#endif
254+
}
255+
}
256+
257+
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
258+
extension TextState {
259+
public func accessibilityHeading(_ headingLevel: AccessibilityHeadingLevel) -> Self {
260+
var `self` = self
261+
`self`.modifiers.append(.accessibilityHeading(headingLevel))
262+
return `self`
263+
}
264+
265+
public func accessibilityLabel(_ string: String) -> Self {
266+
var `self` = self
267+
`self`.modifiers.append(.accessibilityLabel(.init(string)))
268+
return `self`
269+
}
270+
271+
public func accessibilityLabel<S:StringProtocol>(_ string: S) -> Self {
272+
var `self` = self
273+
`self`.modifiers.append(.accessibilityLabel(.init(string)))
274+
return `self`
275+
}
276+
277+
public func accessibilityLabel(_ key: LocalizedStringKey, tableName: String? = nil, bundle: Bundle? = nil, comment: StaticString? = nil) -> Self {
278+
var `self` = self
279+
`self`.modifiers.append(.accessibilityLabel(.init(key, tableName: tableName, bundle: bundle, comment: comment)))
280+
return `self`
281+
}
282+
283+
public func accessibilityTextContentType(_ type: AccessibilityTextContentType) -> Self {
284+
var `self` = self
285+
`self`.modifiers.append(.accessibilityTextContentType(type))
286+
return `self`
287+
}
288+
}
289+
209290
extension Text {
210291
public init(_ state: TextState) {
211292
let text: Text
@@ -219,6 +300,39 @@ extension Text {
219300
}
220301
self = state.modifiers.reduce(text) { text, modifier in
221302
switch modifier {
303+
#if compiler(>=5.5.1)
304+
case let .accessibilityHeading(level):
305+
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) {
306+
return text.accessibilityHeading(level.toSwiftUI)
307+
} else {
308+
return text
309+
}
310+
case let .accessibilityLabel(value):
311+
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) {
312+
switch value.storage {
313+
case let .verbatim(string):
314+
return text.accessibilityLabel(string)
315+
case let .localized(key, tableName, bundle, comment):
316+
return text.accessibilityLabel(Text(key, tableName: tableName, bundle: bundle, comment: comment))
317+
case .concatenated(_, _):
318+
assertionFailure("`.accessibilityLabel` does not support contcatenated `TextState`")
319+
return text
320+
}
321+
} else {
322+
return text
323+
}
324+
case let .accessibilityTextContentType(type):
325+
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) {
326+
return text.accessibilityTextContentType(type.toSwiftUI)
327+
} else {
328+
return text
329+
}
330+
#else
331+
case .accessibilityHeading,
332+
.accessibilityLabel,
333+
.accessibilityTextContentType:
334+
return text
335+
#endif
222336
case let .baselineOffset(baselineOffset):
223337
return text.baselineOffset(baselineOffset)
224338
case .bold:
@@ -308,6 +422,8 @@ extension LocalizedStringKey {
308422
}
309423
}
310424

425+
// MARK: - CustomDumpRepresentable
426+
311427
extension TextState: CustomDumpRepresentable {
312428
public var customDumpValue: Any {
313429
func dumpHelp(_ textState: Self) -> String {
@@ -322,6 +438,15 @@ extension TextState: CustomDumpRepresentable {
322438
}
323439
for modifier in textState.modifiers {
324440
switch modifier {
441+
case let .accessibilityHeading(headingLevel):
442+
let tag = "accessibility-heading-level"
443+
output = "<\(tag)=\(headingLevel.rawValue)>\(output)</\(tag)>"
444+
case let .accessibilityLabel(value):
445+
let tag = "accessibility-label"
446+
output = "<\(tag)=\(dumpHelp(value))>\(output)</\(tag)>"
447+
case let .accessibilityTextContentType(type):
448+
let tag = "accessibility-text-content-type"
449+
output = "<\(tag)=\(type.rawValue)>\(output)</\(tag)>"
325450
case let .baselineOffset(baselineOffset):
326451
output = "<baseline-offset=\(baselineOffset)>\(output)</baseline-offset>"
327452
case .bold, .fontWeight(.some(.bold)):

0 commit comments

Comments
 (0)