SJUICollectionView クロージャベース Delegate 設計
概要
SJUICollectionViewにクロージャベースのデリゲート設定機能を追加し、チェーン形式で直感的にイベントハンドラを設定できるようにする。また、カスタムレイアウトにも対応できる拡張性のある設計とする。
構成ファイル
| ファイル |
役割 |
SJUICollectionViewDelegateProxy.swift |
UICollectionViewDelegate/DataSource/FlowLayoutDelegateを実装するプロキシクラス |
SJUICollectionView+Closures.swift |
チェーン可能なExtensionメソッド群 |
SJUILayoutDelegateAdapter.swift |
カスタムレイアウト対応のプロトコルと基底クラス |
使用例
1. 基本的なFlowLayout(標準)
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 }
2. カスタムレイアウト(例: WaterfallLayout)
Step 1: カスタムレイアウトとデリゲートプロトコルの定義(アプリ側)
// 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
}
Step 2: Adapterの定義
// 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
}
}
Step 3: Extensionの追加
// 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
}
}
Step 4: 使用側コード
// 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
}
3. JSONでのレイアウト指定
{
"type": "Collection",
"id": "photo_collection",
"layout": "Waterfall",
"layoutConfig": {
"columns": 2,
"cellPadding": 8
},
"cellClasses": [
{ "className": "PhotoCell" }
],
"setTargetAsDelegate": true,
"setTargetAsDataSource": true
}
対応するデリゲートメソッド
UICollectionViewDataSource
| メソッド |
クロージャ |
numberOfSections(in:) |
onNumberOfSections |
collectionView(_:numberOfItemsInSection:) |
onNumberOfItemsInSection |
collectionView(_:cellForItemAt:) |
onCellForItem |
collectionView(_:viewForSupplementaryElementOfKind:at:) |
onSupplementaryView |
UICollectionViewDelegate
| メソッド |
クロージャ |
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 |
UICollectionViewDelegateFlowLayout
| メソッド |
クロージャ |
collectionView(_:layout:sizeForItemAt:) |
onSizeForItem |
collectionView(_:layout:insetForSectionAt:) |
onInsetForSection |
collectionView(_:layout:minimumLineSpacingForSectionAt:) |
onMinimumLineSpacing |
collectionView(_:layout:minimumInteritemSpacingForSectionAt:) |
onMinimumInteritemSpacing |
collectionView(_:layout:referenceSizeForHeaderInSection:) |
onHeaderReferenceSize |
collectionView(_:layout:referenceSizeForFooterInSection:) |
onFooterReferenceSize |
UIScrollViewDelegate
| メソッド |
クロージャ |
scrollViewDidScroll(_:) |
onDidScroll |
scrollViewWillBeginDragging(_:) |
onWillBeginDragging |
scrollViewDidEndDragging(_:willDecelerate:) |
onDidEndDragging |
scrollViewDidEndDecelerating(_:) |
onDidEndDecelerating |
設計の特徴
| 特徴 |
説明 |
| 型安全 |
カスタムレイアウトごとに専用メソッドを定義し、型安全を保証 |
| 拡張性 |
アプリ側で新しいレイアウトタイプを自由に追加可能 |
| 一貫性 |
標準FlowLayoutと同じチェーン形式で使用可能 |
| 分離 |
レイアウト実装とデリゲート設定が明確に分離 |
| 後方互換 |
従来のsetTargetAsDelegate方式も引き続き利用可能 |
注意事項
- プロキシとの併用: クロージャベースのデリゲートを使用する場合、従来の
setTargetAsDelegateは使用しない
- メモリ管理: クロージャ内での
self参照は[weak self]を使用すること
- レイアウト登録: カスタムレイアウトを使用する場合は、アプリ起動時にアダプターを登録する必要がある