Files
Sportstime/docs/plans/2026-01-12-loading-redesign-implementation.md
Trey t f8204007e6 docs: add loading redesign implementation plan
13 tasks covering LoadingSpinner, LoadingPlaceholder, LoadingSheet
creation, ProgressView replacements, and deprecated component removal.
Includes TDD with 13 new tests.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 22:17:33 -06:00

1194 lines
32 KiB
Markdown

# Loading System Redesign Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Replace all loading indicators with a unified, Apple-style design system using three components: LoadingSpinner, LoadingPlaceholder, and LoadingSheet.
**Architecture:** Create new Loading/ folder with three focused components. Deprecate 6 existing components in AnimatedComponents.swift. Replace 5 native ProgressView() usages. Refactor LoadingTripsView to use new skeletons.
**Tech Stack:** SwiftUI, Swift Testing
---
## Task 1: Create LoadingSpinner Component
**Files:**
- Create: `SportsTime/Core/Theme/Loading/LoadingSpinner.swift`
- Test: `SportsTimeTests/Loading/LoadingSpinnerTests.swift`
**Step 1: Create the Loading directory**
```bash
mkdir -p SportsTime/Core/Theme/Loading
mkdir -p SportsTimeTests/Loading
```
**Step 2: Write the failing test**
Create `SportsTimeTests/Loading/LoadingSpinnerTests.swift`:
```swift
//
// LoadingSpinnerTests.swift
// SportsTimeTests
//
import Testing
import SwiftUI
@testable import SportsTime
struct LoadingSpinnerTests {
@Test func smallSizeHasCorrectDimensions() {
let config = LoadingSpinner.Size.small
#expect(config.diameter == 16)
#expect(config.strokeWidth == 2)
}
@Test func mediumSizeHasCorrectDimensions() {
let config = LoadingSpinner.Size.medium
#expect(config.diameter == 24)
#expect(config.strokeWidth == 3)
}
@Test func largeSizeHasCorrectDimensions() {
let config = LoadingSpinner.Size.large
#expect(config.diameter == 40)
#expect(config.strokeWidth == 4)
}
@Test func spinnerCanBeCreatedWithAllSizes() {
let small = LoadingSpinner(size: .small)
let medium = LoadingSpinner(size: .medium)
let large = LoadingSpinner(size: .large)
#expect(small.size == .small)
#expect(medium.size == .medium)
#expect(large.size == .large)
}
@Test func spinnerCanHaveOptionalLabel() {
let withLabel = LoadingSpinner(size: .medium, label: "Loading...")
let withoutLabel = LoadingSpinner(size: .medium)
#expect(withLabel.label == "Loading...")
#expect(withoutLabel.label == nil)
}
}
```
**Step 3: Run test to verify it fails**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/LoadingSpinnerTests test`
Expected: FAIL with "Cannot find 'LoadingSpinner' in scope"
**Step 4: Write the LoadingSpinner implementation**
Create `SportsTime/Core/Theme/Loading/LoadingSpinner.swift`:
```swift
//
// LoadingSpinner.swift
// SportsTime
//
// Apple-style indeterminate spinner with theme-aware colors.
//
import SwiftUI
struct LoadingSpinner: View {
enum Size {
case small, medium, large
var diameter: CGFloat {
switch self {
case .small: return 16
case .medium: return 24
case .large: return 40
}
}
var strokeWidth: CGFloat {
switch self {
case .small: return 2
case .medium: return 3
case .large: return 4
}
}
var labelFont: Font {
switch self {
case .small, .medium: return .subheadline
case .large: return .body
}
}
}
let size: Size
let label: String?
@State private var rotation: Double = 0
@Environment(\.colorScheme) private var colorScheme
init(size: Size = .medium, label: String? = nil) {
self.size = size
self.label = label
}
var body: some View {
HStack(spacing: Theme.Spacing.sm) {
spinnerView
if let label {
Text(label)
.font(size.labelFont)
.foregroundStyle(Theme.textSecondary(colorScheme))
}
}
}
private var spinnerView: some View {
ZStack {
// Background track
Circle()
.stroke(Theme.warmOrange.opacity(0.15), lineWidth: size.strokeWidth)
// Rotating arc (270 degrees)
Circle()
.trim(from: 0, to: 0.75)
.stroke(Theme.warmOrange, style: StrokeStyle(lineWidth: size.strokeWidth, lineCap: .round))
.rotationEffect(.degrees(rotation))
}
.frame(width: size.diameter, height: size.diameter)
.onAppear {
withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
rotation = 360
}
}
}
}
// MARK: - Preview
#Preview("Loading Spinner Sizes") {
VStack(spacing: 40) {
LoadingSpinner(size: .small, label: "Loading...")
LoadingSpinner(size: .medium, label: "Loading games...")
LoadingSpinner(size: .large, label: "Planning trip")
LoadingSpinner(size: .medium)
}
.padding(40)
.themedBackground()
}
```
**Step 5: Run tests to verify they pass**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/LoadingSpinnerTests test`
Expected: All 5 tests PASS
**Step 6: Commit**
```bash
git add SportsTime/Core/Theme/Loading/LoadingSpinner.swift SportsTimeTests/Loading/LoadingSpinnerTests.swift
git commit -m "$(cat <<'EOF'
feat: add LoadingSpinner component
Apple-style indeterminate spinner with three sizes (small/medium/large),
optional label, and theme-aware colors. Uses 270-degree arc with
1-second rotation.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
EOF
)"
```
---
## Task 2: Create LoadingPlaceholder Component
**Files:**
- Create: `SportsTime/Core/Theme/Loading/LoadingPlaceholder.swift`
- Test: `SportsTimeTests/Loading/LoadingPlaceholderTests.swift`
**Step 1: Write the failing test**
Create `SportsTimeTests/Loading/LoadingPlaceholderTests.swift`:
```swift
//
// LoadingPlaceholderTests.swift
// SportsTimeTests
//
import Testing
import SwiftUI
@testable import SportsTime
struct LoadingPlaceholderTests {
@Test func rectangleHasCorrectDimensions() {
let rect = LoadingPlaceholder.rectangle(width: 100, height: 20)
#expect(rect.width == 100)
#expect(rect.height == 20)
}
@Test func circleHasCorrectDiameter() {
let circle = LoadingPlaceholder.circle(diameter: 40)
#expect(circle.diameter == 40)
}
@Test func capsuleHasCorrectDimensions() {
let capsule = LoadingPlaceholder.capsule(width: 80, height: 24)
#expect(capsule.width == 80)
#expect(capsule.height == 24)
}
@Test func animationCycleDurationIsCorrect() {
#expect(LoadingPlaceholder.animationDuration == 1.2)
}
@Test func opacityRangeIsSubtle() {
#expect(LoadingPlaceholder.minOpacity == 0.3)
#expect(LoadingPlaceholder.maxOpacity == 0.5)
}
}
```
**Step 2: Run test to verify it fails**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/LoadingPlaceholderTests test`
Expected: FAIL with "Cannot find 'LoadingPlaceholder' in scope"
**Step 3: Write the LoadingPlaceholder implementation**
Create `SportsTime/Core/Theme/Loading/LoadingPlaceholder.swift`:
```swift
//
// LoadingPlaceholder.swift
// SportsTime
//
// Skeleton placeholder shapes with gentle opacity pulse animation.
//
import SwiftUI
enum LoadingPlaceholder {
static let animationDuration: Double = 1.2
static let minOpacity: Double = 0.3
static let maxOpacity: Double = 0.5
static func rectangle(width: CGFloat, height: CGFloat) -> PlaceholderRectangle {
PlaceholderRectangle(width: width, height: height)
}
static func circle(diameter: CGFloat) -> PlaceholderCircle {
PlaceholderCircle(diameter: diameter)
}
static func capsule(width: CGFloat, height: CGFloat) -> PlaceholderCapsule {
PlaceholderCapsule(width: width, height: height)
}
static var card: PlaceholderCard {
PlaceholderCard()
}
static var listRow: PlaceholderListRow {
PlaceholderListRow()
}
}
// MARK: - Placeholder Rectangle
struct PlaceholderRectangle: View {
let width: CGFloat
let height: CGFloat
@State private var isAnimating = false
@Environment(\.colorScheme) private var colorScheme
var body: some View {
RoundedRectangle(cornerRadius: 4)
.fill(placeholderColor)
.frame(width: width, height: height)
.opacity(isAnimating ? LoadingPlaceholder.maxOpacity : LoadingPlaceholder.minOpacity)
.onAppear {
withAnimation(.easeInOut(duration: LoadingPlaceholder.animationDuration).repeatForever(autoreverses: true)) {
isAnimating = true
}
}
}
private var placeholderColor: Color {
colorScheme == .dark ? Theme.cardBackgroundElevated(colorScheme) : Theme.textMuted(colorScheme)
}
}
// MARK: - Placeholder Circle
struct PlaceholderCircle: View {
let diameter: CGFloat
@State private var isAnimating = false
@Environment(\.colorScheme) private var colorScheme
var body: some View {
Circle()
.fill(placeholderColor)
.frame(width: diameter, height: diameter)
.opacity(isAnimating ? LoadingPlaceholder.maxOpacity : LoadingPlaceholder.minOpacity)
.onAppear {
withAnimation(.easeInOut(duration: LoadingPlaceholder.animationDuration).repeatForever(autoreverses: true)) {
isAnimating = true
}
}
}
private var placeholderColor: Color {
colorScheme == .dark ? Theme.cardBackgroundElevated(colorScheme) : Theme.textMuted(colorScheme)
}
}
// MARK: - Placeholder Capsule
struct PlaceholderCapsule: View {
let width: CGFloat
let height: CGFloat
@State private var isAnimating = false
@Environment(\.colorScheme) private var colorScheme
var body: some View {
Capsule()
.fill(placeholderColor)
.frame(width: width, height: height)
.opacity(isAnimating ? LoadingPlaceholder.maxOpacity : LoadingPlaceholder.minOpacity)
.onAppear {
withAnimation(.easeInOut(duration: LoadingPlaceholder.animationDuration).repeatForever(autoreverses: true)) {
isAnimating = true
}
}
}
private var placeholderColor: Color {
colorScheme == .dark ? Theme.cardBackgroundElevated(colorScheme) : Theme.textMuted(colorScheme)
}
}
// MARK: - Placeholder Card (Trip Card Skeleton)
struct PlaceholderCard: View {
@State private var isAnimating = false
@Environment(\.colorScheme) private var colorScheme
var body: some View {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
// Header row
HStack {
placeholderRect(width: 60, height: 20)
Spacer()
placeholderRect(width: 40, height: 16)
}
// Title and subtitle
VStack(alignment: .leading, spacing: 4) {
placeholderRect(width: 120, height: 16)
placeholderRect(width: 160, height: 14)
}
// Stats row
HStack(spacing: Theme.Spacing.sm) {
placeholderRect(width: 70, height: 14)
placeholderRect(width: 60, height: 14)
}
}
.padding(Theme.Spacing.md)
.frame(width: 200)
.background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
.overlay {
RoundedRectangle(cornerRadius: Theme.CornerRadius.large)
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
}
.onAppear {
withAnimation(.easeInOut(duration: LoadingPlaceholder.animationDuration).repeatForever(autoreverses: true)) {
isAnimating = true
}
}
}
private func placeholderRect(width: CGFloat, height: CGFloat) -> some View {
RoundedRectangle(cornerRadius: 4)
.fill(placeholderColor)
.frame(width: width, height: height)
.opacity(isAnimating ? LoadingPlaceholder.maxOpacity : LoadingPlaceholder.minOpacity)
}
private var placeholderColor: Color {
colorScheme == .dark ? Theme.cardBackgroundElevated(colorScheme) : Theme.textMuted(colorScheme)
}
}
// MARK: - Placeholder List Row
struct PlaceholderListRow: View {
@State private var isAnimating = false
@Environment(\.colorScheme) private var colorScheme
var body: some View {
HStack(spacing: Theme.Spacing.md) {
placeholderCircle(diameter: 40)
VStack(alignment: .leading, spacing: 6) {
placeholderRect(width: 140, height: 16)
placeholderRect(width: 100, height: 12)
}
Spacer()
placeholderRect(width: 50, height: 14)
}
.padding(Theme.Spacing.md)
.onAppear {
withAnimation(.easeInOut(duration: LoadingPlaceholder.animationDuration).repeatForever(autoreverses: true)) {
isAnimating = true
}
}
}
private func placeholderRect(width: CGFloat, height: CGFloat) -> some View {
RoundedRectangle(cornerRadius: 4)
.fill(placeholderColor)
.frame(width: width, height: height)
.opacity(isAnimating ? LoadingPlaceholder.maxOpacity : LoadingPlaceholder.minOpacity)
}
private func placeholderCircle(diameter: CGFloat) -> some View {
Circle()
.fill(placeholderColor)
.frame(width: diameter, height: diameter)
.opacity(isAnimating ? LoadingPlaceholder.maxOpacity : LoadingPlaceholder.minOpacity)
}
private var placeholderColor: Color {
colorScheme == .dark ? Theme.cardBackgroundElevated(colorScheme) : Theme.textMuted(colorScheme)
}
}
// MARK: - Preview
#Preview("Loading Placeholders") {
VStack(spacing: 30) {
HStack(spacing: 20) {
LoadingPlaceholder.rectangle(width: 100, height: 20)
LoadingPlaceholder.circle(diameter: 40)
LoadingPlaceholder.capsule(width: 80, height: 24)
}
LoadingPlaceholder.card
LoadingPlaceholder.listRow
.background(Theme.cardBackground(.dark))
}
.padding(20)
.themedBackground()
}
```
**Step 4: Run tests to verify they pass**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/LoadingPlaceholderTests test`
Expected: All 5 tests PASS
**Step 5: Commit**
```bash
git add SportsTime/Core/Theme/Loading/LoadingPlaceholder.swift SportsTimeTests/Loading/LoadingPlaceholderTests.swift
git commit -m "$(cat <<'EOF'
feat: add LoadingPlaceholder component
Skeleton placeholder shapes with gentle opacity pulse animation (0.3-0.5
over 1.2s). Includes rectangle, circle, capsule primitives plus card and
listRow composites. Theme-aware colors.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
EOF
)"
```
---
## Task 3: Create LoadingSheet Component
**Files:**
- Create: `SportsTime/Core/Theme/Loading/LoadingSheet.swift`
- Test: `SportsTimeTests/Loading/LoadingSheetTests.swift`
**Step 1: Write the failing test**
Create `SportsTimeTests/Loading/LoadingSheetTests.swift`:
```swift
//
// LoadingSheetTests.swift
// SportsTimeTests
//
import Testing
import SwiftUI
@testable import SportsTime
struct LoadingSheetTests {
@Test func sheetRequiresLabel() {
let sheet = LoadingSheet(label: "Planning trip")
#expect(sheet.label == "Planning trip")
}
@Test func sheetCanHaveOptionalDetail() {
let withDetail = LoadingSheet(label: "Exporting", detail: "Generating maps...")
let withoutDetail = LoadingSheet(label: "Loading")
#expect(withDetail.detail == "Generating maps...")
#expect(withoutDetail.detail == nil)
}
@Test func backgroundOpacityIsCorrect() {
#expect(LoadingSheet.backgroundOpacity == 0.5)
}
}
```
**Step 2: Run test to verify it fails**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/LoadingSheetTests test`
Expected: FAIL with "Cannot find 'LoadingSheet' in scope"
**Step 3: Write the LoadingSheet implementation**
Create `SportsTime/Core/Theme/Loading/LoadingSheet.swift`:
```swift
//
// LoadingSheet.swift
// SportsTime
//
// Full-screen blocking loading overlay with centered card.
//
import SwiftUI
struct LoadingSheet: View {
static let backgroundOpacity: Double = 0.5
let label: String
let detail: String?
@Environment(\.colorScheme) private var colorScheme
init(label: String, detail: String? = nil) {
self.label = label
self.detail = detail
}
var body: some View {
ZStack {
// Dimmed background
Color.black.opacity(Self.backgroundOpacity)
.ignoresSafeArea()
// Centered card
VStack(spacing: Theme.Spacing.lg) {
LoadingSpinner(size: .large)
VStack(spacing: Theme.Spacing.xs) {
Text(label)
.font(.headline)
.foregroundStyle(Theme.textPrimary(colorScheme))
if let detail {
Text(detail)
.font(.subheadline)
.foregroundStyle(Theme.textSecondary(colorScheme))
.multilineTextAlignment(.center)
}
}
}
.padding(Theme.Spacing.xl)
.background(Theme.cardBackground(colorScheme))
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
.shadow(color: .black.opacity(0.2), radius: 16, y: 8)
}
.transition(.opacity)
}
}
// MARK: - Preview
#Preview("Loading Sheet") {
ZStack {
Color.gray.ignoresSafeArea()
LoadingSheet(label: "Planning trip")
}
}
#Preview("Loading Sheet with Detail") {
ZStack {
Color.gray.ignoresSafeArea()
LoadingSheet(label: "Exporting PDF", detail: "Generating maps...")
}
}
```
**Step 4: Run tests to verify they pass**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/LoadingSheetTests test`
Expected: All 3 tests PASS
**Step 5: Commit**
```bash
git add SportsTime/Core/Theme/Loading/LoadingSheet.swift SportsTimeTests/Loading/LoadingSheetTests.swift
git commit -m "$(cat <<'EOF'
feat: add LoadingSheet component
Full-screen blocking overlay with dimmed background (0.5 opacity),
centered card with large spinner, label, and optional detail text.
Replaces LoadingOverlay and PlanningProgressView.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
EOF
)"
```
---
## Task 4: Replace ProgressView in SportsStep
**Files:**
- Modify: `SportsTime/Features/Trip/Views/Wizard/Steps/SportsStep.swift:30-38`
**Step 1: Replace ProgressView with LoadingSpinner**
In `SportsStep.swift`, replace lines 30-38:
```swift
// OLD (lines 30-38):
if isLoading {
HStack {
ProgressView()
.scaleEffect(0.8)
Text("Checking game availability...")
.font(.caption)
.foregroundStyle(Theme.textMuted(colorScheme))
}
.padding(.vertical, Theme.Spacing.sm)
}
```
With:
```swift
if isLoading {
LoadingSpinner(size: .small, label: "Checking availability...")
.padding(.vertical, Theme.Spacing.sm)
}
```
**Step 2: Build to verify**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build`
Expected: Build succeeds
**Step 3: Commit**
```bash
git add SportsTime/Features/Trip/Views/Wizard/Steps/SportsStep.swift
git commit -m "$(cat <<'EOF'
refactor: replace ProgressView with LoadingSpinner in SportsStep
Use new LoadingSpinner(size: .small) for consistent loading UI.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
EOF
)"
```
---
## Task 5: Replace ProgressView in ReviewStep
**Files:**
- Modify: `SportsTime/Features/Trip/Views/Wizard/Steps/ReviewStep.swift:48-53`
**Step 1: Replace ProgressView with LoadingSpinner**
In `ReviewStep.swift`, replace the button content (lines 48-56):
```swift
// OLD:
Button(action: onPlan) {
HStack {
if isPlanning {
ProgressView()
.scaleEffect(0.8)
.tint(.white)
}
Text(isPlanning ? "Planning..." : "Plan My Trip")
.fontWeight(.semibold)
}
```
With:
```swift
Button(action: onPlan) {
HStack(spacing: Theme.Spacing.sm) {
if isPlanning {
LoadingSpinner(size: .small)
.colorScheme(.dark) // Force white on orange button
}
Text(isPlanning ? "Planning..." : "Plan My Trip")
.fontWeight(.semibold)
}
```
**Step 2: Build to verify**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build`
Expected: Build succeeds
**Step 3: Commit**
```bash
git add SportsTime/Features/Trip/Views/Wizard/Steps/ReviewStep.swift
git commit -m "$(cat <<'EOF'
refactor: replace ProgressView with LoadingSpinner in ReviewStep
Use LoadingSpinner(size: .small) in Plan My Trip button.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
EOF
)"
```
---
## Task 6: Replace ProgressView in StadiumVisitHistoryView
**Files:**
- Modify: `SportsTime/Features/Progress/Views/StadiumVisitHistoryView.swift:16-17`
**Step 1: Replace ProgressView with LoadingSpinner**
In `StadiumVisitHistoryView.swift`, replace line 17:
```swift
// OLD:
if isLoading {
ProgressView()
```
With:
```swift
if isLoading {
LoadingSpinner(size: .medium)
```
**Step 2: Build to verify**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build`
Expected: Build succeeds
**Step 3: Commit**
```bash
git add SportsTime/Features/Progress/Views/StadiumVisitHistoryView.swift
git commit -m "$(cat <<'EOF'
refactor: replace ProgressView with LoadingSpinner in StadiumVisitHistoryView
Use LoadingSpinner(size: .medium) for consistent loading UI.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
EOF
)"
```
---
## Task 7: Replace ProgressView in GamesHistoryView
**Files:**
- Modify: `SportsTime/Features/Progress/Views/GamesHistoryView.swift:17`
**Step 1: Replace ProgressView with LoadingSpinner**
In `GamesHistoryView.swift`, replace line 17:
```swift
// OLD:
ProgressView("Loading games...")
```
With:
```swift
LoadingSpinner(size: .medium, label: "Loading games...")
```
**Step 2: Build to verify**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build`
Expected: Build succeeds
**Step 3: Commit**
```bash
git add SportsTime/Features/Progress/Views/GamesHistoryView.swift
git commit -m "$(cat <<'EOF'
refactor: replace ProgressView with LoadingSpinner in GamesHistoryView
Use LoadingSpinner with label for consistent loading UI.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
EOF
)"
```
---
## Task 8: Replace ProgressView in StadiumVisitSheet
**Files:**
- Modify: `SportsTime/Features/Progress/Views/StadiumVisitSheet.swift:234-236`
**Step 1: Replace ProgressView with LoadingSpinner**
In `StadiumVisitSheet.swift`, replace lines 234-236:
```swift
// OLD:
HStack {
if isLookingUpGame {
ProgressView()
.scaleEffect(0.8)
} else {
```
With:
```swift
HStack {
if isLookingUpGame {
LoadingSpinner(size: .small)
} else {
```
**Step 2: Build to verify**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build`
Expected: Build succeeds
**Step 3: Commit**
```bash
git add SportsTime/Features/Progress/Views/StadiumVisitSheet.swift
git commit -m "$(cat <<'EOF'
refactor: replace ProgressView with LoadingSpinner in StadiumVisitSheet
Use LoadingSpinner(size: .small) in Look Up Game button.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
EOF
)"
```
---
## Task 9: Replace planningOverlay in TripCreationView
**Files:**
- Modify: `SportsTime/Features/Trip/Views/TripCreationView.swift:867-876`
**Step 1: Replace PlanningProgressView with LoadingSheet**
In `TripCreationView.swift`, replace lines 867-876:
```swift
// OLD:
private var planningOverlay: some View {
ZStack {
Color.black.opacity(0.5)
.ignoresSafeArea()
PlanningProgressView()
.padding(40)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 24))
}
}
```
With:
```swift
private var planningOverlay: some View {
LoadingSheet(label: "Planning trip")
}
```
**Step 2: Build to verify**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build`
Expected: Build succeeds
**Step 3: Commit**
```bash
git add SportsTime/Features/Trip/Views/TripCreationView.swift
git commit -m "$(cat <<'EOF'
refactor: replace PlanningProgressView with LoadingSheet
Use new LoadingSheet component for planning overlay.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
EOF
)"
```
---
## Task 10: Refactor LoadingTripsView to Use New Placeholders
**Files:**
- Modify: `SportsTime/Features/Home/Views/LoadingTripsView.swift`
**Step 1: Rewrite LoadingTripsView to use LoadingPlaceholder**
Replace the entire file with:
```swift
//
// LoadingTripsView.swift
// SportsTime
//
// Loading state for suggested trips carousel using skeleton placeholders.
//
import SwiftUI
struct LoadingTripsView: View {
let message: String
@Environment(\.colorScheme) private var colorScheme
var body: some View {
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
// Header
HStack {
Text("Featured Trips")
.font(.title2)
.foregroundStyle(Theme.textPrimary(colorScheme))
Spacer()
}
// Loading indicator with message
LoadingSpinner(size: .small, label: message)
.padding(.vertical, Theme.Spacing.xs)
// Placeholder cards
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: Theme.Spacing.md) {
ForEach(0..<3, id: \.self) { _ in
LoadingPlaceholder.card
}
}
}
}
}
}
#Preview {
VStack {
LoadingTripsView(message: "Loading trips...")
.padding()
}
.themedBackground()
}
```
**Step 2: Build to verify**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build`
Expected: Build succeeds
**Step 3: Commit**
```bash
git add SportsTime/Features/Home/Views/LoadingTripsView.swift
git commit -m "$(cat <<'EOF'
refactor: simplify LoadingTripsView with new loading components
Use LoadingSpinner and LoadingPlaceholder.card instead of custom
LoadingDots and PlaceholderCard with shimmer animation.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
EOF
)"
```
---
## Task 11: Remove Deprecated Components from AnimatedComponents.swift
**Files:**
- Modify: `SportsTime/Core/Theme/AnimatedComponents.swift`
**Step 1: Remove deprecated loading components**
Remove the following from `AnimatedComponents.swift`:
- `ThemedSpinner` (lines 106-136)
- `ThemedSpinnerCompact` (lines 138-164)
- `PlanningProgressView` (lines 245-279)
- `LoadingOverlay` (lines 348-417)
- Preview for "Themed Spinners" (lines 419-436)
- Preview for "Loading Overlay" (lines 459-467)
- Preview for "Loading Overlay with Progress" (lines 469-479)
Keep:
- `AnimatedRouteGraphic`
- `PulsingDot`
- `RoutePreviewStrip`
- `StatPill`
- `EmptyStateView`
- Their previews
**Step 2: Build to verify**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build`
Expected: Build succeeds
**Step 3: Commit**
```bash
git add SportsTime/Core/Theme/AnimatedComponents.swift
git commit -m "$(cat <<'EOF'
refactor: remove deprecated loading components from AnimatedComponents
Remove ThemedSpinner, ThemedSpinnerCompact, PlanningProgressView,
and LoadingOverlay. These are replaced by the new Loading/ components.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
EOF
)"
```
---
## Task 12: Delete LoadingTextGenerator.swift
**Files:**
- Delete: `SportsTime/Core/Services/LoadingTextGenerator.swift`
**Step 1: Remove the file**
```bash
git rm SportsTime/Core/Services/LoadingTextGenerator.swift
```
**Step 2: Build to verify no references remain**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build`
Expected: Build succeeds (if there are errors, search for `LoadingTextGenerator` usages and remove them)
**Step 3: Commit**
```bash
git commit -m "$(cat <<'EOF'
refactor: remove LoadingTextGenerator
Loading messages are now static context-specific labels,
no longer AI-generated or rotating.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
EOF
)"
```
---
## Task 13: Delete LoadingDots and PlaceholderCard (Cleanup)
**Files:**
- Modify: `SportsTime/Features/Home/Views/LoadingTripsView.swift` (if still contains old components)
**Step 1: Verify LoadingDots and PlaceholderCard are no longer in codebase**
```bash
grep -r "LoadingDots\|PlaceholderCard" SportsTime/
```
If any usages remain, remove them.
**Step 2: Build full test suite**
Run: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test`
Expected: All tests pass
**Step 3: Final commit if needed**
```bash
git add -A
git commit -m "$(cat <<'EOF'
chore: final cleanup of deprecated loading components
Remove any remaining references to LoadingDots and PlaceholderCard.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
EOF
)"
```
---
## Summary
| Task | Description | Tests Added |
|------|-------------|-------------|
| 1 | Create LoadingSpinner | 5 |
| 2 | Create LoadingPlaceholder | 5 |
| 3 | Create LoadingSheet | 3 |
| 4 | Replace ProgressView in SportsStep | - |
| 5 | Replace ProgressView in ReviewStep | - |
| 6 | Replace ProgressView in StadiumVisitHistoryView | - |
| 7 | Replace ProgressView in GamesHistoryView | - |
| 8 | Replace ProgressView in StadiumVisitSheet | - |
| 9 | Replace planningOverlay in TripCreationView | - |
| 10 | Refactor LoadingTripsView | - |
| 11 | Remove deprecated components | - |
| 12 | Delete LoadingTextGenerator | - |
| 13 | Final cleanup and verification | - |
**Total new tests:** 13
**Files created:** 6 (3 components + 3 test files)
**Files modified:** 8
**Files deleted:** 1 (LoadingTextGenerator.swift)
**Components removed:** 6 (ThemedSpinner, ThemedSpinnerCompact, LoadingDots, PlaceholderCard, PlanningProgressView, LoadingOverlay)