見出し画像

SwiftUIのアニメーション

アニメーションの設定による違いを「ゴミ箱アイコンタップで書類が吸い込まれて消える」など具体的なサンプルで確認しましょう。

今回のサンプルの一部を動画にまとめました。


※この記事ではSwift言語の基本的な知識を前提にしています。
コード内のキーワードや書式などの不明点は Swift5初級ガイド などを参照してください。

※SwiftUIについて全体的なことは『SwiftUI最初の一歩』、コードの基本とビューについては『SwiftUIの文法 その1 View』を参照してください。

お知らせ
電子書籍『Swift5初級ガイド』をAppleのブックストアから出しています。サンプルは無料です。MacでもiPadでもiPhoneでも読めます。
WWDC2020で発表されたSwift 5.3に対応した第6版がダウンロード可能です。(ご購入済みの場合は無料アップデートです)
(2020年7月4日に第6版にアップデートしました)
ブックストアから一度購入すると今後のアップデートは無料で読めます。

6宣伝store

・・・

・画像クリックで拡大表示できます
・画像を拡大表示中は画像の左右をクリックで画像だけを順に表示できます
・ソースコード部分は横にスクロール表示できます

サンプルはXcode 11.6のPlaygroundで作成しMacとiPadの Playgrounds 3.3.1 で動作を確認しました。
この記事の最後(有料部分)にあるリンクから完全なサンプルをダウンロードできます。


1 SwiftUIのアニメーションとは

ビューの大きさや色などを変化させると、通常はいきなり変化した状態に切り替わります。
この変化を一定時間をかけて滑らかな移り変わりとして表示するのがアニメーションです。

SwiftUIでは基本的に、既に画面上にあるビューの『変化』だけをアニメーション表示できます。


1-1 用語

引数やメソッド名でしばしば登場する用語を最初にまとめておきます。

duration アニメーションを表示する時間(画面が変化している時間)です。
単位は秒です。

画面変化をカーブと呼ぶ場合があります。
カーブにはlinear、easeInOut、springなどがあります。

linear 一定の速度で変化します。
突然一定速度で動き出すことは自然界ではありえませんが、アニメーションでは可能です。

easeInOut ゆっくりはじまり、ゆっくり終わります。
だんだん変化が早くなり一定速度になりだんだんゆっくりになり最終的に止まります。
物理的に自然な動きに近いのがeaseInOutです。

ほかにゆっくりはじまる easeIn と、ゆっくり終わる easeOut があります。

spring ぷるんとした動きなど引数で調整できる、バネのような振動する動作です。


1-2 Animation型

アニメーション表示のための型(struct)です。
アニメーションのdurationやカーブなどを指定しインスタンスを作成します。

カーブ別のタイププロパティがあります。

// Structure Animation Type Properties
static let `default`: Animation
static var easeIn: Animation
static var easeInOut: Animation
static var easeOut: Animation
static var linear: Animation

durationも同時に指定する場合はタイプメソッドが使えます。
easeIn(duration:)
easeInOut(duration:)
easeOut(duration:)
linear(duration:)

springは引数が多いので別途本文で説明します。


1-3 ビューごとのアニメーション

Viewのanimation(_:)モディファイアを使い引数のアニメーションを、このビュー内のすべてのアニメーション可能な値に適用します。

// Viewのモディファイア
func animation(_ animation: Animation?) -> some View

引数はひとつだけです。
引数の型はAnimation?です。
nilを設定するとアニメーションのキャンセルになります。

最もシンプルなアニメーションのサンプルです:

import SwiftUI
import PlaygroundSupport

// Viewのanimationモディファイア
struct Sample07View: View {
  @State private var rotation = true
  var body: some View {
     VStack {
        Image(systemName: "cloud.sun.bolt")
           .font(.largeTitle)
           .rotationEffect(Angle.degrees(rotation ? 0 : 360))
           .animation(Animation.easeInOut)
           .padding()
        Button(action: {
           self.rotation.toggle()
        }) {
           Text("アニメ")
        }
        .padding()
     }
  }
}

// playgroundで実行する場合に必要なコード
PlaygroundPage.current.setLiveView(Sample07View())

iPadでの実行画面

画像2

「アニメ」ボタンをタップするとアイコンが回転します。

ボタンタップのアクションでrotationプロパティを反転(trueをfalseに変化)させています。
rotationプロパティは次のrotationEffectモディファイアの引数を0から360に切り替えます。
.rotationEffect(Angle.degrees(rotation ? 0 : 360))

rotationEffect(_:anchor:)はanchorで指定した点を中心に回転させて表示します。

// rotationEffect(_:anchor:)モディファイア
func rotationEffect(_ angle: Angle, anchor: UnitPoint = .center) -> some View

最初の引数で角度を指定し、2つ目のanchorで中心を指定します。
anchorを省略すると.centerになります。

rotationEffect(_:anchor:)は指定角度回転した静止状態のビューを表示しますが、アニメーションが0から360度の連続した変化を実行することでアニメーションとして表示しています。


1-4 アニメーション効果と指定順序

アニメーション効果にはanimation(_:)モディファイアの位置が関係します。

Image(systemName: "cloud.sun.bolt")
  .font(.largeTitle)
  .rotationEffect(Angle.degrees(rotation ? 0 : 360))
  .animation(Animation.easeInOut)

Imageビューイニシャライザとanimation(_:)モディファイアの間のモディファイア引数の変化がアニメーション表示されます。
animation(_:)モディファイアの後にrotationEffect(_:anchor:)モディファイアの順序を変更すると、ほかのコードは同じでも表示は静止状態のままでアニメーションしません(いきなり回転した状態に切り替わります)。


2 アニメーションの変化カーブ

操作にともなうアニメーション表示はユーザー体験を高めます。
しかし一定速度での変化は不自然に見える場合があります。

このためアニメーションの『変化』、アニメーションを構成する各フレーム間の変化する値(初期値から最終値)を縦軸、時間を横軸にしたグラフで考えます。
このグラフが 1-1 用語 で紹介した『カーブ』です。

カーブと言っても linear は一定速度のためグラフは直線となります。
linearはループ処理で一定の増分を加え続ける処理で簡単に実現できます。

easeInOut はフレームワークによっては easeIn easeOut と呼ばれる場合もあります。
アニメーションの開始時は速度ゼロから徐々に加速し、一定速度を経て、アニメーション終了時に徐々に速度をゼロに戻す関数です。

この二つを比較するサンプルです。

最初のimport文2行と最後のsetLiveViewの行は共通なので割愛しています。

let animationDuration = 2.0

// カーブとduration
struct Sample07View: View {
  @State private var doAnimation = true
  var body: some View {
     VStack {
        HStack {
           VStack {
              Text("easeInOut").padding()
              Image(systemName: "cloud.sun.bolt")
                 .resizable()
                 .frame(width:100, height:100)
                 .rotationEffect(Angle.degrees(doAnimation ? 0 : 360))
                 .animation(.easeInOut(duration:animationDuration))
           }
           .padding()
           VStack {
              Text("linear").padding()
              Image(systemName: "cloud.sun.bolt")
                 .resizable()
                 .frame(width:100, height:100)
                 .rotationEffect(Angle.degrees(doAnimation ? 0 : 360))
                 .animation(.linear(duration: animationDuration))
           }
           .padding()
        }
        Button(action: {
           self.doAnimation.toggle()
        }) {
           Text("アニメ")
        }
        .padding()
        
     }
  }
}

二つのImageにそれぞれeaseInOutとlinearのアニメーションを設定しています。
両方ともdoAnimationプロパティで回転角度を変化させるため、ボタンのアクションでdoAnimationが変化すると同時にアニメーション表示します。

各ビューのコードは状態の変化に対応したアニメーションの指定だけですが、それだけで複数のビューのアニメーションが同時に開始します

ここではanimationDurationで2秒かけてゆっくりアニメーション表示するように指定しています。


3 withAnimation( _: _:)関数

各ビューにanimation(_:)モディファイアを指定するのではなく、状態の変化にアニメーションを指定するのがwithAnimation( _: _:)関数です。

func withAnimation<Result>(_ animation: Animation? = .default, 
_ body: () throws -> Result) rethrows -> Result

二つの引数を持つ関数です。
最初の引数はAnimation型のインスタンスでカーブやDurationを指定します。
2つ目の引数はクロージャでアニメーションさせる状態の変化後の値を設定します。

次のサンプルで説明しましょう。

let height: CGFloat = 150.0

// withAnimationによるアニメーション
struct Sample07View: View {
  @State private var cornerRadius: CGFloat = 30.0
  @State private var width: CGFloat = 220.0
  var body: some View {
     VStack {
        HStack {
           Button(action: {
              withAnimation(.easeInOut){
                 self.width = 220.0
                 self.cornerRadius = 0.0
              }
           }) {
              Text("長方形")
           }
           .padding()

           Button(action: {
              withAnimation(.spring()){
                 self.width = 220.0
                 self.cornerRadius = 30.0
              }
           }) {
              Text("角丸")
           }
           .padding()

           Button(action: {
              withAnimation(.easeInOut(duration: 0.2)){
                 self.width = 150
                 self.cornerRadius = 75.0
              }
           }) {
              Text("円")
           }
           .padding()

        }

        RoundedRectangle(cornerRadius: cornerRadius)
           .foregroundColor(.orange)
           .frame(width: width, height: height)
     }
  }
}

アニメーション表示するビューは一番下のRoundedRectangleだけです。

「長方形」「角丸」「円」三つのボタンをタップするとオレンジ色の画面がアニメーションを伴って変化します。

iPadで実行した画面です。

画像3

たとえば長方形ボタンのアクションは

withAnimation(.easeInOut){
   self.width = 220.0
   self.cornerRadius = 0.0
}

幅を220、角丸をゼロにして(二つの状態を同時に変化させて)います。
同様に「角丸」は幅を220、角丸を30、円は幅を150、角丸を75にしています。
このようにwithAnimation( _: _:)関数は複数の状態を一度に変化させその結果をアニメーション表示できます。
通常のトレイリングクロージャの書き方ですので、もちろん関数を呼び出すことも可能です。

Viewのanimation(_:)モディファイアを使いビューごとにアニメーション指定をするよりも使いやすいため、withAnimation( _: _:)関数が使われる方が多いようです。

withAnimation( _: _:)を使う場合は、アニメーション表示する個々のビューにはanimation(_:)モディファイアは不要です。

RoundedRectangle(cornerRadius: cornerRadius)
  .foregroundColor(.orange)
  .frame(width: width, height: height)

RoundedRectangleの引数に指定しているcornerRadiusとframeモディファイアのwidthの変化を指定カーブeaseInOutでアニメーション表示します。
このようにシェイプの形状変化もアニメーション表示できます。


3-1 showDetailサンプル

これはSwiftUIの公式チュートリアル Animating Views and Transitions のコードを利用したサンプルです。

ボタンのアイコン"chevron.right.circle"がボタンをタップすると90度回転し1.5倍サイズで表示します。

struct Sample07View: View {
  @State private var showDetail = false
  var body: some View {
     VStack {
        HStack {
           Button(action: {
              withAnimation {
                 self.showDetail.toggle()
              }
           }) {
              Image(systemName: "chevron.right.circle")
                 .imageScale(.large)
                 .rotationEffect(.degrees(showDetail ? 90 : 0))
                 .scaleEffect(showDetail ? 1.5 : 1)
                 .padding()
           }
           Spacer()
        }
        if showDetail {
           Text(
              """
              詳細表示
              適切なアニメーションを使おう。
              -このサンプルはチュートリアルをもとにしています-
              """)
           .padding()
        }
        Spacer()
     }
  }
}

showDetailプロパティをwithAnimationのクロージャ内で切り替えています。

ボタンのイメージが回転し拡大するアニメーションと同時に、テキストを表示します。

ボタンのアクションではwithAnimation関数の最初の引数は省略され、トレイリングクロージャだけを指定しています。
引数のAnimation省略時は.defaultアニメーションが使われます。

iPadでの実行画面です。

画像4


4 Bindingのアニメーション

animation(_:)メソッドにはプロパティラッパーのBinding型で定義されたメソッドもあります。
スペルは同じですがView型のanimation(_:)とは別のメソッドです。
バインドしている値の変化をアニメーション表示します。
Bindingのanimation(_:)メソッドはwithAnimation( _: _:)の代わりに使え、同じような効果を得られます。

// Bindingのanimation(_:)メソッド
func animation(_ animation: Animation? = .default) -> Binding<Value>

メソッド名も引数も同じで、コントロールなどで使えます。
Bindingのアニメーションの有無を比較するサンプルです。

// バインディングのアニメーション
struct Sample07View: View {
  @State var sValue: CGFloat = 3

  var body: some View {
     VStack {
        Text("Bindingのアニメーション")
           .font(.title)
           .padding()
        Stepper(value:$sValue, in:1...4) {
           Text("アニメなし")
        }
        .padding()
        Stepper(value:$sValue.animation(.interactiveSpring( dampingFraction:0.2)),
              in:1...4) {
                 Text("interactiveSpring\nアニメーションあり")
        }
        .padding()
        Text("\(Int(sValue))")
           .font(Font.largeTitle.monospacedDigit())
           .frame(width: 60*sValue, height: 34*sValue)
           .background(Color.green)
           .padding()
        Spacer()
     }
  }
}

Stepperの増減でTextの幅と高さを変更します。
二つのStepperで同じ値を変更します。
下のStepperだけアニメーションを付けています。
このアニメーションは interactiveSpring で、引数を調整しプルンとした動きを出しています。

// interactiveSpring
static func interactiveSpring(response: Double = 0.15, 
                dampingFraction: Double = 0.86, 
                blendDuration: Double = 0.25) -> Animation

引数は三つあり、デフォルト値を持っているのですべて省略も可能です。
このサンプルではdampingFractionだけ0.2に変更しています。

iPadでの実行画面です。

画像5

テキストの.frame(width: 60*sValue, height: 34*sValue)で幅と高さをStepper操作で変更します。

このサンプルのように複数のバインディング(State属性のprojectedValue)があっても、animation(_:)メソッドを設定したバインディングの変化だけアニメーションが付きます。


5 Transitionの指定

Transition(トランジション)は既に表示しているビューではなく、ビューの追加や削除にアニメーションを加えます。

トランジションを設定するにはView型のtransition(_:)メソッドを使います。

func transition(_ t: AnyTransition) -> some View

モディファイアと同様に使います。

引数のAnyTransition型インスタンスでアニメーション効果の異なったトランザクションを指定します。
(Animation型インスタンスは直接設定できません)


5-1 AnyTransition型

トランザクションも基本的なものはタイププロパティでシンプルに設定できるようになっています。

static let identity: AnyTransition

static let opacity: AnyTransition

static var scale: AnyTransition { get }

static var slide: AnyTransition { get }

identity ビューをそのまま(アニメーションなしで)表示します。
opacity 挿入時には透明から不透明へ、除去時には不透明から透明へとアニメーション表示します。
scale 挿入時には小さいサイズから拡大し、除去時には現在のサイズから小さくなるアニメーション表示です。
slide 挿入時にはリーディングエッジ側からスライドし、除去時にはトレイリングエッジ方向へスライドして消えるアニメーションを表示します。

引数でより細かな指定が可能なタイプメソッドもあります。

static func offset(_ offset: CGSize) -> AnyTransition

static func move(edge: Edge) -> AnyTransition

など

offset(_:) 引数で指定した方向からスライドし除去時にはもとの方向へスライドして消えます。
move(edge:) エッジ指定は.bottom、.leading、.top、.trailingから選べます。

トランジションのシンプルなサンプルです。

struct Sample07View: View {
  @State private var isShow = false
  var body: some View {
     VStack {
        HStack {
           if isShow {
              Image(systemName: "cloud.sun.bolt")
                 .resizable()
                 .frame(width:100, height:100)
                 .transition(.scale)
//      .transition(.identity)
//      .transition(.offset(CGSize(width: -150, height: -150)))   //.scale
//      .transition(.opacity)
                 .padding()
           }
        }
        Button(action: {
           withAnimation(.easeInOut){
              self.isShow.toggle()
           }
        }) {
           Text("アニメ")
        }
        .padding()
     }
  }
}

コメントの行を切り替えそれぞれの違いを確認してください。
isShowの切り替えをwithAnimationで囲む部分はアニメーション表示には必要です。
iPadで実行した画面です。

画像6

ここから先は

11,284字 / 7画像 / 1ファイル
この記事のみ ¥ 500

今後も記事を増やすつもりです。 サポートしていただけると大変はげみになります。