Collection Viewは通常、アイテム間に区切り線が表示されません。
アイテム間に区切り線を表示するには、
- 各アイテムCellに、区切り線を表示するビューを持たせる
- Decoration Viewを使う
前者は比較的実現しやすい方法ですが、Cellに本来のコンテンツ以外の要素を含めなければならず、端の要素に対して区切り線の表示制御が必要になるでしょう。
後者の方法は原則としてUICollectionViewLayout
のサブクラスを実装する必要があります。
理解が難しく解説もあまり多くされていなかった(特に日本語で)ので、この実装手順について解説してみたいと思います。
この記事のサンプルコードは、以下のプロジェクトに含まれていますので参考にしてください。
(アプリ起動後、View タブの Grid を選択すると該当画面が表示されます)
UICollectionViewLayout
に登場する「要素」の種類
まずUICollectionViewLayout
には、以下の要素が登場することを理解しておくとよいと思います。
-
UICollectionElementCategory.cell
: セル -
UICollectionElementCategory.supplementaryView
: ヘッダやフッタなどの補助的なビュー -
UICollectionElementCategory.decorationView
: 区切り線や背景などの装飾ビュー
表示する情報を変えたり、UIを提供する場合は.supplementaryView
を、
決まったデザインを表示するだけであれば.decorationView
を用いるとよいと思います。
(.supplementaryView
はUICollectionViewDataSource
でコントロールできるが、.decorationView
はほとんどコントロールできない)
今回は区切り線を表示したいだけなので、.decorationView
を利用します。
UICollectionViewFlowLayout
を継承して実装する
UICollectionViewLayout
のサブクラスを実装するのはなかなか容易ではありません。
そこで UICollectionViewFlowLayout
を継承したクラスを定義し、必要な実装だけを追加していくアプローチを取ります。
装飾ビューとして用いるNibを登録する
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?
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
をカスタマイズして、セクション毎に背景ビューを敷く方法も実装しています。