// // AccessibilityHelpers.swift // Feels // // 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 } } } /// View modifier that respects reduce motion preference 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 { /// Applies animation only when reduce motion is disabled func accessibleAnimation(_ animation: Animation? = .default, reduced: Animation? = nil) -> some View { modifier(ReduceMotionModifier(animation: animation, reducedAnimation: reduced)) } /// 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 formatter = DateFormatter() formatter.dateStyle = .medium return self .accessibilityLabel("\(mood.strValue) on \(formatter.string(from: date))") .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) } } 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) } }