Utilizing TimelineView and Canvas in SwiftUI

0
11


Each model of SwiftUI brings new options and new views. Some increase SwiftUI with extra options in UIKit or AppKit, whereas others add new and distinctive performance. The third model of SwiftUI, which arrived with iOS 15 and corresponding working methods, introduced higher-performance drawing with the canvas view. It additionally launched a brand new, time-based methodology to replace views, which is known as TimelineView.

On this tutorial, you’ll learn to:

  • Use Canvas and TimelineView.
  • Mix these two to provide animated graphics within the type of a clock.

It’s time to begin!

Getting Began

Obtain the starter venture by clicking Obtain Supplies on the high or backside of this tutorial. Open the venture within the starter listing in Xcode. Construct and run.

World Clock app starter project

You’ll discover a World Clock app that permits you to add a number of places and think about the present time in every place. Faucet the map icon to maneuver to the view the place you possibly can add, delete and transfer cities within the checklist. So as to add a location, slide down the view till you see a search discipline the place you possibly can enter the situation’s title. If you return to the primary view, you’ll see every metropolis’s time zone and the distinction between the time within the first metropolis and every of the others.

World Clock app showing times in four cities

Under every metropolis, a line exhibits the native time because it pertains to day and evening. Tapping a metropolis takes you to a abstract of the knowledge for that metropolis.

Summary of the information for a selected city

Whereas exploring the app, chances are you’ll discover a bug, which you’ll now handle. Run the app, and wait till the time modifications to a brand new minute. The instances displayed within the app nonetheless present the time when the view first appeared. Faucet a metropolis to navigate to a different view and return. The app now updates the time. So long as the person stays on the view, the app falls behind the passage of time:

App time out of sync with clock

Within the subsequent part, you’ll use a TimelineView to repair this downside.

Utilizing TimelineView

Open ContentView.swift and discover the NavigationView. Embed the whole contents of NavigationView inside TimelineView:


TimelineView(.everyMinute) { context in
  // Your entire content material of NavigationView closure goes right here.
}

After the above replace, the NavigationView seems to be like this:


NavigationView {
  TimelineView(.everyMinute) { context in
    Checklist(places.places) { ... }
    .onAppear { ... }
    .onChange(of: places.places) { ... }
    .navigationTitle("World Clock")
    .toolbar { ... }
  }
}

TimelineView updates on a time schedule specified utilizing a price implementing the TimelineSchedule protocol. This protocol consists of a number of built-in static varieties, one in every of which is the everyMinute parameter. This specifies the view ought to replace in the beginning of each minute.

Change the currentDate parameter of LocationView to:


currentDate: context.date,

The context handed into the closure of a TimelineView incorporates two properties. First, a date property that you just use right here and incorporates the date of the replace. It additionally incorporates a cadence property that defines the speed at which the timeline updates the view.

Run the app and wait on the preliminary view till the minute modifications. This time, you’ll see the view replace in sync with the cellphone’s clock:

App in sync with clock

Within the subsequent part, you’ll discover the brand new drawing view accessible in SwiftUI 3.0 — the canvas.

Introducing the Canvas View

The brand new canvas view helps high-performance, rapid mode drawing in SwiftUI. The view gives each a graphics context of sort GraphicsContext and a CGSize property with the size of the canvas to its closure, permitting you to adapt to the dimensions of the canvas.

Open DaytimeGraphicsView.swift. This view creates the day/evening picture for a supplied day and site utilizing commonplace SwiftUI drawing views. On this part, you’ll modify this view to make use of a canvas as a substitute. The view makes use of GeometryReader to adapt the shapes to the dimensions of the containing view. A canvas already gives this data as one of many parameters to the closure. Change GeometryReader with the next:


Canvas { context, measurement in

A canvas isn’t a drop-in alternative for GeometryReader, however on this case, it gives the identical data. The measurement parameter from Canvas incorporates the identical dimensions that you just get from GeometryReader.

Change the 4 references to proxy.measurement.width contained in the closure to measurement.width and the one reference to proxy.measurement.top to measurement.top.

You’re not performed but.

The view attracts three rectangles, one every for:

  • Pre-dawn hours, in black.
  • Daytime hours, in blue.
  • After-sunset hours, in black once more.

The context represents the drawing space of the canvas. Should you’re aware of Core Graphics, you’ll really feel proper at residence right here. The canvas drawing mannequin builds on Core Graphics and makes use of lots of the identical strategies and buildings.

Change the primary Rectangle view with the next code:


// 1
let preDawnRect = CGRect(
  x: 0,
  y: 0,
  width: sunrisePosition,
  top: measurement.top)
// 2
context.fill(
  // 3
  Path(preDawnRect),
  // 4
  with: .shade(.black))

This code attracts the pre-dawn portion of the drawing within the following steps:

  1. To attract a rectangle in a canvas, you create a CGRect struct that defines the place and measurement of the rectangle. Not like a SwiftUI view, you have to specify all dimensions and may’t assume the rectangle will fill the view. The dimension and axes inside Canvas comply with the identical guidelines as Core Graphics. The origin, the place the x and y each equal zero, lies on the top-left nook. Values of x enhance shifting proper, and values of y enhance taking place. The coordinates right here put the top-left nook of the rectangle on the top-left nook of the drawing house. You employ the width of the body beforehand utilized to the Rectangle view because the width of the rectangle and measurement.top as a top parameter. This makes the rectangle fill the complete top of the canvas.
  2. You name fill(_:with:) on the context to attract a crammed form.
  3. To specify the thing to attract, you create a path utilizing the rectangle as the primary parameter. This defines the world to fill.
  4. You move black as the colour to attract the crammed form. Discover that you just use a SwiftUI shade definition.

Run the app, ignoring the warnings for now, and also you’ll see that solely the pre-dawn parts of the drawings seem:

Rectangles representing night show in Canvas view

Subsequent, you’ll handle the warnings.

Drawing Inside a Canvas

The Rectangle views contained in the canvas don’t render as a result of the closure to a canvas isn’t a view builder. That is totally different from virtually each different SwiftUI closure. In trade for shedding the power to make use of SwiftUI views inside a Canvas immediately, you achieve entry to some highly effective Core Graphics APIs which you can now combine and match with SwiftUI.

Change the remaining two rectangle views with the next code:


let dayRect = CGRect(
  x: sunrisePosition,
  y: 0,
  width: sunsetPosition - sunrisePosition,
  top: measurement.top)
context.fill(
  Path(dayRect),
  with: .shade(.blue))

let eveningRect = CGRect(
  x: sunsetPosition,
  y: 0,
  width: measurement.width - sunsetPosition,
  top: measurement.top)
context.fill(
  Path(eveningRect),
  with: .shade(.black))

As earlier than, the rectangle fills the complete vertical house of the canvas. You progress the offset beforehand utilized to the Rectangle view to the rectangle’s x coordinate. As earlier than, the width of the body utilized to Rectangle turns into the width of every rectangle.

The final code on this view attracts a yellow line at midnight and midday on the graph. Change the present ForEach view with:


// 1
for hour in [0, 12] {
  // 2
  var hourPath = Path()
  // 3
  let place = Double(hour) / 24.0 * measurement.width
  // 4
  hourPath.transfer(to: CGPoint(x: place, y: 0))
  // 5
  hourPath.addLine(to: CGPoint(x: place, y: measurement.top))
  // 6
  context.stroke(
    hourPath,
    with: .shade(.yellow),
    lineWidth: 3.0)
}

Right here’s how the code works, step-by-step:

  1. Because you’re not inside a view builder, you utilize a for-in loop to iterate over a group of integers, with 0 representing midnight and 12 representing midday.
  2. You create an empty path that you just’ll add contained in the canvas.
  3. To find out the horizontal coordinate of the road, convert the hour to Double after which 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 place that represents the hour.
  4. transfer(to:) on the trail strikes the present place with out including to the trail. It strikes the present place to the horizontal place from step three and to the highest of the view.
  5. addLine(to:) provides a line from the present place to the place specified to the trail. This place is on the identical horizontal coordinate on the backside of the view.
  6. You now use stroke(_:with:lineWidth:) on the context to attract, not fill, the trail. You specify a yellow shade and a width of three factors to assist the road stand out.

Construct and run. You’ll see the views look the identical as earlier than, however use Canvas as a substitute of SwiftUI form views:

World Clock app showing four cities' times and day/night bars

The primary purpose to make use of a canvas is efficiency. For advanced drawings with many gradients or components, you’ll see a lot better efficiency than with SwiftUI views. A canvas view additionally gives compatibility with Core Graphics, together with entry to a Core-Graphics-enabled wrapper. In case you have present code created utilizing Core Graphics, like customized controls written for UIView and rendered in draw(_:), you possibly can drop it inside a canvas with out modification.

What do you lose in a canvas view? As you noticed on this instance, a canvas usually wants extra verbose code. The canvas exists as a single aspect, and you’ll’t handle and modify the parts individually like with SwiftUI views. You may add onTapGesture(depend:carry out:) to a canvas, however to not a path within the canvas.

A canvas additionally gives yet another perform. You may mix it with TimelineView to carry out animations. You’ll discover that in the remainder of this tutorial as you create an analog clock for the app.

Drawing a Clock Face

TimelineView gives a approach to replace a view frequently, whereas a canvas view affords a approach to create high-performance graphics. On this part, you’ll just do that by creating an animated analog clock exhibiting the chosen metropolis’s time on the main points web page.

Open AnalogClock.swift. Change the physique of the view with the next code:


Canvas { gContext, measurement in
  // 1
  let clockSize = min(measurement.width, measurement.top) * 0.9
  // 2
  let centerOffset = min(measurement.width, measurement.top) * 0.05
  // 3
  let clockCenter = min(measurement.width, measurement.top) / 2.0
  // 4
  let frameRect = CGRect(
    x: centerOffset,
    y: centerOffset,
    width: clockSize,
    top: clockSize)
}

This code defines a Canvas view and calculates the dimensions of the clock face primarily based on the dimensions of the view:

  1. You first decide the smaller dimension between the width and top of the canvas. You multiply this worth by 0.9 to set the dimensions of the face to fill 90% of the smaller dimension.
  2. To middle the clock within the canvas, decide the smaller dimension and multiply it by 0.05 to get half of the ten% remaining from the first step. This worth would be the top-left nook for the rectangle containing the clock face.
  3. You identify the clock’s middle coordinate by dividing the smaller dimension by two. This provides you each the horizontal and vertical middle place for the reason that clock is symmetrical. You’ll use this worth later on this tutorial.
  4. You outline a rectangle utilizing the offset from step two and the dimensions from the first step. This rectangle encloses the clock face.

Now, you’ll draw the clock face. Proceed the closure of the canvas with the next 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(utilizing: .fillStroke)
}

Right here’s how the code works, step-by-step:

  1. As talked about earlier, the Canvas view helps Core Graphics drawing. Nevertheless, the gContext parameter you get contained in the canvas closure continues to be a wrapper round Core Graphics. To get all the best way all the way down to Core Graphics, you name GraphicsContext.withCGContext(content material:). This creates and passes a real Core Graphics context to the corresponding closure, the place you need to use all of the Core Graphics code. Adjustments to the graphics state made in both the canvas or Core Graphics contexts persist till the top of the closure.
  2. You employ the Core Graphics’ setStrokeColor(_:) to set the road shade primarily based on if it’s day on the specified time. For daytime, you set it to black, and for evening, you set it to white. You employ CGColor since it is a Core Graphics name.
  3. Then, you set the fill shade utilizing the dayColor and nightColor properties. You additionally set the road width to 2 factors.
  4. To attract the clock face, name addEllipse(in:) on the Core Graphics context utilizing the rectangle from earlier that defines the perimeters of the ellipse.
  5. Lastly, you draw the trail, consisting of the ellipse from step 4, onto the view.

To view the clock, open LocationDetailsView.swift. Wrap VStack inside TimelineView like this:


TimelineView(.animation) { context in
  // Current VStack
}

This creates TimelineView utilizing the animation static identifier that updates the view as quick as attainable. Change the reference to Date() within the second Textual content view to context.date:


Textual content(timeInLocalTimeZone(context.date, showSeconds: showSeconds))

Now, add the next code after the prevailing textual content fields, earlier than the spacer:


AnalogClock(time: context.date, location: location)

It will present the brand new analog clock on the view. Construct and run, and faucet one of many cities to view its particulars web page. You’ll see your new clock face:

App displaying info for New York with empty clock face

You’ll see a easy black or blue circle. Subsequent, you’ll add static tick marks to assist the person inform the displayed time.

Drawing Tick Marks

Tick marks present twelve equal intervals across the clock face to point hours and five-minute increments. These assist the person higher inform the time displayed on the clock.

You’ll want plenty of trigonometry right here, however thou shalt not be afraid! You’ll stroll via all of the steps required.

Return to AnalogClock.swift and add the next new methodology above the physique of the view:


func drawTickMarks(context: CGContext, measurement: Double, offset: Double) {
  // 1
  let clockCenter = measurement / 2.0 + offset
  let clockRadius = measurement / 2.0
  // 2
  for hourMark in 0..<12 {
    // 3
    let angle = Double(hourMark) / 12.0 * 2.0 * Double.pi
    // 4
    let startX = cos(angle) * clockRadius + clockCenter
    let startY = sin(angle) * clockRadius + clockCenter
    // 5
    let endX = cos(angle) * clockRadius * 0.9 + clockCenter
    let endY = sin(angle) * clockRadius * 0.9 + clockCenter
    // 6
    context.transfer(to: CGPoint(x: startX, y: startY))
    // 7
    context.addLine(to: CGPoint(x: endX, y: endY))
    // 8
    context.strokePath()
  }
}

Separating the parts of the clock into totally different strategies helps cut back litter. You will move within the Core Graphics context to the strategy together with the dimensions and offset you calculated within the view’s physique. Listed below are the steps for the remainder of the strategy:

  1. You calculate the clock face’s middle place by dividing the dimensions of the clock face by two after which including the offset you handed in.
  2. Subsequent, arrange a loop via the integers from zero to 11, one for every tick mark. Discover, once more, you utilize a typical for-in loop as a substitute of ForEach since you are not in a view builder.
  3. You divide the clock face into twelve equal segments. For every section, you calculate the fraction of the complete circle’s diameter the present hourMark represents. Trigonometric calculations in Swift use radians. The traditional 360 levels of a circle equals 2π radians. To find out the variety of radians equal to the present fraction of the circle, multiply the fraction by two and by the Double.pi fixed.

    Word: Technically, you have to now subtract π/2 to shift the angle a quarter-circle counterclockwise. With out this adjustment, the zero angle will probably be to the precise and never upward. For these marks, it does not make a distinction, however in the event you change it to show numbers, then they would seem within the improper positions.

  4. You employ trigonometry right here, however do not panic. All it’s essential to know is that the cosine of an angle provides you the place of the horizontal a part of the complete radius for some extent at a given angle. Sine gives the identical data for the vertical place. Because you need the factors positioned at an equal distance across the middle of the clock face, you add the offset calculated in the first step. This provides you the x and y factors for the angle calculated in step three.
  5. This is similar as step 4, besides you multiply the radius by 0.9 to deliver the purpose contained in the clock face. The ensuing tick mark runs in from the sting of the face inside so far.
  6. With the factors calculated, you progress the context to the beginning level from step 4.
  7. Subsequent, add a line to the endpoint from step 5.
  8. Draw a line alongside the trail on the canvas.

Now, add the decision to the strategy on the backside of the closure, the place you bought the Core Graphics context:


drawTickMarks(
  context: cgContext,
  measurement: clockSize,
  offset: centerOffset)

Run the app, and faucet any metropolis to see the clock face with tick marks:

App showing details for Cairo with tick marks on clock face

With the tick marks in place, now you can add the arms for the clock.

Drawing Clock Palms

You will first create a reusable methodology that pulls all three clock arms. Add the next code after drawTickMarks(context:measurement:offset:):


func drawClockHand(
  context: CGContext,
  angle: Double,
  width: Double,
  size: Double
) {
  // 1
  context.saveGState()
  // 2
  context.rotate(by: angle)
  // 3
  context.transfer(to: CGPoint(x: 0, y: 0))
  context.addLine(to: CGPoint(x: -width, y: -length * 0.67))
  context.addLine(to: CGPoint(x: 0, y: -length))
  context.addLine(to: CGPoint(x: width, y: -length * 0.67))
  context.closePath()
  // 4
  context.fillPath()
  // 5
  context.restoreGState()
}

This methodology attracts a clock hand on the angle, width and size specified. Make the hour, minute and second arms totally different by altering the width and size. This is how the strategy works:

  1. saveGState() pushes a replica of the present graphics state onto a stack. You may restore the present state at a later time from the stack. Saving the state enables you to simply undo the modifications made throughout this methodology.
  2. When creating the tick marks, you calculated the positions of strains utilizing trigonometry. For conditions the place you need to present a number of strains or shapes, this could get tedious. rotate(by:) rotates each path that follows by a specified angle in radians. Utilizing this methodology, now you can draw the clock hand vertically and let this rotation deal with the maths to make it seem on the desired angle. Let computer systems do the laborious work!
  3. These strains transfer to the middle of the canvas — maintain that query for a second. It then attracts a line of the required width to the left and upward two-thirds of the complete size. It continues again to the middle the complete size upward earlier than mirroring the primary line to the precise of the middle. closePath() provides a line again to the preliminary level on the middle.
  4. You fill the form you simply outlined with the present fill shade.
  5. This restores the graphics state you saved in the first step. It undoes the change to the angle from the rotation in step two.

Now that you’ve a way to attract a hand, you possibly can draw the hour hand. Add the next code to the top of the Core Graphics closure simply after the decision to drawTickMarks(context:measurement:offset:):


// 1
cgContext.setFillColor(location.isDaytime(at: time) ?
  UIColor.black.cgColor : UIColor.white.cgColor)
// 2
cgContext.translateBy(x: clockCenter, y: clockCenter)
// 3
let angle = clockDecimalHourInLocalTz / 12.0 * 2 * Double.pi
let hourRadius = clockSize * 0.65 / 2.0
// 4
drawClockHand(
  context: cgContext,
  angle: angle,
  width: 7.5,
  size: hourRadius)

You modify the fill shade and calculate the knowledge wanted for the hand. Listed below are the main points:

  1. Change the fill shade to match the present line shade — black for daytime and white for evening.
  2. When drawing earlier than, you added an offset for the tick marks and clock face to middle them on the canvas. As with rotate(by:) above, you may also change the graphics state. translateBy(x:y:) shifts the origin of the drawing floor to the purpose you need to be the middle of the clock. This variation impacts all drawing operations that comply with. This shift enables you to use the origin in drawClockHand(context:angle:width:size:).
  3. Calculate the angle for the given hour. Word that clockDecimalHourInLocalTz features a fraction, so 1:30 could be 1.5. Together with fractions helps the graceful movement of the clock’s arms. Utilizing rotate(by:) whereas drawing the hand vertically earlier than the rotation means you do not want the shift by π/2 such as you did when manually calculating angles.
  4. Name the strategy that pulls the clock hand.

Run the app, and also you see your clock hand on the clock face:

App displaying info for Shibuya with hour hand on clock

Now, use the identical course of to attract the opposite arms. After the code to attract the hour hand, add:


let minuteRadius = clockSize * 0.75 / 2.0
let minuteAngle = clockMinuteInLocalTz / 60.0 * 2 * Double.pi
drawClockHand(
  context: cgContext,
  angle: minuteAngle,
  width: 5.0,
  size: minuteRadius)

cgContext.saveGState()
cgContext.setFillColor(UIColor.crimson.cgColor)
let secondRadius = clockSize * 0.85 / 2.0
let secondAngle = clockSecondInLocalTz / 60.0 * 2 * Double.pi
drawClockHand(
  context: cgContext,
  angle: secondAngle,
  width: 2.0,
  size: secondRadius)
cgContext.restoreGState()

You employ a bigger radius multiplier and narrower width to attract the minute hand. Then, you modify the fill shade to crimson and draw an extended, narrower second hand. Save and restore the graphics state round drawing the second hand to revive the unique fill shade.

Run the app, and you may see the minute and second arms together with the hour hand:

App displaying details for New York with all hands on the clock

Including a Middle Button

Subsequent, you will subsequent add a small circle on the middle of the clock face the place the arms meet. After the code to attract the second hand, add:


let buttonDiameter = clockSize * 0.05
let buttonOffset = buttonDiameter / 2.0
let buttonRect = CGRect(
  x: -buttonOffset,
  y: -buttonOffset,
  width: buttonDiameter,
  top: buttonDiameter)
cgContext.addEllipse(in: buttonRect)
cgContext.fillPath()

You calculate the diameter of 5 p.c of the clock face. Then, you shift the nook of the rectangle half of that diameter towards the higher left. Subsequent, you add an ellipse outlined by that rectangle and fill it.

Run the app to see the up to date clock face:

App displaying info for Cairo with full clock with center button

Yet one more factor, as Steve Jobs would say! You will add a show exhibiting the day of the month within the chosen metropolis to see combine SwiftUI views with a canvas.

Mixing SwiftUI Views Right into a Canvas

The canvas is not a ViewBuilder, that means you possibly can’t embrace SwiftUI views immediately. You noticed this when the rectangle views from DaytimeGraphicsView.swift did not present till you transformed them to canvas methodology calls. As an alternative, you possibly can move SwiftUI views to the canvas and reference them when drawing.

Open AnalogClock.swift, and beginning on the road with the closing brace for Canvas, add the next code:


symbols: {
  ClockDayView(time: time, location: location)
    .tag(0)
}

You move in SwiftUI views utilizing the symbols parameter of the canvas view initializer. You should tag every view with a novel identifier utilizing tag(_:).

Subsequent, it’s essential to use that tag to reference the view. On the high of the canvas closure, add the next code:


let dayView = gContext.resolveSymbol(id: 0)

This code seems to be for a SwiftUI view tagged with the handed identifier. If one exists, it is saved in dayView. If not, then the strategy returns nil.

On the finish of the view, after the top of withCGContext(content material:), add the next code to indicate the SwiftUI view on the clock face:


if let dayView = dayView {
  gContext.draw(
    dayView,
    at: CGPoint(x: clockCenter * 1.6, y: clockCenter))
}

You try and unwrap dayView. If profitable, you utilize the GraphicsContext’s draw(_:at:) methodology to attract the view on the canvas utilizing the image. Word that although you’ve got left the closure the place you modified the origin, it stays at its new place on the clock middle. Therefore, you utilize the clockCenter you calculated earlier and shift the horizontal place to the precise. Run the app to see the ultimate clock face:

App showing details for Shibuya with full clock face with date badge

The place to Go From Right here?

You may obtain the completed venture by clicking Obtain Supplies on the high or backside of this tutorial.

You simply created an app that not solely syncs instances but in addition exhibits the time on an analog clock you constructed from scratch — nice job!

For extra background on rotations and the trigonometry used to attract the clock face, see Trigonometry for Recreation Programming — SpriteKit and Swift Tutorial: Half 1/2 and Trigonometry for Recreation Programming — SpriteKit and Swift Tutorial: Half 2/2.

The Starting Core Graphics video course is one other nice useful resource. Plus, raywenderlich.com has many extra Core Graphics tutorials. Listed below are just a few of them:

We hope you loved this tutorial. In case you have any questions or feedback, please be part of the discussion board dialogue under.

LEAVE A REPLY

Please enter your comment!
Please enter your name here