SwiftUI withAnimation の基本

SwiftUI でアニメーションを実装する最も基本的な方法が withAnimation 関数だ。状態の変更をこの関数で囲むことで、その変更に伴う UI の更新がアニメーション付きで実行される。

基本的な使い方

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 ベース、約 0.35 秒)が適用される。

複数の状態を同時に変更

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

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

タップするたびに、位置・透明度・スケールが同時にアニメーションする。

withAnimation と暗黙的アニメーションの違い

SwiftUI には .animation() モディファイアを使う「暗黙的アニメーション」もある。

withAnimation(明示的)

状態変更のタイミングを明示的に制御。どの変更をアニメーションさせるか選べる。

animation モディファイア(暗黙的)

特定のビューに対して、値が変わるたびに自動でアニメーション。制御が難しい場面も。

意図しないアニメーションを避けるため、withAnimation を使う明示的な方法が推奨される場面が多い。

completionHandler(iOS 17 以降)

iOS 17 からは、アニメーション完了時のコールバックを受け取れるようになった。

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

連続したアニメーションや、アニメーション後の処理を実装する際に便利な機能だ。