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