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