What You Will Build
A full-screen particle system where dozens of small translucent circles float upward against a dark background, fading as they rise. This effect works for ambient loading screens, celebration overlays, or decorative backgrounds in music and entertainment apps.
Why This Pattern Matters
Particle systems make an app feel alive. Apple uses subtle particle animations in Screen Time charts and the Apple TV screensaver. Building one in SwiftUI teaches you three core skills: managing arrays of animated objects with @State, staggering animations with random delays, and using .repeatForever for continuous motion.
Key SwiftUI Concepts
- @State arrays for managing many animated objects at once.
- withAnimation(.repeatForever) to create perpetual motion loops.
- DispatchQueue.main.asyncAfter to stagger particle start times.
Step 1: Define the Particle Model
private struct BubbleParticle: Identifiable {
let id = UUID()
var size: CGFloat
var positionX: CGFloat
var positionY: CGFloat
var speed: Double
var opacity: Double
}
Step 2: Generate 100 Particles Below the Screen
On appear, create particles just off the bottom edge with randomized properties:
func createBubbleParticles() {
for _ in 0..<100 {
let size = CGFloat.random(in: 5...15)
let positionX = CGFloat.random(in: 0...UIScreen.main.bounds.width)
let positionY = UIScreen.main.bounds.height + size
let speed = Double.random(in: 4.0...8.0)
let opacity = Double.random(in: 0.5...1.0)
bubbles.append(BubbleParticle(
size: size, positionX: positionX,
positionY: positionY, speed: speed, opacity: opacity
))
}
}
Step 3: Animate Each Particle Upward
Each particle gets a random delay so they launch at different times. The animation moves positionY above the screen and fades opacity to zero:
func animateBubbleParticle(at index: Int) {
let delay = Double.random(in: 0...5)
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
withAnimation(.easeOut(duration: bubbles[index].speed)
.repeatForever(autoreverses: false)) {
bubbles[index].positionY = -100
bubbles[index].opacity = 0
}
}
}
Step 4: Assemble the View
struct BubbleAnimationDemo: View {
@State private var bubbles: [BubbleParticle] = []
var body: some View {
ZStack {
Color.black.ignoresSafeArea()
ForEach(bubbles.indices, id: \.self) { index in
Circle()
.fill(.white.opacity(0.7))
.frame(width: bubbles[index].size,
height: bubbles[index].size)
.position(x: bubbles[index].positionX,
y: bubbles[index].positionY)
.opacity(bubbles[index].opacity)
.onAppear { animateBubbleParticle(at: index) }
}
}
.onAppear(perform: createBubbleParticles)
}
}
Tips and Pitfalls
- Performance: 100+ animated views can drop frames on older devices. Use
Canvasfor 200+ particles. - Colored variation: Replace
.white.opacity(0.7)with random colors for a confetti effect. - Gravity variation: Use
.easeIninstead of.easeOutto simulate lighter-than-air physics.
iOS Version: iOS 15+