As an iOS Developer, you have surely faced the challenge of updating the user interface based on the passage of time. Whether you are building a clock, a timer, a countdown, or complex animations that depend on real-time, Swift programming historically forced us to rely on the Timer class, managing states with @State or @Published, and dealing with lifecycles and cancellations to avoid memory leaks.
All of this changed with the evolution of Apple’s framework. If you are working with SwiftUI, you now have at your disposal a native, declarative, and highly optimized tool: TimelineView.
In this tutorial article, we will dive deep into what it is, how it works, and how to use TimelineView in SwiftUI. Throughout this guide, we will open Xcode and write pure Swift code to create dynamic views that work seamlessly on iOS, macOS, and watchOS.
What is TimelineView in SwiftUI?
Introduced in iOS 15, macOS 12, tvOS 15, and watchOS 8, TimelineView is a view container that updates its content (its body) based on a predetermined schedule.
Instead of creating an external timer that pushes updates to your view, TimelineView pulls updates as needed. It acts as a time loop built directly into the SwiftUI rendering system. When you declare a TimelineView, you provide a scheduling strategy (the schedule) and a block of code (the closure) that defines what the view should look like at any given time.
The Anatomy of TimelineView
The basic structure of a TimelineView in Swift looks like this:
TimelineView(.periodic(from: Date(), by: 1.0)) { context in
// Define your view here, using context.date
}
The context that the closure receives is an instance of TimelineView.Context. This context contains crucial information:
date: The exact moment in time for which the view is being rendered.cadence: A value indicating how fast the view is expected to update (e.g.,live,seconds,minutes). This is especially useful on platforms like watchOS to handle the Always-On Display mode.
Scheduling Types (Schedules) in TimelineView
For TimelineView in SwiftUI to know when to redraw the screen, it uses protocols that conform to TimelineSchedule. SwiftUI provides several built-in “schedules” that cover the vast majority of use cases for an iOS Developer.
1. .everyMinute
This is the most battery-efficient scheduler. It updates the view exactly at the beginning of each minute. It is ideal for clocks that do not show seconds or for widgets that do not need real-time precision.
2. .periodic(from:by:)
This scheduler allows you to define a start date and a specific interval (in seconds). It is the perfect replacement for a traditional one-second Timer. It is commonly used for countdowns or clocks with second hands.
3. .animation
This is perhaps the most fascinating one for visual development. It updates the view as fast as possible, syncing with the screen’s refresh rate (up to 60 or 120 times per second on ProMotion devices). It is essential for creating smooth, time-driven animations, often combined with the Canvas component in SwiftUI.
4. Explicit Scheduling (Array of Dates)
You can also pass an array of Date objects. The TimelineView will update exactly on those dates and then stop.
Step-by-Step Tutorial: Building with TimelineView in Xcode
Now that we understand the theory, let’s put it into practice. Open Xcode, create a new SwiftUI project, and make sure to select Swift as your programming language.
Example 1: A Multiplatform Digital Clock
The most classic use case to understand TimelineView in SwiftUI is a clock. Let’s create one that shows hours, minutes, and seconds, and updates every second.
import SwiftUI
struct DigitalClockView: View {
var body: some View {
// We use a periodic schedule, updating every 1 second
TimelineView(.periodic(from: .now, by: 1.0)) { context in
VStack(spacing: 10) {
Text("Current Time")
.font(.headline)
.foregroundColor(.secondary)
// We format context.date to show the time
Text(context.date.formatted(date: .omitted, time: .standard))
.font(.system(size: 50, weight: .bold, design: .rounded))
.foregroundColor(.blue)
}
.padding()
.background(Color(UIColor.secondarySystemBackground))
.cornerRadius(15)
.shadow(radius: 5)
}
}
}
struct DigitalClockView_Previews: PreviewProvider {
static var previews: some View {
DigitalClockView()
}
}
Code Analysis:
.periodic(from: .now, by: 1.0): We tell Xcode to start the timeline right now and update every exact second.context.date: Instead of usingDate()inside the view, you should always usecontext.date. This ensures that the view is rendered with the exact time the system has calculated for that frame, preventing flickering and inconsistencies.- The beauty of SwiftUI: Notice how there are no
@Statevariables or functions managing timers. It’s declarative Swift programming at its finest.
Example 2: Creating a Focus Timer (Pomodoro Style)
An iOS Developer frequently needs to build timers for productivity or fitness apps. Let’s see how to make a countdown.
import SwiftUI
struct TimerView: View {
// We define an end date (example: 1 minute in the future)
let endDate: Date = Calendar.current.date(byAdding: .minute, value: 1, to: Date())!
var body: some View {
TimelineView(.periodic(from: .now, by: 1.0)) { context in
VStack {
Text("Time Remaining")
.font(.title2)
Text(timeRemaining(from: context.date))
.font(.system(size: 60, weight: .heavy, design: .monospaced))
.foregroundColor(context.date >= endDate ? .red : .green)
}
}
}
// Helper function to calculate the time difference
func timeRemaining(from currentDate: Date) -> String {
let remaining = endDate.timeIntervalSince(currentDate)
if remaining <= 0 {
return "00:00"
}
let minutes = Int(remaining) / 60
let seconds = Int(remaining) % 60
return String(format: "%02d:%02d", minutes, seconds)
}
}
This approach is vastly superior to using a traditional Timer because if the main thread of the application freezes briefly, the TimelineView will simply “catch up” on the next frame using the actual context.date, preventing the timer from going out of sync.
Example 3: Complex Animations with .animation and Canvas
Where TimelineView in SwiftUI shines spectacularly is in generating dynamic graphics and high-performance animations. For this, we use the .animation schedule combined with the Canvas view (introduced in the same version of SwiftUI).
Imagine we want to draw an abstract radar that rotates continuously based on time.
import SwiftUI
struct RadarAnimationView: View {
var body: some View {
// Animation schedule for maximum refresh rate
TimelineView(.animation) { context in
Canvas { graphicsContext, size in
// We extract the current seconds, including fractions of a second
let time = context.date.timeIntervalSinceReferenceDate
// We calculate an angle based on the passing time
// We multiply by the desired speed (e.g., 1 radian per second)
let angle = Angle(radians: time * 2.0)
let rect = CGRect(origin: .zero, size: size)
// We center the drawing context
graphicsContext.translateBy(x: size.width / 2, y: size.height / 2)
graphicsContext.rotate(by: angle)
// We draw the radar line
var path = Path()
path.move(to: .zero)
path.addLine(to: CGPoint(x: size.width / 2, y: 0))
graphicsContext.stroke(
path,
with: .linearGradient(
Gradient(colors: [.green, .clear]),
startPoint: .zero,
endPoint: CGPoint(x: size.width / 2, y: 0)
),
lineWidth: 4
)
}
.frame(width: 200, height: 200)
.background(Circle().fill(Color.black))
.overlay(Circle().stroke(Color.green, lineWidth: 2))
}
}
}
In this case, Swift programming applied to the Canvas allows us to draw directly using low-level graphics operations. The TimelineView drives the drawing, asking the canvas to redraw at 60 fps (or 120 fps on ProMotion devices), achieving a mathematically perfect rotation based on the UNIX timestamp (timeIntervalSinceReferenceDate), without relying on .rotationEffect or the standard SwiftUI state animation system.
TimelineView Across Different Platforms
One of the great advantages of being an iOS Developer in today’s Apple ecosystem is the ease of code sharing. TimelineView in SwiftUI is truly multiplatform, but it has important particularities depending on the device.
Apple Watch (watchOS) and the Always-On Display
The Apple Watch is the device where TimelineView is absolutely critical. When the user lowers their wrist, the watch enters a low-power mode (Always-On Display).
In this state, the watch reduces its refresh rate to 1 time per minute or 1 time per second, depending on the model. This is where context.cadence comes into play:
TimelineView(.periodic(from: .now, by: 1.0)) { context in
if context.cadence == .live {
// The user is actively looking: show milliseconds or fluid animations
Text(context.date.formatted(.dateTime.second().secondFraction(.fractional(2))))
} else {
// The screen is dimmed: show only hours and minutes to save battery
Text(context.date.formatted(.dateTime.hour().minute()))
}
}
Xcode makes it easy to simulate these cadences directly in the Previews canvas.
Mac (macOS)
On macOS, TimelineView is perfect for Menu Bar Extras apps that need to update system statistics or background times without consuming excessive CPU. Thread management is done efficiently in the background by the operating system.
iPhone / iPad (iOS)
For iOS development, the recently introduced Live Activities and Interactive Widgets rely heavily on similar concepts, where views must be rendered in the future. TimelineView is the preferred tool for any “in-app” UI that needs to live and breathe with the system clock.
Creating a Custom Schedule (Advanced Level)
If you are an experienced iOS Developer, perhaps the default schedules are not enough. Fortunately, SwiftUI allows us to conform to the TimelineSchedule protocol.
Imagine you are building an astronomy app and only want to update the view at random intervals, or perhaps create a “heartbeat” that beats fast, pauses, and beats again.
To achieve this in Swift, we must create a CustomSchedule:
struct HeartbeatSchedule: TimelineSchedule {
// We define the type of iterator required by the protocol
typealias Entries = HeartbeatIterator
func entries(from startDate: Date, mode: TimelineScheduleMode) -> HeartbeatIterator {
HeartbeatIterator(currentDate: startDate)
}
}
struct HeartbeatIterator: IteratorProtocol {
var currentDate: Date
var fastBeat = true // State toggler
mutating func next() -> Date? {
// If it's a fast beat, we wait 0.2 seconds. If it's a pause, we wait 0.8 seconds.
let interval = fastBeat ? 0.2 : 0.8
// We calculate the next date
let nextDate = currentDate.addingTimeInterval(interval)
// We update the state for the next iteration
currentDate = nextDate
fastBeat.toggle()
return nextDate
}
}
// Extension to use it easily in TimelineView
extension TimelineSchedule where Self == HeartbeatSchedule {
static var heartbeat: HeartbeatSchedule { .init() }
}
And this is how you would use it in your SwiftUI view:
struct HeartView: View {
@State private var scale: CGFloat = 1.0
var body: some View {
// We use our custom schedule!
TimelineView(.heartbeat) { context in
Image(systemName: "heart.fill")
.resizable()
.scaledToFit()
.frame(width: 100, height: 100)
.foregroundColor(.red)
// We use the modulo of the seconds to determine whether to expand or contract
.scaleEffect(context.date.timeIntervalSince1970.truncatingRemainder(dividingBy: 1) < 0.5 ? 1.2 : 1.0)
.animation(.spring(), value: context.date)
}
}
}
This flexibility demonstrates why Swift and SwiftUI make such a powerful duo within Xcode. You have the ease of use for simple tasks and the architectural extensibility for highly customized business logic.
Best Practices, Performance, and Considerations
Despite being a fantastic tool, as a good professional in Swift programming, you must use TimelineView responsibly. Here are some golden rules:
- Keep the closure lightweight: The closure inside the
TimelineView(especially if you use.animation) will run dozens of times per second. Do not perform heavy calculations, network calls, or JSON parsing inside this block. Pre-calculate your logic or use aViewModel(MVVM architecture) to provide data ready to render. - Do not abuse .animation: If your view only needs to update once per second (for example, to show numerical seconds), use
.periodic(from: .now, by: 1.0). Using.animationfor a text clock will force the device to redraw the text at 60 fps, unnecessarily draining your user’s iPhone battery. - Encapsulation: If you have a very complex screen, do not wrap the entire screen in the
TimelineView. Wrap only the small part of the view (e.g., the clock icon or the timer text) in theTimelineView. SwiftUI is very smart at optimizing, but you should help by minimizing the scope of view updates. - Use context.date: We insist on this. Never use
Date()inside the closure to calculate your state. Always rely on thedatevariable injected byTimelineView.Context. This ensures that all animated views stay synchronized under the hood. - Debugging in Xcode: If you notice your
TimelineViewbehaving strangely, remember that you can useprint()inside the body (usinglet _ = print(context.date)) to see in the Xcode console exactly how often and when the view is being fired.
Summary and Conclusion
The introduction of TimelineView in SwiftUI represented a paradigm shift for any iOS Developer. By adopting a time-driven approach in a declarative manner, Apple has eliminated the friction, potential memory leaks, and boilerplate code that plagued older implementations in Swift programming.
We have learned how the basic structure works, how to handle different schedules such as .everyMinute, .periodic, and .animation, and even how to create our own custom rhythms by conforming to the underlying protocols. From a simple clock to complex Canvas-based animations, the possibilities that Xcode offers you now are practically endless.
The Apple ecosystem evolves rapidly. Keeping your skills sharp by using the latest and most efficient APIs will not only make your apps run better and drain less battery, but it will also make your development experience writing Swift much more enjoyable.