Files
MLBApp/mlbTVOS/Views/Components/DesignSystem.swift
Trey t 588b42ffed Fix tvOS memory crash: cap highlights to 50, replace blurs with gradients
The app was crashing from memory pressure on tvOS. Three causes fixed:

1. Feed was rendering all 418 highlights at once — capped to 50 items.

2. FeaturedGameCard had 3 blur effects (radius 80-120) on large circles
   for team color glow — replaced with a single LinearGradient. Same
   visual effect, fraction of the GPU memory.

3. BroadcastBackground had 3 blurred circles (radius 120-140, 680-900px)
   rendering on every screen — replaced with RadialGradients which are
   composited by the GPU natively without offscreen render passes.

Also fixed iOS build: replaced tvOS-only font refs (tvSectionTitle,
tvBody) with cross-platform equivalents in DashboardView fallback state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 16:44:25 -05:00

181 lines
6.9 KiB
Swift

import SwiftUI
enum DS {
enum Colors {
static let background = Color(red: 0.03, green: 0.05, blue: 0.10)
static let backgroundElevated = Color(red: 0.06, green: 0.10, blue: 0.18)
static let navFill = Color(red: 0.07, green: 0.10, blue: 0.18).opacity(0.94)
static let panelFill = Color(red: 0.08, green: 0.11, blue: 0.18).opacity(0.94)
static let panelFillMuted = Color(red: 0.10, green: 0.14, blue: 0.23).opacity(0.84)
static let panelStroke = Color.white.opacity(0.09)
static let live = Color(red: 0.94, green: 0.25, blue: 0.28)
static let positive = Color(red: 0.24, green: 0.86, blue: 0.63)
static let warning = Color(red: 0.98, green: 0.76, blue: 0.24)
static let interactive = Color(red: 1.00, green: 0.75, blue: 0.20)
static let media = Color(red: 0.35, green: 0.78, blue: 0.95)
static let textPrimary = Color.white.opacity(0.96)
static let textSecondary = Color.white.opacity(0.76)
static let textTertiary = Color.white.opacity(0.50)
static let textQuaternary = Color.white.opacity(0.24)
static let onDarkPrimary = textPrimary
static let onDarkSecondary = textSecondary
static let onDarkTertiary = textTertiary
}
enum Shadows {
static let card = Color.black.opacity(0.30)
static let cardRadius: CGFloat = 28
static let cardY: CGFloat = 14
static let cardLifted = Color.black.opacity(0.44)
static let cardLiftedRadius: CGFloat = 42
static let cardLiftedY: CGFloat = 18
}
enum Fonts {
static let heroScore = Font.system(size: 72, weight: .black, design: .rounded).monospacedDigit()
static let largeScore = Font.system(size: 42, weight: .black, design: .rounded).monospacedDigit()
static let score = Font.system(size: 28, weight: .black, design: .rounded).monospacedDigit()
static let scoreCompact = Font.system(size: 22, weight: .bold, design: .rounded).monospacedDigit()
static let sectionTitle = Font.system(size: 30, weight: .bold, design: .rounded)
static let cardTitle = Font.system(size: 20, weight: .bold, design: .rounded)
static let cardTitleCompact = Font.system(size: 17, weight: .bold, design: .rounded)
static let dataValue = Font.system(size: 18, weight: .bold, design: .rounded).monospacedDigit()
static let dataValueCompact = Font.system(size: 15, weight: .semibold, design: .rounded).monospacedDigit()
static let body = Font.system(size: 15, weight: .medium)
static let bodySmall = Font.system(size: 13, weight: .medium)
static let caption = Font.system(size: 11, weight: .bold, design: .rounded)
#if os(tvOS)
static let tvHeroScore = Font.system(size: 94, weight: .black, design: .rounded).monospacedDigit()
static let tvSectionTitle = Font.system(size: 40, weight: .bold, design: .rounded)
static let tvCardTitle = Font.system(size: 28, weight: .bold, design: .rounded)
static let tvScore = Font.system(size: 36, weight: .black, design: .rounded).monospacedDigit()
static let tvDataValue = Font.system(size: 24, weight: .bold, design: .rounded).monospacedDigit()
static let tvBody = Font.system(size: 22, weight: .medium)
static let tvCaption = Font.system(size: 20, weight: .bold, design: .rounded)
#endif
}
enum Spacing {
#if os(tvOS)
static let panelPadCompact: CGFloat = 18
static let panelPadStandard: CGFloat = 24
static let panelPadFeatured: CGFloat = 32
static let sectionGap: CGFloat = 42
static let cardGap: CGFloat = 20
static let itemGap: CGFloat = 12
static let edgeInset: CGFloat = 50
#else
static let panelPadCompact: CGFloat = 12
static let panelPadStandard: CGFloat = 16
static let panelPadFeatured: CGFloat = 24
static let sectionGap: CGFloat = 28
static let cardGap: CGFloat = 14
static let itemGap: CGFloat = 8
static let edgeInset: CGFloat = 20
#endif
}
enum Radii {
static let compact: CGFloat = 16
static let standard: CGFloat = 24
static let featured: CGFloat = 30
}
}
struct BroadcastBackground: View {
var body: some View {
ZStack {
LinearGradient(
colors: [
Color(red: 0.03, green: 0.05, blue: 0.10),
Color(red: 0.04, green: 0.08, blue: 0.16),
Color(red: 0.02, green: 0.04, blue: 0.09),
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
// Subtle color washes radial gradients instead of blurred circles for performance
RadialGradient(
colors: [Color(red: 0.00, green: 0.46, blue: 0.72).opacity(0.12), .clear],
center: UnitPoint(x: 0.1, y: 0.15),
startRadius: 50,
endRadius: 500
)
RadialGradient(
colors: [DS.Colors.interactive.opacity(0.10), .clear],
center: UnitPoint(x: 0.85, y: 0.15),
startRadius: 50,
endRadius: 450
)
RadialGradient(
colors: [DS.Colors.live.opacity(0.06), .clear],
center: UnitPoint(x: 0.8, y: 0.85),
startRadius: 50,
endRadius: 400
)
BroadcastGridOverlay()
.opacity(0.30)
}
}
}
private struct BroadcastGridOverlay: View {
var body: some View {
GeometryReader { proxy in
let size = proxy.size
Path { path in
let verticalSpacing: CGFloat = 110
let horizontalSpacing: CGFloat = 90
var x: CGFloat = 0
while x <= size.width {
path.move(to: CGPoint(x: x, y: 0))
path.addLine(to: CGPoint(x: x, y: size.height))
x += verticalSpacing
}
var y: CGFloat = 0
while y <= size.height {
path.move(to: CGPoint(x: 0, y: y))
path.addLine(to: CGPoint(x: size.width, y: y))
y += horizontalSpacing
}
}
.stroke(Color.white.opacity(0.05), lineWidth: 1)
}
.allowsHitTesting(false)
}
}
struct DataLabelStyle: ViewModifier {
func body(content: Content) -> some View {
content
#if os(tvOS)
.font(DS.Fonts.tvCaption)
.kerning(1.0)
#else
.font(DS.Fonts.caption)
.kerning(1.5)
#endif
.foregroundStyle(DS.Colors.textTertiary)
.textCase(.uppercase)
}
}
extension View {
func dataLabelStyle() -> some View {
modifier(DataLabelStyle())
}
}