SwiftUI withAnimation の基本

SwiftUI でアニメーションを実装する方法は大きく 2 つある。明示的アニメーションと暗黙的アニメーションだ。withAnimation は明示的アニメーションの代表格で、「このタイミングでアニメーションさせたい」という意図を明確に示せる。

基本的な使い方

withAnimation は状態の変更をラップし、その変更に伴うビューの更新をアニメーション化する。

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)
            
            Button("Toggle") {
                withAnimation {
                    isExpanded.toggle()
                }
            }
        }
    }
}

ボタンをタップすると、四角形がスムーズに拡大・縮小する。withAnimation で囲んだ状態変更に依存するすべてのビューがアニメーションの対象になる点が重要だ。

アニメーションの種類を指定

withAnimation にはアニメーションの種類を引数として渡せる。

withAnimation(.easeInOut) {
    isExpanded.toggle()
}

withAnimation(.spring()) {
    isExpanded.toggle()
}

withAnimation(.linear(duration: 0.5)) {
    isExpanded.toggle()
}

デフォルトは .default で、これは .easeInOut に近い挙動を示す。用途に応じて使い分けることで、アプリの印象が大きく変わる。

複数の状態を同時に変更

withAnimation 内で複数の状態を変更すると、すべてが同じアニメーションで動く。

struct MultipleStatesView: View {
    @State private var offset: CGFloat = 0
    @State private var opacity: Double = 1
    @State private var scale: CGFloat = 1
    
    var body: some View {
        Circle()
            .fill(.orange)
            .frame(width: 100, height: 100)
            .offset(y: offset)
            .opacity(opacity)
            .scaleEffect(scale)
            .onTapGesture {
                withAnimation(.easeInOut(duration: 0.8)) {
                    offset = offset == 0 ? -100 : 0
                    opacity = opacity == 1 ? 0.3 : 1
                    scale = scale == 1 ? 1.5 : 1
                }
            }
    }
}

位置、透明度、スケールが同時にアニメーションし、統一感のある動きが実現できる。

completionCriteria と completion

iOS 17 からは、アニメーション完了時のコールバックを指定できるようになった。

withAnimation(.spring(duration: 0.5)) {
    isExpanded.toggle()
} completion: {
    print("アニメーション完了")
}

completionCriteria パラメータで、どの時点を「完了」とみなすかも指定できる。

withAnimation(
    .spring(duration: 0.5),
    completionCriteria: .logicallyComplete
) {
    isExpanded.toggle()
} completion: {
    // logicallyComplete: スプリングの主要な動きが終わった時点
    // removed: ビューが完全に削除された時点
}

これにより、アニメーション後に次のアクションを実行するといった制御が容易になった。

withAnimation を使うべき場面

withAnimation が適している

ユーザーアクション(タップ、スワイプ)に応じてアニメーションさせたい場合。状態変更のタイミングが明確で、コード上で「ここでアニメーション」と示したいとき。

暗黙的アニメーションが適している

状態が変わったら常にアニメーションさせたい場合。ビュー側でアニメーションの有無を決めたいとき。

明示的アニメーションはコントロールしやすい反面、すべての状態変更に withAnimation を書く必要がある。暗黙的アニメーション(.animation モディファイア)と使い分けることで、保守しやすいコードになる。