Wes Matlock

SwiftUI Canvas — The Art of Drawing in Code

Let’s get real for a minute. There’s something raw about writing code that transforms into art, and Canvas in SwiftUI makes that happen. On…


SwiftUI Canvas — The Art of Drawing in Code


Let’s get real for a minute. There’s something raw about writing code that transforms into art, and Canvas in SwiftUI makes that happen. On iOS 17+, this isn’t your everyday UI work — it’s like firing up a guitar solo right in your app. Today’s setlist is packed with a fresh lineup of views that combine kinetic visuals with real-world utility. Whether you’re forging a pulsating circle, a fluid sine wave, a spinning square, a burst of explosive particles, or a high-octane progress meter, these examples are sure to get your code head-banging.


The Essentials of Canvas

Canvas gives you a blank slate — a digital slab where you can carve out custom shapes, strokes, and animations. Think of it as your personal anvil for hammering out ideas into tangible art. Below, I’ve updated our lineup with code that’s been battle-tested in real projects.


1. Pulsating Circle — A Heartbeat of Metal

Imagine a circle that throbs like the steady pulse of a bass drum in a metal anthem. This snippet creates a circle that expands and contracts with pure energy:

struct PulsatingCircle: View {  
    var body: some View {  
        TimelineView(.animation) { timeline in  
            let t = timeline.date.timeIntervalSinceReferenceDate  
            let anim = (sin(t * 2 * .pi) + 1) / 2  
              
            Canvas { context, size in  
                let baseRadius = size.width * 0.3  
                let radius = baseRadius * (0.8 + 0.4 * anim)  
                let rect = CGRect(  
                    x: (size.width - radius) / 2,  
                    y: (size.height - radius) / 2,  
                    width: radius,  
                    height: radius  
                )  
                let circle = Path(ellipseIn: rect)  
                context.fill(circle, with: .color(.blue))  
            }  
        }  
    }  
}

Core Beats:

  • The animation is continuously driven by TimelineView for a smooth pulsation effect.
  • A sine function modulates the circle’s size, mimicking a natural heartbeat.
  • Canvas enables direct, declarative drawing within SwiftUI, keeping everything in one cohesive framework.


2. Sine Wave — Math in Motion, Metal in Spirit

Next up, we have a sine wave that roars across your screen — an elegant fusion of math and art. This example transforms basic trigonometry into a flowing visual experience:

struct SineWave: View {  
    var body: some View {  
        TimelineView(.animation) { timeline in  
            let phase = CGFloat(timeline.date.timeIntervalSinceReferenceDate)  
                .truncatingRemainder(dividingBy: (2 * .pi))  
              
            Canvas { context, size in  
                var path = Path()  
                let midHeight = size.height / 2  
                let amplitude = size.height / 4  
                let frequency = CGFloat.pi * 2 / size.width  
                  
                path.move(to: CGPoint(x: 0, y: midHeight))  
                for x in stride(from: 0, to: size.width, by: 1) {  
                    let y = midHeight + amplitude * sin(frequency * x + phase)  
                    path.addLine(to: CGPoint(x: x, y: y))  
                }  
                  
                context.stroke(path, with: .color(.red), lineWidth: 2)  
            }  
        }  
    }  
}

Core Beats:

  • Continuously recalculates the wave’s phase, resulting in a smooth, flowing motion.
  • Combines mathematical precision with visual creativity for a striking effect.
  • Canvas provides the perfect environment for custom-drawn, animated graphics.

3. Rotating Square — Spinning Metal in Motion

Crank up the volume with a square that spins like a record on a turntable blasting metal anthems. This view translates the drawing context to the center, rotates it, and then translates it back — ensuring the square rotates around its core:

struct RotatingSquare: View {  
    var body: some View {  
        TimelineView(.animation) { timeline in  
            let time = timeline.date.timeIntervalSinceReferenceDate.truncatingRemainder(dividingBy: 3)  
            let angle = time / 3 * 360  
              
            Canvas { context, size in  
                let squareRect = CGRect(  
                    x: size.width * 0.25,  
                    y: size.height * 0.25,  
                    width: size.width * 0.5,  
                    height: size.height * 0.5  
                )  
                var square = Path()  
                square.addRect(squareRect)  
                  
                context.translateBy(x: size.width / 2, y: size.height / 2)  
                context.rotate(by: .degrees(angle))  
                context.translateBy(x: -size.width / 2, y: -size.height / 2)  
                context.fill(square, with: .color(.green))  
            }  
        }  
    }  
}

Core Beats:

  • Real-time rotation is calculated for a continuously spinning square.
  • Context translation centers the rotation, ensuring smooth, precise movement.
  • A minimalist design that demonstrates how to harness Canvas for dynamic transformations.

4. Particle Explosion — Unleash the Chaos

Experience a burst of explosive energy with our ParticleExplosion view. Using Combine’s Timer publisher, this example reinitializes an explosion cycle every 1.5 seconds, drawing yellow sparks on a pitch-black backdrop during the first second of each cycle:

struct Particle: Identifiable {  
    let id = UUID()  
    let initialPosition: CGPoint  
    let velocity: CGVector  
    let radius: CGFloat  
}  
  
struct ParticleExplosion: View {  
    let particleCount = 50  
    let cycle: TimeInterval = 1.5  
    let explosionDuration: TimeInterval = 1.0  
    @State private var particles: [Particle] = []  
    @State private var startTime: Date = Date()  
    @State private var timerCancellable: AnyCancellable? = nil  
      
    var body: some View {  
        TimelineView(.animation) { timeline in  
            let t = timeline.date.timeIntervalSince(startTime)  
            let dt = t.truncatingRemainder(dividingBy: cycle)  
              
            Canvas { context, size in  
                guard dt < explosionDuration else { return }  
                  
                for particle in particles {  
                    let newX = particle.initialPosition.x + particle.velocity.dx * CGFloat(dt)  
                    let newY = particle.initialPosition.y + particle.velocity.dy * CGFloat(dt)  
                    let newPosition = CGPoint(x: newX, y: newY)  
                    let rect = CGRect(  
                        x: newPosition.x - particle.radius,  
                        y: newPosition.y - particle.radius,  
                        width: particle.radius * 2,  
                        height: particle.radius * 2  
                    )  
                    let circle = Path(ellipseIn: rect)  
                    context.fill(circle, with: .color(.yellow))  
                }  
            }  
        }  
        .frame(height: 300)  
        .background(Color.black)  
        .onAppear {  
            startTime = Date()  
            initializeParticles(size: CGSize(width: 300, height: 300))  
              
            timerCancellable = Timer.publish(every: cycle, on: .main, in: .common)  
                .autoconnect()  
                .sink { _ in  
                    initializeParticles(size: CGSize(width: 300, height: 300))  
                    startTime = Date()  
                }  
        }  
        .onDisappear {  
            timerCancellable?.cancel()  
        }  
    }  
      
    func initializeParticles(size: CGSize) {  
        let center = CGPoint(x: size.width / 2, y: size.height / 2)  
        particles = (0..<particleCount).map { _ in  
            let angle = Double.random(in: 0..<2 * Double.pi)  
            let speed = Double.random(in: 50...150)  
            let velocity = CGVector(dx: cos(angle) * speed, dy: sin(angle) * speed)  
            let radius = CGFloat.random(in: 2...5)  
            return Particle(initialPosition: center, velocity: velocity, radius: radius)  
        }  
    }  
}

Core Beats:

  • A Timer publisher resets the explosion cycle, creating rhythmic bursts of energy.
  • Particles are dynamically reinitialized, ensuring continuous, fresh visuals.
  • The effect is confined to a specific time window for a crisp, impactful explosion.

5. Progress Ring — The Metallic Progress Meter

Turn up the visual intensity with a sleek circular progress indicator — our ProgressRing. It transforms a mundane loading screen into a high-octane gauge that adds flair to your app:

struct ProgressRing: View {  
    var progress: Double // value between 0 and 1  
      
    var body: some View {  
        Canvas { context, size in  
            let lineWidth: CGFloat = 10  
            let radius = min(size.width, size.height) / 2 - lineWidth  
            let center = CGPoint(x: size.width / 2, y: size.height / 2)  
              
            var background = Path()  
            background.addArc(center: center, radius: radius, startAngle: .degrees(0), endAngle: .degrees(360), clockwise: false)  
            context.stroke(background, with: .color(.gray), lineWidth: lineWidth)  
              
            var foreground = Path()  
            foreground.addArc(center: center, radius: radius, startAngle: .degrees(-90), endAngle: .degrees(-90 + 360 * progress), clockwise: false)  
            context.stroke(foreground, with: .color(.orange), lineWidth: lineWidth)  
        }  
        .frame(width: 150, height: 150)  
    }  
}

Core Beats:

  • Renders a static background ring alongside a dynamic foreground arc.
  • Use Canvas to achieve precise circular drawing and styling.
  • Perfect for converting standard progress indicators into visual statements.

6. ProgressRingDemo — Watch the Meter Rock

To bring the ProgressRing to life, we built ProgressRingDemo. This view uses TimelineView to animate the progress value with a sine wave, smoothly cycling from empty to full and back again:

struct ProgressRingDemo: View {  
    var body: some View {  
        TimelineView(.animation) { timeline in  
            let t = timeline.date.timeIntervalSinceReferenceDate  
            let progress = (sin(t * 2 * .pi) + 1) / 2  
            ProgressRing(progress: progress)  
        }  
    }  
}

Core Beats:

  • Animates progress with a smooth, rhythmic sine wave.
  • Integrates seamlessly with the custom-drawn ProgressRing.
  • Offers a visually engaging way to represent progress that goes beyond the ordinary.


Wrapping It Up in Heavy Metal

Working with Canvas in SwiftUI is like forging art out of molten metal — watching your ideas heat up, cool down, and finally take shape on the screen. Whether it’s the pulsating heartbeat of a circle, the fluid motion of a sine wave, the spinning of a square, the explosive burst of particles, or the kinetic energy of a progress meter, these examples prove that your UI can rock as hard as you do.


If you’re hungry for more tips, tricks, and a few battle-tested stories from the trenches of native mobile development, swing by my collection of articles: https://medium.com/@wesleymatlock.

These posts are packed with real-world solutions, some laughs, and the kind of knowledge that’s saved me from a few late-night debugging sessions. Let’s keep building apps that rock — and if you’ve got questions or stories of your own, drop me a line. I’d love to hear from you.

Fire up your IDE, hammer out some code, and let your app roar like a true metalhead. Rock on! 🤘🏻

By Wesley Matlock on April 7, 2025.

Canonical link

Exported from Medium on May 10, 2025.

Written on April 7, 2025