feat: add WCAG AA accessibility app-wide, fix CloudKit container config, remove debug logs
- Add VoiceOver labels, hints, and element grouping across all 60+ views - Add Reduce Motion support (Theme.Animation.prefersReducedMotion) to all animations - Replace fixed font sizes with semantic Dynamic Type styles - Hide decorative elements from VoiceOver with .accessibilityHidden(true) - Add .minimumHitTarget() modifier ensuring 44pt touch targets - Add AccessibilityAnnouncer utility for VoiceOver announcements - Improve color contrast values in Theme.swift for WCAG AA compliance - Extract CloudKitContainerConfig for explicit container identity - Remove PostHog debug console log from AnalyticsManager Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -143,9 +143,6 @@ final class AnalyticsManager {
|
||||
var props: [String: Any] = ["screen_name": screenName]
|
||||
if let properties { props.merge(properties) { _, new in new } }
|
||||
|
||||
#if DEBUG
|
||||
print("[Analytics] screen_viewed: \(screenName)")
|
||||
#endif
|
||||
PostHogSDK.shared.capture("screen_viewed", properties: props)
|
||||
}
|
||||
|
||||
|
||||
14
SportsTime/Core/Services/CloudKitContainerConfig.swift
Normal file
14
SportsTime/Core/Services/CloudKitContainerConfig.swift
Normal file
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// CloudKitContainerConfig.swift
|
||||
// SportsTime
|
||||
//
|
||||
|
||||
import CloudKit
|
||||
|
||||
enum CloudKitContainerConfig {
|
||||
nonisolated static let identifier = "iCloud.com.88oakapps.SportsTime"
|
||||
|
||||
nonisolated static func makeContainer() -> CKContainer {
|
||||
CKContainer(identifier: identifier)
|
||||
}
|
||||
}
|
||||
@@ -109,8 +109,7 @@ actor CloudKitService {
|
||||
private let deltaOverlapSeconds: TimeInterval = 120
|
||||
|
||||
private init() {
|
||||
// Use target entitlements (debug/prod) instead of hardcoding a container ID.
|
||||
self.container = CKContainer.default()
|
||||
self.container = CloudKitContainerConfig.makeContainer()
|
||||
self.publicDatabase = container.publicCloudDatabase
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import CloudKit
|
||||
actor ItineraryItemService {
|
||||
static let shared = ItineraryItemService()
|
||||
|
||||
private let container = CKContainer.default()
|
||||
private let container = CloudKitContainerConfig.makeContainer()
|
||||
private var database: CKDatabase { container.privateCloudDatabase }
|
||||
|
||||
private let recordType = "ItineraryItem"
|
||||
|
||||
@@ -108,8 +108,9 @@ struct LocationPermissionView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "location.circle.fill")
|
||||
.font(.system(size: 60))
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(.blue)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Text("Enable Location")
|
||||
.font(.title2)
|
||||
|
||||
@@ -52,8 +52,7 @@ actor PollService {
|
||||
private var pollSubscriptionID: CKSubscription.ID?
|
||||
|
||||
private init() {
|
||||
// Respect target entitlements so Debug and production stay isolated.
|
||||
self.container = CKContainer.default()
|
||||
self.container = CloudKitContainerConfig.makeContainer()
|
||||
self.publicDatabase = container.publicCloudDatabase
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ final class VisitPhotoService {
|
||||
|
||||
init(modelContext: ModelContext) {
|
||||
self.modelContext = modelContext
|
||||
self.container = CKContainer.default()
|
||||
self.container = CloudKitContainerConfig.makeContainer()
|
||||
self.privateDatabase = container.privateCloudDatabase
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,9 @@ struct AnimatedSportsBackground: View {
|
||||
AnimatedSportsIcon(index: index, animate: animate)
|
||||
}
|
||||
}
|
||||
.accessibilityHidden(true)
|
||||
.onAppear {
|
||||
guard !Theme.Animation.prefersReducedMotion else { return }
|
||||
withAnimation(.easeInOut(duration: 5.0).repeatForever(autoreverses: true)) {
|
||||
animate = true
|
||||
}
|
||||
@@ -173,6 +175,8 @@ struct AnimatedSportsIcon: View {
|
||||
}
|
||||
|
||||
private func triggerGlow() {
|
||||
guard !Theme.Animation.prefersReducedMotion else { return }
|
||||
|
||||
// Slow fade in
|
||||
withAnimation(.easeIn(duration: 0.8)) {
|
||||
glowOpacity = 1
|
||||
|
||||
@@ -68,7 +68,9 @@ struct AnimatedRouteGraphic: View {
|
||||
)
|
||||
}
|
||||
}
|
||||
.accessibilityHidden(true)
|
||||
.onAppear {
|
||||
guard !Theme.Animation.prefersReducedMotion else { return }
|
||||
withAnimation(.easeInOut(duration: Theme.Animation.routeDrawDuration).repeatForever(autoreverses: false)) {
|
||||
animationProgress = 1
|
||||
}
|
||||
@@ -126,7 +128,9 @@ struct PulsingDot: View {
|
||||
.frame(width: size, height: size)
|
||||
.shadow(color: color.opacity(0.5), radius: 4)
|
||||
}
|
||||
.accessibilityHidden(true)
|
||||
.onAppear {
|
||||
guard !Theme.Animation.prefersReducedMotion else { return }
|
||||
withAnimation(.easeOut(duration: 1.5).repeatForever(autoreverses: false)) {
|
||||
isPulsing = true
|
||||
}
|
||||
@@ -171,6 +175,7 @@ struct RoutePreviewStrip: View {
|
||||
}
|
||||
.padding(.horizontal, Theme.Spacing.md)
|
||||
}
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
|
||||
private func abbreviateCity(_ city: String) -> String {
|
||||
@@ -196,6 +201,7 @@ struct StatPill: View {
|
||||
Text(value)
|
||||
.font(.footnote)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
@@ -219,6 +225,7 @@ struct EmptyStateView: View {
|
||||
Image(systemName: icon)
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(Theme.warmOrange.opacity(0.7))
|
||||
.accessibilityHidden(true)
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text(title)
|
||||
|
||||
@@ -47,7 +47,9 @@ struct PlaceholderRectangle: View {
|
||||
.fill(placeholderColor)
|
||||
.frame(width: width, height: height)
|
||||
.opacity(isAnimating ? LoadingPlaceholder.maxOpacity : LoadingPlaceholder.minOpacity)
|
||||
.accessibilityHidden(true)
|
||||
.onAppear {
|
||||
guard !Theme.Animation.prefersReducedMotion else { return }
|
||||
withAnimation(.easeInOut(duration: LoadingPlaceholder.animationDuration).repeatForever(autoreverses: true)) {
|
||||
isAnimating = true
|
||||
}
|
||||
@@ -72,7 +74,9 @@ struct PlaceholderCircle: View {
|
||||
.fill(placeholderColor)
|
||||
.frame(width: diameter, height: diameter)
|
||||
.opacity(isAnimating ? LoadingPlaceholder.maxOpacity : LoadingPlaceholder.minOpacity)
|
||||
.accessibilityHidden(true)
|
||||
.onAppear {
|
||||
guard !Theme.Animation.prefersReducedMotion else { return }
|
||||
withAnimation(.easeInOut(duration: LoadingPlaceholder.animationDuration).repeatForever(autoreverses: true)) {
|
||||
isAnimating = true
|
||||
}
|
||||
@@ -98,7 +102,9 @@ struct PlaceholderCapsule: View {
|
||||
.fill(placeholderColor)
|
||||
.frame(width: width, height: height)
|
||||
.opacity(isAnimating ? LoadingPlaceholder.maxOpacity : LoadingPlaceholder.minOpacity)
|
||||
.accessibilityHidden(true)
|
||||
.onAppear {
|
||||
guard !Theme.Animation.prefersReducedMotion else { return }
|
||||
withAnimation(.easeInOut(duration: LoadingPlaceholder.animationDuration).repeatForever(autoreverses: true)) {
|
||||
isAnimating = true
|
||||
}
|
||||
@@ -145,7 +151,10 @@ struct PlaceholderCard: View {
|
||||
RoundedRectangle(cornerRadius: Theme.CornerRadius.large)
|
||||
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
|
||||
}
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel("Loading content")
|
||||
.onAppear {
|
||||
guard !Theme.Animation.prefersReducedMotion else { return }
|
||||
withAnimation(.easeInOut(duration: LoadingPlaceholder.animationDuration).repeatForever(autoreverses: true)) {
|
||||
isAnimating = true
|
||||
}
|
||||
@@ -185,6 +194,7 @@ struct PlaceholderListRow: View {
|
||||
}
|
||||
.padding(Theme.Spacing.md)
|
||||
.onAppear {
|
||||
guard !Theme.Animation.prefersReducedMotion else { return }
|
||||
withAnimation(.easeInOut(duration: LoadingPlaceholder.animationDuration).repeatForever(autoreverses: true)) {
|
||||
isAnimating = true
|
||||
}
|
||||
|
||||
@@ -25,10 +25,11 @@ struct LoadingSheet: View {
|
||||
// Dimmed background
|
||||
Color.black.opacity(Self.backgroundOpacity)
|
||||
.ignoresSafeArea()
|
||||
.accessibilityHidden(true)
|
||||
|
||||
// Centered card
|
||||
VStack(spacing: Theme.Spacing.lg) {
|
||||
LoadingSpinner(size: .large)
|
||||
LoadingSpinner(size: .large, label: label)
|
||||
|
||||
VStack(spacing: Theme.Spacing.xs) {
|
||||
Text(label)
|
||||
|
||||
@@ -56,6 +56,7 @@ struct LoadingSpinner: View {
|
||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||
}
|
||||
}
|
||||
.accessibilityLabel(label ?? "Loading")
|
||||
}
|
||||
|
||||
private var spinnerView: some View {
|
||||
@@ -63,15 +64,18 @@ struct LoadingSpinner: View {
|
||||
// Background track - subtle gray like Apple's native spinner
|
||||
Circle()
|
||||
.stroke(Color.secondary.opacity(0.2), lineWidth: size.strokeWidth)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
// Rotating arc (270 degrees) - gray like Apple's ProgressView
|
||||
Circle()
|
||||
.trim(from: 0, to: 0.75)
|
||||
.stroke(Color.secondary, style: StrokeStyle(lineWidth: size.strokeWidth, lineCap: .round))
|
||||
.rotationEffect(.degrees(rotation))
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.frame(width: size.diameter, height: size.diameter)
|
||||
.onAppear {
|
||||
guard !Theme.Animation.prefersReducedMotion else { return }
|
||||
withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
|
||||
rotation = 360
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ struct SportActionButton: View {
|
||||
Image(systemName: sport.iconName)
|
||||
.font(.title3)
|
||||
.foregroundStyle(sport.themeColor)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
|
||||
Text(sport.rawValue)
|
||||
@@ -100,13 +101,15 @@ struct SportActionButton: View {
|
||||
.scaleEffect(isPressed ? 0.9 : 1.0)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(sport.rawValue)
|
||||
.accessibilityAddTraits(.isButton)
|
||||
.simultaneousGesture(
|
||||
DragGesture(minimumDistance: 0)
|
||||
.onChanged { _ in
|
||||
withAnimation(Theme.Animation.spring) { isPressed = true }
|
||||
Theme.Animation.withMotion(Theme.Animation.spring) { isPressed = true }
|
||||
}
|
||||
.onEnded { _ in
|
||||
withAnimation(Theme.Animation.spring) { isPressed = false }
|
||||
Theme.Animation.withMotion(Theme.Animation.spring) { isPressed = false }
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -140,23 +143,27 @@ struct SportToggleButton: View {
|
||||
Image(systemName: sport.iconName)
|
||||
.font(.title3)
|
||||
.foregroundStyle(isSelected ? .white : sport.themeColor)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
|
||||
Text(sport.rawValue)
|
||||
.font(.system(size: 10, weight: isSelected ? .semibold : .medium))
|
||||
.font(.caption2.weight(isSelected ? .semibold : .medium))
|
||||
.foregroundStyle(isSelected ? Theme.textPrimary(colorScheme) : Theme.textSecondary(colorScheme))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.scaleEffect(isPressed ? 0.9 : 1.0)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(sport.rawValue)
|
||||
.accessibilityAddTraits(.isToggle)
|
||||
.accessibilityValue(isSelected ? "Selected" : "Not selected")
|
||||
.simultaneousGesture(
|
||||
DragGesture(minimumDistance: 0)
|
||||
.onChanged { _ in
|
||||
withAnimation(Theme.Animation.spring) { isPressed = true }
|
||||
Theme.Animation.withMotion(Theme.Animation.spring) { isPressed = true }
|
||||
}
|
||||
.onEnded { _ in
|
||||
withAnimation(Theme.Animation.spring) { isPressed = false }
|
||||
Theme.Animation.withMotion(Theme.Animation.spring) { isPressed = false }
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -190,6 +197,7 @@ struct SportProgressButton: View {
|
||||
Image(systemName: sport.iconName)
|
||||
.font(.title3)
|
||||
.foregroundStyle(isSelected ? sport.themeColor : Theme.textMuted(colorScheme))
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
|
||||
Text(sport.rawValue)
|
||||
@@ -200,13 +208,17 @@ struct SportProgressButton: View {
|
||||
.scaleEffect(isPressed ? 0.9 : 1.0)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("\(sport.rawValue), \(Int(progress * 100)) percent visited")
|
||||
.accessibilityValue(isSelected ? "Selected" : "Not selected")
|
||||
.accessibilityAddTraits(.isButton)
|
||||
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
||||
.simultaneousGesture(
|
||||
DragGesture(minimumDistance: 0)
|
||||
.onChanged { _ in
|
||||
withAnimation(Theme.Animation.spring) { isPressed = true }
|
||||
Theme.Animation.withMotion(Theme.Animation.spring) { isPressed = true }
|
||||
}
|
||||
.onEnded { _ in
|
||||
withAnimation(Theme.Animation.spring) { isPressed = false }
|
||||
Theme.Animation.withMotion(Theme.Animation.spring) { isPressed = false }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -259,7 +259,15 @@ enum Theme {
|
||||
}
|
||||
|
||||
static var darkSurfaceGlow: Color {
|
||||
warmOrange.opacity(0.15)
|
||||
switch current {
|
||||
case .teal: return Color(hex: "92AEAB")
|
||||
case .orbit: return Color(hex: "708BAA")
|
||||
case .retro: return Color(hex: "87A0BA")
|
||||
case .clutch: return Color(hex: "6D8399")
|
||||
case .monochrome: return Color(hex: "707070")
|
||||
case .sunset: return Color(hex: "84719A")
|
||||
case .midnight: return Color(hex: "7F95B0")
|
||||
}
|
||||
}
|
||||
|
||||
static var darkTextPrimary: Color {
|
||||
@@ -288,13 +296,13 @@ enum Theme {
|
||||
|
||||
static var darkTextMuted: Color {
|
||||
switch current {
|
||||
case .teal: return Color(hex: "7FADA8")
|
||||
case .orbit: return Color(hex: "8090A0")
|
||||
case .retro: return Color(hex: "7898B8")
|
||||
case .clutch: return Color(hex: "8898A8")
|
||||
case .monochrome: return Color(hex: "707070")
|
||||
case .sunset: return Color(hex: "9D8AA8")
|
||||
case .midnight: return Color(hex: "64748B")
|
||||
case .teal: return Color(hex: "B4CFCC")
|
||||
case .orbit: return Color(hex: "A0B1C3")
|
||||
case .retro: return Color(hex: "A4BBCF")
|
||||
case .clutch: return Color(hex: "A8B7C6")
|
||||
case .monochrome: return Color(hex: "B0B0B0")
|
||||
case .sunset: return Color(hex: "C9B5D3")
|
||||
case .midnight: return Color(hex: "A3B3C8")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,7 +337,15 @@ enum Theme {
|
||||
static var lightCardBackgroundElevated: Color { lightBackground1 }
|
||||
|
||||
static var lightSurfaceBorder: Color {
|
||||
warmOrange.opacity(0.3)
|
||||
switch current {
|
||||
case .teal: return Color(hex: "568E88")
|
||||
case .orbit: return Color(hex: "5F86AE")
|
||||
case .retro: return Color(hex: "5D90B8")
|
||||
case .clutch: return Color(hex: "6E8194")
|
||||
case .monochrome: return Color(hex: "7A7A7A")
|
||||
case .sunset: return Color(hex: "B98474")
|
||||
case .midnight: return Color(hex: "7789A3")
|
||||
}
|
||||
}
|
||||
|
||||
static var lightTextPrimary: Color {
|
||||
@@ -358,12 +374,12 @@ enum Theme {
|
||||
|
||||
static var lightTextMuted: Color {
|
||||
switch current {
|
||||
case .teal: return Color(hex: "5A9A94")
|
||||
case .orbit: return Color(hex: "5A7A9A")
|
||||
case .retro: return Color(hex: "5A8AAA")
|
||||
case .clutch: return Color(hex: "6A7A8A")
|
||||
case .teal: return Color(hex: "497F79")
|
||||
case .orbit: return Color(hex: "537596")
|
||||
case .retro: return Color(hex: "4A7A99")
|
||||
case .clutch: return Color(hex: "5A6B7E")
|
||||
case .monochrome: return Color(hex: "707070")
|
||||
case .sunset: return Color(hex: "9A6A5A")
|
||||
case .sunset: return Color(hex: "8A5A4A")
|
||||
case .midnight: return Color(hex: "64748B")
|
||||
}
|
||||
}
|
||||
@@ -442,6 +458,20 @@ enum Theme {
|
||||
static var gentleSpring: SwiftUI.Animation {
|
||||
.spring(response: 0.5, dampingFraction: 0.8)
|
||||
}
|
||||
|
||||
/// Whether the system Reduce Motion preference is enabled.
|
||||
static var prefersReducedMotion: Bool {
|
||||
UIAccessibility.isReduceMotionEnabled
|
||||
}
|
||||
|
||||
/// Performs a state change with animation, or instantly if Reduce Motion is enabled.
|
||||
static func withMotion(_ animation: SwiftUI.Animation = spring, _ body: () -> Void) {
|
||||
if prefersReducedMotion {
|
||||
body()
|
||||
} else {
|
||||
withAnimation(animation) { body() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,10 @@ struct PressableButtonStyle: ButtonStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.scaleEffect(configuration.isPressed ? scale : 1.0)
|
||||
.animation(Theme.Animation.spring, value: configuration.isPressed)
|
||||
.animation(
|
||||
Theme.Animation.prefersReducedMotion ? nil : Theme.Animation.spring,
|
||||
value: configuration.isPressed
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +74,36 @@ extension View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Minimum Hit Target Modifier
|
||||
|
||||
private struct MinimumHitTargetModifier: ViewModifier {
|
||||
let size: CGFloat
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.frame(minWidth: size, minHeight: size, alignment: .center)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
/// Ensures interactive elements meet the recommended 44x44pt touch area.
|
||||
func minimumHitTarget(_ size: CGFloat = 44) -> some View {
|
||||
modifier(MinimumHitTargetModifier(size: size))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Accessibility Announcements
|
||||
|
||||
enum AccessibilityAnnouncer {
|
||||
static func announce(_ message: String) {
|
||||
guard !message.isEmpty else { return }
|
||||
DispatchQueue.main.async {
|
||||
UIAccessibility.post(notification: .announcement, argument: message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Shimmer Effect Modifier
|
||||
|
||||
struct ShimmerEffect: ViewModifier {
|
||||
@@ -79,22 +112,25 @@ struct ShimmerEffect: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.overlay {
|
||||
GeometryReader { geo in
|
||||
LinearGradient(
|
||||
colors: [
|
||||
.clear,
|
||||
Color.white.opacity(0.3),
|
||||
.clear
|
||||
],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
.frame(width: geo.size.width * 2)
|
||||
.offset(x: -geo.size.width + (geo.size.width * 2 * phase))
|
||||
if !Theme.Animation.prefersReducedMotion {
|
||||
GeometryReader { geo in
|
||||
LinearGradient(
|
||||
colors: [
|
||||
.clear,
|
||||
Color.white.opacity(0.3),
|
||||
.clear
|
||||
],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
.frame(width: geo.size.width * 2)
|
||||
.offset(x: -geo.size.width + (geo.size.width * 2 * phase))
|
||||
}
|
||||
.mask(content)
|
||||
}
|
||||
.mask(content)
|
||||
}
|
||||
.onAppear {
|
||||
guard !Theme.Animation.prefersReducedMotion else { return }
|
||||
withAnimation(.linear(duration: 1.5).repeatForever(autoreverses: false)) {
|
||||
phase = 1
|
||||
}
|
||||
@@ -120,8 +156,12 @@ struct StaggeredAnimation: ViewModifier {
|
||||
.opacity(appeared ? 1 : 0)
|
||||
.offset(y: appeared ? 0 : 20)
|
||||
.onAppear {
|
||||
withAnimation(Theme.Animation.spring.delay(Double(index) * delay)) {
|
||||
if Theme.Animation.prefersReducedMotion {
|
||||
appeared = true
|
||||
} else {
|
||||
withAnimation(Theme.Animation.spring.delay(Double(index) * delay)) {
|
||||
appeared = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,7 +224,7 @@ struct ThemedBackground: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.background {
|
||||
if DesignStyleManager.shared.animationsEnabled {
|
||||
if DesignStyleManager.shared.animationsEnabled && !Theme.Animation.prefersReducedMotion {
|
||||
AnimatedSportsBackground()
|
||||
.ignoresSafeArea()
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user