// // AccessibilityHelpers.swift // Reflect // // Accessibility utilities for supporting VoiceOver, Dynamic Type, and Reduce Motion. // import SwiftUI // MARK: - Reduce Motion Support /// Environment key for accessing reduce motion preference struct ReduceMotionKey: EnvironmentKey { static let defaultValue: Bool = false } extension EnvironmentValues { var reduceMotion: Bool { get { self[ReduceMotionKey.self] } set { self[ReduceMotionKey.self] = newValue } } } extension View { /// Wraps content in withAnimation respecting reduce motion func withAccessibleAnimation(_ animation: Animation? = .default, value: V, action: @escaping () -> Void) -> some View { self.onChange(of: value) { _, _ in if UIAccessibility.isReduceMotionEnabled { action() } else { withAnimation(animation) { action() } } } } } // MARK: - Accessibility Helpers extension View { /// Adds accessibility label with optional hint func accessibleMoodCell(mood: Mood, date: Date) -> some View { let dateString = DateFormattingCache.shared.string(for: date, format: .dateMedium) return self .accessibilityLabel("\(mood.strValue) on \(dateString)") .accessibilityHint("Double tap to edit mood") } /// Makes a button accessible with custom label func accessibleButton(label: String, hint: String? = nil) -> some View { self .accessibilityLabel(label) .accessibilityHint(hint ?? "") .accessibilityAddTraits(.isButton) } /// Groups related elements for VoiceOver func accessibilityGrouped(label: String) -> some View { self .accessibilityElement(children: .combine) .accessibilityLabel(label) } } // MARK: - Dynamic Type Support extension Font { /// Returns a scalable font that respects Dynamic Type static func scalable(_ style: Font.TextStyle, weight: Font.Weight = .regular) -> Font { Font.system(style, design: .rounded).weight(weight) } } /// Property wrapper for scaled metrics that respect Dynamic Type @propertyWrapper struct ScaledValue: DynamicProperty { @ScaledMetric private var scaledValue: CGFloat var wrappedValue: CGFloat { scaledValue } init(wrappedValue: CGFloat, relativeTo textStyle: Font.TextStyle = .body) { _scaledValue = ScaledMetric(wrappedValue: wrappedValue, relativeTo: textStyle) } } extension View { /// Ensures minimum touch target size for accessibility (44x44 points) func accessibleTouchTarget() -> some View { self.frame(minWidth: 44, minHeight: 44) } } // MARK: - Accessibility Announcements struct AccessibilityAnnouncement { /// Announces a message to VoiceOver users static func announce(_ message: String) { UIAccessibility.post(notification: .announcement, argument: message) } /// Notifies VoiceOver that screen content has changed static func screenChanged() { UIAccessibility.post(notification: .screenChanged, argument: nil) } /// Notifies VoiceOver that layout has changed static func layoutChanged(focusElement: Any? = nil) { UIAccessibility.post(notification: .layoutChanged, argument: focusElement) } } // MARK: - Reduce Motion View Modifier /// View modifier that provides alternative content when Reduce Motion is enabled struct ReduceMotionContent: ViewModifier { @Environment(\.accessibilityReduceMotion) private var reduceMotion let reducedContent: () -> Reduced func body(content: Content) -> some View { if reduceMotion { reducedContent() } else { content } } } extension View { /// Provides alternative content when Reduce Motion is enabled func reduceMotionAlternative(@ViewBuilder _ reduced: @escaping () -> V) -> some View { modifier(ReduceMotionContent(reducedContent: reduced)) } /// Conditionally applies animation based on Reduce Motion setting func animationIfAllowed(_ animation: Animation?, value: V) -> some View { modifier(ConditionalAnimationModifier(animation: animation, value: value)) } } struct ConditionalAnimationModifier: ViewModifier { @Environment(\.accessibilityReduceMotion) private var reduceMotion let animation: Animation? let value: V func body(content: Content) -> some View { if reduceMotion { content } else { content.animation(animation, value: value) } } } // MARK: - VoiceOver Detection extension View { /// Runs an action when VoiceOver is running func onVoiceOverChange(perform action: @escaping (Bool) -> Void) -> some View { self.onReceive(NotificationCenter.default.publisher(for: UIAccessibility.voiceOverStatusDidChangeNotification)) { _ in action(UIAccessibility.isVoiceOverRunning) } .onAppear { action(UIAccessibility.isVoiceOverRunning) } } }