feat(ui): replace loading indicators with Apple-style LoadingSpinner

- Add LoadingSpinner component with small/medium/large sizes using system gray color
- Add LoadingPlaceholder for skeleton loading states
- Add LoadingSheet for full-screen blocking overlays
- Replace ThemedSpinner/ThemedSpinnerCompact across all views
- Remove deprecated loading components from AnimatedComponents.swift
- Delete LoadingTextGenerator.swift
- Fix PhotoImportView layout to fill full width

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-01-12 22:43:33 -06:00
parent f8204007e6
commit c0f1645434
21 changed files with 544 additions and 460 deletions

View File

@@ -1,99 +0,0 @@
//
// LoadingTextGenerator.swift
// SportsTime
//
// Generates unique loading messages using Apple Foundation Models.
// Falls back to predefined messages on unsupported devices.
//
import Foundation
#if canImport(FoundationModels)
import FoundationModels
#endif
actor LoadingTextGenerator {
static let shared = LoadingTextGenerator()
private static let fallbackMessages = [
"Hang tight, we're finding the best routes...",
"Scanning stadiums across the country...",
"Building your dream road trip...",
"Calculating the perfect game day schedule...",
"Finding the best matchups for you...",
"Mapping out your adventure...",
"Checking stadium schedules...",
"Putting together some epic trips...",
"Hold on, great trips incoming...",
"Crunching the numbers on routes...",
"Almost there, planning magic happening...",
"Finding games you'll love..."
]
private var usedMessages: Set<String> = []
/// Generates a unique loading message.
/// Uses Foundation Models if available, falls back to predefined messages.
func generateMessage() async -> String {
#if canImport(FoundationModels)
// Try Foundation Models first
if let message = await generateWithFoundationModels() {
return message
}
#endif
// Fall back to predefined messages
return getNextFallbackMessage()
}
#if canImport(FoundationModels)
private func generateWithFoundationModels() async -> String? {
// Check availability
guard case .available = SystemLanguageModel.default.availability else {
return nil
}
do {
let session = LanguageModelSession(instructions: """
Generate a short, friendly loading message for a sports road trip planning app.
The message should be casual, fun, and 8-12 words.
Don't use emojis. Don't start with "We're" or "We are".
Examples: "Hang tight, finding the best routes for you...",
"Calculating the perfect game day adventure...",
"Almost there, great trips are brewing..."
"""
)
let response = try await session.respond(to: "Generate one loading message")
let message = response.content.trimmingCharacters(in: .whitespacesAndNewlines)
// Validate message isn't empty and is reasonable length
guard message.count >= 10 && message.count <= 80 else {
return nil
}
return message
} catch {
return nil
}
}
#endif
private func getNextFallbackMessage() -> String {
// Reset if we've used all messages
if usedMessages.count >= Self.fallbackMessages.count {
usedMessages.removeAll()
}
// Pick a random unused message
let availableMessages = Self.fallbackMessages.filter { !usedMessages.contains($0) }
let message = availableMessages.randomElement() ?? Self.fallbackMessages[0]
usedMessages.insert(message)
return message
}
/// Reset used messages (for testing or new session)
func reset() {
usedMessages.removeAll()
}
}

View File

@@ -48,7 +48,6 @@ final class SuggestedTripsGenerator {
// MARK: - Dependencies
private let dataProvider = AppDataProvider.shared
private let loadingTextGenerator = LoadingTextGenerator.shared
// MARK: - Grouped Trips
@@ -78,8 +77,8 @@ final class SuggestedTripsGenerator {
error = nil
suggestedTrips = []
// Start with a loading message
loadingMessage = await loadingTextGenerator.generateMessage()
// Set loading message
loadingMessage = "Finding the best routes..."
// Ensure data is loaded
if dataProvider.teams.isEmpty {
@@ -141,7 +140,6 @@ final class SuggestedTripsGenerator {
}
func refreshTrips() async {
await loadingTextGenerator.reset()
await generateTrips()
}