UICollectionViewで等幅正方形のcellを並べるのを楽にしてみる

デフォルトの写真アプリ

![Simulator Screen Shot 2016.01.31 19.04.33.png](https://qiita-image-store.s3.amazonaws.com/0/27802/26e4016e-92ef-9eab-1930-5e564e2c17bf.png)


のように、横幅を等分した長さで正方形の*cell*を並べる場合に、
*itemSize*を計算したり、*cell*と*cell*の間に隙間入れる場合を考慮したり、*sectionInset*も反映させたい...となると、結構面倒だったりします。
決め打ちでも良いのですが、*collectionView*の横幅がAuto-Layoutで変動する場合に太刀打ち出来ないし...
基本画面横いっぱいに*collectionView*を拡げるんだし画面の横幅を*UIScreen*から取得して計算すればいいじゃん！ってのもありますが、そうでないケースもあるのでできれば避けたい。

ということで、少しでも面倒なことを考えず楽できるようにextensionを書いてみました。

![sample.gif](https://qiita-image-store.s3.amazonaws.com/0/27802/5aebbe76-60f0-931d-2b70-620c294b5327.gif)

## 特徴
- 複雑な計算を記述する必要がなくなる
- 処理を呼べば即座にレイアウトが変更される
- sectionInsetを指定しなければ、spaceで指定した値をinsetの全要素に入れるので、全てにおいて間隔が一定になります。
- *collectionView*の*scrollDirection*が**Vertical**なら横幅を、**Horizontal**なら縦幅を基準にして等分します。
- ※明らかにspaceの値が大きかったり、perRowの値が0以下の場合はレイアウト処理をしません。

## 実装
こんな感じです。

```swift:extension
import UIKit

public extension UICollectionView {
    
    public func adaptBeautifulGrid(numberOfGridsPerRow: Int, gridLineSpace space: CGFloat) {
        let inset = UIEdgeInsets(
            top: space, left: space, bottom: space, right: space
        )
        adaptBeautifulGrid(numberOfGridsPerRow, gridLineSpace: space, sectionInset: inset)
    }
    
    public func adaptBeautifulGrid(numberOfGridsPerRow: Int, gridLineSpace space: CGFloat, sectionInset inset: UIEdgeInsets) {
        guard let layout = collectionViewLayout as? UICollectionViewFlowLayout else {
            return
        }
        guard numberOfGridsPerRow > 0 else {
            return
        }
        let isScrollDirectionVertical = layout.scrollDirection == .Vertical
        var length = isScrollDirectionVertical ? self.frame.width : self.frame.height
        length -= space * CGFloat(numberOfGridsPerRow - 1)
        length -= isScrollDirectionVertical ? (inset.left + inset.right) : (inset.top + inset.bottom)
        let side = length / CGFloat(numberOfGridsPerRow)
        guard side > 0.0 else {
            return
        }
        layout.itemSize = CGSize(width: side, height: side)
        layout.minimumLineSpacing = space
        layout.minimumInteritemSpacing = space
        layout.sectionInset = inset
        layout.invalidateLayout()
    }

}
```

## 使ってみる

前提として、collectionViewに、collectionViewFlowLayoutが追加されていることが条件です。

更新したいタイミングで、

```swift
// 横に4等分した幅を持つcellでレイアウトする。間隔は2.0px空ける
collectionView.adaptBeautifulGrid(4, gridLineSpace: 2.0)

// 最初に貼った写真アプリとほぼ同様のレイアウトをする。
// 横に4等分した幅を持つcellでレイアウトする。間隔は1.0px空ける
// 更に、sectionInset(top,bottomが10px,left,rightが0.0px)を指定する。
let inset = UIEdgeInsetsMake(10.0, 0.0, 10.0, 0.0)
collectionView.adaptBeautifulGrid(4, gridLineSpace: 1.0, sectionInset: inset)

```

のように呼んであげればその場で変更されます。
デモでは、指定する値をstepperで変更したときに、上記の処理を呼んで更新しています。
ケースにもよりけりですが、*viewDidLayoutSubviews()*や*viewDidAppear()*あたりに仕込んでおけば、
Auto-LayoutによってcollectionViewのframeが確定してからレイアウトを設定できるので、
4インチでも4.7インチ、5.5インチでも同様にレイアウトができます。

もし、回転可能な画面で、縦画面と横画面でグリッド数を変えたい場合は、

```swift
override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    view.layoutIfNeeded()
    let isLandscape = UIDeviceOrientationIsLandscape(UIDevice.currentDevice().orientation)
    let perRow = 4 + (isLandscape ? 2 : 0)
    collectionView.adaptBeautifulGrid(perRow, gridLineSpace: 2.0)
}
```

みたいに書けば、回転した時に縦横でグリッド数を変えられます。
似たような感じで、5.5インチの時だけグリッド数を1増やす、なんかもできるかと思います。

## さいごに

ソースとデモは[こちら](https://github.com/sgr-ksmt/BeautifulGridLayout)に置いてあります。一応carthage対応はしてあるのでライブラリとして導入できます。