Help us understand the problem. What is going on with this article?

RxDataSources で複数のSectionに複数のItemを管理する。

More than 1 year has passed since last update.

RxDataSourcesを使って、複数のSectionで複数のItemを管理する方法の備忘録です。

RxDataSourcesの使い方

DataSourceについて

RxDataSourcesにRxSwiftのRxCollectionViewDataSourceType RxTableViewDataSourceType に準拠しているclassが用意されている。
これで、データソースを作成しrx.items(dataSource:) にbindする。

  • UICollectionViewの場合

    • RxCollectionViewSectionedReloadDataSource<S: SectionModelType>
    • RxCollectionViewSectionedAnimatedDataSource<S: AnimatableSectionModelType>
  • UITableViewの場合

    • RxTableViewSectionedReloadDataSource<S: SectionModelType>
    • RxTableViewSectionedAnimatedDataSource<S: AnimatableSectionModelType>

SectionModelTypeについて

各DataSourceを使うには、SectionModelType、もしくはAnimatableSectionModelTypeに準拠している型を用意する必要があります。
この中のitemsがそのSection内のセルモデルにあたります。

protocol SectionModelType {
    associatedtype Item
    var items: [Item] { get }
    init(original: Self, items: [Item])
}

※AnimatableSectionModelTypeの場合は、自身がIdentifiableTypeに準拠しているかつ、ItemがIdentifiableType, Equatable に準拠している必要があります。

protocol AnimatableSectionModelType: SectionModelType, IdentifiableType where Item: IdentifiableType, Item: Equatable {}
protocol IdentifiableType {
    associatedtype Identity: Hashable
    var identity : Identity { get }
}

RxDataSourcesでは、上記それぞれに準拠している以下の型が用意されています。

struct SectionModel<Section, ItemType> {
    var model: Section
    var items: [Item]

    init(model: Section, items: [Item]) {
        self.model = model
        self.items = items
    }
}

struct AnimatableSectionModel<Section: IdentifiableType, ItemType: IdentifiableType & Equatable> {
    var model: Section
    var items: [Item]
    init(model: Section, items: [ItemType]) {
        self.model = model
        self.items = items
    }
}

実装してみる

1つのセクション

用意されている、AnimatableSectionModel<Section, ItemType>を使って実装してみます。
まずは、IdentifiableTypeEquatableに準拠している型を用意します。

struct Card: IdentifiableType, Equatable {
    typealias Identity = String
    var identity: String {
        return id
    }
    let id: String
    let color: UIColor
}

AnimatableSectionModel<String, Card>として扱うと以下のような形になります。
※Sectionは、 RxDataSourcesで extension String : IdentifiableType が用意されているのでそれを使ってます。

var dataSource: RxCollectionViewSectionedAnimatedDataSource<AnimatableSectionModel<String, Card>> {
    return RxCollectionViewSectionedAnimatedDataSource<AnimatableSectionModel<String, Card>>(
        configureCell: { _, collectionView, indexPath, card -> UICollectionViewCell in
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "任意のidentifier", for: indexPath)!
            cell.backgroundColor = card.color
            return cell
    })
}

cards.asObservable()
    .map { cards: [Card] in
        [AnimatableSectionModel<String, Card>(model: "", items: cards)] 
    }
    .bind(to: collectionView.rx.items(dataSource: dataSource))
    .disposed(by: disposeBag)

↓追加とシャッフルと削除をしてます。

アニメーションあり(Section1つ)

複数のセクション(共通のセルモデル)

Sectionが複数ある場合はAnimatableSectionModel<Section: IdentifiableType, ItemType: IdentifiableType & Equatable>を複数渡せばOKです。

ただし、同じidentifierを用いた場合、fatalError()になるので注意です。
※異なるSection、異なる型の場合でも同じidentifierだと駄目

var dataSource: RxCollectionViewSectionedAnimatedDataSource<AnimatableSectionModel<String, Card>> {
    return RxCollectionViewSectionedAnimatedDataSource<AnimatableSectionModel<String, Card>>(
        configureCell: { model, collectionView, indexPath, card -> UICollectionViewCell in
            // Section毎にcellを返す
            let sectionModel = model.sectionModels[indexPath.section]
            switch sectionModel.model {
            case "1":
                let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "任意のidentifier", for: indexPath)!
                cell.backgroundColor = card.color
                return cell
            case "2":
                let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "任意のidentifier", for: indexPath)!
                cell.backgroundColor = card.color
                return cell
            default:
                return UICollectionViewCell()
            }
    })
}
cards.asObservable()
    .map { cards: [Card] in
        // 雑ですが同じ値を使います。idだけ重複しないよう`2_`を先頭に加えています。
        let copy = cards.map { Card(id: "2_\($0.id)", color: $0.color) }
        return [AnimatableSectionModel<String, Card>(model: "1", items: cards),
                AnimatableSectionModel<String, Card>(model: "2", items: copy)]
    }
    .bind(to: collectionView.rx.items(dataSource: dataSource))
    .disposed(by: disposeBag)
アニメーションあり(Section2つ)

複数のセクション(複数のセルモデル)

異なるセルモデルの場合は、型でラップなりキャストする用意する必要があります。(IdentifiableType, Equatableに準拠している)
今回はenumを用意してやってみます。

struct Item01: IdentifiableType, Equatable {
    typealias Identity = String
    var identity: Identity {
        return id
    }
    let id: String
    let color: UIColor
}

struct Item02: IdentifiableType, Equatable {
    typealias Identity = String
    var identity: Identity {
        return id
    }
    let id: String
    let color: UIColor
}

struct Item03: IdentifiableType, Equatable {
    typealias Identity = String
    var identity: Identity {
        return id
    }
    let id: String
    let color: UIColor
}
enum Item: IdentifiableType, Equatable {
    typealias Identity = String
    var identity: Identity {
        switch self {
        case .item01(let item):
            return item.identity
        case .item02(let item):
            return item.identity
        case .item03(let item):
            return item.identity
        }
    }
    case item01(Item01)
    case item02(Item02)
    case item03(Item03)
}

あとは、同じようにRxCollectionViewSectionedAnimatedDataSourceを実装します。
cellを生成する場所はSection(String)やItemでお好みに分岐でOKです。

var dataSource: RxCollectionViewSectionedAnimatedDataSource<AnimatableSectionModel<String, Item>> {
    return RxCollectionViewSectionedAnimatedDataSource<AnimatableSectionModel<String, Item>>(
        configureCell: { model, collectionView, indexPath, item -> UICollectionViewCell in
            let sectionModel = model.sectionModels[indexPath.section]
            switch sectionModel.model {
            case "0":
                switch item {
                case let .item01(i):
                    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "任意のidentifier", for: indexPath)
                    cell.backgroundColor = i.color
                    return cell
                case let .item02(i):
                    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "任意のidentifier", for: indexPath)
                    cell.backgroundColor = i.color
                    return cell
                case let .item03(i):
                    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "任意のidentifier", for: indexPath)
                    cell.backgroundColor = i.color
                    return cell
                }
            case "1":
                switch item {
                case let .item01(i):
                    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "任意のidentifier", for: indexPath)
                    cell.backgroundColor = i.color
                    return cell
                case let .item02(i):
                    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "任意のidentifier", for: indexPath)
                    cell.backgroundColor = i.color
                    return cell
                case let .item03(i):
                    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "任意のidentifier", for: indexPath)
                    cell.backgroundColor = i.color
                    return cell
                }
            default:
                return UICollectionViewCell()
            }
    })
}
Observable
    .combineLatest(item01.asObservable(),
                   item02.asObservable())
    .map { (item01: [Item], item02: [Item]) in
        return [AnimatableSectionModel<String, Item>(model: "0", items: item01),
                AnimatableSectionModel<String, Item>(model: "1", items: item02)]
    }
    .bind(to: collectionView.rx.items(dataSource: delegate.dataSource))
    .disposed(by: disposeBag)
アニメーションあり(Section2つで複数のセルモデル)

以上です!
あとは、StringにしているSectionを型にした、cellの生成部分を整理すると良いと思います。

※おまけ (tvOS)

アニメーションあり(Section2つで複数のセルモデル)
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away