70
47

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

UICollectionViewのカスタムレイアウトの作り方

Last updated at Posted at 2018-05-01

UICollectionViewでは、UICollectionViewLayoutのサブクラスを作成することで、独自のレイアウトを定義することができます。
本記事では、カスタムレイアウトの作り方を紹介します。

なお、本記事では説明に必要な箇所のコードのみ掲載しています。
ベースとなる実装については以下の記事を参照してください。

UICollectionViewを必要最低限のコードで実装してみる

作成するレイアウト

ランダムな大きさのセルを積み木のように積み立てたレイアウトを作成していきます。

image.png

レイアウト処理の流れを理解する

まず、レイアウト処理を司る3つのプロパティおよびメソッドについて理解する必要があります。

prepare()メソッド

セルの配置を決めるために必要となる計算をする場所です。
セルの大きさや位置を計算し、あとで使用するためにそれらを保持しておきます。

collectionViewContentSizeプロパティ

コンテンツ全体を収容する領域の大きさを返すゲッタープロパティです。
領域の大きさが画面サイズを超える場合、横方向および縦方向どちらにもスクロールできるように設定されます。

layoutAttributesForElements(in: CGRect)メソッド

コレクションビューは現在のスクロール位置に基づき、以下のようにある矩形(Visible rectの部分)を指定してその中にあるセルの属性を問い合わせます。
引数のinにはその矩形のサイズ、位置情報が入っています。
ここでいう属性とはUICollectionViewLayoutAttributesインスタンスのことで、セルの大きさ、位置、重なり順などの情報を持つオブジェクトです。
ここで返された属性のリストをもとに、コレクションビューはセルの配置を行います。

image.png

これら以外にもメソッドは定義されており、それらをオーバーライドすることでより高度なレイアウトを作成することが可能です。
本記事では上記3つのプロパティおよびメソッドについてのみ触れます。

カスタムレイアウトクラス

カスタムレイアウトクラスの実装は以下の通りです。
UICollectionViewLayoutを継承したCustomLayoutクラスと、CustomDelegateプロトコルを定義しています。
CustomLayoutでは、先程挙げた3つのプロパティ、メソッドに加え、いくつか追加のプロパティを定義しています。
CustomDelegateプロトコルは、セルに表示するデータに関する情報を取得するためにデリゲートオブジェクトが実装するべきメソッドを定義しています。

CustomLayout.swift
import UIKit

class CustomLayout: UICollectionViewLayout {
    weak var delegate: CustomDelegate!
    var numColumns = 4
    var padding: CGFloat = 3
    var attributesArray = [UICollectionViewLayoutAttributes]()
    var contentHeight: CGFloat = 0

    var contentWidth: CGFloat {
        guard let collectionView = collectionView else { return 0 }
        return collectionView.bounds.width
    }

    override var collectionViewContentSize: CGSize {
        return CGSize(width: contentWidth, height: contentHeight)
    }

    override func prepare() {
        guard attributesArray.isEmpty, let collectionView = collectionView else { return }

        let columnWidth = contentWidth / CGFloat(numColumns)
        var xOffsets = [CGFloat]()
        for column in 0..<numColumns {
            xOffsets.append(columnWidth * CGFloat(column))
        }

        var column = 0
        var yOffsets = [CGFloat](repeating: 0, count: numColumns)

        for item in 0..<collectionView.numberOfItems(inSection: 0) {
            let indexPath = IndexPath(item: item, section: 0)
            let itemHeight = delegate.collectionView(collectionView, heightForItemAt: indexPath)
            let height = itemHeight + padding * 2
            let frame = CGRect(x: xOffsets[column], y: yOffsets[column], width: columnWidth, height: height)
            let insetFrame = frame.insetBy(dx: padding, dy: padding)

            let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
            attributes.frame = insetFrame
            attributesArray.append(attributes)

            contentHeight = max(contentHeight, frame.maxY)
            yOffsets[column] = yOffsets[column] + height

            column = column < (numColumns - 1) ? (column + 1) : 0
        }
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        var visibleLayoutAttributes = [UICollectionViewLayoutAttributes]()

        for attributes in attributesArray {
            if attributes.frame.intersects(rect) {
                visibleLayoutAttributes.append(attributes)
            }
        }
        return visibleLayoutAttributes
    }
}

protocol CustomDelegate: class {
    func collectionView(_ collectionView: UICollectionView, heightForItemAt indexPath: IndexPath) -> CGFloat
}

ポイントを整理します。

  1. このクラスはCustomDelegate型のデリゲートオブジェクトへの参照を持っています。デリゲートオブジェクトからセルの高さを取得します。
  2. セルの配置に関する計算は全てprepare()メソッドにて行います。計算結果はUICollectionViewLayoutAttributesの配列としてattributesArrayプロパティで保持します。
  3. prepare()メソッドでは各セルの高さの計算と並行してコンテンツ全体の高さcontentHeightも計算しています。
  4. コンテンツの幅contentWidthはベースの実装によりデバイスの幅と同じになるように設定されています。
  5. layoutAttributesForElements(in: CGRect)メソッドでは、画面に表示されている矩形領域(可視領域)にあるセルの属性情報を返すようにしています。attributes.frame.intersects(rect)はセルの位置と可視領域が交差しているか、つまりセルが画面に表示されるかどうかの判定をしており、trueであればそのセルの属性情報を返します。

わかりやすくするために、上記のコード中に出てくるプロパティやローカル変数が指している部分を図式化しました。

image.png

ビューコントローラの実装

ViewControllerをCustomDelegateプロトコルに準拠させます。
以下のエクステンションを実装してください。

ViewController.swift
extension ViewController: CustomDelegate {
    func collectionView(_ collectionView: UICollectionView, heightForItemAt indexPath: IndexPath) -> CGFloat {
        return CGFloat((arc4random_uniform(11) + 1) * 20)
    }
}

ここでは高さの値を20〜200の範囲でランダムに返すような実装をしています。

最後に、コレクションビューにCustomLayoutオブジェクトを設定します。
CustomLayoutオブジェクトを作成し、デリゲートとしてViewControllerを設定したら、それをcollectionView.collectionViewLayoutプロパティに設定します。

以下のコードをviewDidLoadメソッドに追記してください。

ViewController.swift
let customLayout = CustomLayout()
customLayout.delegate = self
collectionView.collectionViewLayout = customLayout

以上の実装でアプリを起動すると、カスタムレイアウトが適用されたコレクションビューが表示されます。

image.png

70
47
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
70
47

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?