はじめに
UICollectionViewLayoutに関して、iOS13からUICollectionViewCompositionalLayoutという新しいレイアウト方法が追加されたため、iOS12以前で使用していたレイアウト方法と比較してみました。
主な違い
- UICollectionViewLayoutの生成方法
- UICollectionViewのDataSourceの管理
※ UICollectionViewCompositionalLayout自体の仕組みについては、[こちら](時代の変化に応じて進化するCollectionView ~Compositional LayoutsとDiffable Data Sources~)の記事が非常にわかりやすかったです。
UICollectionViewLayoutの生成方法
iOS12以前では、UICollectionViewLayoutやUICollectionViewFlowLayoutのサブクラスを独自に用意したり、UICollectionViewDelegateFlowLayoutを利用したりしてレイアウトを生成していました。
その中に、iOS13からUICollectionViewCompositionalLayoutが加わったイメージです。
以下、3つの方法でグリッド形式(3×n)のレイアウトを作ってみています。
- UICollectionViewFlowLayoutのサブクラスを用意
- UICollectionViewLayoutのサブクラスを用意
- UICollectionViewCompositionalLayoutを生成するクラスを用意

例1: UICollectionViewFlowLayoutのサブクラスを用意(iOS12以前)
※ iOS13以降でも使用できます。
class GridLayout: UICollectionViewFlowLayout {
let itemCount = CGFloat(3)
let itemSpacing = CGFloat(1)
override func prepare() {
super.prepare()
itemSize = itemSize()
minimumLineSpacing = itemSpacing
minimumInteritemSpacing = itemSpacing
sectionInsetReference = .fromSafeArea
}
private func itemSize() -> CGSize {
guard let cv = collectionView else { return CGSize.zero }
let itemLength = cv.bounds.width / itemCount - itemSpacing
return CGSize(width: itemLength, height: itemLength)
}
}
// レイアウトの適用
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: GridLayout())
例2: UICollectionViewLayoutのサブクラスを用意(iOS12以前)
※ iOS13以降でも使用できます。
/// グリッド形式のレイアウトを生成するクラス
class GridLayout: UICollectionViewLayout {
// MARK: - Private Propaty
private var cachedAttributes = [UICollectionViewLayoutAttributes]()
// レイアウトの総Height
private var contentHeight: CGFloat = 0
// レイアウトの総Width
private var contentWidth: CGFloat {
guard let collectionView = collectionView else { return 0 }
let insets = collectionView.contentInset
return collectionView.bounds.width - (insets.left + insets.right)
}
// MARK: - Life Cycle
override func prepare() {
guard cachedAttributes.isEmpty, let collectionView = collectionView else { return }
let numberOfColumns = 3
let lineCount = numberOfColumns - 1
let itemSpacing = CGFloat(1)
let cellPadding = CGFloat.zero
let cellLength = (contentWidth - (itemSpacing * CGFloat(lineCount))) / CGFloat(numberOfColumns)
let cellXOffsets = (0 ..< numberOfColumns).map { CGFloat($0) * (cellLength + 1) }
var cellYOffsets = [CGFloat](repeating: 0, count: numberOfColumns)
var currentColumnNumber = 0
(0 ..< collectionView.numberOfItems(inSection: 0)).forEach {
let indexPath = IndexPath(item: $0, section: 0)
let cellFrame = CGRect(x: cellXOffsets[currentColumnNumber],
y: cellYOffsets[currentColumnNumber],
width: cellLength,
height: cellLength)
cellYOffsets[currentColumnNumber] = cellYOffsets[currentColumnNumber] + (cellLength + itemSpacing)
currentColumnNumber = currentColumnNumber < (numberOfColumns - 1) ? currentColumnNumber + 1 : 0
let itemFrame = cellFrame.insetBy(dx: cellPadding, dy: cellPadding)
// Attributesを追加
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = itemFrame
cachedAttributes.append(attributes)
// ContentSizeを更新
contentHeight = max(contentHeight, cellFrame.maxY)
}
}
override var collectionViewContentSize: CGSize {
// prepareが終わった後に呼ばれるので、計算したcontentHeightを返す
return CGSize(width: contentWidth, height: contentHeight)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return cachedAttributes.filter({ (layoutAttributes) -> Bool in
rect.intersects(layoutAttributes.frame)
})
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cachedAttributes[indexPath.item]
}
}
// レイアウトの適用
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: GridLayout())
例3: UICollectionViewCompositionalLayoutを生成するクラスを用意(iOS13以降)
/// グリッド形式のレイアウトを生成する
/// - Parameters:
/// - collectionViewBounds: UICollectionViewのBounds
/// - itemCount: 1列に表示するitemの個数
func gridLayout(collectionViewBounds: CGRect, itemCount: Int) -> UICollectionViewLayout {
let lineCount = itemCount - 1
let itemSpacing = CGFloat(1) // セル間のスペース
let itemLength = (collectionViewBounds.width - (itemSpacing * CGFloat(lineCount))) / CGFloat(itemCount)
// 1つのitemを生成
// .absoluteは固定値で指定する方法
let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .absolute(itemLength),
heightDimension: .absolute(itemLength)))
// itemを3つ横並びにしたGroupを生成
// .fractional~は親Viewとの割合
let items = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0)),
subitem: item,
count: itemCount)
// Group内のitem間のスペースを設定
items.interItemSpacing = .fixed(itemSpacing)
// 生成したGroup(items)が縦に並んでいくGroupを生成(実質これがSection)
let groups = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(itemLength)),
subitems: [items])
// 用意したGroupを基にSectionを生成
let section = NSCollectionLayoutSection(group: groups)
// Section間のスペースを設定
section.interGroupSpacing = itemSpacing
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
// レイアウトの適用
CollectionView = UICollectionView(frame: view.bounds, collectionViewLayout: PhotoListLayout.gridLayout(collectionViewBounds: view.bounds, itemCount: 3))
UICollectionViewのDataSourceの管理
iOS13からUICollectionViewDiffableDataSourceというUICollectionViewDataSourceを継承したクラスが登場し、このクラス内で表示するデータを管理できるようになりました。
これもコードベースで比較してみます。iOS12以前では、UICollectionViewDataSourceを直接クラスに準拠させる方法でデータを管理しています。そしてiOS13では、UICollectionViewDiffableDataSourceを使ってデータ管理をおこなってみます。
UICollectionViewDataSourceをControllerクラスに準拠させる(iOS12以前)
var collectionView: UICollectionView!
var photoList = [PHAsset]()
collectionView.dataSource = self
---------------------------------------------------------------
extension PhotoListViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return photoList.count // 配列等で持っているデータの数
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PhotoListCell.identifier, for: indexPath) as! PhotoListCell
cell.setImage(asset: photoList[indexPath.item]) // 配列等で持っているデータをIndexPathで指定して取得
return cell
}
}
UICollectionViewDiffableDataSourceでデータソースを管理(iOS13以降)
var collectionView: UICollectionView!
var dataSource: UICollectionViewDiffableDataSource<Int, PHAsset>!
var photoList = [PHAsset]()
---------------------------------------------------------------
dataSource = UICollectionViewDiffableDataSource<Int, PHAsset>(collectionView: collectionView, cellProvider: { (collectionView, indexPath, asset) -> UICollectionViewCell? in
// セル生成処理
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PhotoListCell", for: indexPath) as! PhotoListCell
cell.setImage(asset: asset)
return cell
})
// 用意したリソースの反映
var resource = NSDiffableDataSourceSnapshot<Int, PHAsset>()
resource.appendSections([0]) // セクション数をセット
resource.appendItems(photoList) // アイテムをセット
// 注意点: 複数セクションを使用する場合は、セクション毎にアイテムのセットも行う
dataSource.apply(resource, animatingDifferences: true)
ざっくり言うと、iOS12以前までUICollectionViewDataSourceで手続き的にやっていたことが、iOS13からは宣言的に書けるようになったように思いました。
以上
サンプル
今回作成したサンプルは、以下のリポジトリにあります。
※ サンプルはiOS13用のみです。
https://github.com/ddd503/UICollectionViewCompositionalLayout-UICollectionViewDiffableDataSource-Sample