// ── Accessibility Helpers ───────────────────────────────────── // VoiceOver labels, Dynamic Type support, Reduce Motion, High Contrast import SwiftUI // MARK: - Timer Accessibility Descriptions enum TimerAccessibility { /// Full VoiceOver description for a timer static func description(for timer: CMTimer, now: Date) -> String { let remaining = getRemainingSeconds(timer, now: now) let timeStr = formatDuration(remaining) let urgencyLabel = getUrgencyConfig(timer.urgency).label switch timer.state { case .active: return "\(timer.label), \(urgencyLabel) urgency, \(timeStr) remaining" case .warning: let firedCount = timer.warnings.filter(\.fired).count let total = timer.warnings.count return "\(timer.label), warning, \(firedCount) of \(total) cascade warnings fired, \(timeStr) remaining" case .firing: return "\(timer.label), firing now, \(urgencyLabel) urgency, requires attention" case .paused: return "\(timer.label), paused, \(timeStr) remaining when resumed" case .snoozed: let snoozeStr = timer.snoozedUntil.map { formatRelativeTime($0, now: now) } ?? "soon" return "\(timer.label), snoozed, will resume \(snoozeStr)" case .completed: return "\(timer.label), completed" case .dismissed: return "\(timer.label), dismissed" case .idle: return "\(timer.label), idle" } } /// Hint for VoiceOver actions static func hint(for timer: CMTimer) -> String { switch timer.state { case .active, .warning: return "Double tap to view details. Swipe up for pause and snooze options." case .firing: return "Double tap to dismiss. Swipe up for snooze options." case .paused: return "Double tap to resume." default: return "Double tap to view details." } } /// State announcement for VoiceOver static func stateAnnouncement(_ state: CMTimerState) -> String { switch state { case .active: return "Timer active" case .warning: return "Warning, cascade alert" case .firing: return "Timer firing, requires attention" case .paused: return "Timer paused" case .snoozed: return "Timer snoozed" case .completed: return "Timer completed" case .dismissed: return "Timer dismissed" case .idle: return "Timer idle" } } } // MARK: - Reduce Motion struct ReduceMotionModifier: ViewModifier { @Environment(\.accessibilityReduceMotion) var reduceMotion let animation: Animation let reducedAnimation: Animation func body(content: Content) -> some View { content.animation(reduceMotion ? reducedAnimation : animation, value: UUID()) } } extension View { /// Apply animation that respects Reduce Motion setting func motionSafe(_ animation: Animation, reduced: Animation = .linear(duration: 0)) -> some View { modifier(ReduceMotionModifier(animation: animation, reducedAnimation: reduced)) } } // MARK: - High Contrast Support struct HighContrastModifier: ViewModifier { @Environment(\.colorSchemeContrast) var contrast let normalOpacity: Double let highContrastOpacity: Double func body(content: Content) -> some View { content.opacity(contrast == .increased ? highContrastOpacity : normalOpacity) } } extension View { /// Adjust opacity based on High Contrast mode func contrastAdaptive(normal: Double = 1.0, increased: Double = 1.0) -> some View { modifier(HighContrastModifier(normalOpacity: normal, highContrastOpacity: increased)) } } // MARK: - Dynamic Type Scaling struct ScaledFontModifier: ViewModifier { @Environment(\.dynamicTypeSize) var dynamicTypeSize let baseSize: CGFloat let maxSize: CGFloat func body(content: Content) -> some View { content.font(.system(size: scaledSize)) } private var scaledSize: CGFloat { let scale: CGFloat switch dynamicTypeSize { case .xSmall: scale = 0.8 case .small: scale = 0.9 case .medium: scale = 1.0 case .large: scale = 1.0 case .xLarge: scale = 1.1 case .xxLarge: scale = 1.2 case .xxxLarge: scale = 1.3 case .accessibility1: scale = 1.5 case .accessibility2: scale = 1.7 case .accessibility3: scale = 1.9 case .accessibility4: scale = 2.1 case .accessibility5: scale = 2.3 @unknown default: scale = 1.0 } return min(baseSize * scale, maxSize) } } // MARK: - Accessibility View Extension extension View { /// Add comprehensive timer accessibility func timerAccessible(_ timer: CMTimer, now: Date) -> some View { self .accessibilityElement(children: .combine) .accessibilityLabel(TimerAccessibility.description(for: timer, now: now)) .accessibilityHint(TimerAccessibility.hint(for: timer)) .accessibilityAddTraits(timer.state == .firing ? .updatesFrequently : .isButton) } }