Loading states and error handling
Wire the Reflect button to the model call with a spinner, a graceful error path, and a retry — the difference between a demo and a real feature.
A network call can be slow or fail. A demo ignores that; a shippable app handles it visibly. We'll model the feature's states explicitly so the UI is never a mystery to the user.
import SwiftUI
struct ReflectButton: View {
let entry: Entry
enum Phase { case idle, loading, failed }
@State private var phase: Phase = .idle
var body: some View {
switch phase {
case .idle:
Button("Reflect", systemImage: "sparkles") {
Task { await reflect() }
}
case .loading:
ProgressView("Reflecting…")
case .failed:
VStack(spacing: 8) {
Text("Couldn't generate insights.")
.foregroundStyle(.secondary)
Button("Retry") { Task { await reflect() } }
}
}
}
private func reflect() async {
phase = .loading
do {
let insights = try await InsightsService.generate(
for: entry.body, apiBase: AppConfig.apiBase
)
entry.aiTitle = insights.title
entry.mood = insights.mood
entry.reflection = insights.reflection
phase = .idle
} catch {
phase = .failed
}
}
}The Phase enum makes the three realities of a network feature — waiting, success, failure — impossible to forget. On success we write the results straight onto the Entry (SwiftData persists them); on failure we show a retry instead of a spinner that hangs forever.
Beginners scatter booleans (isLoading, hasError) that can contradict each other. A single enum with named cases can only ever be in one valid state. Reach for this any time a feature has more than two modes.
Ask your assistant to "add a timeout so the call fails after 20 seconds instead of hanging," and to "show the specific error message in a small caption when phase is .failed during development." Small resilience touches like these are what testers notice.