SwiftUI animation モディファイアと暗黙的アニメーション

SwiftUI には .animation モディファイアを使った暗黙的アニメーションという仕組みがある。withAnimation が「このタイミングで」と明示するのに対し、暗黙的アニメーションは「この値が変わったら常に」という宣言的なアプローチだ。

animation モディファイアの基本

.animation モディファイアは、特定の値の変化を監視し、その値に依存するビューの変更をアニメーション化する。

struct ContentView: View {
    @State private var isExpanded = false
    
    var body: some View {
        VStack {
            Rectangle()
                .fill(.blue)
                .frame(width: isExpanded ? 200 : 100,
                       height: isExpanded ? 200 : 100)
                .animation(.easeInOut, value: isExpanded)
            
            Button("Toggle") {
                isExpanded.toggle()
            }
        }
    }
}

withAnimation で囲まなくても、isExpanded が変わるたびに自動でアニメーションが実行される。

value パラメータの重要性

iOS 15 以降、.animation には必ず value パラメータを指定する必要がある。これは「どの値の変化に反応するか」を明示するためだ。

// iOS 15 以降の正しい書き方
.animation(.spring(), value: someValue)

// 非推奨(iOS 15 で deprecated)
.animation(.spring())

value を省略した古い書き方は、意図しないアニメーションが発生する原因になっていた。どの状態変化に対してアニメーションするかを明確にすることで、予測可能な動作が保証される。

複数の値を監視

複数の値の変化に対してアニメーションさせたい場合は、animation モディファイアを複数つけるか、値をまとめる。

struct MultiValueView: View {
    @State private var offset: CGFloat = 0
    @State private var rotation: Double = 0
    
    var body: some View {
        Rectangle()
            .frame(width: 100, height: 100)
            .offset(y: offset)
            .rotationEffect(.degrees(rotation))
            .animation(.spring(), value: offset)
            .animation(.easeInOut, value: rotation)
    }
}

offset の変化にはスプリング、rotation の変化には easeInOut と、別々のアニメーションを適用できる。

暗黙的 vs 明示的アニメーション

暗黙的アニメーション(.animation)

ビューの定義時にアニメーションを宣言。値が変われば常にアニメーション。宣言的で SwiftUI らしいアプローチ。

明示的アニメーション(withAnimation)

状態変更時にアニメーションを指定。特定のアクションだけアニメーションさせたい場合に有効。手続き的なアプローチ。

どちらを使うかは設計思想による。「このビューは常にアニメーションする」なら暗黙的、「このボタンを押したときだけ」なら明示的が適している。

animation(nil) でアニメーションを無効化

特定の変更だけアニメーションさせたくない場合、.animation(nil, value:) を使う。

struct SelectiveAnimationView: View {
    @State private var position: CGFloat = 0
    @State private var color: Color = .blue
    
    var body: some View {
        Rectangle()
            .fill(color)
            .frame(width: 100, height: 100)
            .offset(x: position)
            .animation(.spring(), value: position)
            .animation(nil, value: color)  // 色の変化はアニメーションしない
    }
}

これにより、位置の変化はアニメーションするが、色の変化は即座に反映される。

適用順序の注意点

animation モディファイアは、それより上に書かれたモディファイアにのみ影響する。

Rectangle()
    .fill(color)
    .animation(.easeInOut, value: color)  // fill に影響
    .frame(width: size, height: size)
    .animation(.spring(), value: size)     // frame に影響

モディファイアチェーンの順序を意識することで、部分的なアニメーション制御が可能になる。このあたりは SwiftUI のモディファイア適用順序の理解が前提となる。