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