Files
Sportstime/SportsTime/Core/Theme/ViewModifiers.swift
Trey t e1d84ac769 feat(ui): apply animated background to all screens via themedBackground modifier
Extract AnimatedSportsBackground components to shared file and update
ThemedBackground modifier to conditionally show animations when enabled
in settings. All views using .themedBackground() now get animated background.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 18:06:58 -06:00

231 lines
6.1 KiB
Swift

//
// ViewModifiers.swift
// SportsTime
//
// Reusable view modifiers for consistent styling across the app.
//
import SwiftUI
// MARK: - Card Style Modifier
struct CardStyle: ViewModifier {
@Environment(\.colorScheme) private var colorScheme
var cornerRadius: CGFloat = Theme.CornerRadius.large
var padding: CGFloat = Theme.Spacing.lg
func body(content: Content) -> some View {
content
.padding(padding)
.background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: cornerRadius))
.overlay {
RoundedRectangle(cornerRadius: cornerRadius)
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
}
.shadow(color: Theme.cardShadow(colorScheme), radius: 10, y: 5)
}
}
extension View {
func cardStyle(cornerRadius: CGFloat = Theme.CornerRadius.large, padding: CGFloat = Theme.Spacing.lg) -> some View {
modifier(CardStyle(cornerRadius: cornerRadius, padding: padding))
}
}
// MARK: - Glow Effect Modifier
struct GlowEffect: ViewModifier {
var color: Color = Theme.warmOrange
var radius: CGFloat = 8
func body(content: Content) -> some View {
content
.shadow(color: color.opacity(0.5), radius: radius / 2, y: 0)
.shadow(color: color.opacity(0.3), radius: radius, y: 0)
}
}
extension View {
func glowEffect(color: Color = Theme.warmOrange, radius: CGFloat = 8) -> some View {
modifier(GlowEffect(color: color, radius: radius))
}
}
// MARK: - Pressable Button Style
struct PressableButtonStyle: ButtonStyle {
var scale: CGFloat = 0.96
func makeBody(configuration: Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? scale : 1.0)
.animation(Theme.Animation.spring, value: configuration.isPressed)
}
}
extension View {
func pressableStyle(scale: CGFloat = 0.96) -> some View {
buttonStyle(PressableButtonStyle(scale: scale))
}
}
// MARK: - Shimmer Effect Modifier
struct ShimmerEffect: ViewModifier {
@State private var phase: CGFloat = 0
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))
}
.mask(content)
}
.onAppear {
withAnimation(.linear(duration: 1.5).repeatForever(autoreverses: false)) {
phase = 1
}
}
}
}
extension View {
func shimmer() -> some View {
modifier(ShimmerEffect())
}
}
// MARK: - Staggered Animation Modifier
struct StaggeredAnimation: ViewModifier {
var index: Int
var delay: Double = Theme.Animation.staggerDelay
@State private var appeared = false
func body(content: Content) -> some View {
content
.opacity(appeared ? 1 : 0)
.offset(y: appeared ? 0 : 20)
.onAppear {
withAnimation(Theme.Animation.spring.delay(Double(index) * delay)) {
appeared = true
}
}
}
}
extension View {
func staggeredAnimation(index: Int, delay: Double = Theme.Animation.staggerDelay) -> some View {
modifier(StaggeredAnimation(index: index, delay: delay))
}
}
// MARK: - Badge Style Modifier
struct BadgeStyle: ViewModifier {
@Environment(\.colorScheme) private var colorScheme
var color: Color = Theme.warmOrange
var filled: Bool = true
func body(content: Content) -> some View {
content
.font(.caption)
.padding(.horizontal, 10)
.padding(.vertical, 5)
.background(filled ? color : color.opacity(0.2))
.foregroundStyle(filled ? .white : color)
.clipShape(Capsule())
}
}
extension View {
func badgeStyle(color: Color = Theme.warmOrange, filled: Bool = true) -> some View {
modifier(BadgeStyle(color: color, filled: filled))
}
}
// MARK: - Section Header Style
struct SectionHeaderStyle: ViewModifier {
@Environment(\.colorScheme) private var colorScheme
func body(content: Content) -> some View {
content
.font(.title2)
.foregroundStyle(Theme.textPrimary(colorScheme))
}
}
extension View {
func sectionHeaderStyle() -> some View {
modifier(SectionHeaderStyle())
}
}
// MARK: - Themed Background Modifier
struct ThemedBackground: ViewModifier {
@Environment(\.colorScheme) private var colorScheme
func body(content: Content) -> some View {
content
.background {
if DesignStyleManager.shared.animationsEnabled {
AnimatedSportsBackground()
.ignoresSafeArea()
} else {
Theme.backgroundGradient(colorScheme)
.ignoresSafeArea()
}
}
}
}
extension View {
func themedBackground() -> some View {
modifier(ThemedBackground())
}
}
// MARK: - Sport Color Bar
struct SportColorBar: View {
let sport: Sport
var body: some View {
RoundedRectangle(cornerRadius: 2)
.fill(sport.themeColor)
.frame(width: 4)
}
}
// MARK: - Sport Extension for Theme Colors
extension Sport {
var themeColor: Color {
switch self {
case .mlb: return Theme.mlbRed
case .nba: return Theme.nbaOrange
case .nhl: return Theme.nhlBlue
case .nfl: return Theme.nflBrown
case .mls: return Theme.mlsGreen
case .wnba: return Theme.wnbaPurple
case .nwsl: return Theme.nwslTeal
}
}
}