SwiftUIで少し凝ったアニメーションを実装した話

はじめに

はじめまして。KyashのモバイルチームでiOSアプリの開発を担当している emoto です。 Kyash Advent Calendar 2022の12/11担当分です。

今回、下のような少し凝ったアニメーションを SwiftUI で実装しましたので、その概要を紹介します。

このアニメーションを実装したのは、Kyashアプリでカード発行を申し込む際に最初に表示される画面です。
Kyashで発行するカードは利便性だけでなく、デザインにもこだわっているので申し込み時に実際のデザインや選んだカラーを表面と裏面で切り替えて確認できるようにしています。

実装のポイント

この記事のポイントは下記の2つです。

  • アニメーションを2つに分割する
  • アニメーションの完了を判定する方法

こちらのサンプルアプリでコードを一部紹介します。

前提

まず前提から。
Viewの「Z軸方向の上下」と「位置」を変えたいので、ZStackで囲って .offset.zIndex を使います。

ZStack {
    blueView
        .offset(x:y:) // 位置を指定
        .zIndex() // Z軸方向の上下を指定

    redView
        .offset(x:y:)
        .zIndex()
}

アニメーションを2つに分割する

ここからが本題です。
アニメーションの動きを見て reverse を上手く使って実装できないか考えたのですが、カードの上下が入れ替わって画面中央に戻る時の座標が元の座標と異なるので実現できませんでした。 そこで、

  • ①左右に広がるアニメーション
  • ②カードの上下を入れ替えて画面中央に戻るアニメーション

の2つに分割しました。こうすることでアニメーションの記述がとても単純になります。

①左右に広がるアニメーション

let animationDuration = 0.2

// RedViewが上に来ているかどうか
@State var isRedViewAbove = true

ZStack {
    blueView
        .offset(x:y:)
        .zIndex()
        // アニメーションの実行時間と、どの値の変更を監視してアニメーションするかを指定
        .animation(.easeInOut(duration: animationDuration), value: isRedViewAbove) 

    /*
     * redViewも同様
     */ 

    Button {
        // どちらのViewが上にあるかで場合分けして、左右に開いた時のOffsetに更新する
        if isRedViewAbove {
            // Update Offset for Slide Horizontally
        } else {
            // Update Offset for Slide Horizontally
        }

        // アニメーションのtriggerになるisFrontCardAboveを更新する
        isRedViewAbove.toggle()
    } label: {
        Text("Tap to Change")
            .foregroundColor(.white)
            .background(
                 Color.black
                 .contentShape(Rectangle())
                 .frame(width: 150, height: 44)
            )
    }

}

②カードの上下を入れ替えて画面中央に戻るアニメーション

let animationDuration = 0.2

// RedViewが上に来ているかどうか
@State var isRedViewAbove = true

ZStack {
    blueView
        .offset(x:y:)
        .zIndex()
        .animation(.easeInOut(duration: animationDuration), value: isRedViewAbove)
        // アニメーション①のtriggerを監視して、アニメーション②を実行
        .onChange(of: isRedViewAbove) { newValue in

                // どちらのViewが上にあるかで場合分けして、画面中央に戻る時のOffsetとZIndexに更新する
                if isFrontCardAbove {
                     // Update Offset for Slide Center
                     // Update ZIndex
                } else {
                     // Update Offset for Slide Center
                     // Update ZIndex
                }
            
        } 

    /*
     * redViewも同様
     */
    
}

アニメーションの完了を判定する方法

アニメーションを上記の①②に分けたことで、アニメーション①の完了を検知してアニメーション②を実行する必要が出てきます。SwiftUIではアニメーションの完了を検知する手段が用意されていないので、

「アニメーション①の実行時間と同じ時間だけ、アニメーション②を遅延実行する」

ことで間接的に検知しました。正直ここは改善の余地があると思っています。

let animationDuration = 0.2

// RedViewが上に来ているかどうか
@State var isRedViewAbove = true

ZStack {
    blueView
        .offset(x:y:)
        .zIndex()
        .animation(.easeInOut(duration: animationDuration), value: isRedViewAbove)
        // アニメーション①のtriggerを監視して、遅延実行
        .onChange(of: isRedViewAbove) { newValue in

            // アニメーション①の実行時間と同じ時間だけ、アニメーション②を遅延実行する
            DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration) {

                if isFrontCardAbove {
                     // Update Offset for Slide Center
                     // Update ZIndex
                } else {
                     // Update Offset for Slide Center
                     // Update ZIndex
                }
            }
        } 

    /*
     * redViewも同様
     */
    
}

最後に

Kyashでは一緒に働いてくださるエンジニアを募集中です。 Fintechにご興味のある方是非是非選考のご応募待っております

herp.careers