How to create custom animated drawings with TimelineView and Canvas
How to create custom animated drawings with TimelineView and Canvas êŽë š
Updated for Xcode 15
New in iOS 15
SwiftUIâs Canvas
view lets us have free control over drawing in our app, and its TimelineView
lets us redraw views as often as we need. If we combine these two views together we can create much more advanced effects, including particle systems to simulate rain, snow, fog, fire, and more.
To demonstrate this we can produce some code that creates a simple rain effect.
This starts by defining the data a single drop of rain needs to know in order to work: where it is located horizontally, and when it should be removed from the screen, and how the speed it should fall. That speed property is important, because it means our raindrops wonât all fall at a uniform speed â it makes the whole effect look a lot more realistic.
Alongside a single raindrop, weâre also going to create a class to manage the whole rainstorm. This will have a set of Raindrop
instances, and a method that removes any old raindrops, and creates a new one with a random location and speed.
Start with this:
struct Raindrop: Hashable, Equatable {
var x: Double
var removalDate: Date
var speed: Double
}
class Storm: ObservableObject {
var drops = Set<Raindrop>()
func update(to date: Date) {
drops = drops.filter { $0.removalDate > date }
drops.insert(Raindrop(x: Double.random(in: 0...1), removalDate: date + 1, speed: Double.random(in: 1...2)))
}
}
There are two important things about that code:
- Each rain dropâs X position is a value between 0 and 1, meaning somewhere between the left edge (0) and the right (1).
- The
removalDate
property is set to the current time plus 1 second, so all our raindrops live for 1 second.
Finally, we can create a TimelineView
and Canvas
. This will:
- Use the
.animation
schedule for the timeline so that it draws as fast as possible. - Call
update()
on our storm, passing in the current date. - For each rain drop, figure out how much time we have until it is removed.
- The dropâs X position is calculated as its
x
value multiplied by the width of our drawing context. Remember,x
will be between 0 and 1, so this will produce a value between 0 and the width of our drawing context. - The dropâs Y position will be its age multiplied by its speed, multiplied by the height of the canvas. If we left it there our drops would âfallâ upwards, but we subtract that from the canvas height they will fall downwards.
- We can now fill a thin and long capsule at that X and Y coordinate, with a suitably rainy color.
Hereâs that in code:
struct ContentView: View {
@StateObject private var storm = Storm()
let rainColor = Color(red: 0.25, green: 0.5, blue: 0.75)
var body: some View {
TimelineView(.animation) { timeline in
Canvas { context, size in
storm.update(to: timeline.date)
for drop in storm.drops {
let age = timeline.date.distance(to: drop.removalDate)
let rect = CGRect(x: drop.x * size.width, y: size.height - (size.height * age * drop.speed), width: 2, height: 10)
let shape = Capsule().path(in: rect)
context.fill(shape, with: .color(rainColor))
}
}
}
.background(.black)
.ignoresSafeArea()
}
}
Thatâs not a huge amount of code, but it already creates a pretty compelling effect. From here you can start to consider things like having different opacities for raindrops, placing them at a slight angle, and more.
Over on Hacking with Swift+ I have a whole series of tutorials teaching you how to recreate the particle effects in Appleâs Weather app using Canvas
and TimelineView
: .