Skip to main content

Interaktive Ladeanimationen

Beschreibung

Diese View zeigt eine verspielte Ladeanimation mit mehreren Icons, die in einer kreisförmigen Bewegung wirbeln und beim Tippen auf den Bildschirm springen. Nutzer können die Animation durch Tippen oder Ziehen beeinflussen, um die Wartezeit unterhaltsamer zu gestalten.

🔍 Zweck

  • Beim Laden von Inhalten in Social-Media-Apps.
  • Während des Abrufs großer Datensätze in Business-Anwendungen.
  • Im Onboarding-Prozess, wenn Daten im Hintergrund vorbereitet werden.
  • Während der Netzwerkverbindung oder Authentifizierung.
  • Als kreative Überbrückung bei spielerischen Apps und Lernanwendungen.

📄 Codebeispiel

import SwiftUI

struct InteractiveLoadingAnimationView: View {
    // Number of icons in the swirl
    private let iconCount = 6
    // Array of SF Symbols for variety
    private let icons = ["star.fill", "bolt.fill", "heart.fill", "leaf.fill", "flame.fill", "cloud.fill"]
    
    @State private var rotation: Double = 0
    @State private var isBouncing: [Bool]
    @GestureState private var dragAngle: Angle = .zero
    
    init() {
        // Initialize all bounce states to false
        _isBouncing = State(initialValue: Array(repeating: false, count: iconCount))
    }
    
    var body: some View {
        GeometryReader { geo in
            ZStack {
                // Background swirl pattern
                ForEach(0..<iconCount, id: \.self) { i in
                    let angle = (Double(i) / Double(iconCount)) * 2 * .pi + rotation + dragAngle.radians
                    let radius = min(geo.size.width, geo.size.height) * 0.28
                    
                    IconBounceView(
                        systemName: icons[i % icons.count],
                        color: Color.accentColor.opacity(0.8),
                        isBouncing: isBouncing[i]
                    )
                    .frame(width: 44, height: 44)
                    .position(
                        x: geo.size.width/2 + CGFloat(cos(angle)) * radius,
                        y: geo.size.height/2 + CGFloat(sin(angle)) * radius
                    )
                    .onTapGesture {
                        triggerBounce(index: i)
                    }
                }
                // Center spinner
                ProgressView()
                    .progressViewStyle(CircularProgressViewStyle(tint: Color.accentColor))
                    .scaleEffect(1.4)
            }
            .contentShape(Rectangle())
            .gesture(
                DragGesture(minimumDistance: 5)
                    .updating($dragAngle) { value, state, _ in
                        let center = CGPoint(x: geo.size.width/2, y: geo.size.height/2)
                        let startVector = CGVector(dx: value.startLocation.x - center.x, dy: value.startLocation.y - center.y)
                        let currentVector = CGVector(dx: value.location.x - center.x, dy: value.location.y - center.y)
                        let angleDiff = atan2(currentVector.dy, currentVector.dx) - atan2(startVector.dy, startVector.dx)
                        state = Angle(radians: Double(angleDiff))
                    }
            )
            .onAppear {
                withAnimation(Animation.linear(duration: 3.5).repeatForever(autoreverses: false)) {
                    rotation = 2 * .pi
                }
            }
        }
        .background(Color(.systemGroupedBackground).ignoresSafeArea())
        .frame(minWidth: 200, minHeight: 200)
        // Tap anywhere to bounce all icons!
        .onTapGesture {
            for i in isBouncing.indices {
                triggerBounce(index: i)
            }
        }
    }
    
    // Triggers a bounce animation for the given index
    private func triggerBounce(index: Int) {
        guard !isBouncing[index] else { return }
        isBouncing[index] = true
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.46) {
            isBouncing[index] = false
        }
    }
}

struct IconBounceView: View {
    let systemName: String
    let color: Color
    let isBouncing: Bool
    
    @State private var bounceAmount: CGFloat = 0
    
    var body: some View {
        Image(systemName: systemName)
            .resizable()
            .scaledToFit()
            .padding(10)
            .background(
                Circle().fill(color.opacity(0.19))
            )
            .foregroundColor(color)
            .scaleEffect(isBouncing ? 1.28 : 1)
            .offset(y: isBouncing ? -24 : 0)
            .animation(
                Animation.interpolatingSpring(stiffness: 300, damping: 8),
                value: isBouncing
            )
            .shadow(color: color.opacity(0.22), radius: isBouncing ? 9 : 4, x: 0, y: isBouncing ? 8 : 3)
    }
}

#Preview {
    InteractiveLoadingAnimationView()
}