【SwiftUI】Animationを図解してみる
はじめに
本記事では、SwiftUIにおけるアニメーションの考え方とその実装方法をイメージ付きで解説していきます。
普段アニメーションを付与する際に、linear
, ease
系を多用していたのですが、表現の幅を広げる上でアニメーションについて深掘りしてみたくなったので、実際に学習したことを言語化してまとめていきます。
記事の構成
- アニメーションの種類
- 【UnitCurveモデル】基本のlinear + ease系のアニメーション
- 【Springモデル】バネの動きをシミュレートするアニメーション
- まとめ
環境
- Xcode 16.0
- Swift 6.0
1. アニメーションの種類
SwiftUIには大きく2つのAnimationのモデルがあります。
- UnitCurveモデル
- Springモデル
モデルが分けられている理由は、『それぞれのモデルで、動きのベースや構成が異なるから』 です。
ざっくり、以下のイメージを持ってもらえればよいと思います。
UnitCurve = linear, ease系
Spring = バネの動き
以降のセクションでは、それぞれのモデルにおいて、アニメーションを構成する考え方を紹介します。
2.【UnitCurveモデル】基本のlinear + ease系のアニメーション
UnitCurveモデルにおけるアニメーションの考え方は、シンプルです。
『xy座標において、2点(0,0)
,(1,1)
を通る曲線を考え、その曲線形状によってアニメーションの速度を変える』
これだけです。
Appleの公式ドキュメントも引用します。
UnitCurve
A function defined by a two-dimensional curve that maps an input progress in the range [0,1] to an output progress that is also in the range [0,1]. By changing the shape of the curve, the effective speed of an animation or other interpolation can be changed.
引用: https://developer.apple.com/documentation/swiftui/unitcurve
早速、UnitCurveモデルのアニメーションを1つずつコードと図を交えながらみていきましょう。
2.1 linear
@State private var isAnimating: Bool = false
// (中略)
VStack {
Circle()
.frame(width: 40)
.animation(.linear(duration: 1.0), value: isAnimating)
}
.frame(maxWidth: .infinity, alignment: isAnimating ? .trailing : .leading)
.onTapGesture {
isAnimating.toggle()
}
2.2 easeOut
@State private var isAnimating: Bool = false
// (中略)
VStack {
Circle()
.frame(width: 40)
.animation(.easeOut(duration: 1.0), value: isAnimating)
}
.frame(maxWidth: .infinity, alignment: isAnimating ? .trailing : .leading)
.onTapGesture {
isAnimating.toggle()
}
circularEaseOutとの違い
easeOut
とcircularEaseOut
の違いは、『曲線形状に単位円を使用しているか』です。
circularEaseOut
は、曲線形状が単位円の第2象限に等しいです。
Discussion
The shape of the curve is equal to the second (top left) quadrant of a unit circle.
引用: https://developer.apple.com/documentation/swiftui/unitcurve/circulareaseout#discussion
@State private var isAnimating: Bool = false
// (中略)
VStack {
Circle()
.frame(width: 40)
.animation(.timingCurve(.circularEaseOut, duration: 1.0), value: isAnimating)
}
.frame(maxWidth: .infinity, alignment: isAnimating ? .trailing : .leading)
.onTapGesture {
isAnimating.toggle()
}
2.3 easeInOut
@State private var isAnimating: Bool = false
// (中略)
VStack {
Circle()
.frame(width: 40)
.animation(.easeInOut(duration: 1.0), value: isAnimating)
}
.frame(maxWidth: .infinity, alignment: isAnimating ? .trailing : .leading)
.onTapGesture {
isAnimating.toggle()
}
circularEaseInOutとの違い
circularEaseOut
とcircularEaseIn
(後述)の組み合わせで曲線形状が成り立っています。
つまり、以下2つを組み合わせたものです。
- 単位円の第4象限
- 単位円の第2象限
Discussion
The shape of the curve is defined by a piecewise combination of circularEaseIn and circularEaseOut.
引用: https://developer.apple.com/documentation/swiftui/unitcurve/circulareaseinout#discussion
@State private var isAnimating: Bool = false
// (中略)
VStack {
Circle()
.frame(width: 40)
.animation(.timingCurve(.circularEaseInOut, duration: 1.0), value: isAnimating)
}
.frame(maxWidth: .infinity, alignment: isAnimating ? .trailing : .leading)
.onTapGesture {
isAnimating.toggle()
}
2.4 easeIn
@State private var isAnimating: Bool = false
// (中略)
VStack {
Circle()
.frame(width: 40)
.animation(.easeIn(duration: 1.0), value: isAnimating)
}
.frame(maxWidth: .infinity, alignment: isAnimating ? .trailing : .leading)
.onTapGesture {
isAnimating.toggle()
}
circularEaseInとの違い
easeIn
とcircularEaseIn
の違いは、『曲線形状に単位円を使用しているか』です。
circularEaseIn
は、曲線形状が単位円の第4象限に等しいです。
Discussion
The shape of the curve is equal to the fourth (bottom right) quadrant of a unit circle.
引用: https://developer.apple.com/documentation/swiftui/unitcurve/circulareasein#discussion
@State private var isAnimating: Bool = false
// (中略)
VStack {
Circle()
.frame(width: 40)
.animation(.timingCurve(.circularEaseIn, duration: 1.0), value: isAnimating)
}
.frame(maxWidth: .infinity, alignment: isAnimating ? .trailing : .leading)
.onTapGesture {
isAnimating.toggle()
}
2.5 カスタムのアニメーション
UnitCurveモデルをベースとするアニメーションは、基本的にCubic Bézier Curve(3次ベジェ曲線)によって、アニメーションの速度を構成することができます。
timingCurve(_:_:_:_:duration:)
メソッドは、始点(0,0)
、終点(1,1)
を固定とし、制御点(p1x,p1y)
,(p2x,p2y)
を与えることで曲線の形状を変えます。
static func timingCurve(
_ p1x: Double,
_ p1y: Double,
_ p2x: Double,
_ p2y: Double,
duration: TimeInterval = 0.35
) -> Animation
実例
こんなイメージのアニメーションを作成するとします。
最初と最後は、速度が速く、中間は穏やかなイメージです。
制御点(0.0,0.8)
,(1.0,0.2)
をパラメータとして与えて実装してみます。
@State private var isAnimating: Bool = false
// (中略)
VStack {
Circle()
.frame(width: 40)
.animation(.timingCurve(0.0, 0.8, 1.0, 0.2, duration: 1.0), value: isAnimating)
}
.frame(maxWidth: .infinity, alignment: isAnimating ? .trailing : .leading)
.onTapGesture {
isAnimating.toggle()
}
イメージ通りのアニメーションができました🙌
3.【Springモデル】バネの動きをシミュレートするアニメーション
同様に、Springモデルのアニメーションの構成も紹介します。
以下2つのどちらかでアニメーションを構成します。
- バネ(stiffness)・マス(mass)・ダンパ(damping)のパラメータを指定
- アニメーションの持続時間・バネの弾み具合を指定
前者は伝統的な方法であり、mass
,stiffness
,damping
を指定することでバネを表現します。
一方、後者はより直感的な方法として提案されていて、duration
,bounce
の2軸でバネを表現します。
Appleの公式ドキュメントにもSpringの2つの表現方法を記載しているので、ここにリンクを載せます。
まずは伝統的なバネ・マス・ダンパによるSpringアニメーションの表現方法を紹介します。
その後、アニメーションの持続時間・バネの弾み具合によるSpringアニメーションの表現方法を紹介します。
3.1 バネ・マス・ダンパによるSpringアニメーションの表現
先に実装方法を紹介します。
@State private var isAnimating: Bool = false
// (中略)
VStack {
Circle()
.frame(width: 40)
.animation(.spring(Spring(mass: 0.1, stiffness: 10, damping: 0.1)), value: isAnimating)
}
.frame(maxWidth: .infinity, alignment: isAnimating ? .center : .leading)
.onTapGesture {
isAnimating.toggle()
}
パラメータの違いによるバネの比較を載せておきます。
3.1.1 バネ(stiffness)パラメータによるアニメーション比較
左は弾性力のあるバネ、右は弾性力のない柔らかいバネです。
3.1.2 マス(mass)パラメータによるアニメーション比較
左は重たいものをつけたバネ、右は軽いものをつけたバネです。
3.1.3 ダンパ(damping)パラメータによるアニメーション比較
左から順に、バネの減衰度合が強くなっています。
3.2 アニメーションの持続時間・バネの弾み具合によるSpringアニメーションの表現
3.1の方法では、どれくらいアニメーションが持続するのかわからない、バネの弾み具合をバネ・マス・ダンパから間接的に割り出さなければいけないため、試行錯誤した上で実装が必要になる印象でした。
それに対して、次の実装方法は上記の問題点に対してより直感的だと言えます。
パラメータをそれぞれ
- アニメーションの持続時間 =
duration
- バネの弾み具合 =
bounce
として、Spring(duration:bounce:)
メソッドを使用します。
実装方法を先に紹介します。
@State private var isAnimating: Bool = false
// (中略)
VStack {
Circle()
.frame(width: 40)
.animation(.spring(Spring(duration: 1.0, bounce: 0.5)), value: isAnimating)
}
.frame(maxWidth: .infinity, alignment: isAnimating ? .center : .leading)
.onTapGesture {
isAnimating.toggle()
}
同様に、パラメータの違いによるバネの比較を載せておきます。
duration
パラメータによるアニメーション比較
3.2.1 アニメーションの持続時間
bounce
パラメータによるアニメーション比較
3.2.2 バネの弾み具合
smooth
,snappy
,bouncy
の違い
3.3 プリセットのSpringアニメーションSwiftUIにはSpringのプリセットとしてsmooth
,snappy
,bouncy
が用意されています。
WWDC23の解説を引用します。
smooth
: no bounce
snappy
: small bounce
bouncy
: medium bounce
4. まとめ
今回の記事では、SwiftUIにおけるアニメーションの表現を図解してみました。
SwiftUIでは、アニメーションが『UnitCurveモデル』と『Springモデル』の2種類から構成可能であ
り、ベースの考え方・実装方法・パラメータによるアニメーションの違いを記事で紹介しました。
今回の学習を通して、linearやease系よりもSpring系がより自然なアニメーションの表現方法であり推奨されていることも知りました。
実装していく中で、引き続きUIの表現の幅を広げていきたいと思います。
参考
今回の学習と記事の執筆にあたって、参考にしたものを以下に掲載しております。
WWDC23のセッション
WWDC23のセッションでは、SwiftUIのAnimationについて包括的に触れられているので、ぜひ参考にしてみてください。
Bezier Curves
DesmosでCubic Bézier Curveをシミュレートできます。
Apple公式ドキュメント
UnitCurve
Spring
Discussion