-
Notifications
You must be signed in to change notification settings - Fork 0
CollectionView Closure Based Delegate
Taichiro Kimura edited this page Apr 19, 2026
·
1 revision
SJUICollectionViewにクロージャベースのデリゲート設定機能を追加し、チェーン形式で直感的にイベントハンドラを設定できるようにする。また、カスタムレイアウトにも対応できる拡張性のある設計とする。
| ファイル | 役割 |
|---|---|
SJUICollectionViewDelegateProxy.swift |
UICollectionViewDelegate/DataSource/FlowLayoutDelegateを実装するプロキシクラス |
SJUICollectionView+Closures.swift |
チェーン可能なExtensionメソッド群 |
SJUILayoutDelegateAdapter.swift |
カスタムレイアウト対応のプロトコルと基底クラス |
collectionView
// UICollectionViewDataSource
.onNumberOfSections { _ in 2 }
.onNumberOfItemsInSection { _, section in items[section].count }
.onCellForItem { collectionView, indexPath in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
return cell
}
// UICollectionViewDelegate
.onDidSelectItem { _, indexPath in
handleSelection(indexPath)
}
.onWillDisplayCell { _, cell, indexPath in
// セル表示前の処理
}
// UICollectionViewDelegateFlowLayout
.onSizeForItem { _, layout, indexPath in
CGSize(width: 100, height: 100)
}
.onInsetForSection { _, layout, section in
UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
}
.onMinimumLineSpacing { _, layout, section in 10 }
.onMinimumInteritemSpacing { _, layout, section in 5 }// WaterfallLayout.swift
class WaterfallLayout: UICollectionViewLayout {
weak var delegate: WaterfallLayoutDelegate?
var numberOfColumns: Int = 2
var cellPadding: CGFloat = 8
// ...レイアウト実装
}
protocol WaterfallLayoutDelegate: AnyObject {
func collectionView(_ collectionView: UICollectionView,
heightForItemAt indexPath: IndexPath) -> CGFloat
func numberOfColumns(in collectionView: UICollectionView) -> Int
}// WaterfallLayoutAdapter.swift
class WaterfallLayoutAdapter: SJUILayoutDelegateAdapter, WaterfallLayoutDelegate {
// クロージャプロパティ
var heightForItem: ((UICollectionView, IndexPath) -> CGFloat)?
var numberOfColumns: ((UICollectionView) -> Int)?
// SJUILayoutDelegateAdapter
override func attachToLayout(_ layout: UICollectionViewLayout) {
guard let waterfallLayout = layout as? WaterfallLayout else { return }
waterfallLayout.delegate = self
}
// WaterfallLayoutDelegate
func collectionView(_ collectionView: UICollectionView,
heightForItemAt indexPath: IndexPath) -> CGFloat {
heightForItem?(collectionView, indexPath) ?? 100
}
func numberOfColumns(in collectionView: UICollectionView) -> Int {
numberOfColumns?(collectionView) ?? 2
}
}// SJUICollectionView+WaterfallLayout.swift
extension SJUICollectionView {
/// WaterfallLayoutアダプターを登録(アプリ起動時に1回呼び出し)
static func registerWaterfallLayoutSupport() {
SJUICollectionViewDelegateProxy.registerLayoutAdapter(
forLayoutType: WaterfallLayout.self,
adapterType: WaterfallLayoutAdapter.self
)
}
@discardableResult
func onWaterfallHeightForItem(_ handler: @escaping (UICollectionView, IndexPath) -> CGFloat) -> Self {
if let adapter = delegateProxy.layoutAdapter as? WaterfallLayoutAdapter {
adapter.heightForItem = handler
}
return self
}
@discardableResult
func onWaterfallNumberOfColumns(_ handler: @escaping (UICollectionView) -> Int) -> Self {
if let adapter = delegateProxy.layoutAdapter as? WaterfallLayoutAdapter {
adapter.numberOfColumns = handler
}
return self
}
}// AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions...) {
// カスタムレイアウトサポートを登録
SJUICollectionView.registerWaterfallLayoutSupport()
}
// ViewController.swift
collectionView
.onNumberOfItemsInSection { _, _ in photos.count }
.onCellForItem { cv, indexPath in
let cell = cv.dequeueReusableCell(withReuseIdentifier: "PhotoCell", for: indexPath) as! PhotoCell
cell.configure(with: photos[indexPath.item])
return cell
}
.onDidSelectItem { _, indexPath in
showPhotoDetail(photos[indexPath.item])
}
// Waterfall専用メソッド
.onWaterfallHeightForItem { _, indexPath in
photos[indexPath.item].calculatedHeight
}
.onWaterfallNumberOfColumns { _ in
UIDevice.current.orientation.isLandscape ? 3 : 2
}{
"type": "Collection",
"id": "photo_collection",
"layout": "Waterfall",
"layoutConfig": {
"columns": 2,
"cellPadding": 8
},
"cellClasses": [
{ "className": "PhotoCell" }
],
"setTargetAsDelegate": true,
"setTargetAsDataSource": true
}| メソッド | クロージャ |
|---|---|
numberOfSections(in:) |
onNumberOfSections |
collectionView(_:numberOfItemsInSection:) |
onNumberOfItemsInSection |
collectionView(_:cellForItemAt:) |
onCellForItem |
collectionView(_:viewForSupplementaryElementOfKind:at:) |
onSupplementaryView |
| メソッド | クロージャ |
|---|---|
collectionView(_:didSelectItemAt:) |
onDidSelectItem |
collectionView(_:didDeselectItemAt:) |
onDidDeselectItem |
collectionView(_:willDisplay:forItemAt:) |
onWillDisplayCell |
collectionView(_:didEndDisplaying:forItemAt:) |
onDidEndDisplayingCell |
collectionView(_:shouldSelectItemAt:) |
onShouldSelectItem |
collectionView(_:shouldDeselectItemAt:) |
onShouldDeselectItem |
collectionView(_:shouldHighlightItemAt:) |
onShouldHighlightItem |
collectionView(_:didHighlightItemAt:) |
onDidHighlightItem |
collectionView(_:didUnhighlightItemAt:) |
onDidUnhighlightItem |
| メソッド | クロージャ |
|---|---|
collectionView(_:layout:sizeForItemAt:) |
onSizeForItem |
collectionView(_:layout:insetForSectionAt:) |
onInsetForSection |
collectionView(_:layout:minimumLineSpacingForSectionAt:) |
onMinimumLineSpacing |
collectionView(_:layout:minimumInteritemSpacingForSectionAt:) |
onMinimumInteritemSpacing |
collectionView(_:layout:referenceSizeForHeaderInSection:) |
onHeaderReferenceSize |
collectionView(_:layout:referenceSizeForFooterInSection:) |
onFooterReferenceSize |
| メソッド | クロージャ |
|---|---|
scrollViewDidScroll(_:) |
onDidScroll |
scrollViewWillBeginDragging(_:) |
onWillBeginDragging |
scrollViewDidEndDragging(_:willDecelerate:) |
onDidEndDragging |
scrollViewDidEndDecelerating(_:) |
onDidEndDecelerating |
| 特徴 | 説明 |
|---|---|
| 型安全 | カスタムレイアウトごとに専用メソッドを定義し、型安全を保証 |
| 拡張性 | アプリ側で新しいレイアウトタイプを自由に追加可能 |
| 一貫性 | 標準FlowLayoutと同じチェーン形式で使用可能 |
| 分離 | レイアウト実装とデリゲート設定が明確に分離 |
| 後方互換 | 従来のsetTargetAsDelegate方式も引き続き利用可能 |
-
プロキシとの併用: クロージャベースのデリゲートを使用する場合、従来の
setTargetAsDelegateは使用しない -
メモリ管理: クロージャ内での
self参照は[weak self]を使用すること - レイアウト登録: カスタムレイアウトを使用する場合は、アプリ起動時にアダプターを登録する必要がある