Instagram の Story 画面作ってみた


## はじめに

この記事では、Instagram の Story 画面を作ってみたので、主に実装方針について書いていきます。
Instagram の Story 画面は、ご存知の通りユーザーの投稿した画像や動画を、スワイプで見ることができる画面です。
今回は、この画面に着目して、その特徴的な UI を再現してみました。

## 作成したもの

上記の通り、作成したものは Instagram の Story 画面を模した画面です。
デモは以下の動画の通りです。（回転の感じは微妙に違いますがそこはご容赦ください、、）
![Videotogif (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263740/4d711609-00ab-d461-9f52-48a0a5257d08.gif)


## 作成にあたって

まずは Story 画面に着目して、どのような動きをしているのかをみていきたいと思います。
Story 画面の動きを見ると、指の動きに追従して隣り合う面が回転していきながら、3D 回転しながら遷移していくように見えます。
また、回転の軸についてもイメージを膨らませると、下図のように直方体の中心を軸として回転しているように見えます。

![Screenshot 2024-01-11 at 22.32.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263740/0d6121a3-4567-976b-4ce8-0684c0c03705.png)

このような動きを実現するために、画面構造および使用する部品について考えていきます。
まずは基本的な構造は繰り返しの面の構造となるので今回は CollectionView を使用して実装していきます。

![Screenshot 2024-01-11 at 22.33.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263740/387a3161-037c-76b3-c232-b74d4a58e962.png)

次にスワイプ検知についてはスワイプの始まりや
完了なども簡単に検知したかったので CollectionViewDelegate を使用せずに、PanGestureRecognaizer を使用してスワイプを検視することにしました。
回転については、ご存知 3dTransform を使用して回転させていきます。
部品については準備できましたので、次に平面から 3D 回転させることを考えていきます。

### 操作している面

まず平面から 3D 回転させるには、回転の軸を決める必要があります。
回転軸については、デフォルトだと面の中心となりますが、このままだと以下のイメージ図の通り、動かしている面と次の面との間に隙間ができてしまいます。

![Screenshot 2024-01-11 at 22.34.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263740/f97ff261-6f32-f91b-0787-4c3c2a17024f.png)

なのでこの隙間をなくすために、回転軸を次に表示する面の端に設定して、回転させることにしました。

![Screenshot 2024-01-11 at 22.35.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263740/4214707b-39a1-e61d-f05e-b7e50cca855e.png)

これで回転軸については決まりましたので、次に回転の角度について考えていきます。
回転の角度については、スワイプの距離に応じて回転させていくことにしました。角度のイメージは以下の通りです。

![Screenshot 2024-01-11 at 22.35.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263740/617cb916-a2ff-210e-3da0-ada0a42617af.png)

ということで指の動きを x とすると、以下のように回転角度を考えることができます。（高校数学の単位円の概念を使います）

![Screenshot 2024-01-11 at 22.36.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/263740/f8ee4949-f9e8-3615-24e5-c0ad7d9df1f6.png)

今、単位円を考えていますので y 軸の値は √(1-x^2)となります。
ということで求めたい角度 θ は逆関数を使って θ=arctan2(y,x)となります。

### 次に表示される面

上記と同様に回転軸から考えていきます。
回転軸については、下図の通り、操作している面との境界面の端に設定して、回転させます。
続いて、直方体のイメージから分かる通り次に表示される面は初期回転角度が 90 度となります。
回転角度については、± と最終角度が 0 度となるように注意しながら計算することで滑らかになります。

### 一部実装紹介

```swift
    @objc func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
        // パンジェスチャーの処理
        // ここで説明にあったように回転角度などを求めています。
        let offset = collectionView.contentOffset.x
        let velocity = gesture.velocity(in: gesture.view!)
        let translationX = gesture.translation(in: gesture.view!.superview).x
        let ratio = translationX / UIScreen.main.bounds.width
        let theta = atan2(sqrt(1 - ratio * ratio), ratio)

        switch gesture.state {
        case .began:
            for cell in collectionView.visibleCells {
                guard let storyCell = cell as? StoryCollectionViewCell else {
                    continue
                }
                if storyCell.frame.minX <= offset && offset <= storyCell.frame.maxX {
                    currentCell = storyCell
                }
            }
        case .changed:
            for cell in collectionView.visibleCells {
                guard let storyCell = cell as? StoryCollectionViewCell else {
                    continue
                }
                if translationX < 0 {
                    // 先方向
                    if offset < storyCell.frame.minX {
                        presentingCell = storyCell
                    } else {
                        currentCell = storyCell
                    }
                } else {
                    // 戻る方向
                    if storyCell.frame.minX < offset {
                        presentingCell = storyCell
                    } else {
                        currentCell = storyCell
                    }
                }
            }
            // 現在操作している面と、次に表示される面について回転するようにしています。
            currentCell?.rotation(theta: theta, isCenter: true)
            presentingCell?.rotation(theta: theta, isCenter: false)

            let progress = abs(ratio)
            currentCell?.changeColor(progress: 1 - progress)
            presentingCell?.changeColor(progress: progress)


```

## 参考資料
https://qiita.com/sarukun99/items/6338206da5cbe70636b6