Using TimelineView and Canvas in SwiftUI
Learn how to use TimelineView and Canvas in SwiftUI and combine them to produce animated graphics. By Bill Morefield.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Using TimelineView and Canvas in SwiftUI
30 mins
Drawing Inside a Canvas
The Rectangle
views inside the canvas don’t render because the closure to a canvas isn’t a view builder. This is different from almost every other SwiftUI closure. In exchange for losing the ability to use SwiftUI views inside a Canvas
directly, you gain access to some powerful Core Graphics APIs that you can now mix and match with SwiftUI.
Replace the remaining two rectangle views with the following code:
let dayRect = CGRect(
x: sunrisePosition,
y: 0,
width: sunsetPosition - sunrisePosition,
height: size.height)
context.fill(
Path(dayRect),
with: .color(.blue))
let eveningRect = CGRect(
x: sunsetPosition,
y: 0,
width: size.width - sunsetPosition,
height: size.height)
context.fill(
Path(eveningRect),
with: .color(.black))
As before, the rectangle fills the full vertical space of the canvas. You move the offset
previously applied to the Rectangle
view to the rectangle’s x
coordinate. As before, the width of the frame
applied to Rectangle
becomes the width of each rectangle.
The last code in this view draws a yellow line at midnight and noon on the graph. Replace the current ForEach
view with:
// 1
for hour in [0, 12] {
// 2
var hourPath = Path()
// 3
let position = Double(hour) / 24.0 * size.width
// 4
hourPath.move(to: CGPoint(x: position, y: 0))
// 5
hourPath.addLine(to: CGPoint(x: position, y: size.height))
// 6
context.stroke(
hourPath,
with: .color(.yellow),
lineWidth: 3.0)
}
Here’s how the code works, step by step:
- Since you’re not inside a view builder, you use a
for-in
loop to iterate over a collection of integers, with 0 representing midnight and 12 representing noon. - You create an empty path that you’ll add inside the canvas.
- To determine the horizontal coordinate of the line, convert the hour to
Double
and then divide it by 24.0 to get the fraction of a full day that the hour represents. Then, multiply this fraction by the width of the canvas to get the horizontal position that represents the hour. -
move(to:)
on the path moves the current position without adding to the path. It moves the current position to the horizontal position from step three and to the top of the view. -
addLine(to:)
adds a line from the current position to the position specified to the path. This position is at the same horizontal coordinate at the bottom of the view. - You now use
stroke(_:with:lineWidth:)
on the context to draw, not fill, the path. You specify a yellow color and a width of three points to help the line stand out.
Build and run. You’ll see the views look the same as before, but use Canvas
instead of SwiftUI shape views:
The main reason to use a canvas is performance. For complex drawings with many gradients or parts, you’ll see much better performance than with SwiftUI views. A canvas view also provides compatibility with Core Graphics, including access to a Core-Graphics-enabled wrapper. If you have existing code created using Core Graphics, like custom controls written for UIView
and rendered in draw(_:)
, you can drop it inside a canvas without modification.
What do you lose in a canvas view? As you saw in this example, a canvas often needs more verbose code. The canvas exists as a single element, and you can’t address and modify the components individually like with SwiftUI views. You can add onTapGesture(count:perform:)
to a canvas, but not to a path in the canvas.
A canvas also provides one more function. You can combine it with TimelineView
to perform animations. You’ll explore that in the rest of this tutorial as you create an analog clock for the app.
Drawing a Clock Face
TimelineView
provides a way to update a view regularly, while a canvas view offers a way to create high-performance graphics. In this section, you’ll do just that by creating an animated analog clock showing the selected city’s time on the details page.
Open AnalogClock.swift. Replace the body of the view with the following code:
Canvas { gContext, size in
// 1
let clockSize = min(size.width, size.height) * 0.9
// 2
let centerOffset = min(size.width, size.height) * 0.05
// 3
let clockCenter = min(size.width, size.height) / 2.0
// 4
let frameRect = CGRect(
x: centerOffset,
y: centerOffset,
width: clockSize,
height: clockSize)
}
This code defines a Canvas
view and calculates the size of the clock face based on the size of the view:
- You first determine the smaller dimension between the width and height of the canvas. You multiply this value by 0.9 to set the size of the face to fill 90% of the smaller dimension.
- To center the clock in the canvas, determine the smaller dimension and multiply it by 0.05 to get half of the 10% remaining from step one. This value will be the top-left corner for the rectangle containing the clock face.
- You determine the clock’s center coordinate by dividing the smaller dimension by two. This gives you both the horizontal and vertical center position since the clock is symmetrical. You’ll use this value later in this tutorial.
- You define a rectangle using the offset from step two and the size from step one. This rectangle encloses the clock face.
Now, you’ll draw the clock face. Continue the closure of the canvas with the following code:
// 1
gContext.withCGContext { cgContext in
// 2
cgContext.setStrokeColor(
location.isDaytime(at: time) ?
UIColor.black.cgColor : UIColor.white.cgColor)
// 3
cgContext.setFillColor(location.isDaytime(at: time) ? dayColor : nightColor)
cgContext.setLineWidth(2.0)
// 4
cgContext.addEllipse(in: frameRect)
// 5
cgContext.drawPath(using: .fillStroke)
}
Here’s how the code works, step by step:
- As mentioned earlier, the
Canvas
view supports Core Graphics drawing. However, thegContext
parameter you get inside the canvas closure is still a wrapper around Core Graphics. To get all the way down to Core Graphics, you callGraphicsContext.withCGContext(content:)
. This creates and passes a true Core Graphics context to the corresponding closure, where you can use all the Core Graphics code. Changes to the graphics state made in either the canvas or Core Graphics contexts persist until the end of the closure. - You use the Core Graphics’
setStrokeColor(_:)
to set the line color based on if it’s day at the specified time. For daytime, you set it to black, and for night, you set it to white. You useCGColor
since this is a Core Graphics call. - Then, you set the fill color using the
dayColor
andnightColor
properties. You also set the line width to two points. - To draw the clock face, call
addEllipse(in:)
on the Core Graphics context using the rectangle from earlier that defines the edges of the ellipse. - Finally, you draw the path, consisting of the ellipse from step four, onto the view.
To view the clock, open LocationDetailsView.swift. Wrap VStack
inside TimelineView
like this:
TimelineView(.animation) { context in
// Existing VStack
}
This creates TimelineView
using the animation
static identifier that updates the view as fast as possible. Change the reference to Date()
in the second Text
view to context.date
:
Text(timeInLocalTimeZone(context.date, showSeconds: showSeconds))
Now, add the following code after the existing text fields, before the spacer:
AnalogClock(time: context.date, location: location)
This will show the new analog clock on the view. Build and run, and tap one of the cities to view its details page. You’ll see your new clock face:
You’ll see a simple black or blue circle. Next, you’ll add static tick marks to help the user tell the displayed time.