The TimelineView

Time based animation for all

published on November 2, 2025

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:

  1. 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 animation
  2. periodic(from:by:) - Updates at a fixed rate.
  3. explicit - Takes a list of dates and renders the content for each value separately
  4. everyMinute - 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.

Follow me on X