SwiftUI カスタム Transition の作成

組み込みの Transition だけでは表現できない動きが必要な場合、カスタム Transition を作成できる。ViewModifier を 2 つ用意し、それらの間を補間することで独自のアニメーション効果を実現する。

AnyTransition.modifier の基本

カスタム Transition は、active 状態と identity 状態の 2 つの ViewModifier を定義して作る。

struct RotateModifier: ViewModifier {
    let angle: Double
    let anchor: UnitPoint
    
    func body(content: Content) -> some View {
        content
            .rotationEffect(.degrees(angle), anchor: anchor)
            .opacity(angle == 0 ? 1 : 0)
    }
}

extension AnyTransition {
    static var rotate: AnyTransition {
        .modifier(
            active: RotateModifier(angle: 90, anchor: .bottomLeading),
            identity: RotateModifier(angle: 0, anchor: .bottomLeading)
        )
    }
}

active は「ビューがまだ見えない状態」、identity は「ビューが完全に見えている状態」を表す。挿入時は active から identity へ、削除時は identity から active へアニメーションする。

Text("回転して登場")
    .transition(.rotate)

より実践的な例:フリップ Transition

カードがめくれるような効果を作ってみる。

struct FlipModifier: ViewModifier {
    let angle: Double
    let axis: (x: CGFloat, y: CGFloat, z: CGFloat)
    
    func body(content: Content) -> some View {
        content
            .rotation3DEffect(
                .degrees(angle),
                axis: axis,
                perspective: 0.5
            )
            .opacity(abs(angle) > 90 ? 0 : 1)
    }
}

extension AnyTransition {
    static var flipFromLeft: AnyTransition {
        .modifier(
            active: FlipModifier(angle: -180, axis: (0, 1, 0)),
            identity: FlipModifier(angle: 0, axis: (0, 1, 0))
        )
    }
    
    static var flipFromBottom: AnyTransition {
        .modifier(
            active: FlipModifier(angle: 90, axis: (1, 0, 0)),
            identity: FlipModifier(angle: 0, axis: (1, 0, 0))
        )
    }
}

3D 回転を使うことで、平面的なスライドやフェードとは違った奥行きのある動きを実現できる。

asymmetric と組み合わせる

カスタム Transition も asymmetric と組み合わせて、挿入と削除で異なる動きにできる。

extension AnyTransition {
    static var rotateInOut: AnyTransition {
        .asymmetric(
            insertion: .modifier(
                active: RotateModifier(angle: -90, anchor: .topTrailing),
                identity: RotateModifier(angle: 0, anchor: .topTrailing)
            ),
            removal: .modifier(
                active: RotateModifier(angle: 90, anchor: .bottomLeading),
                identity: RotateModifier(angle: 0, anchor: .bottomLeading)
            )
        )
    }
}

挿入時は右上を軸に回転して登場し、削除時は左下を軸に回転して消えていく。

Animatable プロトコルとの連携

より複雑なアニメーションを実現するには、ViewModifier に Animatable プロトコルを実装する。

struct WaveModifier: ViewModifier, Animatable {
    var progress: Double
    
    var animatableData: Double {
        get { progress }
        set { progress = newValue }
    }
    
    func body(content: Content) -> some View {
        content
            .offset(y: sin(progress * .pi * 2) * 10)
            .scaleEffect(1 + cos(progress * .pi) * 0.1)
    }
}

animatableData を定義することで、SwiftUI がその値を補間してくれる。これにより、単純な線形補間では表現できない複雑な動きも可能になる。

注意点とベストプラクティス

パフォーマンス

カスタム Transition は描画負荷が高くなりがち。3D 回転や複雑なエフェクトは、多数の要素に同時適用しないよう注意する。

一貫性

アプリ内で使う Transition は統一感を持たせる。あちこちで異なる派手な Transition を使うと、ユーザーが混乱する原因になる。

テスト

シミュレーターだけでなく実機でも確認する。特に古いデバイスではパフォーマンスの問題が顕著に現れることがある。

カスタム Transition は表現の幅を広げる強力なツールだが、使いすぎには注意したい。標準の Transition で十分な場面も多いので、本当に必要な場面で効果的に使うことが大切だ。