Skip to content

Commit b2a9584

Browse files
committed
a11y: honor Reduce Motion for continuous animations
Gate continuously-running motion animations on @Environment(\.accessibilityReduceMotion) so they stop or render statically when Reduce Motion is enabled: - CircularProgressView: indeterminate spinner stops rotating (static arc). - RadarSweepView: TimelineView sweep freezes (paused). - PulsingCircle map halo: renders a static circle instead of breathing. - Looping .variableColor symbol effects (Connect, WifiProvisioning, PositionPopover, AnimatedNodePin sensor pin) become isActive: !reduceMotion. Opacity cross-fades (e.g. LEDIndicator packet flash) are intentionally left alone, per Apple guidance that cross-fades are the recommended substitute for motion.
1 parent e531f15 commit b2a9584

3 files changed

Lines changed: 20 additions & 3 deletions

File tree

Meshtastic/Views/Nodes/Helpers/Map/MapContent/AnimatedNodePin.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,25 @@ struct AnimatedNodePin: View, Equatable {
7373
/// recycles/reconfigures the annotation view (which it does constantly on pan/zoom/declutter). The
7474
/// per-node `calculatedDelay` phase-shifts each node so they don't all pulse in unison.
7575
struct PulsingCircle: View {
76+
@Environment(\.accessibilityReduceMotion) private var reduceMotion
7677
let nodeColor: UIColor
7778
let calculatedDelay: Double
7879

7980
/// Seconds for one full breath (out and back), matching the previous 1.2s-each-way feel.
8081
private let period: Double = 2.4
8182

8283
var body: some View {
84+
if reduceMotion {
85+
// Static halo (no breathing) when Reduce Motion is enabled.
86+
Circle()
87+
.fill(Color(nodeColor.lighter()).opacity(0.3))
88+
.frame(width: 50, height: 50)
89+
} else {
90+
animatedHalo
91+
}
92+
}
93+
94+
private var animatedHalo: some View {
8395
// Cap the clock to ~20 fps: a slow 2.4s breath looks identical, but with many ONLINE pins each
8496
// hosted in an MKAnnotationView, an uncapped `.animation` clock re-renders every pin every
8597
// display frame and never lets the map go idle (janky at rest on Mac Catalyst).

Meshtastic/Views/Settings/Discovery/RadarSweepView.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import SwiftUI
1111
struct RadarSweepView: View {
1212
var isActive: Bool
1313

14+
@Environment(\.accessibilityReduceMotion) private var reduceMotion
1415
@State private var startDate = Date()
1516

1617
/// Seconds for one full 360° rotation
@@ -24,7 +25,8 @@ struct RadarSweepView: View {
2425

2526
var body: some View {
2627
if isActive {
27-
TimelineView(.animation(minimumInterval: 1.0 / 30.0)) { timeline in
28+
// Freeze the sweep on a single static frame when Reduce Motion is enabled.
29+
TimelineView(.animation(minimumInterval: 1.0 / 30.0, paused: reduceMotion)) { timeline in
2830
let elapsed = timeline.date.timeIntervalSince(startDate)
2931
let rotation = (elapsed / rotationDuration).truncatingRemainder(dividingBy: 1.0) * 360.0
3032

Meshtastic/Views/Settings/Firmware/Helpers/CircularProgressView.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ struct CircularProgressView: View {
2323
var subtitleText: String?
2424

2525
@State private var rotation: Double = 0
26+
@Environment(\.accessibilityReduceMotion) private var reduceMotion
2627

2728
private var isComplete: Bool {
2829
// Complete only if 100%, not indeterminate, and NOT an error
@@ -71,11 +72,13 @@ struct CircularProgressView: View {
7172
// Monitor both Indeterminate and Error to stop/start animations
7273
.onChange(of: isIndeterminate) { _, _ in updateAnimationStatus() }
7374
.onChange(of: isError) { _, _ in updateAnimationStatus() }
75+
.onChange(of: reduceMotion) { _, _ in updateAnimationStatus() }
7476
}
7577

7678
private func updateAnimationStatus() {
77-
// Only spin if Indeterminate AND we are not in an Error state
78-
if isIndeterminate && !isError {
79+
// Only spin if Indeterminate AND we are not in an Error state. Respect Reduce Motion by
80+
// leaving the indeterminate segment static instead of spinning it forever.
81+
if isIndeterminate && !isError && !reduceMotion {
7982
rotation = 0
8083
withAnimation(.linear(duration: 2.0).repeatForever(autoreverses: false)) {
8184
rotation = 360

0 commit comments

Comments
 (0)