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

# はじめに
SwiftUIでAnimationさせるときにtransitionというModifierを見かけたのですが、これが何をやっているのかよくわかっていなかったので、ふわっと学びたいと思います。

# 環境
- Xcode16
- iOS17

# transitionとは
公式のドキュメントによると、Viewの表示、非表示切り替えの際のAnimationを制御するModifireのようです。

https://developer.apple.com/documentation/swiftui/view/transition(_:)-5h5h0

>このビューが表示されたり消えたりすると、トランジションが適用され、アニメーション化が可能になります。
>次のコードは、条件に応じて MyView を表示し、表示または非表示になったときにスライド遷移を使用して表示します。
>```swift
>if isActive {
>    MyView()
>        .transition(.slide)
>}
>Button("Toggle") {
>    withAnimation {
>        isActive.toggle()
>    }
>}
>```

`transition`Modifierに`AnyTransition`を指定することで、表示、非表示時のAnimationの指定をしています。

上記では`transition`Modifierに`slide`というAnyTransitionのstaticプロパティを指定しているので、スライドのAnimationになりますが他にもデフォルトで指定できるAnyTransitionのstaticプロパティがありました。

### scale

![scaleTransitionSample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/20b013ee-d29f-95e5-db79-4c14cdb94e2e.gif)

```swift
struct ScaleTransitionView: View {
    
    @State var isShown = true
    
    var body: some View {
        VStack {
            Spacer()
            if isShown {
                Circle()
                    .frame(width: 100)
                    .transition(.scale(scale: 0))
            }
            Spacer()
            Button {
                withAnimation(.linear(duration: 1.0)) {
                    isShown.toggle()
                }
            } label: {
                Text(isShown ? "Hide" : "Show")
            }
            Spacer()
        }
    }
}
```

### blurReplace

![blurTransitionSample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/b2449695-651f-114a-d768-0dafeadb04ae.gif)


```swift
struct BlurReplaceTransitionView: View {
    
    @State var isShown = true
    
    var body: some View {
        VStack {
            Spacer()
            if isShown {
                Circle()
                    .frame(width: 100)
                    .transition(.blurReplace)
            }
            Spacer()
            Button {
                withAnimation(.linear(duration: 1.0)) {
                    isShown.toggle()
                }
            } label: {
                Text(isShown ? "Hide" : "Show")
            }
            Spacer()
        }
    }
}
```

その他にもいろいろありました。

| AnyTransition | 内容 |
|:-:|:-:|
| opacity | 徐々に透明度が変わるAnimation |
| move(Edge) | 引数で指定した方向に移動するAnimation(slideみたいにふわっと消えない) |
| offset(x, y) | 引数で指定したoffset分移動するAnimation(slideみたいにふわっと消えない) |
| push(Edge) | 引数で指定した方向から移動するAnimation(slideみたいにふわっと消える |
| identity | Animationしない |
| asymmetric(insertion, removal) | 表示時と非表示時のAnimationの内容をそれぞれ引数でAnyTransitionとして指定できる |

上記のtransitionのサンプルは以下GitHubリポジトリにありますのでよかったら見てみてください。

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


# カスタムTransition
上記で取り上げたTransitionはAnyTransitionにデフォルトで備わっているものでしたが、自身でオリジナルのTransitionを実装することも可能なようです。

試しに以下のようなTransitionを実装してみました。

![CustomTransitionSample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3359094/62314b89-2e53-7846-9036-6153fc0927a7.gif)


```swift
struct CustomTransitionView: View {
    
    @State var isShown = true
    
    var body: some View {
        VStack {
            Spacer()
            if isShown {
                Circle()
                    .frame(width: 100)
                    // 回転と透明度のtransitionを併用
                    .transition(.rotation.combined(with: .opacity))
            }
            Spacer()
            Button {
                withAnimation(.linear(duration: 1.5)) {
                    isShown.toggle()
                }
            } label: {
                Text(isShown ? "Hide" : "Show")
            }
            Spacer()
        }
    }
}

// 回転効果を付与するModifierを定義
struct RotationModifier: ViewModifier {
    
    let degrees: CGFloat
    let axis: (x: CGFloat, y: CGFloat, z: CGFloat)
    
    init(degrees: CGFloat,
         axis: (x: CGFloat, y: CGFloat, z: CGFloat) = (0, 0, 0)) {
        self.degrees = degrees
        self.axis = axis
    }
    
    func body(content: Content) -> some View {
        content
            .rotation3DEffect(.degrees(degrees), axis: axis)
    }
}

extension AnyTransition {

    // 回転するTransitionを定義
    static var rotation: AnyTransition {
        .modifier(active: RotationModifier(degrees: 720, axis: (1, 5, 5)),
                  identity: RotationModifier(degrees: 0))
    }
}
```

AnyTransitionに`static var`として`AnyTransition`を返すプロパティを実装することで、オリジナルのTransitionを定義できます。
その中で行なっている`.modifier`では表示時、非表示時にどのような効果を付与するかを`ViewModifier`で設定できます。

https://developer.apple.com/documentation/swiftui/anytransition/modifier(active:identity:)

今回は回転効果を付与する`RotationModifier`という`ViewModifier`を実装しそれを指定しました。

またAnyTransitionには`combine`というメソッドがあり、これにより複数のTransitionを合体させることもできます。
上記の例ではオリジナルの`rotation`に加えて`opacity`を合体させてふわっと消えるようにしました。

https://developer.apple.com/documentation/swiftui/anytransition/combined(with:)

# 表示、非表示の定義
冒頭でもあったように、Transitionで制御できるAnimationは対象のViewの表示、非表示時ですがどうなると表示・非表示にあたるのか気になったので以下ケースで実験してみました。

1. `if`制御で表示・非表示切り替えしたとき
2. `opacity`制御で表示・非表示切り替えしたとき

1のケースについては、上述しているケースですので当然Transitionでいうところの表示・非表示にあたります。
2のケースにていては、transitionで指定したAnimationは効きませんでした。

理由としてはおそらくopacityはあくまで透明度が0になり見えなくなっているだけで要素としては存在しているため、SwiftUIとしては表示状態だからでしょう。
よって、上記ケースではTransitionが効くのは1のケースだけでした。

# おわり
transitionについて、キャッチアップ前と比べると理解度が増したように思います。
ぬるぬる動くおしゃれなアプリには必須のテクニックだと思うので、ガンガン使っていきたいです。