SwiftUI で Animatable なシェイプを作ってみる

## シェイプにアニメーションをつけてみました

SwiftUI の勉強をかねて、[以前の記事](https://qiita.com/takehito-koshimizu/items/ffaf15a77831a6da2c13)で作成した SwiftUI のシェイプにアニメーションをつけてみました。

<table>
<tbody>
<tr>
<td>
<blockquote class="twitter-tweet"><p lang="und" dir="ltr"><a href="https://t.co/88xT1KG0dg">pic.twitter.com/88xT1KG0dg</a></p>&mdash; takehito-koshimizu (@takehitokoshim1) <a href="https://twitter.com/takehitokoshim1/status/1280094827489591296?ref_src=twsrc%5Etfw">July 6, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
  </td>
<td>
<blockquote class="twitter-tweet"><p lang="und" dir="ltr"><a href="https://t.co/QM30GIkvA8">pic.twitter.com/QM30GIkvA8</a></p>&mdash; takehito-koshimizu (@takehitokoshim1) <a href="https://twitter.com/takehitokoshim1/status/1280094976622260224?ref_src=twsrc%5Etfw">July 6, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
  </td>
</tr>
</tbody>
</table>

### 環境

- Xcode Version 11.5 (11E608c)

## 参考記事

今回はこちらの記事を参考にして、以前作成したシェイプにアニメーションをつけてみました。
動画が非常にわかりやすくて参考になりました。

Animating simple shapes with animatableData - a free Hacking with iOS: SwiftUI Edition tutorial
https://www.hackingwithswift.com/books/ios-swiftui/animating-simple-shapes-with-animatabledata

Animating complex shapes with AnimatablePair - a free Hacking with iOS: SwiftUI Edition tutorial
https://www.hackingwithswift.com/books/ios-swiftui/animating-complex-shapes-with-animatablepair


## `Shape` はもともと `Animatable`

まずは Xcode で `Shape` の定義にジャンプしてみます。

```swift
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol Shape : Animatable, View {

    /// Describes this shape as a path within a rectangular frame of reference.
    ///
    /// - Parameter rect: The frame of reference for describing this shape.
    /// - Returns: A path that describes this shape.
    func path(in rect: CGRect) -> Path
}
```

実は `Shape` はもともと `Animatable` のサブタイプであることがわかります。
更に、 `Animatable` にジャンプしてみます。

```swift
/// A type that can be animated
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol Animatable {

    /// The type defining the data to be animated.
    associatedtype AnimatableData : VectorArithmetic

    /// The data to be animated.
    var animatableData: Self.AnimatableData { get set }
}
```

`Animatable` は `animatableData` という１つのプロパティを持ち、
これを実装することで、アニメーションをつけることができみたいです。

## 自作のシェイプにアニメーションをつける

[以前の記事](https://qiita.com/takehito-koshimizu/items/ffaf15a77831a6da2c13#%E5%AE%9F%E8%A3%85%E4%BE%8B)で作成した `StarShape` のプロパティ `smoothness` にアニメーションをつけてみます。

```swift
import SwiftUI

struct StarShape: Shape {
    var vertex: UInt = 5
    var smoothness: Double = 0.5
    var rotation: CGFloat = -.pi/2

    var animatableData: Double {
        get { smoothness }
        set { smoothness = newValue }
    }

    func path(in rect: CGRect) -> Path {
        Path { path in
            let points: [CGPoint] = StarParameters(vertex: vertex, smoothness: smoothness)
                .center(x: rect.midX, y: rect.midY)
                .radius(min(rect.midX, rect.midY))
                .rotated(by: rotation)
                .cgPoints
            path.move(to: points.first!)
            points.forEach { point in
                path.addLine(to: point)
            }
            path.closeSubpath()
        }
    }
}
```

ポイントはここ。

```swift
var animatableData: Double {
    get { smoothness }
    set { smoothness = newValue }
}
```

`animatableData` の `get`, `set` でプロパティを指定します。
たったこれだけです。

早速、このシェイプを使ってみます。

```swift
import SwiftUI

private let style = LinearGradient(
    gradient: Gradient(colors: [
        Color(red: 239/255, green: 120.0/255, blue: 221/255),
        Color(red: 239/255, green: 172.0/255, blue: 120/255)
    ]),
    startPoint: UnitPoint(x: 0.5, y: 0),
    endPoint: UnitPoint(x: 0.5, y: 0.6)
)

struct StarView: View {
    /// 頂点の数
    var vertex: UInt = 5
    /// 滑らかさ
    @State var smoothness: Double = 0.5

    var body: some View {
        let shape = StarShape(vertex: vertex, smoothness: smoothness)
        return ZStack {
            shape.fill(style)
            shape.stroke(Color.black, lineWidth: 4)
        }
        .aspectRatio(1.1, contentMode: .fit)
        .onTapGesture {
            withAnimation {
                self.smoothness = Double(Int(self.smoothness * 10 + 1) % 5 + 5)/10
            }
        }
    }
}
```

ビューのタップジェスチャーで `smoothness` を書換えて、再描画します。
アニメーションさせたいので、`withAnimation` 関数で囲んでいます。

## 複数のプロパティにアニメーションをつける

`StarShape` のプロパティ `vertex` もアニメーション可能にしてみます。
`VectorArithmetic` に適合している `AnimatablePair` を利用します。

```swift
import SwiftUI

struct StarShape: Shape {
    var vertex: UInt = 5
    var smoothness: Double = 0.5
    var rotation: CGFloat = -.pi/2

    var animatableData: AnimatablePair<Double, Double> {
        get {
            AnimatablePair(Double(vertex), smoothness)
        }
        set {
            vertex = UInt(newValue.first)
            smoothness = newValue.second
        }
    }
    /* 以下省略 */
}
```

このようなペアを返すことで `animatableData` に複数のプロパティを参照させることができます。
次のように、型パラメータに `AnimatablePair` を指定することで、３つ以上プロパティをアニメーション可能にすることもできるみたいです。

```swift
// ３つ
AnimatablePair<Double, AnimatablePair<Double, Double>>
// ４つ
AnimatablePair<Double, AnimatablePair<Double, AnimatablePair<Double, Double>>>
// ５つ
AnimatablePair<Double, AnimatablePair<Double, AnimatablePair<Double, AnimatablePair<Double, Double>>>>
```

このシェイプを使って、頂点の増減にもアニメーションをかけてみます。
サンプルコードは次の通りです。

```swift
import SwiftUI

private let style = LinearGradient(
    gradient: Gradient(colors: [
        Color(red: 239/255, green: 120.0/255, blue: 221/255),
        Color(red: 239/255, green: 172.0/255, blue: 120/255)
    ]),
    startPoint: UnitPoint(x: 0.5, y: 0),
    endPoint: UnitPoint(x: 0.5, y: 0.6)
)

struct StarView: View {
    /// 頂点の数
    @State var vertex: UInt = 5
    /// 滑らかさ
    @State var smoothness: Double = 0.5

    var body: some View {
        let shape = StarShape(vertex: vertex, smoothness: smoothness)
        return ZStack {
            shape.fill(style)
            shape.stroke(Color.black, lineWidth: 4)
        }
        .aspectRatio(1.1, contentMode: .fit)
        .onTapGesture {
            withAnimation {
                self.vertex = ((self.vertex + 3) % 5) + 5
                self.smoothness = Double(Int(self.smoothness * 10 + 1) % 5 + 5)/10
            }
        }
    }
}
```

## まとめ

単純な図形であれば、簡単にアニメーションの実装ができることがわかりました。

- アニメーション可能なビューにするためには `Animatable` に適合する
- `Shape` はもともと `Animatable` 
- `AnimatablePair` で複数の属性をアニメーション可能にすることができる