Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

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

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

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

ss1.jpg

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

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

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

スクリーンショット 2016-11-22 21.05.04.png

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

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

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

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

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

PrototypeViewSizing.swift
import UIKit

protocol PrototypeViewSizing: class {
}

extension PrototypeViewSizing where Self: UICollectionViewCell {
    /// 原型ビューに準拠した大きさを求める。
    /// self自身のレイアウトを変更するため、表示に利用していないビューから呼び出すこと。
    ///
    /// - Parameters:
    ///   - flowLayout: フローレイアウト
    ///   - nColumns: 列数
    /// - Returns: 大きさを返す
    func propotionalScaledSize(
        for flowLayout: UICollectionViewFlowLayout,
        numberOfColumns nColumns: Int
    ) -> CGSize {
        // 幅は必ず指定のwidthに合わせ、高さはLayout Constraintに則った値とするサイズを求める
        let width = flowLayout.preferredItemWidth(forNumberOfColumns: nColumns)
        self.bounds.size = CGSize(width: width, height: 0)
        self.layoutIfNeeded()

        return self.systemLayoutSizeFitting(
            UILayoutFittingExpandedSize,
            withHorizontalFittingPriority: UILayoutPriorityRequired,
            verticalFittingPriority: UILayoutPriorityDefaultLow
        )
    }
}

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)
    }
}

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

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

最後にUIColletionViewDelegateFlowLayoutを実装します。

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

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

imk2o
フリーランスでエンジニアをやっております。 iOSアプリを出してます。 https://itunes.apple.com/jp/developer/yuichi-kobayashi/id936205746
https://imk2o.github.io/portfolio
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした