小学理科720872 views
高校生物552513 views
中学数学624513 views
数学講師2900085 views
高校物理161327 views
高校日本史190786 views
高校化学2928968 views
高校国語789226 views
いろは3019021 views
世界の国566092 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 以降でのみ使える点に注意が必要だが、従来は複雑だった多段階アニメーションをシンプルに記述できる強力なツールだ。