UICollectionViewは、iOS用UIKitの強力なコンポーネントであり、オブジェクトのコレクションをグリッド、リスト、カスタムレイアウトとして表示します。UITableViewとは異なり、UICollectionViewは1列に制限されません。UICollectionViewLayoutを使用して要素を自由に配置できます。UICollectionViewの本質、FlowLayoutとCompositionalLayoutの仕組み、Swiftでのカスタムセルの作成方法について解説します。IT Sectrでは、ギャラリー、カタログ、掲示板にUICollectionViewを使用しています。テーブルとの比較については、UITableViewの記事をお読みください。
重要なポイント
UICollectionViewは、iOS 6(2012年)で導入されたUIKitフレームワークのクラスです。UICollectionViewLayoutオブジェクトによって管理される柔軟なレイアウトで、セル(UICollectionViewCell)の順序付けられたコレクションを表示します。UICollectionViewはUITableView(リスト)と同じ問題を解決しますが、要素を自由に配置できます:グリッド、カルーセル、モザイク、カスケード。
アーキテクチャ UICollectionViewはMVCパターンに基づいています。データはDataSource、レイアウトはUICollectionViewLayout、動作はUICollectionViewDelegateが担当します。この分離により、データコードを変更せずにレイアウトを交換できます。Apple(WWDC 2025)によると、App Storeトップ100のiOSアプリの72%がコンテンツ表示にUICollectionViewを使用しています。CollectionViewはiOS 6からマルチカラムレイアウトをサポートし、iOS 13から複雑なセクション用にCompositionalLayoutを取得しました。
UICollectionViewは、UITableViewと同様に再利用可能なセルのプール(reuse queue)を使用します。dequeueReusableCell(withReuseIdentifier:for:)メソッドは、プールからセルを返すか、プールが空の場合は新しいセルを作成します。UITableViewとは異なり、UICollectionViewCellには組み込みのcontentViewがあり、すべてのサブビューをここに追加する必要があります。セルに直接追加しないでください。contentViewが正しい選択と編集アニメーションを担当します。
UICollectionViewFlowLayout — iOS 6から動作する標準レイアウトです。セルサイズ、スクロール方向、最小間隔、セクションを設定可能で、要素を行(flow)に配置します。UICollectionViewCompositionalLayout — モダンレイアウト(iOS 13+)で、ブロックの組み合わせ(グループ→セクション→レイアウト)でレイアウトを記述します。各グループは水平、垂直、またはカスタムにできます。
| パラメータ | UICollectionViewFlowLayout | UICollectionViewCompositionalLayout |
|---|---|---|
| 最小バージョン | iOS 6 | iOS 13 |
| 複雑なレイアウト | シンプルなグリッドまたはリストのみ | 異なるレイアウトのセクション、グループ、カルーセル |
| 適応性 | デリゲートサイズ経由 | NSCollectionLayoutDimension.fractionalWidth/Height |
| パフォーマンス | すべての可視要素をレンダリング | 遅延セクション作成 |
| iPadサポート | 手動適応が必要 | fractionalWidthによる適応グループ |
| 実装の複雑さ | 低い(5~10行) | 中程度(15~30行) |
// FlowLayoutを使用した基本的なUICollectionView
class ViewController: UIViewController {
private var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.minimumInteritemSpacing = 8
layout.minimumLineSpacing = 12
layout.sectionInset = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
layout.itemSize = CGSize(width: view.frame.width - 32, height: 100)
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.register(CustomCell.self, forCellWithReuseIdentifier: "cell")
collectionView.dataSource = self
view.addSubview(collectionView)
}
}
// DataSource
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int { 20 }
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
as! CustomCell
cell.configure(with: "Item \(indexPath.row)")
return cell
}
}
Appleの推奨(WWDC 2025):新しいプロジェクトにはCompositionalLayoutを使用してください。適応レイアウトの機能が豊富で、UICollectionView.Representableを介してUIKitやSwiftUIとより良く連携し、自動サイズのセクションヘッダーをサポートします。FlowLayoutは、同一セルのグリッドとiOS 12以下の最小バージョンを持つシンプルな画面に使用します。
UICollectionViewCell — UICollectionViewセルの基本クラスです。各セルには、contentView(メインコンテナ)、backgroundView(デフォルトの背景)、selectedBackgroundView(選択時の背景)が含まれます。セルの外観はcontentView内で設定してください。UICollectionViewCellに直接サブビューを追加しないでください。
// 設定付きカスタムセル
class ProductCell: UICollectionViewCell {
private let imageView = UIImageView()
private let titleLabel = UILabel()
private let priceLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
contentView.addSubview(imageView)
contentView.addSubview(titleLabel)
contentView.addSubview(priceLabel)
// Auto Layoutの制約
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor),
titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 8),
titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8),
titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8),
priceLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),
priceLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8)
])
}
func configure(with product: Product) {
imageView.image = UIImage(named: product.imageName)
titleLabel.text = product.title
priceLabel.text = product.price
}
}
UICollectionViewDelegateは、選択、ハイライト、コンテキストメニュー、並べ替えを処理するメソッドを提供します。UITableViewとは異なり、UICollectionViewDelegateは要素のタップだけでなく、長押し(UIContextMenuInteractionによるコンテキストメニュー)も制御できます。タップを処理するには、collectionView(_:didSelectItemAt:)を実装します。これは選択アニメーションが完了したときに呼び出されます。
UICollectionViewDiffableDataSource — iOS 13で導入されたUICollectionViewDataSourceの型安全な代替です。cellForItemAtによる手動のセル管理の代わりに、DiffableDataSourceはNSDiffableDataSourceSnapshotを使用します。これはデータ状態のスナップショットで、古い状態と新しい状態の差分を自動計算し、変更をアニメーション化します。
// セクション付きDiffableDataSource
enum Section {
case featured, recommended, categories
}
class ModernDataSource: UIViewController {
private var dataSource: UICollectionViewDiffableDataSource<Section, Product>!
override func viewDidLoad() {
super.viewDidLoad()
configureDataSource()
applyInitialSnapshot()
}
private func configureDataSource() {
let cellRegistration = UICollectionView.CellRegistration<ProductCell, Product> { cell, _, product in
cell.configure(with: product)
}
dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) {
collectionView, indexPath, product in
collectionView.dequeueConfiguredReusableCell(
using: cellRegistration, for: indexPath, item: product
)
}
}
private func applyInitialSnapshot() {
var snapshot = NSDiffableDataSourceSnapshot<Section, Product>()
snapshot.appendSections([.featured, .recommended, .categories])
snapshot.appendItems(featuredProducts, toSection: .featured)
snapshot.appendItems(recommendedProducts, toSection: .recommended)
snapshot.appendItems(categories, toSection: .categories)
dataSource.apply(snapshot, animatingDifferences: true)
}
// 自動アニメーションによる更新
func updateProducts(_ products: [Product]) {
var snapshot = dataSource.snapshot()
snapshot.deleteItems(snapshot.itemIdentifiers)
snapshot.appendItems(products)
dataSource.apply(snapshot, animatingDifferences: true)
}
}
DiffableDataSourceの利点:(1)自動アニメーション — insert、delete、reloadがデフォルトのアニメーションで発生;(2)型安全性 — セクションとアイテムは型付けされ、実行時クラッシュではなくコンパイルエラー;(3)スナップショット — 状態スナップショットを保存、元に戻し、比較可能;(4)パフォーマンス — DiffableDataSourceはHellierアルゴリズムを使用して最小の変更セットを計算。Apple(WWDC 2024)は、すべての新しいUICollectionViewにDiffableDataSourceを推奨し、古いDataSourceはiOS 12以前のみに使用することを推奨しています。
UICollectionViewLayout — コレクション要素の配置を完全に制御する抽象クラスです。FlowLayoutとCompositionalLayoutが適切でない場合(例:円形レイアウト、螺旋、図)、UICollectionViewLayoutのサブクラスを作成し、レイアウト準備メソッドをオーバーライドします。
// カスタム円形レイアウト
class CircularLayout: UICollectionViewLayout {
private var attributesCache: [UICollectionViewLayoutAttributes] = []
private let radius: CGFloat = 120
override func prepare() {
super.prepare()
attributesCache.removeAll()
guard let collectionView else { return }
let count = collectionView.numberOfItems(inSection: 0)
let center = CGPoint(x: collectionView.bounds.midX, y: collectionView.bounds.midY)
for i in 0..<count {
let indexPath = IndexPath(item: i, section: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let angle = (CGFloat(i) / CGFloat(count)) * 2 * .pi
attributes.center = CGPoint(
x: center.x + radius * cos(angle),
y: center.y + radius * sin(angle)
)
attributes.size = CGSize(width: 60, height: 60)
attributesCache.append(attributes)
}
}
override var collectionViewContentSize: CGSize {
collectionView?.bounds.size ?? .zero
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
attributesCache.filter { $0.frame.intersects(rect) }
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
attributesCache[indexPath.item]
}
}
パフォーマンス:カスタムUICollectionViewLayoutは、prepare()で属性をキャッシュし、layoutAttributesForItem(at:)でO(1)で返す必要があります。1000要素以上のコレクションでは、shouldInvalidateLayout(forBoundsChange:)を使用したインクリメンタルキャッシュ更新を使用してください。これにより、スクロール時のレイアウト全体の再計算を回避できます。Apple(WWDC 2023)はカスタムレイアウトよりもCompositionalLayoutを推奨しています。ゼロからレイアウトを書かなくても95%のシナリオをカバーできます。
UITableViewとUICollectionViewは、リストを表示するための2つの主要なUIKitコンポーネントです。UITableViewはよりシンプルで、単一列リスト向けに設計されています。UICollectionViewはより柔軟で、任意のレイアウトをサポートします。どちらを選ぶかは、データの視覚的構造と必要なカスタマイズに依存します。
| パラメータ | UITableView | UICollectionView |
|---|---|---|
| レイアウト | 垂直リストのみ | リスト、グリッド、カスケード、カルーセル、カスタム |
| 複雑さ | 低い(シンプルなDataSource) | 中程度(Layout + DataSource + Delegate) |
| デフォルトセル | 4スタイル(basic、subtitle、value1、value2) | 空のみ(contentView経由でカスタム) |
| 編集モード | 組み込み(delete、move、insert) | 手動実装が必要 |
| ヘッダー/フッター | 組み込み(viewForHeaderInSection) | 補助ビュー経由 |
| パフォーマンス | リストで高い | 任意のレイアウトで高い |
| iOS 6+ | iOS 2から利用可能 | iOS 6から利用可能 |
実用的なルール:画面が単純な行リスト(設定、連絡先、メニュー)の場合はUITableViewを使用します。画面がグリッド、水平スクロール、カルーセル、異なるセルサイズ、複雑なグループ化を必要とする場合はUICollectionViewを使用します。iOS 14以降、UITableViewもUICollectionLayoutListConfigurationを介してCompositionalLayoutをサポートし、両方のコンポーネントの利点を組み合わせることができます。
よくある質問
UICollectionViewFlowLayoutでscrollDirection = .horizontalを設定します。CompositionalLayoutの場合は、適応グループ幅にfractionalWidthを使用してNSCollectionLayoutSizeを使用します。水平スクロールはカルーセル、ギャラリー、カテゴリに適しています。ページングの場合はcollectionView.isPagingEnabled = trueを設定するか、itemSize = bounds.widthでUICollectionViewFlowLayoutを使用します。
確認事項:(1)セルがregister(_:forCellWithReuseIdentifier:)で登録されている、(2)DataSourceがアイテム数>0を返す、(3)セルが正しいreuseIdentifierでdequeueされている、(4)itemSizeがゼロでない(FlowLayoutの場合)。典型的なミスはitemSize = CGSize.zeroで、セルの高さがゼロになります。itemSizeを明示的に設定するか、デリゲートcollectionView(_:layout:sizeForItemAt:)を実装してください。
FlowLayoutの場合:layout.minimumInteritemSpacing(行内のアイテム間)とlayout.minimumLineSpacing(行間)。CompositionalLayoutの場合:NSCollectionLayoutGroup.interItemSpacingとグループ/セクションのcontentInsets。sectionInsetはセクションの外部マージンを設定します。Safe Areaを正しく処理するにはsectionInsetReference = .fromContentInsetを使用します。
グループアニメーションにはperformBatchUpdatesを使用します:collectionView.performBatchUpdates { insertItems, deleteItems, reloadItems }。安全でアニメーション化された更新には、UICollectionViewDiffableDataSourceを使用します:dataSource.apply(snapshot, animatingDifferences: true)を呼び出すと、手動のinsert/deleteなしですべての変更が自動計算され適用されます。
SwiftUIでは、UICollectionViewに相当するのはLazyVGridとLazyHGrid(iOS 14+)です。SwiftUI Gridは記述が簡単(宣言的構文)ですが、500要素以上のパフォーマンスとレイアウトカスタマイズではUICollectionViewに劣ります。複雑なコレクションの場合は、UIViewRepresentableを介してUICollectionViewを使用します。IT Sectrのプロジェクトでは、カタログとギャラリーにはUICollectionViewを、シンプルな画面にはSwiftUI Gridを選択しています。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。