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

32 KiB

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

mkdir -p SportsTime/Core/Theme/Loading
mkdir -p SportsTimeTests/Loading

Step 2: Write the failing test

Create SportsTimeTests/Loading/LoadingSpinnerTests.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:

//
//  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

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:

//
//  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:

//
//  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

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:

//
//  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:

//
//  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

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:

// 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:

            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

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):

// OLD:
            Button(action: onPlan) {
                HStack {
                    if isPlanning {
                        ProgressView()
                            .scaleEffect(0.8)
                            .tint(.white)
                    }
                    Text(isPlanning ? "Planning..." : "Plan My Trip")
                        .fontWeight(.semibold)
                }

With:

            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

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:

// OLD:
                if isLoading {
                    ProgressView()

With:

                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

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:

// OLD:
                ProgressView("Loading games...")

With:

                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

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:

// OLD:
                            HStack {
                                    if isLookingUpGame {
                                        ProgressView()
                                            .scaleEffect(0.8)
                                    } else {

With:

                            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

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:

// OLD:
    private var planningOverlay: some View {
        ZStack {
            Color.black.opacity(0.5)
                .ignoresSafeArea()

            PlanningProgressView()
                .padding(40)
                .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 24))
        }
    }

With:

    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

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:

//
//  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

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

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

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

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

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

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)