feat: add reusable ErrorView and LoadingView components

Extract duplicated inline error/loading patterns from TaskListScreen,
MyTasksScreen, BusinessSelectionScreen, AccountScreen, and TaskDetailScreen
into shared components under Views/Components/.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Schwifty 2026-03-24 22:15:03 +00:00
parent 88a3a9d6e0
commit 1c427a0902
7 changed files with 71 additions and 75 deletions

View file

@ -90,10 +90,10 @@ struct AccountScreen: View {
// Payout content // Payout content
if isLoading { if isLoading {
ProgressView() LoadingView()
.padding(.top, 20) .padding(.top, 20)
} else if let error = error { } else if let error = error {
errorView(error) ErrorView(message: error) { Task { await loadData() } }
} else { } else {
VStack(spacing: 16) { VStack(spacing: 16) {
if let tier = tierStatus { if let tier = tierStatus {
@ -386,21 +386,6 @@ struct AccountScreen: View {
.padding(.top, 8) .padding(.top, 8)
} }
// MARK: - Error
private func errorView(_ msg: String) -> some View {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.circle")
.font(.system(size: 48))
.foregroundColor(.red)
Text(msg)
.multilineTextAlignment(.center)
Button("Retry") { Task { await loadData() } }
.buttonStyle(.borderedProminent)
}
.padding()
}
// MARK: - Actions // MARK: - Actions
private func loadAvatar() async { private func loadAvatar() async {

View file

@ -20,9 +20,9 @@ struct BusinessSelectionScreen: View {
ZStack(alignment: .bottomTrailing) { ZStack(alignment: .bottomTrailing) {
Group { Group {
if isLoading { if isLoading {
ProgressView() LoadingView()
} else if let error = error { } else if let error = error {
errorView(error) ErrorView(message: error) { loadBusinesses() }
} else if businesses.isEmpty { } else if businesses.isEmpty {
emptyView emptyView
} else { } else {
@ -246,19 +246,6 @@ struct BusinessSelectionScreen: View {
.padding() .padding()
} }
private func errorView(_ message: String) -> some View {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.circle")
.font(.system(size: 48))
.foregroundColor(.red)
Text(message)
.multilineTextAlignment(.center)
Button("Retry") { loadBusinesses() }
.buttonStyle(.borderedProminent)
}
.padding()
}
// MARK: - Actions // MARK: - Actions
private func loadBusinesses(silent: Bool = false) { private func loadBusinesses(silent: Bool = false) {

View file

@ -0,0 +1,34 @@
import SwiftUI
/// Reusable error state view with icon, message, and optional retry button.
/// Replaces inline error patterns across all screens.
struct ErrorView: View {
let message: String
var onRetry: (() -> Void)?
var body: some View {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.circle")
.font(.system(size: 48))
.foregroundColor(.red)
Text(message)
.multilineTextAlignment(.center)
.foregroundColor(.primary)
if let onRetry = onRetry {
Button("Retry", action: onRetry)
.buttonStyle(.borderedProminent)
}
}
.padding()
}
}
#Preview("With Retry") {
ErrorView(message: "Something went wrong. Please try again.") {
print("Retry tapped")
}
}
#Preview("No Retry") {
ErrorView(message: "Could not load data.")
}

View file

@ -0,0 +1,27 @@
import SwiftUI
/// Reusable loading state view with a spinner and optional message.
/// Replaces inline ProgressView() patterns across all screens.
struct LoadingView: View {
var message: String?
var body: some View {
VStack(spacing: 12) {
ProgressView()
if let message = message {
Text(message)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
#Preview("With Message") {
LoadingView(message: "Loading tasks...")
}
#Preview("No Message") {
LoadingView()
}

View file

@ -94,18 +94,9 @@ struct MyTasksScreen: View {
let tasks = tasksByFilter[filterType] ?? [] let tasks = tasksByFilter[filterType] ?? []
if isLoading { if isLoading {
ProgressView() LoadingView()
} else if let error = error { } else if let error = error {
VStack(spacing: 16) { ErrorView(message: error) { loadTasks(filterType) }
Image(systemName: "exclamationmark.circle")
.font(.system(size: 48))
.foregroundColor(.red)
Text(error)
.multilineTextAlignment(.center)
Button("Retry") { loadTasks(filterType) }
.buttonStyle(.borderedProminent)
}
.padding()
} else if tasks.isEmpty { } else if tasks.isEmpty {
emptyView(filterType) emptyView(filterType)
} else { } else {

View file

@ -42,9 +42,9 @@ struct TaskDetailScreen: View {
ZStack(alignment: .bottomTrailing) { ZStack(alignment: .bottomTrailing) {
Group { Group {
if isLoading { if isLoading {
ProgressView() LoadingView()
} else if let error = error { } else if let error = error {
errorView(error) ErrorView(message: error) { Task { await loadDetails() } }
} else if let details = details { } else if let details = details {
contentView(details) contentView(details)
} }
@ -663,21 +663,6 @@ struct TaskDetailScreen: View {
.background(.ultraThinMaterial) .background(.ultraThinMaterial)
} }
// MARK: - Error
private func errorView(_ message: String) -> some View {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.circle")
.font(.system(size: 48))
.foregroundColor(.red)
Text(message)
.multilineTextAlignment(.center)
Button("Retry") { Task { await loadDetails() } }
.buttonStyle(.borderedProminent)
}
.padding()
}
// MARK: - Actions // MARK: - Actions
private func loadDetails() async { private func loadDetails() async {

View file

@ -17,9 +17,9 @@ struct TaskListScreen: View {
ZStack(alignment: .bottomTrailing) { ZStack(alignment: .bottomTrailing) {
Group { Group {
if isLoading { if isLoading {
ProgressView() LoadingView()
} else if let error = error { } else if let error = error {
errorView(error) ErrorView(message: error) { loadTasks() }
} else if tasks.isEmpty { } else if tasks.isEmpty {
emptyView emptyView
} else { } else {
@ -195,19 +195,6 @@ struct TaskListScreen: View {
} }
} }
private func errorView(_ message: String) -> some View {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.circle")
.font(.system(size: 48))
.foregroundColor(.red)
Text(message)
.multilineTextAlignment(.center)
Button("Retry") { loadTasks() }
.buttonStyle(.borderedProminent)
}
.padding()
}
// MARK: - Actions // MARK: - Actions
private func loadTasks(silent: Bool = false) { private func loadTasks(silent: Bool = false) {