インスタグラムのマイページのようなViewを簡単に作れるライブラリを公開しました

# 概要
インスタグラム,Tiktok,Twitterのマイページで見るようなページを実装してOSSとして公開しました。

https://github.com/itsukiss/TopContentPager

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104317/4c04b9b7-372a-6d78-68b9-476c69cf916a.png" width="300px">
<img src="https://user-images.githubusercontent.com/8970291/109750602-151dbc00-7c20-11eb-9a74-87db128867e5.gif" width="320px">
# TopContentPager とは？
上記のような複雑なページを簡単に作ることができるフレームワークです。
上記のようなページの特徴としては以下。

```
1. 中断にページのタブがあり、上にスクロールすることで上部に固定される
2. 全てのページで共通した縦スクロールが可能なヘッダーが存在する
3. 上にスクロールしてページタブが固定されている状態でページを切り替えると各スクロール量は維持されているが、１つのページで上部まで（ページタブが固定されない部分まで）スクロールした状態でページを切り替えると全てスクロールはトップまで戻ってきている
```

これらの複雑なロジックを考えることなく、ページの実装を実現できます。

# 使い方
## 導入
まずはライブラリをinstallしてください。
Podfileに下記を追加して `pod install` をすればinstall完了です。

```:Podfile
pod 'TopContentPager'
```

## 実装
使うものは
- ContentTableBody
- TopContentView
- TopContentPagerViewController

のみです。

### 親ViewControllerの作成
まず、大元となる親ViewControllerを実装します。
`TopContentPagerViewController` クラスを継承した `ParentViewController` を実装します。
`ParentViewController` には `TopContentPagerDataSource` を設定してください。
DataSourceには下記の二つの関数があります

```swift
func topContentPagerViewControllerTopContentView(_ viewController: TopContentPagerViewController) -> TopContentView
func topContentPagerViewControllerViewControllers(_ viewController: TopContentPagerViewController) -> [ContentTableBody]
```

それぞれ

- `TopContentView` を継承した共通のヘッダー部分になるView
- `ContentTableBody` プロトコルの適応した各ページのViewになるViewControllerの配列

を返します。

上記の関数を設定したら、下記の関数でdataSourceをselfに設定してください。

```swift
func setupWillLoadDataSource()
```

これで以下のようになり、最低限の親ViewControllerの実装は完了です。


```swift:ParentViewController
final class ParentViewController: TopContentPagerViewController {
    override func setupWillLoadDataSource() {
        super.setupWillLoadDataSource()
        dataSource = self
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

extension ParentViewController: TopContentPagerDataSource {
    func topContentPagerViewControllerTopContentView(_ viewController: TopContentPagerViewController) -> TopContentView {
        // return common HeaderView in all pages.
        TopView()
    }
    
    func topContentPagerViewControllerViewControllers(_ viewController: TopContentPagerViewController) -> [ContentTableBody] {
        // return ViewControllers for each page.
        [Page1ViewController(), Page2ViewController(), Page3ViewController()]
    }
}
```

### 共通のヘッダーViewの作成
次に共通となるヘッダーViewを作成します。
`TopContentView` を継承したクラス `TopView` を実装します。

```swift:TopView
final class TopView: TopContentView { }
```

はい、これだけです。あとはいつも通り普通にViewを作ってください。xibにしてもコードにしてもAutoLayoutを使うことを推奨します。

### 各ページのViewControllerの作成
最後にそれぞれのページとなるViewControllerを作っていきます。
`ContentTableBody` を継承させたViewControllerを実装します。
`ContentTableBody` の定義は以下です。

```swift:ContentTableBody
public protocol ContentTableBody: UIViewController {
    var pagerItem: PagerItem { get }
    var scrollView: UIScrollView! { get }
    // 〜省略
}
```
`pagerItem` はページのタブ部分のデザインを設定するものです。
`.text` `.image` `.textAndImage` `.custom` があり好きなものを設定してください。
詳細はここにあります。
-> https://github.com/itsukiss/TopContentPager#pageritem

`scrollView` にはコンテンツとなるViewを設定してください、基本的には `tableView` や `collectionView` です。

下記が最低限設定した時の実装です。

```swift:Page1ViewController
class Page1ViewController: UIViewController, ContentTableBody {
    
    var pagerItem: PagerItem = .text(.init(title: "ページ1"))
    var scrollView: UIScrollView!
    @IBOutlet weak var tableView: UITableView! {
        didSet {
            scrollView = tableView
        }
    }
}
```

### 完成
これだけで上記のような共通のヘッダーがあるPagerを作ることができます。
上記の実装では必要最低限の説明しかしていませんので、[README](https://github.com/itsukiss/TopContentPager/blob/main/README.md)や[サンプルプログラム](https://github.com/itsukiss/TopContentPager/tree/main/Sample)を見ることをお勧めします。
特に[サンプルプログラム](https://github.com/itsukiss/TopContentPager/tree/main/Sample)はわかりやすく作ってますのでぜひ参考にしてください。

# 設計
View構造は下記のようになっていて、基本的には下記のような実装をしています。
（READMeにもかいてますが、 `ContentTopProtocol` はもはや使われていません。 `TopContentView` に置き換えて読んでください）
また、各スクロール時のViewの挙動はGifのアニメーションで表しているので、みていただけるとイメージがわくと思います。

- 横スクロール時に一番最前面のEscapeViewにTopContentViewを貼り付ける。
- 縦スクロール時にはContentTopCellに貼り付けることで縦スクロールのジェスチャも可能に
- ある一定以上までスクロールしたら、TopContentViewをEscapeViewに貼り付けページタブ部分だけをView内にみえるように
- TopContentViewではhitTestをoverrideして判定し、横方向のスクロールを制御しています。
- どこかのページの縦スクロールがTopContentViewまで到達したとき（つまり上部固定のヘッダーが表示された時）に全てのページのcontentOffsetを合わせるように
    - これはことばにするとややこしいですが、上部コンテンツが共通の場合上部コンテンツが見えてる場合に横スクロールすると他のページのコンテンツがcontentOffsetを保持していると、TopContentViewに食い込むような形になるからです。
    - 下のgifアニメーションの最後の「スクロールがどちらも下の方まで行われていた時を見ればわかります。

1.View構造/TopContentView内の横スクロール制御
![TopContentPagerStructure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104317/18b86027-a30b-efaf-a284-d162882b7bd6.png)
2.スクロール時のViewの挙動
<img src="https://user-images.githubusercontent.com/8970291/109751024-f10eaa80-7c20-11eb-8f0d-a388cd0d861c.gif">

ここでは細かく説明しませんが、もし気になる方はコードを読んでみてください。

# 終わりに
今回はぱっと見て簡単にできそうだけど、やってみたら結構複雑なインスタグラムやtiktokのマイページのようなPagerを作りました。
困っている人の助けになればと思います。
もし何か質問や意見があれば、ガンガンください。

# リンク集
- [TopContentPagerのリポジトリ](https://github.com/itsukiss/TopContentPager)