世界一簡単に実装できるタグ風CollectionViewのカスタムレイアウト

現在開発中の[Parnovi](https://parnovi-work.studio.design/)でのCollectionViewのカスタムレイアウトについての記事です🐶

# レイアウトの種類

collectionViewのレイアウトは大きく分けて二つのセグメントに分かれます。

1. UICollectionViewFlowLayout
2. UICollectionViewLayout

後者の`UICollectionViewLayout`はカスタムでレイアウトを作るため比較的敷居が高いですが、だいたいのレイアウトは比較的簡単な前者の`UICollectionViewFlowLayout`で実装することができます。

# 完成イメージ
下記の`お悩みで探す`セクションが完成系のイメージです。
![Simulator Screen Shot - iPhone 11 - 2019-11-08 at 17.01.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292456/ef80bbf3-67ce-ff9b-1bac-f24cf17841bb.png)

# 完成コード

レイアウトClassの実装

```swift:MenuCollectionViewFlowLayout.swift
class MenuCollectionViewFlowLayout: UICollectionViewFlowLayout {
    let itemSpacing: CGFloat = 0.0
    let lineSpacing: CGFloat = 0.0
    private let leftInset: CGFloat = 8.0
    private let column = 3

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let attributes = super.layoutAttributesForElements(in: rect) else {
            return nil
        }
        for index in 0..<attributes.count {
            let columnRow = (column - 1)
            if index <= columnRow{
                attributes[index].frame.origin.x = sectionInset.left
            } else {
                let leftCellIndex = (index - column)
                attributes[index].frame.origin.x = attributes[leftCellIndex].frame.minX + attributes[leftCellIndex].frame.width + leftInset
            }
        }
        return attributes
    }
}
```

CollectionViewClassの実装

```swift:MenuCollectionView.swift
class MenuCollectionView: UICollectionView {
    private let cellName = MenuCollectionViewCell.className()
    private let demoItem = ["涙やけ/目やに", "皮膚炎/かゆみ", "元気がない", "体臭", "下痢", "すぐ空腹になる", "口臭", "便秘", "避妊/去勢", "歯周病", "肥満", "アレルギー", "ルアーフィッシング", "サッカー/フットサル"]

    override func awakeFromNib() {
        super.awakeFromNib()
        setViews()
        setFlowLayout()
    }
    private func setViews() {
        let defaultCollectionHeight: CGFloat = 106.0
        self.heightAnchor.constraint(equalToConstant: defaultCollectionHeight).isActive = true
        self.register(UINib(nibName: cellName, bundle: nil), forCellWithReuseIdentifier: cellName)
        self.dataSource = self
        self.delegate = self
    }

    private func setFlowLayout() {
        let flowLayout = MenuCollectionViewFlowLayout()
        flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
        flowLayout.minimumInteritemSpacing = flowLayout.itemSpacing
        flowLayout.minimumLineSpacing = flowLayout.lineSpacing
        flowLayout.scrollDirection = .horizontal
        self.collectionViewLayout = flowLayout
    }
}

extension MenuCollectionView: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return demoItem.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellName, for: indexPath) as! MenuCollectionViewCell
        cell.set(text: demoItem[indexPath.row])
        return cell
    }
}
```

# 解説

タグ風レイアウトを作る上で問題となってくるのはItem同士のスペーシングです。下記のような感じですね。(適当に再現しているのでItemがくっついている部分は気にしないでください)
画像から見てわかるように、デフォルトでは`Column`の最大幅から次の`Column`のスペースを決定しています。つまり列にある最大幅のItemより小さいItemの場合スペーシングが残念な感じになってしまうということですね。
![Simulator Screen Shot - iPhone 11 - 2019-11-09 at 15.02.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292456/8a6980eb-73f1-cb2e-d4b1-ede1199b788b.png)


### 解決策

タイトルの通り、より簡単にレイアウトを実現するために今回は`UICollectionViewFlowLayout`でレイアウトを実装しました。方法はいたって簡単で、隣のItem(今回は左隣のItem)を基準にスペースを決定するというやり方です。つまり今回の場合、左隣の`minX`と`width`を使用して次のItemのスペーシングをするといった感じです。コードで見ると以下の部分が該当する部分です。

```swift:MenuCollectionViewFlowLayout.swift
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let attributes = super.layoutAttributesForElements(in: rect) else {
            return nil
        }
        for index in 0..<attributes.count {
            let columnRow = (column - 1)
            if index <= columnRow{
                attributes[index].frame.origin.x = sectionInset.left
            } else {
                let leftCellIndex = (index - column)
                attributes[index].frame.origin.x = attributes[leftCellIndex].frame.minX + attributes[leftCellIndex].frame.width + leftInset
            }
        }
        return attributes
    }
```

### layoutAttributesForElementsとは

CollectionViewのレイアウトにはライフサイクルがあり、このSuperクラスのメソッドは、該当するCollectionViewItemの表示領域が決定した時に呼び出されます。つまり、このメソッドで何をしているかというと現在表示されている部分のItemレイアウトを編集して、レイアウトを更新するよ！的な感じのことを行っています。その他にも、事前にItemのレイアウトを決定する`prepareLayout`やItemの追加などで呼ばれる`layoutAttributesForItem`などがあります。

# まとめ
CollectionViewのレイアウトの実装方法は色々あり迷ってしまいますが、それぞれのデザインで最も簡単に実装ができるようこれから意識したいですね🧐個人的にレイアウトを組む際に[クックパッドさんの記事](https://techlife.cookpad.com/entry/2017/06/29/190000)がとても参考になりました。
何かご意見・質問があれば、ぜひお聞かせください。