iOSアプリに欠かせないUITableViewやCollectionViewの表示&状態管理ですが、
自分は日常的にRxDataSourcesを使っており、今回はその実装方法について記録しようと思います。
前提
- iOSのUITableViewやUICollectionViewに関する基本的な理解がある
- CollectionViewに関してお勧めの記事: 【Swift・iOS】XcodeのCollectionViewの使い方。タイル型(カード型)のレイアウト方法を解説
- RxSwiftやReactiveSwiftなどを使った非同期なイベントの管理に抵抗がない
- RxSwiftに関する理解はこちらの記事がおすすめです: RxSwiftについてようやく理解できてきたのでまとめることにした(1)
- Swiftのtypealeaseの仕様ついて基本的な理解がある
- おすすめ記事: typealiasというSwiftの仕様を把握する
- MVVM設計についての基本的な理解がある
- ViewModelの理解についておすすめの記事: 【iOS】MVVMについて
RxDataSources GitHub Repository
RxDataSourcesでは以下のようにSectionModelがstructで定義されているので、そのGenericTypesの二つの値を見て表示するSectionとCellを管理します。TestSectionModelをtypealeaseで定義し、enumでSectionのタイプとcellのタイプを定義するのが良いと思います。私は、以下の場合 TestSectionType = Section 、 TestSectionItemType = Cell という理解をしています。
typealias TestSectionModel = SectionModel<TestSectionType, TestSectionItemType>
enum TestSectionType { case testSection }
enum TestSectionItemType { case testCell(title: String) }
ViewModel
今回はサンプルコードのため、以下のようにしています
import RxCocoa
import RxSwift
import RxDataSources
typealias TestSectionModel = SectionModel<TestSectionType, TestSectionItemType>
enum TestSectionType { case testSection }
enum TestSectionItemType { case testCell(title: String) }
final class TestViewModel {
var items = BehaviorRelay<[TestSectionModel]>(value: [])
init() {
// 通信処理が必要な場合は通信を監視し、完了してからconfigureItems()を呼ぶ
configureItems()
}
private func configureItems() {
var sections: [TestSectionModel] = []
var cellItems: [TestSectionItemType] = []
cellItems.append(.testCell(title: "タイトル"))
sections.append(TestSectionModel(model: .testSection, items: cellItems))
items.accept(sections)
}
}
これらのSectionModelなどの状態やitemsへの格納処理は以下の理由からViewModelに処理を寄せるようにしています。
- configureItemsは通信結果やCellの構成によっては複雑かつ膨大な処理になる可能性が高く、Controllerで保持するとControllerがFatになる可能性がある
- 一般的にCollectionViewの状態は通信結果によって変わることが多いため、通信処理を実装するViewModelに寄せた方が役割がスッキリすると考えている
ViewController
final class TestViewController: UIViewController {
private let viewModel: TestViewModel = TestViewModel()
private let disposeBag = DisposeBag()
private lazy var dataSource = RxCollectionViewSectionedReloadDataSource<TestSectionModel>(configureCell: configureCell)
private lazy var configureCell: RxCollectionViewSectionedReloadDataSource<TestSectionModel>.ConfigureCell = {[weak self] (_, collectionView, indexPath, item) in
return (self?.cell(collectionView, item: item, indexPath: indexPath) ?? UICollectionViewCell())
}
private func cell(_ collectionView: UICollectionView, item: TestSectionModel, indexPath: IndexPath) -> UICollectionViewCell? {
switch item {
case .testCell(let title):
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as? TestCollectionCell {
cell.configureCell(title: title)
return cell
}
return nil
}
}
}
ViewControllerのconfigureCell変数ですが、ここでtypealeaseとして定義したTestSectionModelをRxCollectionViewSectionedReloadDataSourceの型パラメータに定義することで、渡されたTestSectionModelによってどのcellを返すかを定義しています。
最終的にこの値を保持するのはdataSource変数ですが、ViewModelで定義した var items = BehaviorRelay<[TestSectionModel]>(value: [])
と実際に表示しているCollectionViewの状態を一致させたいので、以下の処理でviewModel.itemsとcollectionViewとのバインディングを行います。
viewModel.items
.asObservable()
.bind(to: collectionView.rx.items(dataSource: dataSource)
.disposed(by: disposeBag)
補足
- sizeForItemAtやinsetForSectionAtなどの処理は標準で用意されているDelegateで実装する必要があるみたいなので、CellのサイズやSectionとCellの距離などを調整したい方は
collectionView.delegate = self
を行う必要があります。RxDataSourcesには同じ処理を以下のような形で表現できます。
collectionView.rx.setDelegate(self).disposed(by: disposeBag)
RxDataSourcesを活用するデメリット/メリット
デメリット
- 慣れていない時は構成がわかりづらく、少ない要素のCollectionViewだと逆にこれまで通りDelegateで実装してしまった方が簡単
- 膨大な種類のCellで複雑なレイアウトを実装しようと考えている際にはViewController内のconfigureCellの変数が膨大になってしまい、変数単体として見ると可読性が低下してしまう懸念がある
メリット
- ViewModelとCollectionView間のバインディングが簡単に実装出来る
- 今回は触れていませんが、
RxCollectionViewSectionedAnimatedDataSource
を使うことによってcellの差分更新とアニメーションが簡単になる
まとめ
膨大な種類のCellで複雑なレイアウトを実装しようと考えている際にはViewController内のconfigureCellの変数が膨大になってしまい、
デメリットであげたこちらの懸念ですが、CollectionViewの設定まわり(これまでDelegateで実装していた箇所)のみ別ファイルに切り出すことによって解決できるのかなと感じました。
- サンプルコードはこちらになります