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
Every version of SwiftUI brings new features and new views. Some expand SwiftUI with additional features in UIKit or AppKit, while others add new and unique functionality. The third version of SwiftUI, which arrived with iOS 15 and corresponding operating systems, brought higher-performance drawing with the canvas view. It also introduced a new, time-based method to update views, which is called TimelineView
.
In this tutorial, you’ll learn how to:
- Use
Canvas
andTimelineView
. - Combine these two to produce animated graphics in the form of a clock.
It’s time to start!
Getting Started
Download the starter project by clicking Download Materials at the top or bottom of this tutorial. Open the project in the starter directory in Xcode. Build and run.
You’ll find a World Clock app that allows you to add multiple locations and view the current time in each place. Tap the map icon to move to the view where you can add, delete and move cities in the list. To add a location, slide down the view until you see a search field where you can enter the location’s name. When you go back to the main view, you’ll see each city’s time zone and the difference between the time in the first city and each of the others.
Below each city, a line shows the local time as it relates to day and night. Tapping a city takes you to a summary of the information for that city.
While exploring the app, you may notice a bug, which you’ll now address. Run the app, and wait until the time changes to a new minute. The times displayed in the app still show the time when the view first appeared. Tap a city to navigate to another view and return. The app now updates the time. As long as the user remains on the view, the app falls behind the passage of time:
In the next section, you’ll use a TimelineView
to fix this problem.
Using TimelineView
Open ContentView.swift and find the NavigationView
. Embed the entire contents of NavigationView
inside TimelineView
:
TimelineView(.everyMinute) { context in
// The entire content of NavigationView closure goes here.
}
After the above update, the NavigationView
looks like this:
NavigationView {
TimelineView(.everyMinute) { context in
List(locations.locations) { ... }
.onAppear { ... }
.onChange(of: locations.locations) { ... }
.navigationTitle("World Clock")
.toolbar { ... }
}
}
TimelineView
updates on a time schedule specified using a value implementing the TimelineSchedule
protocol. This protocol includes several built-in static types, one of which is the everyMinute
parameter. This specifies the view should update at the start of every minute.
Change the currentDate
parameter of LocationView
to:
currentDate: context.date,
The context
passed into the closure of a TimelineView
contains two properties. First, a date
property that you use here and contains the date of the update. It also contains a cadence
property that defines the rate at which the timeline updates the view.
Run the app and wait on the initial view until the minute changes. This time, you’ll see the view update in sync with the phone’s clock:
In the next section, you’ll explore the new drawing view available in SwiftUI 3.0 — the canvas.
Introducing the Canvas View
The new canvas view supports high-performance, immediate mode drawing in SwiftUI. The view provides both a graphics context of type GraphicsContext
and a CGSize
property with the dimensions of the canvas to its closure, allowing you to adapt to the size of the canvas.
Open DaytimeGraphicsView.swift. This view creates the day/night image for a provided day and location using standard SwiftUI drawing views. In this section, you’ll modify this view to use a canvas instead. The view uses GeometryReader
to adapt the shapes to the size of the containing view. A canvas already provides this information as one of the parameters to the closure. Replace GeometryReader
with the following:
Canvas { context, size in
A canvas isn’t a drop-in replacement for GeometryReader
, but in this case, it provides the same information. The size
parameter from Canvas
contains the same dimensions that you get from GeometryReader
.
Change the four references to proxy.size.width
inside the closure to size.width
and the one reference to proxy.size.height
to size.height
.
You’re not done yet.
The view draws three rectangles, one each for:
- Pre-dawn hours, in black.
- Daytime hours, in blue.
- After-sunset hours, in black again.
The context
represents the drawing area of the canvas. If you’re familiar with Core Graphics, you’ll feel right at home here. The canvas drawing model builds on Core Graphics and uses many of the same methods and structures.
Replace the first Rectangle
view with the following code:
// 1
let preDawnRect = CGRect(
x: 0,
y: 0,
width: sunrisePosition,
height: size.height)
// 2
context.fill(
// 3
Path(preDawnRect),
// 4
with: .color(.black))
This code draws the pre-dawn portion of the drawing in the following steps:
- To draw a rectangle in a canvas, you create a
CGRect
struct that defines the position and size of the rectangle. Unlike a SwiftUI view, you must specify all dimensions and can’t assume the rectangle will fill the view. The dimension and axes insideCanvas
follow the same rules as Core Graphics. The origin, where the x and y both equal zero, lies at the top-left corner. Values of x increase moving right, and values of y increase going down. The coordinates here put the top-left corner of the rectangle at the top-left corner of the drawing space. You use the width of the frame previously applied to theRectangle
view as the width of the rectangle andsize.height
as a height parameter. This makes the rectangle fill the full height of the canvas. - You call
fill(_:with:)
on the context to draw a filled shape. - To specify the object to draw, you create a path using the rectangle as the first parameter. This defines the area to fill.
- You pass black as the color to draw the filled shape. Notice that you use a SwiftUI color definition.
Run the app, ignoring the warnings for now, and you’ll see that only the pre-dawn portions of the drawings appear:
Next, you’ll take care of the warnings.