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 Canvas for 200+ particles.
  • Colored variation: Replace .white.opacity(0.7) with random colors for a confetti effect.
  • Gravity variation: Use .easeIn instead of .easeOut to simulate lighter-than-air physics.

iOS Version: iOS 15+

Get New Tutorials by Email

No spam. Just clear, practical breakdowns you can apply right away.

Enjoy this tutorial?

Get new practical tech tutorials in your inbox.