matchedGeometryEffect と @Namespaceを使ったアニメーション

## SwiftUI2.0で追加されたAPI

SwiftUI2.0で追加された新機能のうち、Heroアニメーションを簡単に作れるAPIがあったのでちょっと触ってみました。

## とりあえず完成形

こんな感じのSegmentControlっぽいUIを作ってみます。

Githubはこちらです。
https://github.com/hoshi005/matched-geometry-animation

![0001.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7478/bf3ebbd6-0ab7-922c-1532-cd1b86dc1333.gif)

## 開発環境

- Xcode 12.0.1
- iOS 14.0.1

## ボタンを作る

選択に使うボタンのViewを作ります。
そのまえに、適当にenumを定義しておきました。SF Symbolsから、適当に４つほど選出しています。

```swift
enum ButtonType: String, CaseIterable {
    case share = "square.and.arrow.up"
    case trash = "trash"
    case folder = "folder"
    case person = "person"
}
```

ボタンのビューはこんな感じで作ります。
AccentColorについては、適宜Assetsで好きな色を定義してください。

```swift
struct CustomButton: View {
    // 選択状態を表すプロパティ.
    @Binding var selected: ButtonType
    // 自分自身のボタンタイプ.
    let type: ButtonType
    
    var body: some View {
        ZStack {
            // 選択中だったら背景に円を描画する.
            if selected == type {
                Circle()
                    .fill(Color.accentColor) // AccentColorはAssetsで定義すること.
            }
            
            Button(action: {
                selected = type // ボタンをタップしたら、選択状態を自分自身に切り替える.
            }, label: {
                // enumから画像を表示する.
                Image(systemName: type.rawValue)
                    .resizable()
                    .renderingMode(.original)
                    .aspectRatio(contentMode: .fit)
                    .frame(width: 44, height: 44)
            })
        }
        .frame(width: 80, height: 80)
    }
}
```

選択状態の場合とそうじゃない場合で、見た目を確認してみます。
プレビューはこんな感じ。

```swift
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            CustomButton(selected: .constant(.share), type: .share) // 選択状態.
            CustomButton(selected: .constant(.trash), type: .share) // 非選択状態.
        }
        .previewLayout(.fixed(width: 100, height: 100))
    }
}
```

![スクリーンショット 2020-10-18 13.00.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7478/29725b24-d62c-2ec3-1c35-bdb3e79ff9d0.png)

## 画面上にボタンを並べる

では、画面上にボタンを並べてみます

```swift
struct ContentView: View {
    
    @State private var selected = ButtonType.share // 選択状態の初期値.
    
    var body: some View {
        HStack {
            // enumをforeachで回して、CustomButtonを横に並べる.
            ForEach(ButtonType.allCases, id: \.self) { type in
                CustomButton(selected: $selected, type: type)
            }
        }
    }
}
```

プレビューはこんな感じ

```swift
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            ContentView()
            Group {
                CustomButton(selected: .constant(.share), type: .share)
                CustomButton(selected: .constant(.trash), type: .share)
            }
            .previewLayout(.fixed(width: 100, height: 100))
        }
    }
}
```

では動かしてみます。
![0002.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7478/6f27bf05-53bb-3971-4a20-2f214b66ed53.gif)
選択状態を切り替えて、見た目も変わるようになりましたね。
では、ここからアニメーションを追加していきましょう。

## アニメーションさせる

まずはボタンの選択時の状態変化がアニメーションを伴うように、ボタンタップ時の挙動を一部修正します。

```swift

// 一部抜粋.
            
Button(action: {
    // ボタンタップ時の処理を、withAnimationメソッドのクロージャに渡す.
    withAnimation {
        selected = type
    }
}, label: {
    // enumから画像を表示する.
    Image(systemName: type.rawValue)
        .resizable()
        .renderingMode(.original)
        .aspectRatio(contentMode: .fit)
        .frame(width: 44, height: 44)
})
```

## .matchedGeometryEffectを設定する

アニメーションさせたいViewに対して`.matchedGeometryEffect`を指定します。
これは、識別子とNamespaceを与えて、同期したいアニメーションをグルーピングする感じです。

まずはnamespaceを宣言します。

```swift
struct CustomButton: View {
    // 省略

    var namespace: Namespace.ID // namespaceを追加する.
    
    // 省略
}
```

次に、アニメーションさせたい背景ビューに対して`.matchedGeometryEffect`を指定します。

```swift

// 選択中だったら背景に円を描画する.
if selected == type {
    Circle()
        .fill(Color.accentColor) // AccentColorはAssetsで定義すること.
        // 識別子は、アニメーションを同期したいグループ間で一致していれば何でもいいです.
        .matchedGeometryEffect(id: "CustomButton", in: namespace)
}

```

次に、呼び出しているView側に修正を加えます

```swift
struct ContentView: View {
    
    @State private var selected = ButtonType.share

    // ＠Namespaceプロパティラッパーを使って、namespaceを宣言.
    @Namespace var namespace

    var body: some View {
        HStack {
            ForEach(ButtonType.allCases, id: \.self) { type in
                // 引数にnamespaceを与えるように修正する.
                CustomButton(selected: $selected, type: type, namespace: namespace)
            }
        }
    }
}
```

以上で完成です！
とても簡単にできちゃいますね！

プレビューは、こんな感じで修正しておけば動きます

```swift
struct ContentView_Previews: PreviewProvider {
    @Namespace static var namespace // static を忘れずに.
    static var previews: some View {
        Group {
            ContentView()
            Group {
                CustomButton(selected: .constant(.share), type: .share, namespace: namespace)
                CustomButton(selected: .constant(.trash), type: .share, namespace: namespace)
            }
            .previewLayout(.fixed(width: 100, height: 100))
        }
    }
}
```

## まとめ

Heroアニメーションはテンション上がるので、他にも色々と試してみたいですね。