UICollectionViewを便利にかけるライブラリを作りました


## 海で昆布作ってきました
[昆布(Conv)](https://github.com/bannzai/conv)というライブラリを作りました
先日、茅ヶ崎の[海で行われたSwift愛好会](https://love-swift.connpass.com/event/95739/)で新しくライブラリを作ったのでこの記事はそれの紹介になります

Conv: https://github.com/bannzai/conv

## About
Conv は UI **Co** llectio **nV** iewのデータの構造をViewの見た目と同じように表現するライブラリです。
下のような構文でUICollectionViewについてのDataSource・Delegateを定義できます。

```swift
collectionView
    .conv() // ※1
    .create(for: elements) { (sectionType, section) in // ※2
        section.create(.header, headerOrFooter: { (header: SectionHeaderFooter<SectionHeaderReusableView>) in
            header.reusableIdentifier = "SectionHeaderReusableView"
            header.size = CGSize(width: UIScreen.main.bounds.width, height: 50)
            header.configureView { view, _ in
                view.nameLabel.text = "Section: \(sectionType.rawValue)"
                view.backgroundColor = sectionType.backgroundColor
            }
        })
        section.create(for: viewModels(section: sectionType)) { (viewModel, item: Item<ListCollectionViewCell>) in // ※3
            item.reusableIdentifier = "ListCollectionViewCell"
            item.sizeFor({ _ -> CGSize in
                let gridCount: CGFloat = 3
                let edge = floor((UIScreen.main.bounds.width - (gridCount - 1)) / gridCount)
                let size = CGSize(width: edge, height: edge)
                return size
            })
            item.configureCell { (cell, info) in
                cell.configure(with: viewModel)
            }
            item.didSelect { [weak self] (＿) in
                let viewController = DetailViewController(imageName: viewModel.imageName)
                self?.navigationController?.pushViewController(viewController, animated: true)
            }
        }
}
```

これはIndexPathのSectionおよびItemの関係を階層構造としてく表現されています。
これで実行した結果は下のようになります

<img width="320px" src="https://qiita-image-store.s3.amazonaws.com/0/84459/eb8f8b06-2208-ecf3-8b3a-b04cdbb62337.png" />

ここでいう階層構造というのは一つのSectionはItemを複数持っている関係にあることを指しています。
緑色のSection: 0 は Itemを 12個持っている。といった具合です。
コードの方にそれぞれコメントで※印をつけました。どういう役割か続いて記載して行こうと思います。

## collectionView.conv()
このメソッドから`Conv`の定義が始まります。
内部的には`Conv`という`UICollectionViewDataSource & UICollectionViewDelegate(FlowLayout)` に準拠したインスタンスが作られ、それをObjective-C runtime APIである [objc_setAssociatedObject](https://developer.apple.com/documentation/objectivec/1418509-objc_setassociatedobject?language=objc)を使ってUICollectionViewが保持しています。これによって `UICollectionView.reloadData()` 等が実行された時はこの`Conv` が `UICollectionViewDataSource` と `UICollectionViewDelegate(FlowLayout)` のイベントを全て処理する形になります。

また、`Conv`では[Paul Heckel's algorithm](https://gist.github.com/ndarville/3166060)ベースのアルゴリズムを導入して差分更新を実現しています。 `UICollectionView.reload()` といったExtensionが定義してあってこのメソッドを呼び出した場合は差分更新でCollectionViewが更新されるようになります。後ほどこの`reload()`メソッドについてはもう少し補足します。


## .create(for: elements) { (sectionType, section) in ... }
`#2`の説明です。このメソッドは上述した`Conv`インスタンスのメソッドです。先ほどの`collectionView.conv()`は`Conv`のインスタンスを作って返します。このメソッドの役割はいくつSectionを作るか、Sectionにはどのような情報を定義して行くか。といったことを定義のための始まりのメソッドとなります。もっと詳細にこのメソッドを解説するにはメソッドの宣言をまず確認しましょう。

```
public func create<E>(for elements: [E], sections closure: (E, Section) -> Void) -> Conv
```

上記のものは実際のものよりも簡略化したものとなります。正確なものは[GitHub](https://github.com/bannzai/Conv/blob/master/Conv/Conv/Conv.swift#L90)の方を見てみて下さい。

`Conv.create(for:sections)`では、第一引数に配列を渡します。渡した配列の数だけSectionを作る機能を持っています。具体的にはこの配列の数を[numberOfSections(in:)](https://developer.apple.com/documentation/uikit/uicollectionviewdatasource/1618023-numberofsections)の結果として使うことになります。

そして、第二引数のclosureの部分はこの配列の要素を一つと`Section`というクラスのインスタンスのペアが渡ってきます。`Section`は`IndexPath.section`の単位の情報を定義する役割を持っています。例を一つあげると[collectionView(_:numberOfItemsInSection:)](https://developer.apple.com/documentation/uikit/uicollectionviewdatasource/1618058-collectionview)があげられます。これは`Section`が何個`Item`を持っているかを教えるメソッドとなります。上記のコードでは`Section`ごとの`header`を定義してあげたりもしています。

次に`Section`の実質子要素である`Item`の定義の話をしていきます。
`header`については次に説明する`Item`についての説明と被るので割愛します。

## section.create(for: viewModels(section: sectionType)) { (viewModel, item: Item<ListCollectionViewCell>) in ... }

前の`Section`を作るメソッドにシグネチャが似ていますが、役割としても同じようなものです。
このメソッド`Section.create(for:items)`では渡した配列の数だけ`Item`を作ります。そして、それが配列の要素一つと`Item<T: UICollectionViewCell>`のペアを引数として受け取る`closure`に渡ってきます。

ここで第二引数のクロージャーで囲まれている範囲の全容を見てみましょう。

```swift
   section.create(for: viewModels(section: sectionType)) { (viewModel, item: Item<ListCollectionViewCell>) in // ※3
            item.reusableIdentifier = "ListCollectionViewCell"
            item.sizeFor({ _ -> CGSize in
                let gridCount: CGFloat = 3
                let edge = floor((UIScreen.main.bounds.width - (gridCount - 1)) / gridCount)
                let size = CGSize(width: edge, height: edge)
                return size
            })
            item.configureCell { (cell, info) in
                cell.configure(with: viewModel)
            }
            item.didSelect { [weak self] (_) in
                let viewController = DetailViewController(imageName: viewModel.imageName)
                self?.navigationController?.pushViewController(viewController, animated: true)
            }
}
```

`Item` についてプロパティの設定とメソッドの呼び出しを行なっています。上から順に

- item.reusableIdentifier 
    - reuseIdentifierの設定
- item.sizeFor 
    - Cellのサイズの決定
    - [collectionView(_:layout:sizeForItemAt:)](https://developer.apple.com/documentation/uikit/uicollectionviewdelegateflowlayout/1617708-collectionview)のラッパー
- item.configureCell
    - セルのコンテンツを決定
    - [collectionView(_:cellForItemAt:)](https://developer.apple.com/documentation/uikit/uicollectionviewdatasource/1618029-collectionview)のラッパー
- item.didSelect
    - セルがタップされた時
    - [collectionView(_:didSelectItemAt:)](https://developer.apple.com/documentation/uikit/uicollectionviewdelegate/1618032-collectionview)のラッパー

となっています。

ここで`item.configureCell` の部分に着目しましょう。このメソッドは第一引数に`UICollectionViewCell`が渡ってくる`closure`の中身を書いていくメソッドになります。上記のコードではこの部分ですね。

```swift
cell.configure(with: viewModel)
```

この`cell.configure(with:)`は正確には`ListCollectionViewCell`に生えているものです。 `item.configureCell(_:)` で渡ってくるcellは`Item<ListCollectionViewCell>`でジェネリクスで宣言されている`ListCollectionViewCell`にキャストされて渡ってきます。ほとんどの場合で[collectionView(_:cellForItemAt:)](https://developer.apple.com/documentation/uikit/uicollectionviewdatasource/1618029-collectionview)のタイミングでは`dequeue`したCellをキャストして使用すると思いますが、それを事前に行った状態でこのメソッドに渡ってくることになります。この機構により他にもcellをキャストした状態で渡ってくるメソッドがあります。例えば[collectionView(_:willDisplay:forItemAt:)](https://developer.apple.com/documentation/uikit/uicollectionviewdelegate/1618087-collectionview)ですね。各Delegateメソッドでキャストをするといった処理が不要になります。

`Conv`ではこのような形で各DataSource & Delegateメソッドをラップしています。

## なぜ作ったのか
さて、`Conv`の大まかな機能はわかってもらえたと思うのでこのライブラリのいいところをあげていこうと思います。既存のUICollectionViewの問題と一緒に紹介していきます

単純な例ですがよくあるコードを下に書いています。
このコードでは[collectionView(_:numberOfItemsInSection:)](https://developer.apple.com/documentation/uikit/uicollectionviewdatasource/1618058-collectionview)と[collectionView(_:cellForItemAt:)](https://developer.apple.com/documentation/uikit/uicollectionviewdatasource/1618029-collectionview)が使われています。


```swift
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 
	switch section {
	case 0:
	  return users.count 
	case 1:
	  return hobbies.count 
	case 2:
	  return teams.count
	default:
	  fatalError() 
	}
}

public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
	switch (indexPath.section, indexPath.item) { 
	case (0, _):
      let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ACell", for: indexPath) as! UserCell 
	  cell.configure(with: users[indexPath.item])
	  ...
	  return cell
	case (1, _):
	  let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "BCell", for: indexPath) as! HobbyCell
	  cell.configure(with: hobbies[indexPath.item])
	  ...
	  return cell
	case (2, _): 
	  let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CCell", for: indexPath) as! TeamCell 
	  cell.configure(with: teams[indexPath.item])
	  ...
	  return cell
	default:
	   fatalError() 
	}
}
```
`section == 0` の処理に焦点を当てると、このUICollectionViewでは、
`section == 0`の場合は`users`の数だけ`ACell`の`Key`を使って`UserCell`にキャストしてセルのコンテンツの中身を決定する動作になります。しかし、各メソッドごとに定義する内容が違うためにこの関係性が弱くなってしまいます。

上述したコードは正しく動作します。が、例えば `section == 0` が `users` の数だけ `item`を作ると言う書き方をせずに`teams`の数だけ `item` を作る。といった書き方ができてしまいます。これの結果はきっと期待した動作にならない可能性が大きいでしょう `cell.setup(with: users[indexPath.item])`で `out of bounds` を起こすかもしれませんね。

`Conv` ではこの関係性を短いスコープとしてクロージャと記述することで関係性を限定的にしています。
さらに `Section` と `Item` が実質親子関係と見ることができます。この階層構造をコードで表現しています。

`Conv` でこれを書き直してみましょう。違いは `SectionType` と言う `Enum` を用意したくらいです。　　
余計な`fatalError`も消えてスッキリしましたね

```swift

collectionView
		.conv()
		.create(for: SectionType.elements) { (sectionType, section) in // ※2
				switch sectionType {
				case .users:
						section.create(for: users) { user, item: Item<UserCell> in
							item.reusableIdentifier = "ACell"
							item.size = ...
							item.configureCell({ cell, _ in
								cell.configure(with: user)
						})
				}
				case .hobbies:
					section.create(for: hobbies) { hobby, item: Item<ItemCell> in
							item.reusableIdentifier = "BCell"
							item.size = ...
							item.configureCell({ cell, _ in
								cell.configure(with: hobby)
						})
				}
				case .teams:
					section.create(for: teams) { team, item: Item<ItemCell> in
							item.reusableIdentifier = "CCell"
							item.size = ...
							item.configureCell({ cell, _ in
								cell.configure(with: team)
							})
				}
		}
}

```

## 差分更新について
`Conv` が便利なシンタックスを提供していることは紹介しました。ここでは少し差分更新について触れていきましょう。[Paul Heckel's algorithm](https://gist.github.com/ndarville/3166060)をベースにして実装していきました。その他にも[多数のOSSライブラリ](https://github.com/bannzai/conv#algorithm)を参考にして作っていきました。

差分更新を実現するために実装の準備が必要です。上記でも紹介した`Conv.create(...)`, `Section.create(...)`のSectionおよびItemを作るメソッドでは第一引数に配列を渡していきました。この配列のElementは下記のProtocolに準拠している必要があります。

[Differenciable](https://github.com/bannzai/Conv/blob/master/Conv/Algorithm/Differenciable.swift)

この `Differenciable` は `differenceIdentifier` というプロパティを持っている必要があります。差分更新ではこの`differenceIdentfier` が同値だったら変化がないものと見なしてセルの更新をしない。違うものの場合は適切な更新処理をかけてあげる。といった実装になっています。これにより必要のないセルに`reloadData` が走らなくなり更新の際のパフォーマンスがよくなるようにしています。

ConvのリポジトリにあるExampleで、差分更新を確認できるものがあります。ぜひ動かしてみてください

|  Insert and Delete  |  Move item and section  |
| ---- | ---- |
|  <img width="320px" src="https://qiita-image-store.s3.amazonaws.com/0/84459/1ccd3f32-8979-c14b-b05b-a5f922dff08f.gif" />  |  <img width="320px" src="https://qiita-image-store.s3.amazonaws.com/0/84459/99f41678-e597-197e-e634-6f630792adc7.gif" />  |



## 終わりに
このライブラリを作る上で大きく影響を受けたライブラリに[Shoyu](https://github.com/yukiasai/shoyu)というものがあります。言うなれば`Conv`は`Shoyu`のCollectionView版ですね。Viewの見た目に寄せてコードを書いていく発想がとても良くて真似してしまいました。

また、このライブラリ作る上で `昆布` を描いてくれた[noainoue](https://github.com/noainoue)にも感謝です。OSSにロゴ載せてみたい夢が叶いました。

最後にみなさんこのライブラリがいいと思ったらQiitaのいいねも欲しいけど GitHubの方に

<h3>スターください</h3>

おしまい \\(^o^)/