SwiftUIのAnimationについてふわっと学ぶ(phaseAnimator編)

# はじめに
今回はWWDC2023の以下動画で見たphaseAnimatorについて実際に触ってキャッチアップしてみました。

https://developer.apple.com/jp/videos/play/wwdc2023/10157/

普段SwiftUIでAnimationを付与するときは、withAnimationでanimatableなmodifierの値を変更するなどして行うのですが、そういった方法とは少しやり方が違いそうです。

https://qiita.com/stotic-dev/items/9449981afe9b20077c3e

早速みていきましょう。

# phaseAnimatorとは
まずは公式のドキュメントを見てみます。

>アニメーション内の個別のステップを定義する、提供されたフェーズのコレクションを自動的に循環させることでコンテンツをアニメーション化するコンテナー。
>
>概要
アプリで段階的なアニメーションを作成するには、フェーズ アニメーター ビュー モディファイアのいずれかを使用します。phaseAnimator(_:content:animation:)
>
>```swift
>nonisolated
>func phaseAnimator<Phase>(
>    _ phases: some Sequence,
>    @ViewBuilder content: @escaping (PlaceholderContentView<Self>, Phase) -> some View,
>    animation: @escaping (Phase) -> Animation? = { _ in .default }
>) -> some View where Phase : Equatable
>```

https://developer.apple.com/documentation/swiftui/phaseanimator

`phaseAnimator`モディファイアの`phases`に設定した配列を自動で循環してアニメーションさせるようです。
と自分で書いていてもイマイチピンとこないので、手を動かします。

### 無限ループのAnimation

```swift
struct MultiStepAnimationView: View {
    
    var body: some View {
        Text("Hello, World!")
            .font(.system(size: 24, weight: .bold))
            .phaseAnimator([1, 2, 3]) { view, phase in
                view
                    .opacity(phase == 2 ? 1 : 0)
                    .scaleEffect(phase * 0.5)
            }
    }
}
```

![phaseAnimatorサンプル.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/d8b6c856-3dea-5abb-e0a5-ddd691a00dc0.gif)

`phaseAnimator`モディファイアの`content`引数にて、Animationしたい内容を実装することで、`phases`で指定した配列を無限に循環してAnimationが無限ループで再生されるようになりました。

また、以下のように各phase毎にAnimationも指定できました。
そこまで細かくAnimationを指定したいケースは,今のところ遭遇したことがないですが、やろうと思えば相当細かく設定できそうです。

```swift
struct MultiStepAnimationView: View {
    
    var body: some View {
        Text("Hello, World!")
            .font(.system(size: 24, weight: .bold))
            .phaseAnimator([1, 2, 3], content: { view, phase in
                view
                    .opacity(phase == 2 ? 1 : 0)
                    .scaleEffect(phase * 0.5)
            }, animation: { phase in
                
                phase == 2 ? .spring : .linear(duration: 1)
            })
    }
}
```

### トリガー付きのAnimation

```swift
struct TriggeredMultiStepAnimationView: View {
    
    @State var trigger = 0
    
    var body: some View {
        VStack {
            Spacer()
            Rectangle()
                .frame(width: 100, height: 100)
                .phaseAnimator([1,2,3], trigger: trigger) { view, phase in
                    view
                        .opacity(phase == 2 ? 0 : 1)
                        .rotationEffect(.degrees(phase == 2 ? 360 : 0))
                        .foregroundStyle(phase == 1 ? .red : .blue)
                }
            Spacer()
            Button {
                trigger += 1
            } label: {
                Text("START")
            }
        }
    }
}
```

![トリガー付きPhaseAnimatorサンプル.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/720cc33b-a9e7-3c86-42f7-f4f36d7e3a10.gif)

`trigger`付きの`phaseAnimator`モディファイアを使用した場合、前の例とは違い無限ループのAnimationではなくなります。
`trigger`に設定したEqutableの値に変更が加えられると、設定したAnimationが発火します。
また、Animationが発火していない状態では`phases`に指定した最初の値で設定したモディファイアの状態が適用されます。
上記の例ですと、通常の状態は`phases`が1の状態で`opacity`、`rotationEffect`、`foregroundStyle`が適用されています。

# おわり
思ったより簡単にオリジナルのアニメーションが作れて楽しかったです。
試しにオリジナルのロードアニメーション作ってみました。
こういうAnimationは以前はTimerとか使ってやっていましたが、phaseAnimatorを使うと簡単に実装できます。

![ロードアイコン.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/1fd31f20-01a0-c3be-b2aa-7d8cec218035.gif)

実装は以下リポジトリに上げました。

https://github.com/stotic-dev/SwiftUIAnimationSamples