Reaktive Statusanzeigen
Beschreibung
Diese View zeigt einen reaktiven Statusindikator, der seinen Zustand basierend auf Benutzeraktionen oder System-Updates animiert ändert. Beispielsweise wechselt der Indikator von einem Ladesymbol zu einer Erfolgsanimation, sobald eine Aufgabe abgeschlossen ist, und gibt so klares visuelles Feedback.
🔍 Zweck
- Anzeige des Ladefortschritts beim Hochladen von Dateien
- Rückmeldung nach erfolgreichem Abschluss einer Transaktion
- Visualisierung des Status während der Synchronisation von Daten
- Bestätigung nach dem Speichern von Einstellungen
- Anzeigen von Fehlern bei fehlgeschlagenen Aufgaben
📄 Codebeispiel
import SwiftUI
enum StatusState {
case idle, loading, success, error
}
struct ReactiveStatusIndicator: View {
@Binding var status: StatusState
@State private var animateCheckmark = false
@State private var animateCross = false
var body: some View {
ZStack {
switch status {
case .idle:
Circle()
.strokeBorder(Color.gray.opacity(0.3), lineWidth: 4)
.frame(width: 48, height: 48)
.overlay(
Image(systemName: "circle")
.font(.system(size: 24))
.foregroundColor(.gray.opacity(0.5))
)
case .loading:
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
.scaleEffect(1.4)
.frame(width: 48, height: 48)
case .success:
Circle()
.fill(Color.green.opacity(0.2))
.frame(width: 48, height: 48)
.overlay(
CheckmarkShape()
.trim(from: 0, to: animateCheckmark ? 1 : 0)
.stroke(Color.green, style: StrokeStyle(lineWidth: 4, lineCap: .round))
.frame(width: 28, height: 28)
.onAppear {
withAnimation(.easeOut(duration: 0.5)) {
animateCheckmark = true
}
}
)
case .error:
Circle()
.fill(Color.red.opacity(0.15))
.frame(width: 48, height: 48)
.overlay(
CrossShape()
.trim(from: 0, to: animateCross ? 1 : 0)
.stroke(Color.red, style: StrokeStyle(lineWidth: 4, lineCap: .round))
.frame(width: 28, height: 28)
.onAppear {
withAnimation(.easeOut(duration: 0.5)) {
animateCross = true
}
}
)
}
}
// Reset animations if state changes away from success/error
.onChange(of: status) { newValue in
if newValue != .success { animateCheckmark = false }
if newValue != .error { animateCross = false }
}
// Smooth scaling effect on state change
.animation(.spring(), value: status)
}
}
// Custom checkmark shape for animation
struct CheckmarkShape: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
let start = CGPoint(x: rect.minX + rect.width * 0.15, y: rect.midY)
let mid = CGPoint(x: rect.midX - rect.width * 0.05, y: rect.maxY - rect.height * 0.18)
let end = CGPoint(x: rect.maxX - rect.width * 0.14, y: rect.minY + rect.height * 0.18)
path.move(to: start)
path.addLine(to: mid)
path.addLine(to: end)
return path
}
}
// Custom cross shape for error animation
struct CrossShape: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
let insetAmount = rect.width * 0.25
path.move(to: CGPoint(x: rect.minX + insetAmount, y: rect.minY + insetAmount))
path.addLine(to: CGPoint(x: rect.maxX - insetAmount, y: rect.maxY - insetAmount))
path.move(to: CGPoint(x: rect.maxX - insetAmount, y: rect.minY + insetAmount))
path.addLine(to: CGPoint(x: rect.minX + insetAmount, y: rect.maxY - insetAmount))
return path
}
}
// Example usage view with buttons to simulate state changes
struct ReactiveStatusDemoView: View {
@State private var currentState = StatusState.idle
var body: some View {
VStack(spacing: 32) {
ReactiveStatusIndicator(status: $currentState)
HStack(spacing: 16) {
Button("Idle") { currentState = .idle }
.buttonStyle(.borderedProminent)
Button("Loading") { currentState = .loading }
.buttonStyle(.borderedProminent)
Button("Success") { currentState = .success }
.buttonStyle(.borderedProminent)
Button("Error") { currentState = .error }
.buttonStyle(.borderedProminent)
}
.font(.headline)
}
.padding()
}
}
#Preview {
ReactiveStatusDemoView()
}