SwiftUI KeyframeAnimator(iOS 17)

KeyframeAnimator は iOS 17 で追加された、キーフレームベースのアニメーションを実現する機能だ。アニメーションの各時点における値を明示的に指定でき、複雑な動きを細かく制御できる。

KeyframeAnimator の基本構造

KeyframeAnimator は、アニメーションさせる値の構造体と、その値を時間軸に沿って定義するキーフレームで構成される。

struct AnimationValues {
    var scale: CGFloat = 1.0
    var rotation: Double = 0.0
    var yOffset: CGFloat = 0.0
}

struct BouncingShape: View {
    var body: some View {
        KeyframeAnimator(
            initialValue: AnimationValues()
        ) { values in
            Circle()
                .fill(.blue)
                .frame(width: 100, height: 100)
                .scaleEffect(values.scale)
                .rotationEffect(.degrees(values.rotation))
                .offset(y: values.yOffset)
        } keyframes: { _ in
            KeyframeTrack(\.scale) {
                LinearKeyframe(1.2, duration: 0.2)
                SpringKeyframe(1.0, duration: 0.3)
            }
            KeyframeTrack(\.yOffset) {
                LinearKeyframe(-50, duration: 0.15)
                SpringKeyframe(0, duration: 0.5)
            }
        }
    }
}

KeyframeTrack で各プロパティのキーフレームを定義し、時間に沿った値の変化を指定する。

キーフレームの種類

SwiftUI には 4 種類のキーフレームが用意されている。

KeyframeTrack(\.scale) {
    LinearKeyframe(1.5, duration: 0.3)      // 線形補間
    SpringKeyframe(1.0, duration: 0.5)      // スプリング補間
    CubicKeyframe(0.8, duration: 0.2)       // ベジェ曲線補間
    MoveKeyframe(1.2)                       // 即座に移動(補間なし)
}
LinearKeyframe一定速度で直線的に変化
SpringKeyframeバネのような動きで変化
CubicKeyframeなめらかな曲線で変化
MoveKeyframeアニメーションなしで即座に値を変更

trigger でアニメーション開始を制御

trigger パラメータを使えば、値が変化したときにアニメーションを開始できる。

struct TriggeredKeyframe: View {
    @State private var trigger = false
    
    var body: some View {
        VStack {
            KeyframeAnimator(
                initialValue: AnimationValues(),
                trigger: trigger
            ) { values in
                RoundedRectangle(cornerRadius: 20)
                    .fill(.purple)
                    .frame(width: 100, height: 100)
                    .scaleEffect(values.scale)
                    .rotationEffect(.degrees(values.rotation))
            } keyframes: { _ in
                KeyframeTrack(\.scale) {
                    LinearKeyframe(1.3, duration: 0.1)
                    LinearKeyframe(0.9, duration: 0.1)
                    SpringKeyframe(1.0, duration: 0.3)
                }
                KeyframeTrack(\.rotation) {
                    LinearKeyframe(-10, duration: 0.1)
                    LinearKeyframe(10, duration: 0.1)
                    SpringKeyframe(0, duration: 0.2)
                }
            }
            
            Button("Animate") {
                trigger.toggle()
            }
        }
    }
}

ボタンを押すたびに、シェイク風のアニメーションが再生される。

repeatForever との組み合わせ

キーフレームアニメーションを繰り返すには、repeating パラメータを使う。

KeyframeAnimator(
    initialValue: AnimationValues(),
    repeating: true
) { values in
    // ビュー
} keyframes: { _ in
    // キーフレーム
}

全てのキーフレームが終了すると、最初から再び開始する。

実践例:ロケット発射アニメーション

struct RocketValues {
    var yOffset: CGFloat = 0
    var scale: CGFloat = 1.0
    var rotation: Double = 0
    var opacity: Double = 1.0
}

struct RocketLaunch: View {
    @State private var launch = false
    
    var body: some View {
        VStack {
            KeyframeAnimator(
                initialValue: RocketValues(),
                trigger: launch
            ) { values in
                Text("🚀")
                    .font(.system(size: 60))
                    .offset(y: values.yOffset)
                    .scaleEffect(values.scale)
                    .rotationEffect(.degrees(values.rotation))
                    .opacity(values.opacity)
            } keyframes: { _ in
                KeyframeTrack(\.yOffset) {
                    // カウントダウン中は少し震える
                    LinearKeyframe(5, duration: 0.1)
                    LinearKeyframe(-5, duration: 0.1)
                    LinearKeyframe(3, duration: 0.1)
                    LinearKeyframe(0, duration: 0.1)
                    // 発射
                    CubicKeyframe(-400, duration: 0.8)
                }
                KeyframeTrack(\.scale) {
                    LinearKeyframe(1.0, duration: 0.4)
                    LinearKeyframe(1.2, duration: 0.2)
                    LinearKeyframe(0.3, duration: 0.6)
                }
                KeyframeTrack(\.rotation) {
                    LinearKeyframe(-5, duration: 0.1)
                    LinearKeyframe(5, duration: 0.1)
                    LinearKeyframe(0, duration: 0.2)
                    LinearKeyframe(0, duration: 0.8)
                }
            }
            
            Button("発射") {
                launch.toggle()
            }
        }
    }
}

震え、発射、縮小が組み合わさった複雑なアニメーションが、キーフレームで直感的に定義できる。

PhaseAnimator との違い

KeyframeAnimator

時間軸に沿った正確な値の変化を指定。アニメーションの「形」を細かく制御したい場合に最適。

PhaseAnimator

離散的なフェーズを定義し、その間を自動補間。単純な状態遷移やループアニメーションに向く。

KeyframeAnimator は、アニメーションデザイナーが After Effects などで作るような「キーフレームアニメーション」を SwiftUI で再現するための機能だ。細かい動きの調整が必要な場面で威力を発揮する。