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.

Leave a rating/review
Download materials
Save for later
Share

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 and TimelineView.
  • 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.

World Clock app starter project

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.

World Clock app showing times in four cities

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.

Summary of the information for a selected 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:

App time out of sync with clock

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:

App in sync with 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:

  1. 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 inside Canvas 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 the Rectangle view as the width of the rectangle and size.height as a height parameter. This makes the rectangle fill the full height of the canvas.
  2. You call fill(_:with:) on the context to draw a filled shape.
  3. To specify the object to draw, you create a path using the rectangle as the first parameter. This defines the area to fill.
  4. 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:

Rectangles representing night show in Canvas view

Next, you’ll take care of the warnings.