はじめに
SwiftUIで高度なアニメーションを実装するためのAPIで、keyframeAnimator
というmodifierが提供されているようで、今回はこちらを用いてアニメーションを実装する方法を記事にしてみようと思います。
こちらは、WWDC23の動画で紹介されており、私はこちらでキャッチアップしました。
動画内のコードも公開されているので、こちらも参考になるかと思います。
keyframeAnimator
とは
nonisolated func keyframeAnimator<Value>( initialValue: Value, trigger: some Equatable, @ViewBuilder content: @escaping (PlaceholderContentView<Self>, Value) -> some View, @KeyframesBuilder<Value> keyframes: @escaping (Value) -> some Keyframes ) -> some View
任意のアニメーションをタイミングを指定して、複雑なアニメーションを実装することができます。
タイミングを指定するというのは、一つのアニメーションが細かいアニメーションの集合と考えた時に、細かい方のアニメーションの発火タイミングを指定できるという意味です。
例えば、上下するアニメーションであれば、上に動くアニメーションと下に動くアニメーションの集合と考えられます。
その中で上に動くアニメーションはn秒後に、下に動くアニメーションは上に動くアニメーションが動いた後にといったタイミングを指定することができます。
この考え方は、CSSのkeyframes
とほぼ同じと思うので、知っている方はイメージしやすいかもです。
APIの使用例
実際に上下するアニメーションをkeyframeAnimator
を用いて実装する例を見て、使い方を説明していきます。
import SwiftUI
struct SingleKeyframeAnimationView: View {
@State private var animationTrigger = false
var body: some View {
ZStack {
Divider()
Text("😇")
.font(.largeTitle)
.keyframeAnimator(initialValue: 0.0, trigger: animationTrigger) { content, value in
content
.offset(y: value)
} keyframes: { _ in
KeyframeTrack {
LinearKeyframe(0.0, duration: 1.0)
SpringKeyframe(90.0, duration: 4.0)
SpringKeyframe(-90.0, duration: 4.0, spring: .smooth(duration: 4.0))
MoveKeyframe(90.0)
CubicKeyframe(-90.0, duration: 1.0)
CubicKeyframe(90.0, duration: 4.0)
MoveKeyframe(-90.0)
LinearKeyframe(0, duration: 1.0)
}
}
.onTapGesture {
animationTrigger.toggle()
}
}
}
}
initialValue
には、0.0
を指定していますが、これはアニメーション対象のoffsetのy軸を指定しています。
初期は上下には移動していないので、0.0
としています。
続いて、trigger
には@State
のBool
型のプロパティを設定しています。
こちらは名前でも推測できますが、指定した値の変更を検知することでアニメーションのトリガーとなります。
例ではBool
型を指定していますが、Equatable
であればなんでも大丈夫です。
.keyframeAnimator(initialValue: 0.0, trigger: animationTrigger) { content, value in
content
.offset(y: value)
}
content
のクロージャでは、アニメーション対象のView
に対してどんなアニメーションを行うかを指定します。
指定の仕方としては、クロージャの第一引数にkeyframeAnimator
modifierを付与したView
が渡されるので、そいつに対して任意のmodifierを付与してアニメーションを推定します。
例は、上下移動のアニメーションなので、offset(y:)
のmodifierを付与しています。
offset
では第二引数のvalue
を指定しており、初期の値はinitialValue
の設定した値で渡されます。
第二引数の値(value
)は後述するkeyframes
で設定するクロージャ内で変更する値を決定し、変更するたびにcontent
のクロージャが呼び出されてアニメーションが起きます。
一点注意点として、以下ドキュメントの注意喚起もされていますがcontent
のクロージャはアニメーション中頻繁に呼び出されることになるので、高負荷な処理は実行するとパフォーマンスに影響が出る可能性があります。
Note that the content closure will be updated on every frame while animating, so avoid performing any expensive operations directly within content.
日本語訳
contentアニメーション中はフレームごとにクロージャが更新されるので、 内で直接コストのかかる操作を実行しないように注意してくださいcontent。
} keyframes: { _ in
KeyframeTrack {
LinearKeyframe(0.0, duration: 1.0)
SpringKeyframe(90.0, duration: 4.0)
SpringKeyframe(-90.0, duration: 4.0, spring: .smooth(duration: 4.0))
MoveKeyframe(90.0)
CubicKeyframe(-90.0, duration: 1.0)
CubicKeyframe(90.0, duration: 4.0)
MoveKeyframe(-90.0)
LinearKeyframe(0, duration: 1.0)
}
}
最後にkeyframes
クロージャでは、アニメーションを行うための値をどの様なタイミングでどの様な値に変更するかといったことを指定します。
このクロージャ内で返すものとしては、KeyframeTrack
のインスタンスを返すだけです。
KeyframeTrack
のイニシャライザのクロージャ内で、KeyframeTrackContent
に準拠したインスタンスを複数指定することで、具体的なアニメーションの各動作の発火タイミングや値を指定しています。
KeyframeTrackContent
に準拠した型は標準では以下が存在し、それぞれ使い分けることでさまざまなアニメーションを表現できます。
型名 | 概要 |
---|---|
LinearKeyframe | 線形のアニメーションを表現する |
SpringKeyframe | スプリング関数を使用したアニメーションを表現する |
MoveKeyframe | アニメーションをせずに指定した値をUIに反映させる |
CubicKeyframe | 3字曲線を使用したアニメーションを表現する |
ただこれを見てもどの様な動きになるか想像しづらいので、サンプルのコードとgifを見てどういう動きになっているか見ていただければと思います。
またお手元でも、いろいろ値を変えてどの様に動作が変わるか見ると、理解が進むかと思います。
補足すると、以下コードでは
- 1秒間offsetを変化させず待機: セグメント時間1秒
- Springアニメーションで90.0pxの位置に移動する(アニメーションする速度はSpringのデフォルト): セグメント時間4秒
- Springアニメーションで-90.0pxの位置に移動する(アニメーションする時間は4秒): セグメント時間4秒
- 90.0pxの位置にアニメーション無しで移動する: セグメントの時間無し
- -90.0pxの位置に1秒かけてスムーズにアニメーションする様に自動計算された曲線で移動する: セグメント時間1秒
- 90.0pxの位置に4秒かけてスムーズにアニメーションする様に自動計算された曲線で移動する: セグメント時間4秒
- 1秒かけて線形で元の位置に移動する: セグメント時間1秒
KeyframeTrack {
LinearKeyframe(0.0, duration: 1.0) // - 1
SpringKeyframe(90.0, duration: 4.0) // - 2
SpringKeyframe(-90.0, duration: 4.0, spring: .smooth(duration: 4.0)) // - 3
MoveKeyframe(90.0) // - 3
CubicKeyframe(-90.0, duration: 1.0) // - 4
CubicKeyframe(90.0, duration: 4.0) // - 5
MoveKeyframe(-90.0) // - 6
LinearKeyframe(0, duration: 1.0) // - 7
}
このように、KeyframeTrack
のクロージャで指定したKeyframeTrackContent
の順にアニメーションが発火し最終的に一つのアニメーションの動きを実装することができます。
アニメーションの複合実装例
先ほどの例では、y軸のoffsetの値だけをkeyframe
でアニメーションさせましたが、複数の値を同時にアニメーションさせてより複雑なアニメーションを表現することも可能です。
import SwiftUI
struct MultiKeyFrameAnimationView: View {
@State var animationTrigger = true
var body: some View {
getReactionView(reaction: "😇")
.keyframeAnimator(initialValue: AnimationValue(), trigger: animationTrigger) { content, value in
content
.scaleEffect(value.scale)
.rotationEffect(value.rotation)
.scaleEffect(y: value.verticalStretch)
} keyframes: { _ in
KeyframeTrack(\.scale) {
LinearKeyframe(1, duration: 0.36)
SpringKeyframe(3, duration: 1.0)
SpringKeyframe(1, duration: 0.36)
}
KeyframeTrack(\.rotation) {
LinearKeyframe(.degrees(90), duration: 1.0)
SpringKeyframe(.degrees(360), duration: 6.0, spring: .bouncy(duration: 6.0))
SpringKeyframe(.degrees(0), duration: 1.0)
}
KeyframeTrack(\.verticalStretch) {
LinearKeyframe(1, duration: 0.5)
MoveKeyframe(1.5)
SpringKeyframe(3.0, duration: 4.5)
LinearKeyframe(1, duration: 1.0)
}
}
.onTapGesture {
animationTrigger.toggle()
}
}
func getReactionView(reaction: String) -> some View {
Text(reaction)
.font(.largeTitle)
}
}
extension MultiKeyFrameAnimationView {
struct AnimationValue {
var scale = 1.0
var verticalStretch = 1.0
var rotation: Angle = .degrees(0)
}
}
実装方法は、最初の例とそれほど変わりません。
initialValue
では、複数の値を変更するためにstruct
を新たに定義して初期のインスタンスを設定します。
今回の例では、大きさと縦比率、角度を同時にアニメーションさせるので、それぞれアニメーションさせるための値をもったstruct
を定義しています。
struct AnimationValue {
var scale = 1.0
var verticalStretch = 1.0
var rotation: Angle = .degrees(0)
}
content
クロージャでは、大きさと縦比率、角度を変更するmodifireを設定しています。
{ content, value in
content
.scaleEffect(value.scale)
.rotationEffect(value.rotation)
.scaleEffect(y: value.verticalStretch)
}
最後にkeyframes
クロージャでは、アニメーションさせる値ごとにKeyframeTrack
を生成して、アニメーションする要素毎にアニメーションの値とタイミングを指定します。
scale
とrotation
、verticalStretch
の指定したKeyframeTrack
は並列に動き出します。
しかし、それぞれのアニメーションの動作のタイミングや時間は各々のKeyframeTrack
で指定しているので、実際にアニメーションするタイミングやアニメーションが終了するタイミングは別々です。
keyframes: { _ in
KeyframeTrack(\.scale) {
LinearKeyframe(1, duration: 0.36)
SpringKeyframe(3, duration: 1.0)
SpringKeyframe(1, duration: 0.36)
}
KeyframeTrack(\.rotation) {
LinearKeyframe(.degrees(90), duration: 1.0)
SpringKeyframe(.degrees(360), duration: 6.0, spring: .bouncy(duration: 6.0))
SpringKeyframe(.degrees(0), duration: 1.0)
}
KeyframeTrack(\.verticalStretch) {
LinearKeyframe(1, duration: 0.5)
MoveKeyframe(1.5)
SpringKeyframe(3.0, duration: 4.5)
LinearKeyframe(1, duration: 1.0)
}
}
keyframeAnimator
の使い所
見てきた通り、アニメーションの動作をかなり細かく指定できるAPIでした。
同じく高度なアニメーションを実装できるphaseAnimator
と比較しつつ、どういう時に使用すれば良いのか考えてみます。
phaseAnimator
については、過去に記事を書いているので、よければこちらも合わせて参照していただければと思います。
この二つのAPIは結構似ているところはあります。
それは、細かいアニメーションを繋ぎ合わせて一つの複雑なアニメーションを作り上げるという点です。
とはいえ、それぞれ違いもあるのでその違いから、どういう使い分けができるか考えます。
keyframeAnimator
のメリデメ
-
メリット
- アニメーションの要素毎にタイミングやアニメーションの種類(springなど)を制御できること
-
phaseAnimator
でも複数の要素を同時にアニメーションさせることができますが、要素毎にタイミングまで指定することはできず同時に実行するようにしか実装できないため、keyframeAnimator
の方がより高度で複雑なアニメーションを実装できると言えます
-
- アニメーションの要素毎にタイミングやアニメーションの種類(springなど)を制御できること
-
デメリット
- API仕様が複雑
- アニメーションの要素毎に、一動作毎にアニメーションのタイミングや方法を指定するため、簡単なアニメーションを実装するには冗長な記載となってしまいます
- API仕様が複雑
上記より、keyframeAnimator
は表現できることが多い反面、より低レイヤーな要素を意識しなくてはならないためAPI仕様が複雑なのでphaseAnimator
で実装できるのであればそちらを利用した方が良さそうです。
phaseAnimator
で実装できないぐらい複雑なアニメーションを組みたいとなった場合のみ、keyframeAnimator
の利用を検討するのが良いと思いました。
おわり
今回は今まで知らなかったkeyframeAnimator
によるアニメーションの実装の仕方をキャッチアップしました。
触ってみた所感としては、業務で触ることは現状あまりなさそうと思いました。
というのも、keyframeAnimator
を必要とするほどの仕様があまりなさそうなので、ちょっと複雑なアニメーションを組みたいとなってもphaseAnimator
で事足りる気がしています。(無限ループのアニメーションとか)
ただ、いざという時こういう飛び道具があることを知っていればドヤ顔できるので、知れてよかったです。
今回作ったサンプルコードは以下リポジトリに上げていますので、興味がある方はご参照ください。