Skip to content

Commit 8816826

Browse files
authored
VoiceOver Support (#35)
* Animations and FlowStack working with infinite layers with voice control * Stopped tracking xcschemes directory * added observer so that FlowStack knows when to switch the flowView's ZIndex * introduced a AccessibilityModifier to hold everything there
1 parent dea2c3e commit 8816826

File tree

3 files changed

+75
-33
lines changed

3 files changed

+75
-33
lines changed

FlowStackExample/FlowStackExample.xcodeproj/project.pbxproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@
309309
CODE_SIGN_STYLE = Automatic;
310310
CURRENT_PROJECT_VERSION = 1;
311311
DEVELOPMENT_ASSET_PATHS = "\"FlowStackExample/Preview Content\"";
312-
DEVELOPMENT_TEAM = CBXAQN69JX;
312+
DEVELOPMENT_TEAM = DAPFLRCVJA;
313313
ENABLE_PREVIEWS = YES;
314314
GENERATE_INFOPLIST_FILE = YES;
315315
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@@ -342,7 +342,7 @@
342342
CODE_SIGN_STYLE = Automatic;
343343
CURRENT_PROJECT_VERSION = 1;
344344
DEVELOPMENT_ASSET_PATHS = "\"FlowStackExample/Preview Content\"";
345-
DEVELOPMENT_TEAM = CBXAQN69JX;
345+
DEVELOPMENT_TEAM = DAPFLRCVJA;
346346
ENABLE_PREVIEWS = YES;
347347
GENERATE_INFOPLIST_FILE = YES;
348348
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;

Sources/FlowStack/FlowLink.swift

+17-6
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,17 @@ public extension View {
5656
}
5757
}
5858

59+
struct FlowDepthKey: EnvironmentKey {
60+
static var defaultValue: Int = 0
61+
}
62+
63+
extension EnvironmentValues {
64+
var flowDepth: Int {
65+
get { self[FlowDepthKey.self] }
66+
set { self[FlowDepthKey.self] = newValue }
67+
}
68+
}
69+
5970
struct GestureContainer: UIViewRepresentable {
6071

6172
@Binding var isPressed: Bool
@@ -238,8 +249,8 @@ public struct FlowLink<Label>: View where Label: View {
238249
@Environment(\.self) private var capturedEnvironment
239250

240251
@Environment(\.flowPath) private var path
252+
@Environment(\.flowDepth) private var flowDepth
241253
@Environment(\.flowTransaction) private var transaction
242-
@EnvironmentObject var flowDepth: FlowDepth
243254

244255
@State private var overrideAnchor: Anchor<CGRect>?
245256

@@ -270,16 +281,16 @@ public struct FlowLink<Label>: View where Label: View {
270281
}
271282

272283
var isContainedInPath: Bool {
273-
guard let elements = path?.wrappedValue.elements, let value = value, elements.count > Int(flowDepth.zIndex) else { return false }
284+
guard let elements = path?.wrappedValue.elements, let value = value, elements.count > flowDepth else { return false }
274285

275286
// treat -1 as special case to ignore the level on comparisons
276-
let depth = flowDepth.zIndex == -1 ? nil : flowDepth.zIndex
287+
let depth = flowDepth == -1 ? nil : flowDepth
277288

278-
return path?.wrappedValue.contains(value, atLevel: Int(flowDepth.zIndex)) ?? false
289+
return path?.wrappedValue.contains(value, atLevel: depth) ?? false
279290
}
280291

281292
var hasSiblingElement: Bool {
282-
return path?.wrappedValue.elements.map(\.context?.linkDepth).contains(Int(flowDepth.zIndex)) ?? false
293+
return path?.wrappedValue.elements.map(\.context?.linkDepth).contains(flowDepth) ?? false
283294
}
284295

285296
@State private var snapshot: UIImage?
@@ -401,7 +412,7 @@ public struct FlowLink<Label>: View where Label: View {
401412
anchor: configuration.animateFromAnchor ? anchor : nil,
402413
overrideAnchor: configuration.animateFromAnchor ? overrideAnchor : nil,
403414
snapshot: configuration.animateFromAnchor && configuration.transitionFromSnapshot ? snapshot : nil,
404-
linkDepth: Int(flowDepth.zIndex),
415+
linkDepth: flowDepth,
405416
cornerRadius: configuration.cornerRadius,
406417
cornerStyle: configuration.cornerStyle,
407418
shadowRadius: configuration.shadowRadius,

Sources/FlowStack/FlowStack.swift

+56-25
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// Created by Zac White on 2/14/23.
55
//
66

7+
import Combine
78
import SwiftUI
89

910
struct AnyDestination: Equatable {
@@ -24,20 +25,55 @@ class DestinationLookup: ObservableObject {
2425
@Published var table: [String: AnyDestination] = [:]
2526
}
2627

27-
// Tracks Z index for accessibility
28-
class FlowDepth: ObservableObject {
28+
class AccessibilityManager: ObservableObject {
2929
@Published var zIndex: Double = 0.0
30+
var skrimIndex: Double { zIndex - 0.1 }
31+
var behindSkrim: Double { zIndex - 0.2 }
32+
33+
// Setup VoiceOver Observer
34+
@Published var isVoiceOverRunning: Bool = UIAccessibility.isVoiceOverRunning
35+
init() {
36+
NotificationCenter.default
37+
.publisher(for: UIAccessibility.voiceOverStatusDidChangeNotification)
38+
.map { _ in UIAccessibility.isVoiceOverRunning }
39+
.assign(to: &$isVoiceOverRunning)
40+
}
41+
42+
func setIndex(_ input: Int) { self.zIndex = Double(input + 1)}
43+
44+
func decrementIndex() { self.zIndex -= 1.0 }
45+
46+
func calcSkrim() -> Double {
47+
if isVoiceOverRunning { return skrimIndex }
48+
return zIndex > 1.0 ? zIndex : skrimIndex
49+
}
50+
}
51+
52+
struct AccessibilityModifier: ViewModifier {
53+
@EnvironmentObject var accessibilityManager: AccessibilityManager
54+
var isHidden: (Double, Int) -> Bool = { d, i in d != Double(i + 1) }
55+
let element: Int
56+
57+
func body(content: Content) -> some View {
58+
content
59+
.environment(\.flowDepth, element + 1)
60+
.zIndex(Double(accessibilityManager.zIndex))
61+
.accessibilityElement(children: .contain)
62+
.accessibilityHidden(isHidden(accessibilityManager.zIndex, element))
63+
.onAppear { accessibilityManager.setIndex(element) }
64+
}
3065
}
3166

3267
struct FlowDestinationModifier<D: Hashable>: ViewModifier {
3368
@State var dataType: D.Type
3469
@State var destination: AnyDestination
3570
@EnvironmentObject var destinationLookup: DestinationLookup
36-
@EnvironmentObject var flowDepth: FlowDepth
71+
@EnvironmentObject var accessibilityManager: AccessibilityManager
72+
@Environment(\.flowDismiss) var flowDismiss
3773

3874
func body(content: Content) -> some View {
3975
content
40-
.zIndex(flowDepth.zIndex)
76+
.zIndex(accessibilityManager.isVoiceOverRunning ? accessibilityManager.zIndex : accessibilityManager.behindSkrim)
4177
// swiftlint:disable:next force_unwrapping
4278
.onAppear { destinationLookup.table.merge([_mangledTypeName(dataType)!: destination], uniquingKeysWith: { _, rhs in rhs }) }
4379
}
@@ -91,14 +127,12 @@ public extension View {
91127
guard let param = AnyDestination.cast(data: param, to: type) else {
92128
fatalError()
93129
}
94-
95130
return AnyView (
96131
destination(param)
97132
.accessibilityElement(children: .contain)
98133
.accessibilityRespondsToUserInteraction(true)
99134
)
100135
})
101-
102136
return modifier(FlowDestinationModifier(dataType: type, destination: destination))
103137
}
104138
}
@@ -186,7 +220,7 @@ public struct FlowStack<Root: View, Overlay: View>: View {
186220
private var usesInternalPath: Bool = false
187221

188222
@State private var destinationLookup: DestinationLookup = .init()
189-
@StateObject var flowDepth: FlowDepth = .init()
223+
@StateObject var accessibilityManager: AccessibilityManager = .init()
190224

191225
/// Creates a flow stack that manages its own navigation state.
192226
/// - Parameters:
@@ -231,14 +265,14 @@ public struct FlowStack<Root: View, Overlay: View>: View {
231265
private func skrim(for element: FlowElement) -> some View {
232266
if element == pathToUse.wrappedValue.elements.last, element.context?.shouldShowSkrim == true {
233267
Rectangle()
234-
.foregroundColor(Color.black.opacity(0.7))
235-
.transition(.opacity)
236-
.ignoresSafeArea()
237-
.zIndex(flowDepth.zIndex - 0.1)
238-
.id(element.hashValue)
239-
.onTapGesture {
240-
flowDismissAction()
241-
}
268+
.foregroundColor(Color.black.opacity(0.7))
269+
.transition(.opacity)
270+
.ignoresSafeArea()
271+
.zIndex(accessibilityManager.calcSkrim())
272+
.id(element.hashValue)
273+
.onTapGesture {
274+
flowDismissAction()
275+
}
242276
}
243277
}
244278

@@ -250,9 +284,9 @@ public struct FlowStack<Root: View, Overlay: View>: View {
250284
FlowDismissAction(
251285
onDismiss: {
252286
withTransaction(transaction) {
287+
accessibilityManager.decrementIndex()
253288
pathToUse.wrappedValue.removeLast()
254289
}
255-
flowDepth.zIndex -= 1
256290
})
257291
}
258292

@@ -267,35 +301,32 @@ public struct FlowStack<Root: View, Overlay: View>: View {
267301
root()
268302
.contentShape(Rectangle())
269303
.accessibilityElement(children: .contain)
270-
.accessibilityHidden(flowDepth.zIndex != 0)
271-
304+
.accessibilityHidden(accessibilityManager.zIndex != 0)
305+
.environment(\.flowDepth, 0)
272306

273307
ForEach(pathToUse.wrappedValue.elements, id: \.self) { element in
274308
if let destination = destination(for: element.value) {
275309

276310
skrim(for: element)
311+
277312
destination.content(element.value)
278313
.contentShape(Rectangle())
279314
.frame(maxWidth: .infinity, maxHeight: .infinity)
280315
.id(element.hashValue)
281316
.transition(.flowTransition(with: element.context ?? .init()))
282-
.zIndex(flowDepth.zIndex)
283-
.accessibilityElement(children: .contain)
284-
.accessibilityHidden(flowDepth.zIndex != Double(element.index + 1))
285-
.onAppear { flowDepth.zIndex = Double(element.index + 1) }
317+
.modifier(AccessibilityModifier(element: element.index))
286318
}
287319
}
288320
}
289-
.zIndex(flowDepth.zIndex)
290321
.accessibilityElement(children: .contain)
291322
.overlay(alignment: overlayAlignment) {
292323
overlay()
293-
.zIndex(flowDepth.zIndex - 1.0)
324+
.environment(\.flowDepth, -1)
294325
}
295326
.environment(\.flowPath, pathToUse)
296327
.environment(\.flowTransaction, transaction)
297328
.environmentObject(destinationLookup)
298-
.environmentObject(flowDepth)
329+
.environmentObject(accessibilityManager)
299330
.environment(\.flowDismiss, flowDismissAction)
300331
}
301332
}

0 commit comments

Comments
 (0)