▶️

【SwiftUI】Animationを図解してみる

2024/10/16に公開

はじめに

本記事では、SwiftUIにおけるアニメーションの考え方とその実装方法をイメージ付きで解説していきます。

普段アニメーションを付与する際に、linear, ease系を多用していたのですが、表現の幅を広げる上でアニメーションについて深掘りしてみたくなったので、実際に学習したことを言語化してまとめていきます。

記事の構成

  1. アニメーションの種類
  2. 【UnitCurveモデル】基本のlinear + ease系のアニメーション
  3. 【Springモデル】バネの動きをシミュレートするアニメーション
  4. まとめ

環境

  • 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との違い

easeOutcircularEaseOutの違いは、『曲線形状に単位円を使用しているか』です。

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との違い

circularEaseOutcircularEaseIn(後述)の組み合わせで曲線形状が成り立っています。
つまり、以下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との違い

easeIncircularEaseInの違いは、『曲線形状に単位円を使用しているか』です。

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つの表現方法を記載しているので、ここにリンクを載せます。
https://developer.apple.com/documentation/swiftui/spring#overview

まずは伝統的なバネ・マス・ダンパによる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()
}

同様に、パラメータの違いによるバネの比較を載せておきます。

3.2.1 アニメーションの持続時間durationパラメータによるアニメーション比較

3.2.2 バネの弾み具合bounceパラメータによるアニメーション比較

3.3 プリセットのSpringアニメーションsmooth,snappy,bouncyの違い

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について包括的に触れられているので、ぜひ参考にしてみてください。

https://developer.apple.com/videos/play/wwdc2023/10156

Bezier Curves

DesmosでCubic Bézier Curveをシミュレートできます。
https://www.desmos.com/calculator/cahqdxeshd?lang=ja

Apple公式ドキュメント

UnitCurve

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

Spring

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

Discussion