← All shorts
12iOS 17+2 min read

KeyframeAnimator: Independent Timelines Per Property

Animate scale, offset, and rotation on their own timelines with keyframeAnimator, instead of one shared curve.

A realistic shake — offset snapping back and forth quickly while a rotation eases more slowly — needs each property animating on its own timeline. withAnimation applies one curve to every property changed inside it, so getting independent timing meant splitting the animation into several manually staggered blocks.

#The old way

withAnimation(.linear(duration: 0.05)) { offset = -8 }
withAnimation(.linear(duration: 0.05).delay(0.05)) { offset = 8 }
withAnimation(.easeInOut(duration: 0.2)) { rotation = .degrees(5) }

Each property fights for its own delay math, and keeping three or four of these in sync by hand gets fragile fast.

#The new way

import SwiftUI

struct ShakeValues: Equatable {
    var offset: CGFloat = 0
    var rotation: Angle = .zero
}

struct PasswordField: View {
    @State private var errorTrigger = 0

    var body: some View {
        VStack(spacing: 16) {
            SecureField("Password", text: .constant(""))
                .textFieldStyle(.roundedBorder)
                .keyframeAnimator(
                    initialValue: ShakeValues(),
                    trigger: errorTrigger
                ) { content, value in
                    content
                        .offset(x: value.offset)
                        .rotationEffect(value.rotation)
                } keyframes: { _ in
                    KeyframeTrack(\.offset) {
                        LinearKeyframe(0, duration: 0.05)
                        LinearKeyframe(-8, duration: 0.05)
                        LinearKeyframe(8, duration: 0.05)
                        LinearKeyframe(-8, duration: 0.05)
                        LinearKeyframe(0, duration: 0.05)
                    }
                    KeyframeTrack(\.rotation) {
                        LinearKeyframe(.zero, duration: 0.1)
                        LinearKeyframe(.degrees(5), duration: 0.15)
                        LinearKeyframe(.zero, duration: 0.2)
                    }
                }

            Button("Simulate Wrong Password") {
                errorTrigger += 1
            }
        }
    }
}

#Why it matters

  • Each KeyframeTrack is a self-contained timeline for one property — durations for offset and rotation don't need to add up to the same total by hand.
  • LinearKeyframe, SpringKeyframe, and CubicKeyframe mix within a single track, so a value can ease in, spring, and settle without leaving keyframeAnimator.
  • The trigger parameter replays the whole timeline from scratch on demand, which fits one-shot feedback like validation errors better than a looping animation would.

#Gotcha

The first keyframe in a track is not an implicit "from" — it's a jump target. If a track's opening keyframe doesn't match the value's current resting state (here, offset: 0 and rotation: .zero, matching ShakeValues()'s defaults), the property visibly pops to that first keyframe the instant the animation starts, instead of animating smoothly from where it already was. Always set your Value struct's defaults to match the at-rest UI state before writing the rest of the track.

swiftuiios17animation

Related shorts