The TimelineView
Time based animation for all
I've presented many different animation techniques on this blog, however there is one that was left out until now: TimelineView. This simple but rather powerful tool allows for creating complex animations based on the passage of time. To understand how the view can be used, let's start with a very simple example:
@State private var paused: Bool = false
var body: some View {
VStack(spacing: 40) {
Toggle("Pause", isOn: $paused)
TimelineView(.animation(minimumInterval: 1, paused: paused)) { context in
Text("\(context.date.formatted(date: .omitted, time: .complete))")
}
}.padding()
}
Run this code and you will see nothing but the current time ticking upwards with the possibility to pause the animation but it shows us exactly how the view works.
The heart of the view is the scheduler we can pass in. For this example, I've chosen the .animation scheduler, but there are a couple different ones:
animation(minimumInterval:paused)- Updates the content periodically but not more frequently than the set minimum interval. If left nil, the system will pick the right interval for a smooth animationperiodic(from:by:)- Updates at a fixed rate.explicit- Takes a list of dates and renders the content for each value separatelyeveryMinute- As the name suggests, it updates the view at the start of every minute
Now that we now our options, let's see a more complex example
TimelineView(.animation) { context in
let date = context.date
let calendar = Calendar.current
let seconds = calendar.component(.second, from: date)
let minuteFraction = Double(seconds) / 60.0
// A smooth hue shift over the minute
let color = Color(hue: minuteFraction, saturation: 0.8, brightness: 0.9)
VStack(spacing: 16) {
ZStack {
// Background ring
Circle()
.stroke(Color.secondary.opacity(0.2), lineWidth: 14)
// Progress ring for current minute
Circle()
.trim(from: 0, to: minuteFraction)
.stroke(color, style: StrokeStyle(lineWidth: 14, lineCap: .round))
.rotationEffect(.degrees(-90))
.animation(.easeInOut(duration: 0.25), value: minuteFraction)
// Tick marks for each 5 seconds
ForEach(0..<12, id: \.self) { i in
let angle = Angle.degrees(Double(i) * 30) // 360/12
Capsule(style: .continuous)
.fill(Color.secondary.opacity(0.5))
.frame(width: i % 3 == 0 ? 4 : 2, height: i % 3 == 0 ? 14 : 8)
.offset(y: -70)
.rotationEffect(angle)
}
Text(formatter.string(from: date))
.font(.system(.title2, design: .rounded))
.monospacedDigit()
}
.frame(width: 180, height: 180)
}
.padding()
}
Go ahead and run this code. It will present a clock with a colored ring that is growing with every second.
Related articles
Here are some more articles that may interest you. Check them out!
Container Relative Frame
published on September 18, 2025
SwiftUILearn how to use the containerRelativeFrame modifier in SwiftUI to create proportional layouts. This article explains both variants of the modifier, how containers are determined, and provides practical examples for building flexible, responsive UIs.
Read moreMatched Transitions in SwiftUI
published on September 23, 2025
SwiftSwiftUIAnimationsLearn how to use matchedGeometryEffect and navigationTransition in SwiftUI to create smooth, visually engaging transitions between views and screens. This article covers practical examples for synchronizing view geometry and implementing the new zoom navigation transition, helping you build more dynamic and polished UIs.
Read moreSwiftUI Transitions
published on September 7, 2025
SwiftSwiftUIAnimationsLearn how to create smooth, engaging UI animations in SwiftUI using built-in and custom transitions. From simple fades to complex combinations, this guide covers the essentials with best practices and examples.
Read more