[SwiftUI] Path をアニメーションさせる方法

SwiftUI

     
⌛️ 3 min.
SwiftUI の Path での描画をアニメーションさせる方法を説明します。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Big Sur 11.3
  • Xcode 12.5
  • iOS 14.5

Path

SwiftUI の Path を使うと自由に描画することができます。グラフであったり、幾何学模様であったり。

Path をアニメーションしたい

たいていの SwiftUI の要素には、animation する方法が 分かりやすく用意されています。

例えば、Image をゆっくり表示させたい時には、 .easeIn 指定することでできます。

Path で描画したものを なぞるようなアニメーションをさせる方法は、直接的には用意されていません。

以下のようなアニメーションを実現する方法について説明します。

Path をアニメーションさせるための方法

.trim

Shape の modifier として .trim が用意されています。これは、Shape を Path として見て 指定範囲を描画する modifier です。

Apple のドキュメントは、こちら

Path は、Shape の1つでもあるので、この modifier を使用することで、Path の指定範囲を描画することができます。

この .trim を使用して Path をアニメーションさせることができます。

.trim に与えるパラメータをアニメーションに使う

.trim は、開始位置と終了位置の2つを引数として渡して 描画範囲を指定することができます。

例えば、終了位置を 0.0 から 1.0 に変更する経過を アニメーション表示させることで、Path を開始位置からなぞるようなアニメーションを作ることができます。

開始位置を 1.0 から 0.0 に変更する経過を使うと、Path の終了位置からなぞるアニメーションになります。

実装に向けた Path を使ったビュー

アニメーション対象とする Path を使ったビューを作ります。

以下のコードです。 MyGraph は、与えられた Int 配列を折線グラフで表示します。

アニメーションの効果を確認するために、duration: 2 を指定しています。(まだ アニメーションはしません)


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2021/06/02
//  © 2021  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    @State private var graphData = [10,15,8,23,18,35]
    @State private var aniSwitch:Bool = false
    //@State private var aniSwitch:Int = 10
    var body: some View {
        VStack {
            MyGraph(graphData: $graphData, triggerAnimation: $aniSwitch)
                .padding()
        }
        .animation(.easeIn(duration: 2))
    }
}

struct MyGraph: View {
    @Binding var graphData:[Int]
    let offsetFromLL = CGSize(width: 50, height: 50)
    let xScale: CGFloat = 40
    let yScale: CGFloat = 10

    var body: some View {
        GeometryReader { geom in
            Path { path in
                path.move(to: pointOnGraph(geom.size, 0, graphData[0]))
                for (id, data) in graphData.enumerated().dropFirst() {
                    path.addLine(to: pointOnGraph(geom.size, id, data))
                }
            }
            .stroke(lineWidth: 2.5)
            .fill(Color.orange)
        }
    }
    
    func pointOnGraph(_ size: CGSize, _ xValue: Int,_ yValue: Int) -> CGPoint {
        return CGPoint(x: offsetFromLL.width + CGFloat(xValue) * xScale, y: size.height - offsetFromLL.height - CGFloat(yValue) * yScale)
    }
    
}

上のコードの画面イメージ

GraphView
GraphView

以降の説明では、.trim に渡す終了範囲を 0.0 から 1.0 に変更することでのアニメーションを実装していきます。

アニメーションを実装してみる(onAppear を使って)

ポイントは、どのタイミングで、.trim に渡すパラメータを変更するかです。

1つのきっかけとして 表示される時 がありますので、onAppear のタイミングで変更します。

なお、View の内部で変更するので、.trim に渡すパラメータは、@State 指定する必要があります。


struct MyGraph: View {
    @Binding var graphData:[Int]
    let offsetFromLL = CGSize(width: 50, height: 50)
    let xScale: CGFloat = 40
    let yScale: CGFloat = 10
    // (1)
    @State private var end: CGFloat = 0.0

    var body: some View {
        GeometryReader { geom in
            Path { path in
                path.move(to: pointOnGraph(geom.size, 0, graphData[0]))
                for (id, data) in graphData.enumerated().dropFirst() {
                    path.addLine(to: pointOnGraph(geom.size, id, data))
                }
            }
            .trim(from: 0.0, to: end)
            .stroke(lineWidth: 2.5)
            .fill(Color.orange)
        }
        .onAppear{
            // (2)
            end = 1.0
        }
    }
    
    func pointOnGraph(_ size: CGSize, _ xValue: Int,_ yValue: Int) -> CGPoint {
        return CGPoint(x: offsetFromLL.width + CGFloat(xValue) * xScale, y: size.height - offsetFromLL.height - CGFloat(yValue) * yScale)
    }
    
}
コード解説
  1. View 内部から変更するので、変数を @State 指定します
  2. onAppear で end を 1.0 にします (上位の View で .animation 指定されているので、MyGraph では指定していません)

以下のように、ビューが開かれる時に、アニメーションが始まります。

これはこれで良いのですが、自分でアニメーション開始を指定したい時もあります。

アニメーションのタイミングを指定する方法(外部から Binding を渡す)

いろいろなやり方があります。Publisher を 使う方法もありますが、Binding を使って、外部から アニメーションを開始させる方法を実装します。

外部から Publisher を使って View をトリガーする方法は、以下の記事でも説明しています。
[SwiftUI] Combine をつかって サイコロを振ってみる

親 View で Bool 変数を1つ @State 指定で定義して、MyGraph に渡すようにしています。この Bool 変数の変更を契機として更新します。


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2021/06/02
//  © 2021  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    @State private var graphData = [10,15,8,23,18,35]
    // (1)
    @State private var aniSwitch:Bool = false

    var body: some View {
        VStack {
            MyGraph(graphData: $graphData, triggerAnimation: $aniSwitch)
                .padding()
            Button(action: {
                // (2)
                aniSwitch.toggle()
            }, label: {
                Text("Animation")
            })
        }
        .animation(.easeIn(duration: 2))
    }
}

struct MyGraph: View {
    @Binding var graphData:[Int]
    let offsetFromLL = CGSize(width: 50, height: 50)
    let xScale: CGFloat = 40
    let yScale: CGFloat = 10
    @State private var end: CGFloat = 1.0
    // (3)
    @Binding var triggerAnimation:Bool

    var body: some View {
        GeometryReader { geom in
            Path { path in
                path.move(to: pointOnGraph(geom.size, 0, graphData[0]))
                for (id, data) in graphData.enumerated().dropFirst() {
                    path.addLine(to: pointOnGraph(geom.size, id, data))
                }
            }
            .trim(from: 0.0, to: end)
            .stroke(lineWidth: 2.5)
            .fill(Color.orange)
        }
        .onChange(of: triggerAnimation, perform: { value in
            // (4)
            end = 0.0
            withAnimation {
                end = 1.0
            }
        })
    }

    func pointOnGraph(_ size: CGSize, _ xValue: Int,_ yValue: Int) -> CGPoint {
        return CGPoint(x: offsetFromLL.width + CGFloat(xValue) * xScale, y: size.height - offsetFromLL.height - CGFloat(yValue) * yScale)
    }
    
}
コード解説
  1. 上位ビューで @State 指定で Bool 変数を定義しています。
  2. ボタンを配置して、Bool 変数を toggle しています
  3. MyGraph ビューで @Binding で受け取ります
  4. 受け取った Binding 変数の変更を検知して、.trim のパラメータを変更します。(withAnimation なしに、同じ値に変更するとアニメーションしません)

以下のように、ボタンを押下することで、アニメーションを開始させることができます。

まとめ:Path をアニメーションさせる方法

Path をアニメーションさせる方法
  • .trim を使うことで、Path をなぞるようなアニメーションを実現できる

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

SwiftUI おすすめ本

SwiftUI を理解するには、以下の本がおすすめです。

# SwiftUI2.0 が登場したことで少し古くなってしまいましたが、いまでも 定番本です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です