SwiftUI repeatForever と repeatCount

アニメーションを繰り返し実行したい場合、repeatForever や repeatCount を使う。ローディングインジケーターや注目を集めたい要素など、継続的な動きが必要な場面で活躍する。

repeatForever の基本

repeatForever() は、アニメーションを無限に繰り返す。

struct PulsingCircle: View {
    @State private var isPulsing = false
    
    var body: some View {
        Circle()
            .fill(.blue)
            .frame(width: 100, height: 100)
            .scaleEffect(isPulsing ? 1.2 : 1.0)
            .opacity(isPulsing ? 0.6 : 1.0)
            .animation(
                .easeInOut(duration: 1.0).repeatForever(autoreverses: true),
                value: isPulsing
            )
            .onAppear {
                isPulsing = true
            }
    }
}

onAppear で状態を変更し、repeatForever でその変更を永続的にアニメーションさせる。autoreverses: true により、終点に達したら始点に戻る動きを繰り返す。

autoreverses パラメータ

autoreverses は、アニメーションの終点から始点に戻るかどうかを制御する。

// 行って戻る(往復)
.repeatForever(autoreverses: true)

// 行ったら最初から(ループ)
.repeatForever(autoreverses: false)
autoreverses: true

1.0 → 1.2 → 1.0 → 1.2 → … のように往復。パルスや呼吸のような動きに適している。

autoreverses: false

1.0 → 1.2 → 1.0 → 1.2 → … だが、1.2 から 1.0 への戻りは瞬時。回転やプログレスのような一方向の動きに適している。

repeatCount で回数指定

特定の回数だけ繰り返したい場合は repeatCount を使う。

.animation(
    .easeInOut(duration: 0.5).repeatCount(3, autoreverses: true),
    value: isShaking
)

この例では 3 往復(6 回の動き)でアニメーションが停止する。注意を引くための短いアニメーションに便利だ。

回転アニメーション

無限回転は autoreverses: false との組み合わせが定番。

struct SpinningLoader: View {
    @State private var isRotating = false
    
    var body: some View {
        Image(systemName: "arrow.triangle.2.circlepath")
            .font(.system(size: 40))
            .rotationEffect(.degrees(isRotating ? 360 : 0))
            .animation(
                .linear(duration: 1.0).repeatForever(autoreverses: false),
                value: isRotating
            )
            .onAppear {
                isRotating = true
            }
    }
}

linear を使うことで、一定速度で回転し続ける。easeInOut だと回転が不自然に加減速してしまう。

シェイクアニメーション

入力エラーなどを知らせるシェイクアニメーションの例。

struct ShakingTextField: View {
    @State private var text = ""
    @State private var shake = false
    
    var body: some View {
        TextField("入力してください", text: $text)
            .textFieldStyle(.roundedBorder)
            .offset(x: shake ? -10 : 0)
            .animation(
                .easeInOut(duration: 0.1).repeatCount(5, autoreverses: true),
                value: shake
            )
    }
    
    func triggerShake() {
        shake = true
        // アニメーション終了後にリセット
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            shake = false
        }
    }
}

短い duration と repeatCount で、素早い往復運動を実現する。

アニメーションの停止

repeatForever で開始したアニメーションを停止するには、状態を元に戻すか、animation(nil) を適用する。

struct ControllableAnimation: View {
    @State private var isAnimating = false
    @State private var scale: CGFloat = 1.0
    
    var body: some View {
        VStack {
            Circle()
                .fill(.green)
                .frame(width: 100, height: 100)
                .scaleEffect(scale)
                .animation(
                    isAnimating
                        ? .easeInOut(duration: 0.8).repeatForever(autoreverses: true)
                        : .default,
                    value: scale
                )
            
            Button(isAnimating ? "停止" : "開始") {
                if isAnimating {
                    isAnimating = false
                    scale = 1.0
                } else {
                    isAnimating = true
                    scale = 1.3
                }
            }
        }
    }
}

アニメーションの種類自体を条件分岐で切り替えることで、開始・停止を制御できる。

パフォーマンスの注意点

repeatForever は CPU/GPU リソースを継続的に消費する。不要になったアニメーションは必ず停止し、画面外のビューでは onDisappear でアニメーションを止めるなどの配慮が必要だ。特に複数の要素を同時にアニメーションさせる場合は、バッテリー消費にも注意したい。