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
if isLoading {
ProgressView()
LoadingView()
.padding(.top, 20)
} else if let error = error {
errorView(error)
ErrorView(message: error) { Task { await loadData() } }
} else {
VStack(spacing: 16) {
if let tier = tierStatus {
@ -386,21 +386,6 @@ struct AccountScreen: View {
.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
private func loadAvatar() async {

View file

@ -20,9 +20,9 @@ struct BusinessSelectionScreen: View {
ZStack(alignment: .bottomTrailing) {
Group {
if isLoading {
ProgressView()
LoadingView()
} else if let error = error {
errorView(error)
ErrorView(message: error) { loadBusinesses() }
} else if businesses.isEmpty {
emptyView
} else {
@ -246,19 +246,6 @@ struct BusinessSelectionScreen: View {
.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
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] ?? []
if isLoading {
ProgressView()
LoadingView()
} else if let error = error {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.circle")
.font(.system(size: 48))
.foregroundColor(.red)
Text(error)
.multilineTextAlignment(.center)
Button("Retry") { loadTasks(filterType) }
.buttonStyle(.borderedProminent)
}
.padding()
ErrorView(message: error) { loadTasks(filterType) }
} else if tasks.isEmpty {
emptyView(filterType)
} else {

View file

@ -42,9 +42,9 @@ struct TaskDetailScreen: View {
ZStack(alignment: .bottomTrailing) {
Group {
if isLoading {
ProgressView()
LoadingView()
} else if let error = error {
errorView(error)
ErrorView(message: error) { Task { await loadDetails() } }
} else if let details = details {
contentView(details)
}
@ -663,21 +663,6 @@ struct TaskDetailScreen: View {
.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
private func loadDetails() async {

View file

@ -17,9 +17,9 @@ struct TaskListScreen: View {
ZStack(alignment: .bottomTrailing) {
Group {
if isLoading {
ProgressView()
LoadingView()
} else if let error = error {
errorView(error)
ErrorView(message: error) { loadTasks() }
} else if tasks.isEmpty {
emptyView
} 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
private func loadTasks(silent: Bool = false) {