Contenu connexe
Similaire à RxDataSourceをNSDiffableDataSourceへ置き換える際のTips集紹介 (20)
Plus de Fumiya Sakai (20)
RxDataSourceをNSDiffableDataSourceへ置き換える際のTips集紹介
- 1. Several Tips to convert from RxDataSource
to NSDiffableDatasource & UICollectionView
potatotips #79
2022/10/31
Fumiya Sakai
- 2. 自己紹介
・Fumiya Sakai
・Freelance App Engineer
アカウント:
・Twitter: https://twitter.com/fumiyasac
・Facebook: https://www.facebook.com/fumiya.sakai.37
・Github: https://github.com/fumiyasac
・Qiita: https://qiita.com/fumiyasac@github
発表者:
・Born on September 21, 1984
これまでの歩み:
Web Designer
2008 ~ 2010
Web Engineer
2012 ~ 2016
App Engineer
2017 ~ Now
iOS / Android / sometimes Flutter
- 8. RxDataSource用のSectionType・ItemType定義
SectionType・ItemTypeのcaseがUICollectionViewCellと密接に関係する
enum TopSectionType {
case banner(title: String)
case featured(headerViewObject: TopHeaderView.HeaderViewObject)
case news(headerViewObject: TopHeaderView.HeaderViewObject)
case discount(title: String)
case photo(headerViewObject: TopHeaderView.HeaderViewObject)
}
typealias TopSection = SectionModel<TopSectionType, TopItemType>
Banner Header:
スクロール可能
Featured Carousel: News:
enum TopItemType {
case banner(bannerViewObject: TopBannerCell.CellViewObject)
case featured(featuredViewObjects: [TopFeaturedCell.CellViewObject])
case news(newsViewObject: TopNewsCell.CellViewObject)
case discount(discountViewObject: TopDiscountCell.CellViewObject)
case photo(photoViewObject: TopPhotoCell.CellViewObject)
} ※HorizontalScroll部分は中にUICollectionViewがある
… Cell要素1つ分
let items = BehaviorRelay<[TopSection]>(value: [])
1. Enum定義をしてcase内のassociated valueに表示データを定義する :
2. RxDataSource用のTypealias定義とViewControllerでCell要素とバインドする処理用の変数を準備 :
- 9. RxDataSourceで構築された画面処理に関するポイント
必要なSectionModelの内容を決めて、それが変化すると画面に反映される形
※ Header/Footer要素の構築に関してもconfigureSupplementaryViewの部分で構築する形となる
typealias TopSection = SectionModel<TopSectionType, TopItemType>
ViewController側におけるCell生成処理の抜粋 :
private lazy var dataSource = RxCollectionViewSectionedReloadDataSource<TopSection>(
configureCell: configureCell,
configureSupplementaryView: configureSupplementaryView
)
private lazy var configureCell: RxCollectionViewSectionedReloadDataSource<TopSection>
.ConfigureCell = { [weak self] (_, collectionView, indexPath, itemType) in
guard let strongSelf = self else { return UICollectionViewCell() }
switch itemType {
case .banner(let bannerViewObject):
// バナー表示用のCell要素を生成する処理を記載する
case .featured(let featuredViewObjects):
// 水平カルーセル表示用のCell要素を生成する処理を記載する
…(必要なCell要素の生成をitemTypeの条件分岐に合わせて生成していく)…
}
}
viewModel = TopViewModel()
viewModel.items
.asDriver()
.drive(collectionView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
RxSwiftでの処理
ViewModel内の処理で変数itemの内容が更新さ
れたタイミングでDataSourceのClosure内部に
定義しているCell・SupplementaryViewの生成
処理が実行されて、画面に内容が反映される
let items = BehaviorRelay<[TopSection]>(value: [])
※RxDataSourceで提供するSectionModel配列
Cell要素生成処理
- 10. DiffableDataSource用のSectionType・ItemType定義
SectionType・ItemTypeはHashableに準拠する必要がある点に注意!
enum TopSectionType: Hashable {
case banner(title: String)
case featured(headerViewObject: TopHeaderView.HeaderViewObject)
case news(headerViewObject: TopHeaderView.HeaderViewObject)
case discount(title: String)
case photo(headerViewObject: TopHeaderView.HeaderViewObject)
}
Banner Header:
スクロール可能
Featured Carousel: News:
enum TopItemType: Hashable {
case banner(bannerViewObject: TopBannerCell.CellViewObject)
case featured(featuredViewObject: TopFeaturedCell.CellViewObject)
case news(newsViewObject: TopNewsCell.CellViewObject)
case discount(discountViewObject: TopDiscountCell.CellViewObject)
case photo(photoViewObject: TopPhotoCell.CellViewObject)
} ※UICollectionViewCompositionalLayoutを利用する
… Cell要素1つ分
1. Enum定義をしてcase内のassociated valueに表示データを定義する :
2. Hash値を生成するために定義されているViewObjectのassociate valueの値を利用する :
Hash値が重複してしまうとCrashするのでCellViewObject・HeaderViewObjectの値はこの点には注意する必要があります。
- 12. iOS14~利用可能なCellRegistrationにも対応する(1)
CellRegistrationの処理を行いやすい様にする拡張を定義する
protocol DynamicRegistrable: UICollectionViewCell {
associatedtype Item
static var cellNib: UINib { get }
static func makeCellRegistration() -> UICollectionView.CellRegistration<Self, Item>
func configure(_ cellViewObject: Item)
}
extension DynamicRegistrable {
static func makeCellRegistration() -> UICollectionView.CellRegistration<Self, Item> {
return .init(cellNib: cellNib, handler: { cell, _, item in
return cell.configure(item)
})
}
}
UICollectionViewクラスの拡張の応用することでEnumに定義したViewObjectと関連付ける :
extension NSObjectProtocol {
static var className: String {
return String(describing: self)
}
}
extension UICollectionView {
static func makeNibResource<T: UICollectionViewCell>(_ cellType: T.Type) -> UINib {
return UINib(nibName: T.identifier, bundle: nil)
}
}
extension UICollectionReusableView {
static var identifier: String {
return className
}
}
※ItemはItemTypeのassociated valueに対応
従来通りのUICollectionViewCellを作り、
DynamicRegistrableに準拠させる様にする。
※2. UINibのMappingにR.swift等も利用可能
※1. 今回はxibとクラス定義でCellを作る想定
※3. Item不要のCell・SupplementaryViewに
ついてもこの方法を応用することで定義
することができます。
- 13. iOS14~利用可能なCellRegistrationにも対応する(2)
CellRegistrationの拡張に準拠したCellクラスを定義する
該当するCell要素クラスに対してDynamicRegistrableに準拠させた場合の概要 :
final class TopBannerCell: UICollectionViewCell, DynamicRegistrable {
static let cellNib = UICollectionViewCell.makeNibResource(TopBannerCell.self)
typealias Item = CellViewObject
…(Cell表示のために必要な処理を記載する:@IBOutletや内部プロパティ等)…
func configure(_ item: Item) {
// バナー表示用のCell要素を表示する処理を記載する
}
}
extension TopBannerCell {
// TopItemTypeのassociated valueに定義するViewObjectの構造体定義
struct CellViewObject {
let id: Int
let identifier: String
let imageUrl: URL?
}
}
※CellのIdentifierからUINibを取得する
Cell要素生成処理 makeCellRegistration()と連動している部分になります。
ViewModel等DataSourceを作る部分で期待している処理
① ItemTypeがHashableに準拠するためのHash値を作成すること
② ItemTypeに応じたCell表示データを作成すること
- 14. ViewControllerにおけるCell要素構築処理の概要
CellRegistrationを利用したCellの生成処理の例
private func configureTopV2DataSource() {
let topBannerCellRegistration = TopBannerCell.makeCellRegistration()
let topFeaturedCellRegistration = TopFeaturedCell.makeCellRegistration()
…(表示する必要がある分だけCellRegistrationを追加する)…
topDataSource = TopDataSource(collectionView: collectionView) { [weak self] (collectionView, indexPath, itemType) -> UICollectionViewCell? in
guard let self = self else { return UICollectionViewCell() }
switch itemType {
case .banner(let bannerViewObject):
let cell = collectionView.dequeueConfiguredReusableCell(using: topBannerCellRegistration, for: indexPath, item: bannerViewObject)
return cell
case .featured(let featuredViewObject):
let cell = collectionView.dequeueConfiguredReusableCell(using: topFeaturedCellRegistration, for: indexPath, item: featuredViewObject)
return cell
…(表示する必要がある分だけCell生成処理を追加する)…
}
}
}
private var topDataSource: TopDataSource!
topDataSource.supplementaryViewProvider = { … } を利用する形になります。
ViewController側におけるCell生成処理の抜粋 :
※ Header/Footer要素の構築に関して:
※ViewModel内にもDataSourceのインスタンスを渡しておく(viewDidLoad時)
① 表示に必要なCellRegistrationの準備
② CellRegistrationとItemTypeを利用したCell生成処理
- 15. ViewModelにおけるDataSource要素構築処理の概要
この部分で主に実行するのはDiffableDataSourceの構築と反映となる
① Section用のEnumを定義する(associated valueを用いてHeader・Footerに表示するViewObjectを作成する)
概要をまとめると下記の様な形となる :
② Item用のEnumを定義する(associated valueを用いてCellに表示するViewObjectを作成する)
③ 表示順番に配慮してNSDiffableDataSourceSnapshot<◆◆SectionType,◆◆ItemType>に反映する
// MEMO: 変数currentSnapshotに表示対象のSectionTypeとItemTypeを追加する際のalias
typealias SnapshotElement = (section: [TopSectionType], items: [TopItem])
private var currentSnapshot = NSDiffableDataSourceSnapshot<TopSectionType, TopItem>()
// MEMO: ViewControllerと共有しているDiffableDataSource
private var dataSource: TopViewController.TopDataSource!
※viewDidLoad時にViewControllerから渡されるもの
※DataSourceに追加するSnapshot
Task { @MainActor in
do {
let response = try await self.topResponseUseCase.getResponse()
self.updateTopDataSource(using: response)
} catch let error { // TODO: Error Handling. }
}
例. APIリクエストからDataSourceに加工するまでの流れ
1. 順番に配慮した状態でのAPIレスポンスデータを取得する
2. レスポンスデータを元にしてSnapshotに反映して更新
- 16. ViewModel側に定義しているDataSource関連処理(1)
async/awaitを利用したSectionの並び順を担保したResponse取得処理例
概要をまとめると下記の様な形となる :
func getResources() async throws -> [TopResponse] {
var responses: [TopResponse] = []
var endpoints = ["/banner", "/featured”, "/news", "/special", “/photos”]
// エンドポイントの並び順を担保しながらの並列処理を実行する
await withTaskGroup(of: [TopResponse].self, body: { [weak self] group in
guard let self = self else { return }
for endpoint in endpoints {
group.addTask {
// エンドポイントのパス文字列に応じたResponseを取得する処理
return try await APIRequest.shard.get(endpoint: section)
}
}
for await response in group {
responses.append(response)
}
// TODO: Error Handling.
})
return responses
}
RxSwiftを利用する場合は.zipや.flatMap等を活用する。
✨ async/awaitを利用した処理
余談:
protocol TopResponse: Decodable {
var ●●●: String
// …(他に必要なものがあれば定義)…
}
struct TopBannerResponse: TopResponse {
let ●●●: String
// POINT: View表示に必要なデータ格納場所
let content: Content
// TODO: CodingKey設定
}
extension TopBannerResponse {
// POINT: Contentの内容を定義
struct Content: Decodable {
let id: Int
let identifier: String
let imageUrl: URL?
// TODO: CodingKey設定
}
}
Response定義例: 形によっては調整を要する部分
- 17. ViewModel側に定義しているDataSource関連処理(2)
並び順を担保したレスポンス情報の配列からSnapshotを作成し画面に反映する
ViewModel側におけるDataSource更新処理の抜粋 :
private func updateTopDataSource(using responses: [TopResponse]) {
// 1. 現在Snapshopをリセットする
currentSnapshot = NSDiffableDataSourceSnapshot<TopSectionType, TopItem>()
// 2. 引数から取得したレスポンスの型を元に分解してSnapshotを生成する
for response in responses {
if let topBannerResponse = response as? TopBannerResponse {
let snapshotElement = getTopBannerSnapshot(topBannerResponse)
appendSnapshotElementInCurrentSnapshot(snapshotElement)
}
…(表示する対象のResponseの分だけSnapshotを生成する処理をする)…
}
// 3. DiffableDataSourceにデータを反映する
dataSource.apply(currentSnapshot, animatingDifferences: true)
}
// メンバ変数: currentSnapshot(反映対象セクションデータの入れ物)に格納する
private func appendSnapshotElementInCurrentSnapshot(_ snapshotElement: SnapshotElement) {
currentSnapshot.appendSections(snapshotElement.section)
currentSnapshot.appendItems(snapshotElement.items, toSection: snapshotElement.section.first!)
}
private func getTopBannerSnapshot(_ topBannerResponse: TopBannerResponse)
-> SnapShotElement? {
// バナーに関するデータの実体がある場所
let content = topBannerResponse.content
…(表示する対象のResponseの分だけSnapshotを生成する処理をする)…
let section = TopSectionType.banner(title: "2022 A/W Selection")
let item = TopItemType.banner(bannerViewObject: TopBannerCell.CellViewObject(
id: content.id, identifier: content.label, imageUrl: content.imageUrl)
)
return SnapShotElement(section: [section], items: [item])
}
① 受け取ったResponseを分解&Section作成
② 反映対象のSnapshot変数への追加
- 22. Thank you for listening !
この資料では感覚的にはイメージできそうだけども、具体的な例がないとわかりにくい部分もあったかもしれません。
改めてUI実装サンプルとも合わせて年内には記事化できる様に進めていければと思います!