【SwiftUI】iOS17でwithAnimationが強化された

# はじめに
`withAnimation`にパラメータが2つ増えました。
まず、1つ目はアニメーションの終了が検知できるように`completion`が追加されました。
2つ目はアニメーションの終了をどことするかを指定する`completionCriteria`です。

解説していきます。

# アニメーションの終了が検知できるようになった
```swift
import SwiftUI

struct ContentView: View {
    @State private var degrees: Double = 0.0
    
    @State private var status = ""
    
    var body: some View {
        VStack(spacing: 60) {
            Text(status)
            
            Rectangle()
                .frame(width: 200, height: 200)
                .rotationEffect(.degrees(degrees))
            
            Button {
                status = "アニメーション中です"

                withAnimation(.default) {
                    degrees = (degrees == 360 ? 0 : 360)
                } completion: {
                    status = "アニメーションが終了しました！"
                }
            } label: {
                Text("アニメーション開始")
            }
            .buttonStyle(.borderedProminent)
        }
    }
}
```

![Simulator Screen Recording - iPhone 15 - 2024-01-21 at 21.03.47.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/f1fc9e26-f037-b561-7509-23d222ce63c9.gif)

意外と便利ですね。

# 疑問が生まれる
バウンドするようなアニメーションではアニメーションさせるための、力を加えるのを終了しても惰性で動き続けます。
そのようなアニメーションではどこがアニメーションの終わりなのでしょうか？
考えられるアニメーション終了地点は2箇所あります。

- アニメーションさせる力を加えることを終了した時
- アニメーションが完全に停止した時

これを指定するのが`completionCriteria`です。

# `completionCriteria`を指定してみる
### アニメーションさせる力を加えることを終了した時
```swift
import SwiftUI

struct ContentView: View {
    @State private var degrees: Double = 0.0
    
    @State private var status = ""
    
    var body: some View {
        VStack(spacing: 60) {
            Text(status)
            
            Rectangle()
                .frame(width: 200, height: 200)
                .rotationEffect(.degrees(degrees))
            
            Button {
                status = "アニメーション中です"
                
                withAnimation(.bouncy(duration: 3.0, extraBounce: 0.3), completionCriteria: .logicallyComplete) {
                    degrees = (degrees == 360 ? 0 : 360)
                } completion: {
                    status = "アニメーションが終了しました！"
                }
            } label: {
                Text("アニメーション開始")
            }
            .buttonStyle(.borderedProminent)
        }
    }
}
```

![Simulator Screen Recording - iPhone 15 - 2024-01-21 at 21.43.15.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/30da516a-b532-2aa3-a671-a065a19b5537.gif)

### アニメーションが完全に停止した時
```swift
import SwiftUI

struct ContentView: View {
    @State private var degrees: Double = 0.0
    
    @State private var status = ""
    
    var body: some View {
        VStack(spacing: 60) {
            Text(status)
            
            Rectangle()
                .frame(width: 200, height: 200)
                .rotationEffect(.degrees(degrees))
            
            Button {
                status = "アニメーション中です"
                
                withAnimation(.bouncy(duration: 3.0, extraBounce: 0.3), completionCriteria: .removed) {
                    degrees = (degrees == 360 ? 0 : 360)
                } completion: {
                    status = "アニメーションが終了しました！"
                }
            } label: {
                Text("アニメーション開始")
            }
            .buttonStyle(.borderedProminent)
        }
    }
}
```

![Simulator Screen Recording - iPhone 15 - 2024-01-21 at 21.44.48.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/994d399a-0f2a-6199-5690-9f6a6ba2a936.gif)

# おわり
自分で実装するとめんどくさそうですよね

# 公式ドキュメント
https://developer.apple.com/documentation/SwiftUI/withAnimation(_:completionCriteria:_:completion:)

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