LoginSignup
25
17

More than 3 years have passed since last update.

Decoration Viewを用いてCollection Viewに区切り線と背景を追加する

Last updated at Posted at 2017-10-31

Collection Viewは通常、アイテム間に区切り線が表示されません。
アイテム間に区切り線を表示するには、

  • 各アイテムCellに、区切り線を表示するビューを持たせる
  • Decoration Viewを使う

前者は比較的実現しやすい方法ですが、Cellに本来のコンテンツ以外の要素を含めなければならず、端の要素に対して区切り線の表示制御が必要になるでしょう。

後者の方法は原則としてUICollectionViewLayoutのサブクラスを実装する必要があります。
理解が難しく解説もあまり多くされていなかった(特に日本語で)ので、この実装手順について解説してみたいと思います。

IMG_6236.PNG

この記事のサンプルコードは、以下のプロジェクトに含まれていますので参考にしてください。
(アプリ起動後、View タブの Grid を選択すると該当画面が表示されます)

UICollectionViewLayout に登場する「要素」の種類

まずUICollectionViewLayoutには、以下の要素が登場することを理解しておくとよいと思います。

  • UICollectionElementCategory.cell: セル
  • UICollectionElementCategory.supplementaryView: ヘッダやフッタなどの補助的なビュー
  • UICollectionElementCategory.decorationView: 区切り線や背景などの装飾ビュー

表示する情報を変えたり、UIを提供する場合は.supplementaryViewを、
決まったデザインを表示するだけであれば.decorationViewを用いるとよいと思います。
(.supplementaryViewUICollectionViewDataSourceでコントロールできるが、.decorationViewはほとんどコントロールできない)

今回は区切り線を表示したいだけなので、.decorationViewを利用します。

UICollectionViewFlowLayout を継承して実装する

UICollectionViewLayout のサブクラスを実装するのはなかなか容易ではありません。
そこで UICollectionViewFlowLayout を継承したクラスを定義し、必要な実装だけを追加していくアプローチを取ります。

装飾ビューとして用いるNibを登録する

CollectionViewFlowLayoutWithDecorations.swift
class CollectionViewFlowLayoutWithDecorations: UICollectionViewFlowLayout {
    enum DecorationViewElementKind: String {
        case horizontalSeparator
        case verticalSeparator
        case sectionBackground
        // ...省略...
    }

    // ...省略...

    override func awakeFromNib() {
        let separatorNib = UINib(nibName: "SeparatorDecorationView", bundle: nil)
        self.register(
            separatorNib,
            forDecorationViewOfKind: DecorationViewElementKind.horizontalSeparator.rawValue
        )
        self.register(
            separatorNib,
            forDecorationViewOfKind: DecorationViewElementKind.verticalSeparator.rawValue
        )
        // ...省略...
    }

    // ...省略...
}

要素のレイアウト情報に、区切り線のレイアウト情報を追加するには

まず、以下のメソッドをオーバライドします。
このメソッドは通常、セルおよびセクションヘッダ&フッタのレイアウト情報を返します。
これを参考にして、区切り線のレイアウト情報を追加し、返すようにします。

// rect内にある要素のレイアウト情報を求める
func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?

もうひとつ、以下のメソッドをオーバライドします。
このメソッドでは、タイプがelementKindの.decorationViewについて、特定のindexPathのレイアウト情報を求める必要があります。サンプルコードでは、セル間にその高さと同じ区切り線を、行間に横断的な区切り線が引かれるよう、レイアウトを求めています。

// 位置indexPathにおける、elementKindタイプの装飾ビューに対するレイアウト情報を求める
func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?
CollectionViewFlowLayoutWithDecorations.swift
class CollectionViewFlowLayoutWithDecorations: UICollectionViewFlowLayout {
    enum DecorationViewElementKind: String {
        case horizontalSeparator
        case verticalSeparator
        case sectionBackground
        // ...省略...
    }

    var numberOfColumns: Int = 3    // 列数
    var separatorSize: CGSize = CGSize(width: 0.375, height: 0.375)    // 区切り線の太さ(0の場合は表示しない)
    var showsSectionBackground: Bool = true    // セクションごとに背景をつけるか?

    override func awakeFromNib() {
        // ...省略...
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let layoutAttributesArray = super.layoutAttributesForElements(in: rect) else {
            return nil
        }

        // 各Itemに対応するセパレータのLayoutAttributesを求め、追加する
        var decorationLayoutAttributesArray: [UICollectionViewLayoutAttributes] = []
        var sectionIndexes: Set<Int> = []
        for layoutAttributes in layoutAttributesArray {
            let indexPath = layoutAttributes.indexPath
            decorationLayoutAttributesArray += DecorationViewElementKind.separators.flatMap {
                return self.layoutAttributesForDecorationView(ofKind: $0.rawValue, at: indexPath)
            }
            sectionIndexes.insert(indexPath.section)
        }

        // ...省略...

        return layoutAttributesArray + decorationLayoutAttributesArray
    }

    override func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        switch DecorationViewElementKind(rawValue: elementKind) {
        case .horizontalSeparator?:
            // 2行目以降の最初の列に対するItemの上部に水平線を引く
            guard
                self.separatorSize.height > 0,
                indexPath.item > 0 && (indexPath.item % self.numberOfColumns == 0),
                let collectionView = self.collectionView,
                let itemLayoutAttributes = self.layoutAttributesForItem(at: indexPath)
            else {
                return nil
            }

            let layoutAttributes = UICollectionViewLayoutAttributes(forDecorationViewOfKind: elementKind, with: indexPath)
            layoutAttributes.frame = CGRect(
                x: self.sectionInset.left,
                y: itemLayoutAttributes.frame.origin.y - ((self.minimumLineSpacing + self.separatorSize.height) / 2).rounded(.down),
                width: collectionView.bounds.width - (self.sectionInset.left + self.sectionInset.right),
                height: self.separatorSize.height
            )

            return layoutAttributes
        case .verticalSeparator?:
            // 最初の行以外に対するItemの左部に垂直な線を引く
            guard
                self.separatorSize.width > 0,
                (indexPath.item % self.numberOfColumns) > 0,
                let itemLayoutAttributes = self.layoutAttributesForItem(at: indexPath)
            else {
                return nil
            }

            let layoutAttributes = UICollectionViewLayoutAttributes(forDecorationViewOfKind: elementKind, with: indexPath)
            layoutAttributes.frame = CGRect(
                x: itemLayoutAttributes.frame.origin.x - ((self.minimumInteritemSpacing + self.separatorSize.width) / 2).rounded(.down),
                y: itemLayoutAttributes.frame.origin.y,
                width: self.separatorSize.width,
                height: itemLayoutAttributes.frame.size.height
            )

            return layoutAttributes
        case .sectionBackground?:
            // ...省略...
        default:
            return nil
        }
    }

    // ...省略...
}

CollectionViewにInsert, Deleteする場合

Collection ViewでInsert, Delete操作を行う場合は以下のメソッドも適宜実装が必要です。
(サンプルコードでは実装していません)

func indexPathsToInsertForDecorationView(ofKind elementKind: String) -> [IndexPath]
func indexPathsToDeleteForDecorationView(ofKind elementKind: String) -> [IndexPath]

実装したカスタムレイアウトを利用する

StoryboardからCollection Viewの下にあるCollection View Flow Layoutを選択し、
Custom Classをカスタムレイアウトを実装したクラス名に変更します。
仕切り線の太さや列の数は適宜調整してください。

これで区切り線が表示されるはずです。

まとめ

  • UICollectionViewFlowLayoutを継承したクラスを実装することで、比較的簡単かつ確実に、区切り線を表示させることができる
  • .decorationViewはあらかじめregisterNib()したものを表示するだけなので、色やデザインを変える柔軟性を持たせたい場合、更なる工夫が必要

おまけ

サンプルコードにはUICollectionViewFlowLayoutをカスタマイズして、セクション毎に背景ビューを敷く方法も実装しています。

25
17
1

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
25
17