Auto Layoutに準拠したUICollectionViewCellのサイジング

`UICollectioView`のSelf-sizing Cell、とても便利ですよね！
ただ以下のようなケースは、単純にいかなかったりします。

* Vertical Flow Layoutを使ったグリッド表示を行う
* 各セルは画面サイズに合わせ、幅と高さを調整する

例えば画面全体に`UICollectionView`を配置しこの中にセルを2列にグリッド表示したい場合、カスタムの`UICollectionViewCell`のレイアウトを画面半分の大きさで作れば良さそうです。
想像がつくかと思いますが、仮にiPhone7の大きさに合わせてレイアウトしても、iPhone7 Plusでは無駄な余白ができ、iPhone SEでは2列に表示されません！

![ss1.jpg](https://qiita-image-store.s3.amazonaws.com/0/25727/cd86ee51-84bf-980f-04ee-d9301d173a9b.jpeg)

これを解決するには、`UICollectionViewDelegateFlowLayout`の`sizeForItemAtIndexPath()`を実装する必要があります。
(あぁ、せっかくのSelf-sizing Cellが！)

セルの幅の計算はなんとなく想像できます。
高さは？
もし、以下のようなデザイン要件だった場合はどうしますか？

* 画像を表示する`UIImageView`はAspect Ratioを1:1にする
* タイトルを表示する`UILabel`の高さは最大2行表示できるよう、40ptとする

<img width="172" alt="スクリーンショット 2016-11-22 21.05.04.png" src="https://qiita-image-store.s3.amazonaws.com/0/25727/b4799bf9-edac-4996-996e-803dc87292c6.png">

> ちなみにセルのAspect Ratioを維持するように高さを求めても、期待どおりになりませんよ！

こんなときは、Auto Layoutを使って計算してみましょう！

## Auto Layoutを使ったセルサイズの計算

> サンプルコードはGitHubからダウンロードできます。
> https://github.com/imk2o/UICatalog

デザイン要件を満たすレイアウトを準備しておきます。
次に`UICollectionViewCell`のサブクラスを定義し、サイズ計算するメソッドを実装しますが、せっかくなので`protocol extension`で自由に着脱できるようにしましょう。

```swift:PrototypeViewSizing.swift
import UIKit

protocol PrototypeViewSizing: class {
}

extension PrototypeViewSizing where Self: UICollectionViewCell {
    /// 原型ビューに準拠した大きさを求める。
    ///
    /// - Parameters:
    ///   - flowLayout: フローレイアウト
    ///   - nColumns: 列数
    /// - Returns: 大きさを返す
    func propotionalScaledSize(
        for flowLayout: UICollectionViewFlowLayout,
        numberOfColumns nColumns: Int
    ) -> CGSize {
        // 幅は必ず指定のwidthに合わせ、高さはLayout Constraintに則った値とするサイズを求める
        let width = flowLayout.preferredItemWidth(forNumberOfColumns: nColumns)
        
        return self.systemLayoutSizeFitting(
            CGSize(width: width, height: 0),
            withHorizontalFittingPriority: .required,
            verticalFittingPriority: .fittingSizeLevel
        )
    }
}

private extension UICollectionViewFlowLayout {
    /// 列数に対するアイテムの推奨サイズ(幅)を求める。
    ///
    /// - Parameter nColumns: 列数
    /// - Returns: 幅を返す
    func preferredItemWidth(forNumberOfColumns nColumns: Int) -> CGFloat {
        guard nColumns > 0 else {
            return 0
        }
        guard let collectionView = self.collectionView else {
            fatalError()
        }
        
        let collectionViewWidth = collectionView.bounds.width
        let inset = self.sectionInset
        let spacing = self.minimumInteritemSpacing
        
        // コレクションビューの幅から、各余白を除いた幅を均等に割る
        return (collectionViewWidth - (inset.left + inset.right + spacing * CGFloat(nColumns - 1))) / CGFloat(nColumns)
    }
}
```

あとはこれを実装したいカスタムセルクラスに付与するだけです。

```swift:PropotionalSizingCell.swift
class PropotionalSizingCell: UICollectionViewCell, PrototypeViewSizing {
    ...
}
```

最後に`UIColletionViewDelegateFlowLayout`を実装します。

```swift:PropotionalSizingCollectionViewController.swift
class PropotionalSizingCollectionViewController: UIViewController {

    var computedCellSize: CGSize?

    @IBOutlet weak var collectionView: UICollectionView!

    override func viewDidLoad() {
        super.viewDidLoad()

        self.collectionView.register(PropotionalSizingCell.nib, forCellWithReuseIdentifier: "Cell")
    }

    ...
}

extension PropotionalSizingCollectionViewController: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        // 一度計算したらキャッシュし、負荷を軽減
        // TODO: landscape表示に対応している場合は再計算を行うこと
        if let cellSize = self.computedCellSize {
            return cellSize
        } else {
            // PropotionalSizingCell.nibから原型セルを生成し、2列表示に適切なサイズを求める
            guard
                let prototypeCell = PropotionalSizingCell.nib.instantiate(withOwner: nil, options: nil).first as? PropotionalSizingCell,
                let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout
            else {
                fatalError()
            }
            
            let cellSize = prototypeCell.propotionalScaledSize(for: flowLayout, numberOfColumns: 2)
            self.computedCellSize = cellSize
            
            return cellSize
        }
    }
}

private extension PropotionalSizingCell {
    static var nib: UINib {
        return UINib(nibName: String(describing: self), bundle: nil)
    }
}
```

これで、デザイン要件を満たす表示になりました！

![ss2.jpg](https://qiita-image-store.s3.amazonaws.com/0/25727/bca097a8-d3d2-9176-c030-41eddbd083d7.jpeg)

もしもっとスマートで効率の良いやり方をご存知でしたら、コメントいただけると幸いです。