Slide 1

Slide 1 text

Several Tips to convert from RxDataSource to NSDiffableDatasource & UICollectionView potatotips #79 2022/10/31 Fumiya Sakai

Slide 2

Slide 2 text

自己紹介 ・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

Slide 3

Slide 3 text

iOSのUI実装本を執筆しています! 書籍に掲載したサンプルのバージョンアップや続編等に現在着手中です。 少しの工夫で実現できるTIPS集やライブラリ表現の活用集をはじめとした、iOSア プリ開発の中でも特にUI実装やUIKitを利用した画面の中で特徴を与える様な表現 という題材に焦点を当てた書籍となっております。 現在は電子書籍版のみとなります。 こちらは全て¥1,000となっております。 https://just1factory.booth.pm/ 概要: https://book-tech.com/ 価格: 📖 Booth 📖 Book Tech

Slide 4

Slide 4 text

UI実装であると嬉しいレシピブックの最新情報 UI実装であると嬉しいレシピブックVol.3として昨年10月に商業化しました! Still WIP これまでの同人誌として頒布したものに加えて、Vol.1及びVol.2に頒布したものの 中で書籍に載せきれなかったものや表現や動きが特徴的でユーザーにもほんの少し 遊び心を与える様なUI実装を紹介したものをVol.3としています。 概要: これからの構想: こちらで購入可能です: Amazon / Google Play / Apple Books / KINOKUNIYA / Rakuten BOOKS etc.. 🏊 iOS: SwiftUIを利用したUI実装や動画関連の実装 🏊 Android: Jetpack Composeの基本やその他気になるUI表現の考察

Slide 5

Slide 5 text

今回のスライドにつきまして 業務や個人開発でRxDataSourceからDiffableDataSourceへのリプレイスを実施 RxDataSourceを利用した処理を置き換えていく部分を中心にご紹介をできればと思います。 1. RxDataSource&RxSwiftベースの実装においてDiffableDataSource部分だけを置き換えていきたい: RxDataSource&RxSwiftベースでも画面処理には差し支えがないものの、iOS14以降であればDiffableDataSourceでの差分更新は もちろんCellRegistrationを利用したCellの生成処理も活用可能である点は魅力に感じていました。 2. UICollectionViewCompositionalLayoutへの布石とする: 実はDiffableDataSourceを利用していたとしても従来通りのUICollectionViewDelegateLayoutを利用することはできるものの、 UICollectionViewCompositionalLayoutを利用する方が画面構造的にもシンプルに済ませられるケースも多そうでした。 3. 置き換えを進めていくにあたって考え方を変える必要がある部分もあった: 最初は結構似ているかもしれないと思っていたけれども、しっかりと見ていくと実は意外と落とし穴があって見事にハマった り、想定以上に時間を要してしまった部分もありました(ここは私自身がRxDataSourceをあまり使わなかったため)。

Slide 6

Slide 6 text

RxDataSourceに関する簡単な概要紹介 RxSwift&UITableView・UICollectionViewでの実装を便利にする優れもの (参考)RxDataSource: https://github.com/RxSwiftCommunity/RxDataSources

Slide 7

Slide 7 text

RxDataSourceで構築された画面の置き換え方針 一番のポイントとなる部分はDataSourceやCell要素構築に関する部分 1. DataSource部分の乗せ替え対応: 必要に応じてRxSwiftベースで作られているMVVMまたはMVPのViewModelやPresenter側の処理に変更が必要となる。 2. 従来のLayout処理をUICollectionViewCompositionalLayoutへの乗せ替え対応: UICollectionViewCompositionalLayoutの宣言的なLayout構成を活用する事でよりシンプルな形になるよう変更する。 アプリのTOP画面の様に1つの画面に多くのSectionがある画面を対応する際はまずはここを完璧に整える事を目標に! 補足. HorizontalなCarousel表現について: UICollectionViewCompositionalLayoutでは特にUICollectionViewを入れ子にしなくて も実現可能(そうでなければUICollectionView in UICollectionViewCellの形) スクロール可能 👉 Cellへ表示データを渡す際に少しだけ形を変形する必要があります。

Slide 8

Slide 8 text

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 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要素とバインドする処理用の変数を準備 :

Slide 9

Slide 9 text

RxDataSourceで構築された画面処理に関するポイント 必要なSectionModelの内容を決めて、それが変化すると画面に反映される形 ※ Header/Footer要素の構築に関してもconfigureSupplementaryViewの部分で構築する形となる typealias TopSection = SectionModel ViewController側におけるCell生成処理の抜粋 : private lazy var dataSource = RxCollectionViewSectionedReloadDataSource( configureCell: configureCell, configureSupplementaryView: configureSupplementaryView ) private lazy var configureCell: RxCollectionViewSectionedReloadDataSource .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要素生成処理

Slide 10

Slide 10 text

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の値はこの点には注意する必要があります。

Slide 11

Slide 11 text

DiffableDataSourceを利用する際のひと工夫 DiffableDataSourceのどの処理をどの責務で実行するかを考えてみる 1. DiffableDataSourceにまつわる処理をViewControllerで頑張りすぎない形にする : DataSourceの構築とデータの反映・Cell要素の生成・画面レイアウト定義等、実は必要な処理がたくさんあります。 ViewController ViewModel UseCase・Repository Infrastructure どこに置くべきか悩ましい😓 ViewModel側 : DiffableDataSource構築 ViewController側 : Layout定義やCell構築 2. AnyHashableで頑張りすぎない形にするための工夫 : 画面専用のDiffableDataSourceクラスを準備する事で定義したEnumと対応ができる様な形としておく。 private var dataSource: UICollectionViewDiffableDataSource extension TopViewController { class TopDataSource: UICollectionViewDiffableDataSource {} } △ ○ DataSourceとViewController・ViewModelが対応 できる様な形を作っておく点がポイント。

Slide 12

Slide 12 text

iOS14~利用可能なCellRegistrationにも対応する(1) CellRegistrationの処理を行いやすい様にする拡張を定義する protocol DynamicRegistrable: UICollectionViewCell { associatedtype Item static var cellNib: UINib { get } static func makeCellRegistration() -> UICollectionView.CellRegistration func configure(_ cellViewObject: Item) } extension DynamicRegistrable { static func makeCellRegistration() -> UICollectionView.CellRegistration { 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(_ 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に     ついてもこの方法を応用することで定義 することができます。

Slide 13

Slide 13 text

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表示データを作成すること

Slide 14

Slide 14 text

ViewControllerにおけるCell要素構築処理の概要 CellRegistrationを利用したCellの生成処理の例 private func configureTopDataSource() { 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生成処理

Slide 15

Slide 15 text

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() // 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に反映して更新

Slide 16

Slide 16 text

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定義例: 形によっては調整を要する部分

Slide 17

Slide 17 text

ViewModel側に定義しているDataSource関連処理(2) 並び順を担保したレスポンス情報の配列からSnapshotを作成し画面に反映する ViewModel側におけるDataSource更新処理の抜粋 : private func updateTopDataSource(using responses: [TopResponse]) { // 1. 現在Snapshopをリセットする currentSnapshot = NSDiffableDataSourceSnapshot() // 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変数への追加

Slide 18

Slide 18 text

おまけ(1):個人的に気になっていた部分の紹介 UICollectionViewCompositionalLayoutでもDrag&Dropは可能(ただしバグ有り) 記事はこちら: https://briancoyner.github.io/articles/2021-10-12-reorderable-collection-view/

Slide 19

Slide 19 text

1. 機能の一部としてUICollectionViewを活用 おまけ(2):UICollectionViewで正攻法ではない表現例 積極的に選択はしないがUICollectionViewを利用して構築可能な事例をいくつか ※UICollectionViewLayoutを応用して実現する 2. レイアウト属性部分を自前で1から定義する 旧Safariの様な 動き方をする 横・縦・斜めで スクロール可能 ※UICollectionViewLayoutクラスを継承したもの

Slide 20

Slide 20 text

今後更に考えていきたい部分について 特にiOS13から利用可能になったUIKitの便利なAPI等の活用 実務の中で改善や新機能のアイデアや試してみたい事はたくさん見つかった様にも感じています。 1. RxSwiftベースで実装している部分を徐々に剥がしてCombineへ置き換え: RxSwiftでのコードについては私自身もかれこれ4-5年程使ってきて馴染みがある実装である&Combineでは標準搭載されていない オペレータもあるのでこの部分を見極めながら置き換えていく必要がありそうと感じています。 2. async/awaitでの処理の更なる活用と実践 : async/awaitの登場によりこれまで複雑になりがちな部分であった部分をシンプルに実装できる可能性には注目しています。(た だしこの部分は私自身実務内でも経験はあるものの、正直未だに手探り感は否めません)。 3. SwiftUIとUIKitの上手な共存とデザインシステム等とも連携した整理: 業務の中でデザインシステム整備に携わる経験があったので、その際に「SwiftUIの機能でPreviewできると確認しやすいかな」 と感じる事が多くありつつもUIKitがベースになっている場合が多いので他社事例も見ながら考えていきたいです。

Slide 21

Slide 21 text

まとめ UICollectionViewを利用した実装はここ数年の進化が目覚ましい部分 1. Section・Itemに対応するEnum定義部分をまずは確認すると良い: RxDataSource・DiffableDataSourceを考えていく場合には、共にSection・Itemに対応するEnum定義の整備が前提ではありますが DiffableDataSourceではHashableへの準拠がポイントになる点があると思います。(特にHash値の重複に注意) 2. Enumのassociated valueを有効活用してCell要素やSupplementaryView要素を上手に構築する: associated valueにおいてCell要素やSupplementaryView要素に表示する構造体を定義しておき、UICollectionViewに表示する要 素を生成するCellRegistrationやSupplementaryRegistrationとも連動する様にしておくと便利かと思います。 現在ではiOS14以上のバージョンであるならばその便利な機能を活用可能な余地が多いと感じました。 3. 画面実装側での処理とDataSourceの内容構築部分に関する部分を整える: DataSourceに関する処理については、ViewController側ではCell・SupplementaryView・UICollectionViewCompositionalLayout の構築処理に専念させて、ViewModelやPresenter層でDataSourceの構築を行う方が良いかと思います。

Slide 22

Slide 22 text

Thank you for listening ! この資料では感覚的にはイメージできそうだけども、具体的な例がないとわかりにくい部分もあったかもしれません。 改めてUI実装サンプルとも合わせて年内には記事化できる様に進めていければと思います!