Skip to main content

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()
}