【Swift】UICollectionViewCompositionalLayoutでレイアウトを組む

#はじめに
今回はUICollectionViewCompositionalLayoutを使ってGridとInstagram風のレイアウトを組んでみたいと思います。
![ezgif.com-gif-maker.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/954451/498f025a-961e-777a-eff1-529fd6612df4.gif)

#GitHub
以下のCollectionCompositeLayoutフォルダに今回のプロジェクトはあります。

https://github.com/0429oonishi/CollectionViews

#カスタムセル実装
表示させるカスタムセルの背景色は以下のように設定しました。

```swift:CustomCollectionViewCell
import UIKit

final class CustomCollectionViewCell: UICollectionViewCell {

    static var identifier: String {
        return String(describing: self)
    }
    
    private let colors: [UIColor] = [.red,
                                     .blue,
                                     .green,
                                     .yellow,
                                     .orange,
                                     .cyan,
                                     .magenta
    ]
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        contentView.backgroundColor = colors.randomElement()
        
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
}
```

#Gridレイアウト実装
まず、Gridの方から作ってみたいと思います。

```swift:GridViewController
import UIKit

final class GridViewController: UIViewController {

    @IBOutlet private weak var collectionView: UICollectionView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        collectionView.collectionViewLayout = UICollectionViewCompositionalLayout(
            section: gridSection()
        )
        collectionView.register(CustomCollectionViewCell.self,
                                forCellWithReuseIdentifier: CustomCollectionViewCell.identifier)
        collectionView.dataSource = self
        
    }
    
    private func gridSection() -> NSCollectionLayoutSection {
        
        let itemCount = 3
        let lineCount = itemCount - 1
        let itemSpacing = CGFloat(2)
        let itemLength = (self.view.frame.size.width - (itemSpacing * CGFloat(lineCount))) / CGFloat(itemCount)
        //一つのitem
        let item = NSCollectionLayoutItem(
            layoutSize: NSCollectionLayoutSize(
                widthDimension: .absolute(itemLength),
                heightDimension: .absolute(itemLength)
            )
        )
        //itemを三つ横並びに
        //.fractionalは親Viewとの割合
        let items = NSCollectionLayoutGroup.horizontal(
            layoutSize: NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1),
                heightDimension: .fractionalHeight(1)
            ),
            subitem: item,
            count: itemCount
        )
        //item間のスペース
        items.interItemSpacing = .fixed(itemSpacing)
        //itemsが縦に並ぶグループを作成
        let groups = NSCollectionLayoutGroup.vertical(
            layoutSize: NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1),
                heightDimension: .absolute(itemLength)
            ),
            subitems: [items]
        )
        //groupsからsectionを生成
        let section = NSCollectionLayoutSection(group: groups)
        section.interGroupSpacing = itemSpacing
        return section
        
    }
    
}

extension GridViewController: UICollectionViewDataSource {
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 200
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.identifier,
                                                      for: indexPath) as! CustomCollectionViewCell
        return cell
    }
    
}
```

#Gridレイアウト解説

大切なところのみ解説したいと思います。
基本的な構成は以下の画像のようになっていて、Itemを決めてGroupを決めてSectionを決めてLayoutを設定すると言う流れになります。
![スクリーンショット 2021-04-11 22.12.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/954451/9915ebd8-fc8c-a1e3-e6c2-3a12b9fa9836.png)

それぞれ使う定数を事前に定義しておきます。

```swift
let itemCount = 3
let lineCount = itemCount - 1
let itemSpacing = CGFloat(2)
let itemLength = (self.view.frame.size.width - (itemSpacing * CGFloat(lineCount))) / CGFloat(itemCount)
```

一つのアイテムのサイズを決めます。
`absolute`とは絶対値と言う意味で、決まった値を幅にする設定にしています。

```swift
let item = NSCollectionLayoutItem(
    layoutSize: NSCollectionLayoutSize(
        widthDimension: .absolute(itemLength),
        heightDimension: .absolute(itemLength)
    )
)
```

次に、アイテムが三つ横並びになったグループを一つ作ります。
横並びのグループを作りたいので、`horizontal`を指定しています。
`fractionalWidth`や`fractionalHeight`とは親Viewにたいしてどれぐらいの割合で幅を決めるかと言う意味です。親ビューと同じにしたいのであれば、1(100%)にします。70%にしたい場合は`.fractionalWidth(7/10)`や`.fractionalWidth(0.7)`のようにします。
`subItem`は先ほど作成したItemdです。それをitemCount(=3)個並べていると言う意味です。

```swift
let items = NSCollectionLayoutGroup.horizontal(
    layoutSize: NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1),
        heightDimension: .fractionalHeight(1)
    ),
    subitem: item,
    count: itemCount
)
```
これは先ほど作ったグループ(=items)に`itemSpacing(=2)`だけスペースを入れる設定をしています。

```swift
items.interItemSpacing = .fixed(itemSpacing)
```

そして先ほど作成したグループ（=items）を縦に並べる設定をします。
縦に並べたいので、`vertical`にします。

```swift
let groups = NSCollectionLayoutGroup.vertical(
    layoutSize: NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1),
        heightDimension: .absolute(itemLength)
    ),
    subitems: [items]
)
```

groupsからsectionを作成し、スペースを開けます。

```swift
let section = NSCollectionLayoutSection(group: groups)
section.interGroupSpacing = itemSpacing
return section
```

そして、最後にこの関数(`func gridSection() -> NSCollectionLayoutSectionf`)を`collectionView`に設定します。

```swift
collectionView.collectionViewLayout = UICollectionViewCompositionalLayout(
    section: gridSection()
)
```

#Instagram風レイアウト実装
次に、Instagram風レイアウトを組んでみたいと思います。先ほどのGridレイアウトより少し難しくなりますが、基本は同じです。

```swift
final class InstagramViewController: UIViewController {
    
    @IBOutlet private weak var collectionView: UICollectionView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        collectionView.collectionViewLayout = UICollectionViewCompositionalLayout(
            section: instagramSection()
        )
        collectionView.register(CustomCollectionViewCell.self,
                                forCellWithReuseIdentifier: CustomCollectionViewCell.identifier)
        collectionView.dataSource = self
        
    }

    private func instagramSection() -> NSCollectionLayoutSection {

        let itemSpacing = CGFloat(2)
        let smallItemLength = (self.view.frame.size.width - (itemSpacing * 2)) / 3
        let largeItemLength = smallItemLength * 2 + itemSpacing
        
        // 小itemが縦に2つ並んだグループ
        let smallItem = NSCollectionLayoutItem(
            layoutSize: NSCollectionLayoutSize(
                widthDimension: .absolute(smallItemLength),
                heightDimension: .absolute(smallItemLength)
            )
        )
        let smallItemVerticalGroup = NSCollectionLayoutGroup.vertical(
            layoutSize: NSCollectionLayoutSize(
                widthDimension: .absolute(smallItemLength),
                heightDimension: .absolute(largeItemLength)
            ),
            subitem: smallItem,
            count: 2
        )
        smallItemVerticalGroup.interItemSpacing = .fixed(itemSpacing)
        
        // 小itemが縦に2つ並んだグループを横に3つ並べたグループ
        let smallItemsGroup = NSCollectionLayoutGroup.horizontal(
            layoutSize: NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1.0),
                heightDimension: .absolute(largeItemLength)
            ),
            subitem: smallItemVerticalGroup,
            count: 3
        )
        smallItemsGroup.interItemSpacing = .fixed(itemSpacing)

        // 大item + 小item*2 のグループ
        let largeItem = NSCollectionLayoutItem(
            layoutSize: NSCollectionLayoutSize(
                widthDimension: .absolute(largeItemLength),
                heightDimension: .absolute(largeItemLength)
            )
        )

        let largeItemLeftGroup = NSCollectionLayoutGroup.horizontal(
            layoutSize: NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1.0),
                heightDimension: .absolute(largeItemLength)
            ),
            subitems: [largeItem, smallItemVerticalGroup]
        )
        largeItemLeftGroup.interItemSpacing = .fixed(itemSpacing)
        
        let largeItemRightGroup = NSCollectionLayoutGroup.horizontal(
            layoutSize: NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1.0),
                heightDimension: .absolute(largeItemLength)
            ),
            subitems: [smallItemVerticalGroup, largeItem]
        )
        largeItemRightGroup.interItemSpacing = .fixed(itemSpacing)

        // 各グループを縦に並べたグループ
        let subitems = [largeItemLeftGroup, smallItemsGroup, largeItemRightGroup, smallItemsGroup]
        let heightDimension = NSCollectionLayoutDimension.absolute(
            //sectionの間隔は50
            largeItemLength * CGFloat(subitems.count) + (itemSpacing * 50)
        )

        // 高さの計算は後に追加するスペース分も足す
        let groups = NSCollectionLayoutGroup.vertical(
            layoutSize: NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1.0),
                heightDimension: heightDimension),
            subitems: subitems
        )
        groups.interItemSpacing = .fixed(itemSpacing)
        let section = NSCollectionLayoutSection(group: groups)
        section.interGroupSpacing = itemSpacing
        return section

    }
    
}

extension InstagramViewController: UICollectionViewDataSource {
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 200
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CustomCollectionViewCell.identifier,
                                                      for: indexPath) as! CustomCollectionViewCell
        return cell
    }
    
}
```

#Instagram風レイアウト解説

こちらも大切のところのみ解説します。

今回はitemのサイズが異なるのでそれぞれ設定します。

```swift
let itemSpacing = CGFloat(2)
let smallItemLength = (self.view.frame.size.width - (itemSpacing * 2)) / 3
let largeItemLength = smallItemLength * 2 + itemSpacing
```

小さいアイテムが縦に2つ並んだグループを作成しています。

```swift
let smallItem = NSCollectionLayoutItem(
    layoutSize: NSCollectionLayoutSize(
        widthDimension: .absolute(smallItemLength),
        heightDimension: .absolute(smallItemLength)
    )
)
let smallItemVerticalGroup = NSCollectionLayoutGroup.vertical(
    layoutSize: NSCollectionLayoutSize(
        widthDimension: .absolute(smallItemLength),
        heightDimension: .absolute(largeItemLength)
    ),
    subitem: smallItem,
    count: 2
)
smallItemVerticalGroup.interItemSpacing = .fixed(itemSpacing)
```

そして、そのグループを横に三つ並べたグループを作ります。

```swift
let smallItemsGroup = NSCollectionLayoutGroup.horizontal(
    layoutSize: NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1.0),
        heightDimension: .absolute(largeItemLength)
    ),
    subitem: smallItemVerticalGroup,
    count: 3
)
smallItemsGroup.interItemSpacing = .fixed(itemSpacing)
```

次に、左側に大きいアイテムと小さいアイテムの組み合わせ、右側に大きいアイテムと小さいアイテムの組み合わせのグループを作成しています。

```swift
let largeItem = NSCollectionLayoutItem(
    layoutSize: NSCollectionLayoutSize(
        widthDimension: .absolute(largeItemLength),
        heightDimension: .absolute(largeItemLength)
    )
)

let largeItemLeftGroup = NSCollectionLayoutGroup.horizontal(
    layoutSize: NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1.0),
        heightDimension: .absolute(largeItemLength)
    ),
    subitems: [largeItem, smallItemVerticalGroup]
)
largeItemLeftGroup.interItemSpacing = .fixed(itemSpacing)

let largeItemRightGroup = NSCollectionLayoutGroup.horizontal(
    layoutSize: NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1.0),
        heightDimension: .absolute(largeItemLength)
    ),
    subitems: [smallItemVerticalGroup, largeItem]
)
largeItemRightGroup.interItemSpacing = .fixed(itemSpacing)
```
そして、作ったグループ三つを縦に並べたグループを作成します。

```swift
let subitems = [largeItemLeftGroup, smallItemsGroup, largeItemRightGroup, smallItemsGroup]
let heightDimension = NSCollectionLayoutDimension.absolute(
    //sectionの間隔は50
    largeItemLength * CGFloat(subitems.count) + (itemSpacing * 50)
)
let groups = NSCollectionLayoutGroup.vertical(
    layoutSize: NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1.0),
        heightDimension: heightDimension),
    subitems: subitems
)
groups.interItemSpacing = .fixed(itemSpacing)
```
最後に、このグループを一つのセクションとして返します。

```swift
let section = NSCollectionLayoutSection(group: groups)
section.interGroupSpacing = itemSpacing
return section
```
Gridの時と同じように、`collectionView`に設定すればInstagram風のレイアウト完成です。

```swift
collectionView.collectionViewLayout = UICollectionViewCompositionalLayout(
    section: instagramSection()
)
```

#おわりに
UITableViewがいらない日が来るかもしれませんね。