中学理科1626207 views
中学英語808712 views
ヒストリア284143 views
高校倫理1433119 views
英語607877 views
中学社会667106 views
中学数学621382 views
MathPython491378 views
世界の国560595 views
高校生物549842 views
Help
Tools

English

SwiftUI PhaseAnimator(iOS 17)

iOS 17 で導入された PhaseAnimator は、複数のフェーズ(状態)を自動的に循環するアニメーションを簡単に作れる機能だ。従来は DispatchQueue や Timer を使って手動で制御していた段階的なアニメーションが、宣言的に記述できるようになった。

PhaseAnimator の基本

PhaseAnimator は、定義したフェーズの配列を順番にループする。

struct PulsingDot: View {
    var body: some View {
        PhaseAnimator([false, true]) { phase in
            Circle()
                .fill(.blue)
                .frame(width: 50, height: 50)
                .scaleEffect(phase ? 1.2 : 1.0)
                .opacity(phase ? 0.6 : 1.0)
        }
    }
}

[false, true] という 2 つのフェーズを定義し、ビューはこれを無限にループする。phase パラメータに現在のフェーズが渡されるので、それに応じてビューを変化させる。

フェーズに enum を使う

より複雑なアニメーションでは、フェーズを enum で定義すると可読性が上がる。

enum AnimationPhase: CaseIterable {
    case initial
    case expanded
    case rotated
    case final
}

struct MultiPhaseAnimation: View {
    var body: some View {
        PhaseAnimator(AnimationPhase.allCases) { phase in
            RoundedRectangle(cornerRadius: 20)
                .fill(.purple)
                .frame(width: phase == .expanded ? 150 : 100,
                       height: phase == .expanded ? 150 : 100)
                .rotationEffect(.degrees(phase == .rotated ? 45 : 0))
                .opacity(phase == .final ? 0.5 : 1.0)
        }
    }
}

CaseIterable に準拠した enum を使えば、allCases でフェーズ配列を自動生成できる。

animation パラメータでフェーズごとのアニメーションを指定

各フェーズへの遷移に異なるアニメーションを適用できる。

PhaseAnimator([0, 1, 2]) { phase in
    Circle()
        .fill(.green)
        .frame(width: 100, height: 100)
        .offset(y: phase == 1 ? -100 : 0)
        .scaleEffect(phase == 2 ? 1.5 : 1.0)
} animation: { phase in
    switch phase {
    case 0: .spring(duration: 0.3)
    case 1: .easeOut(duration: 0.5)
    default: .bouncy
    }
}

animation クロージャで、次のフェーズに移行するときのアニメーションを指定できる。フェーズによって動きの性格を変えたい場合に便利だ。

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

デフォルトでは PhaseAnimator は自動でループするが、trigger パラメータを使えばユーザーアクションに応じて開始できる。

struct TriggeredAnimation: View {
    @State private var trigger = false
    
    var body: some View {
        VStack {
            PhaseAnimator(
                [false, true],
                trigger: trigger
            ) { phase in
                Star()
                    .fill(.yellow)
                    .frame(width: 100, height: 100)
                    .scaleEffect(phase ? 1.3 : 1.0)
                    .rotationEffect(.degrees(phase ? 360 : 0))
            }
            
            Button("Animate") {
                trigger.toggle()
            }
        }
    }
}

trigger の値が変わるたびに、フェーズを最初から最後まで 1 回実行する。

実践例:ローディングインジケーター

struct LoadingIndicator: View {
    var body: some View {
        HStack(spacing: 8) {
            ForEach(0..<3) { index in
                PhaseAnimator([false, true]) { phase in
                    Circle()
                        .fill(.blue)
                        .frame(width: 12, height: 12)
                        .offset(y: phase ? -10 : 0)
                } animation: { _ in
                    .easeInOut(duration: 0.4)
                        .delay(Double(index) * 0.15)
                }
            }
        }
    }
}

3 つのドットが時間差で上下に動く、よく見るローディングアニメーションが簡潔に実装できる。

実践例:通知バッジアニメーション

enum BadgePhase: CaseIterable {
    case normal, bump, settle
}

struct NotificationBadge: View {
    @State private var count = 0
    
    var body: some View {
        ZStack {
            Circle()
                .fill(.red)
                .frame(width: 24, height: 24)
            
            PhaseAnimator(
                BadgePhase.allCases,
                trigger: count
            ) { phase in
                Text("\(count)")
                    .font(.caption.bold())
                    .foregroundStyle(.white)
                    .scaleEffect(phase == .bump ? 1.4 : 1.0)
            } animation: { phase in
                switch phase {
                case .normal: .spring(duration: 0.1)
                case .bump: .spring(duration: 0.15, bounce: 0.5)
                case .settle: .spring(duration: 0.2)
                }
            }
        }
        .onTapGesture {
            count += 1
        }
    }
}

カウントが変わるたびに、数字がポンと弾むアニメーションが再生される。

PhaseAnimator vs withAnimation

PhaseAnimator が適している

複数フェーズの循環、段階的な状態変化、ローディングなど継続的なアニメーション。宣言的に書きたい場合。

withAnimation が適している

単発のアニメーション、ユーザーアクションへの即座の反応、細かいタイミング制御が必要な場合。

PhaseAnimator は iOS 17 以降でのみ使える点に注意が必要だが、従来は複雑だった多段階アニメーションをシンプルに記述できる強力なツールだ。