Instagram の iOS チームが開発している CollectionView を便利に扱うための OSS として IGListKit があります。リストの途中に広告や他の差込みコンテンツを入れたい時にとても便利で長年愛用しています。
とは言え SwiftUI がいい感じになってきたのであと1年くらいで IGListKit も使わなくなりそうなのだけど…。
で、今回は IGListKit のサンプルにある GridSectionController を一般化して ListSingleSectionController のように単一の Cell を簡単に扱えるクラスのマルチカラム版として作ります。と言っても ListSingleSectionControllerDelegate は作成せずに didSelectItem だけハンドリングするので簡易版ですね。
まずはコンテンツとして表示するための情報を持った GridItem を定義します(ListDiffable に準拠していればなんでもいいので割愛します)。
汎用性を考えると class ではなくて protocol にしても良いと思います。
|
1 2 3 |
class GridItem: NSObject: ListDiffable { // 割愛 } |
続いてその GridItem を格納するための入れ物を用意します。このクラス自体はサンプルにあった通りの実装で名前だけ分かりづらいので Container というワードを追加しています。 ListDiffable に準拠している必要がありますのでこうなります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
final class GridItemContainer: NSObject { let items: [GridItem] init(items: [GridItem]) { self.items = items super.init() } } extension GridItemContainer: ListDiffable { func diffIdentifier() -> NSObjectProtocol { return self } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { return self === object ? true : self.isEqual(object) } } |
これで GridItemContainer が GridItem のリストを持っているので、ListSectionController で適切に numberOfItems() を override すれば GridItem が Grid 表示されるようになります。そのための SectionController 実装はこんな感じです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
final class GridSingleSectionController<T: UICollectionViewCell>: ListSectionController { typealias DidSelectBlock = (Int, GridItem) -> () private var object: GridItemContainer? private var configureBlock: ListSingleSectionCellConfigureBlock private var sizeBlock: ListSingleSectionCellSizeBlock private var didSelectBlock: DidSelectBlock? private var nibName: String? private var bundle: Bundle? init(configureBlock: @escaping ListSingleSectionCellConfigureBlock, sizeBlock: @escaping ListSingleSectionCellSizeBlock, didSelect: DidSelectBlock? = nil) { self.configureBlock = configureBlock self.sizeBlock = sizeBlock self.didSelectBlock = didSelect super.init() self.minimumLineSpacing = 1 self.minimumInteritemSpacing = 1 } init(nibName: String, bundle: Bundle?, configureBlock: @escaping ListSingleSectionCellConfigureBlock, sizeBlock: @escaping ListSingleSectionCellSizeBlock, didSelect: DidSelectBlock? = nil) { self.nibName = nibName self.bundle = bundle self.configureBlock = configureBlock self.sizeBlock = sizeBlock self.didSelectBlock = didSelect super.init() self.minimumLineSpacing = 1 self.minimumInteritemSpacing = 1 } override func numberOfItems() -> Int { return object?.items.count ?? 0 } override func sizeForItem(at index: Int) -> CGSize { guard let object = object, index < object.items.count else { return .zero } return sizeBlock(object.items[index], collectionContext!) } override func cellForItem(at index: Int) -> UICollectionViewCell { let cell: UICollectionViewCell if let nibName = nibName { cell = collectionContext!.dequeueReusableCell(withNibName: nibName, bundle: bundle, for: self, at: index) } else { cell = collectionContext!.dequeueReusableCell(of: T.self, for: self, at: index) } guard let object = object, index < object.items.count else { return cell } configureBlock(object.items[index], cell) return cell } override func didSelectItem(at index: Int) { guard let object = object, index < object.items.count else { return } didSelectBlock?(index, object.items[index]) } override func didUpdate(to object: Any) { self.object = object as? GridItemContainer } } |
GridSectionController と ListSingleSectionController の実装を見ていただければわかる通り、2つを掛け合わせただけです。私は使わないので delegate と storyboard は省略しています。
使い方は ListSingleSectionController とほぼ同じく、ItemCell という Cell を表示する場合の使用例は以下のようになります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
extension XXXDataSource: ListAdapterDataSource { func objects(for listAdapter: ListAdapter) -> [ListDiffable] { return [GridItemContainer(items: objects)] } func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil } func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { let configureBlock = { (item: Any, cell: UICollectionViewCell) in guard let cell = cell as? ItemCell else { return } guard let model = item as? GridItem else { return } // GridItem の情報を使って cell の描画処理 } let sizeBlock = { [weak self] (item: Any, context: ListCollectionContext?) -> CGSize in guard let context = context else { return .zero } return CGSize(width: context.containerSize.width / 3 - 1, height: 100) // 分割数3 } let didSelectBlock = { [weak self] (index: Int, item: GridItem) in print("tapped item of index: \(index)") } return GridSingleSectionController<ItemCell>( configureBlock: configureBlock, sizeBlock: sizeBlock, didSelect: didSelectBlock) } } |
ポイントは objects(for:) にて GridItemContainer のリストを返す点と、sizeBlock で利用する側の都合で分割数を自由に変更できる点でしょうか。self をキャプチャする場合は適切に weak self なりして循環参照対策を行ってください。
以上です。