【SwiftUI】VectorArithmetic を自作して AnimatablePair の型パラメータ地獄から解脱する

## はじめに

### モチベーション

SwiftUI でアニメーション可能なシェイプを作成する場合は `animatableData` を実装する必要があります。

アニメーション可能なパラメータが１つの場合は次のような実装をします。

```swift
public struct HogeShape: Shape {
    public var foo: CGFloat

    public var animatableData: CGFloat {
        get { foo }
        set { foo = newValue }
    }

    public func path(in rect: CGRect) -> Path {
        // foo をパラメータにした Path を作る
    }
}
```

`animatableData` の型は必ずしも `CGFloat` である必要はなく、`VectorArithmetic` に適合していれば OK です。次の型が既知で適合しています。

- CGFloat
- Double
- Float
- AnimatablePair
- EmptyAnimatableData

`AnimatablePair` を利用することで、次のようにパラメータを２つ指定することができます。

```swift
public struct FugaShape: Shape {
    public var foo: CGFloat
    public var bar: CGFloat

    public var animatableData: AnimatablePair<CGFloat, CGFloat> {
        get {
            AnimatablePair(foo, bar)
        }
        set {
            foo = newValue.first
            bar = newValue.second
        }
    }

    public func path(in rect: CGRect) -> Path {
        // foo, bar をパラメータにした Path を作る
    }
}
```

`AnimatablePair` の型パラメータには、任意の `VectorArithmetic` を指定することが可能なので、型パラメータに `AnimatablePair` を指定することができます。

これを利用してパラメータを３つにすることができます。

```swift
public struct PiyoShape: Shape {
    public var foo: CGFloat
    public var bar: CGFloat
    public var baz: CGFloat

    public var animatableData: AnimatablePair<CGFloat, AnimatablePair<CGFloat, CGFloat>> {
        get {
            .init(foo, .init(bar, baz))
        }
        set {
            foo = newValue.first
            bar = newValue.second.first
            baz = newValue.second.second
        }
    }

    public func path(in rect: CGRect) -> Path {
        // foo, bar, baz をパラメータにした Path を作る
    }
}
```

`AnimatablePair` の型パラメータに `AnimatablePair` を設定していくことで、いくらでもアニメーション可能なプロパティを追加することができますが、型パラメータがネストされていくので可読性が落ちていきます。パラメータを５つにすると次のようになります。

```swift
var animatableData: AnimatablePair<CGFloat, AnimatablePair<CGFloat, AnimatablePair<CGFloat, AnimatablePair<CGFloat, CGFloat>>>> {
        get {
            .init(prop0, .init(prop1, .init(prop2, .init(prop3, prop4))))
        }
        set {
            prop0 = newValue.first
            prop1 = newValue.second.first
            prop2 = newValue.second.second.first
            prop3 = newValue.second.second.second.first
            prop4 = newValue.second.second.second.second
        }
    }
```

プロパティが５つあることが、現実の解決すべき問題として多いか少ないかについては何とも言えないところですが、単純なシェイプでも柔軟にアニメーションをつけようとすると、プロパティの数はそれなりに必要になってくると思います。

たとえば、角を丸くした長方形のシェイプで考えてみます。
角丸の半径に加えて、各四隅の角丸の有無を自由に制御できるようにすると、単純なシェイプでも５つのプロパティが必要になります。
これらが自由に制御できれば、ビューのマスク処理にアニメーションをつけることも容易になります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63842/8c9ccc3d-65d3-f4a2-bcd3-b57d3237d366.gif" width="300">
本記事では、`VectorArithmetic` に適合した `AnimatableValues` を自作することで、上記のネストを次のようなシンプルな実装にします。

```swift
public var animatableData: AnimatableValues {
    get {
        .init(prop0, prop1, prop2, prop3, prop4)
    }
    set {
        let values: [CGFloat] = newValue.values()
        prop0 = values[0]
        prop1 = values[1]
        prop2 = values[2]
        prop3 = values[3]
        prop4 = values[4]
    }
}
```

### 環境

- Xcode 11.6 (11E708)
- Swift 5.2.4

## VectorArithmetic を実装する

アニメーション可能なプロパティをフラットに複数保持するために Array で値を管理します。

```swift
public struct AnimatableValues: VectorArithmetic {
    private var values: [Double]
}
```

あとは、VectorArithmetic に適合するために必要な実装をします。

### 必要最低限の実装

`AnimatableValues` を `VectorArithmetic` に適合するためには、ベクトル空間の公理系を満たす必要があり、以下のように実装します。

#### zero

```swift
static var zero: Self { get }
```

ゼロ元を返す必要があります。ゼロ元はどのような `AnimatableValues` と加算してもプロパティを変更しないように実装します。

#### `+` 演算子

```swift
static func + (lhs: Self, rhs: Self) -> Self
```

各要素ごとに加算したオブジェクトを返す必要があります。

#### `-` 演算子

```swift
static func - (lhs: Self, rhs: Self) -> Self
```

各要素ごとに減算したオブジェクトを返す必要があります。

#### scale(by:)

```swift
mutating func scale(by rhs: Double)
```

各要素ごとに `rhs` 倍したオブジェクトを返す必要があります。

#### magnitudeSquared

```swift
var magnitudeSquared: Double { get }
```

各要素の二乗した和を返します。

### 実装例

上記を満たすような実装例です。

 固定長のリストであれば `.zero` は `[0, 0, ..., 0]` のような実装が望ましいところですが、Array で保持している都合で実現できないので、空のリスト `[]` で代替しておき、`+`, `-` 演算を実行するタイミングでゼロ埋めしているのが実装のポイントです。これで公理系を満たすことができます。

```swift
import SwiftUI
import enum Accelerate.vDSP

public struct AnimatableValues: VectorArithmetic, Hashable {
    private var values: [Double]

    public init(values: [Double]) {
        self.values = values
    }

    public init<F: BinaryFloatingPoint>(values: [F]) {
        self.init(values: values.map(Double.init))
    }

    public init<F: BinaryFloatingPoint>(_ values: F...) {
        self.init(values: values.map(Double.init))
    }

    public func values<F: BinaryFloatingPoint>(_ type: F.Type = F.self) -> [F] {
        values.map(F.init)
    }
}

// MARK: - VectorArithmetic
public extension AnimatableValues {
    mutating func scale(by rhs: Double) {
        values = vDSP.multiply(rhs, values)
    }

    var magnitudeSquared: Double {
        vDSP.sum(vDSP.multiply(values, values))
    }
}

// MARK: - AdditiveArithmetic
public extension AnimatableValues {
    static var zero: Self {
        .init(values: [])
    }

    static func + (lhs: Self, rhs: Self) -> Self {
        .init(values: operate(vDSP.add, lhs, rhs))
    }

    static func - (lhs: Self, rhs: Self) -> Self {
        .init(values: operate(vDSP.subtract, lhs, rhs))
    }

    private static func operate(_ operation: ([Double], [Double]) -> [Double], _ lhs: Self, _ rhs: Self) -> [Double] {
        let count = max(lhs.values.count, rhs.values.count)
        let lhs = lhs.values + Array(repeating: 0, count: count - lhs.values.count)
        let rhs = rhs.values + Array(repeating: 0, count: count - rhs.values.count)
        return operation(lhs, rhs)
    }
}
```

この実装によって、`animatableData` は次のように簡潔に記述できるようになります。

```swift
public var animatableData: AnimatableValues {
    get {
        .init(prop0, prop1, prop2, prop3, prop4)
    }
    set {
        let values: [CGFloat] = newValue.values()
        prop0 = values[0]
        prop1 = values[1]
        prop2 = values[2]
        prop3 = values[3]
        prop4 = values[4]
    }
}
```