rx-swiftrxdatasources

RxDataSources headerView and footerView


Problem: CollectionView header and footer are not shown before results are retrieved from network service. They are shown after items are retrieved from service.

Result I want:: Show CollectionView header and footer before items are retrieved from api call

What i've tried: Used different datasource with header and footer on initial viewDidLoad and set that datasource to nil when results loaded. Didn't work

My (not so elegant) solution: Add headerView and footerView subviews on viewDidLoad and replace them with collectionView when results come from service.

itemsLoading.drive(onNext: { loading in 
 if loading {
   view.addSubview(headerView)
   view.addSubview(footerView) 
} else {  
  headerView.removeFromSuperview()
  footerView.removeFromSuperView()
  view.addSubview(collectionView)

Is there more elegant solution to this problem ?

My Code: Left only parts related to Rxswift/RxDataSources

struct Input { 

let id: Driver<String>

} 

struct Output {

let items: Driver<[Item]>

}

class ViewModel {

    func transform(input: Input) -> Output {

        let items = networkService.getItems(id: input.id) // async operation
 
        return Output(items: items)
    }
}
class ViewController: UIViewController {

    private let viewModel = ViewModel()
    private let disposeBag = DisposeBag()
    private let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())

    override func viewDidLoad() {
        super.viewDidLoad()
        bindViewModel()
    }

    private func bindViewModel() {

        let dataSource = ViewController.dataSource()
        let id = .just(id)
        let input = Input(id: id)
        let output = viewModel.transform(input: input)

        output.items.map { items in
            items.map { item in
                SectionModel(model: "first Section", items: [item])
            }
        }
        .drive(collectionView.rx.items(dataSource: dataSource))
        .disposed(by: disposeBag)
    }


extension ViewController {

    typealias DataSource = RxCollectionViewSectionedReloadDataSource
    static func dataSource() -> DataSource<SectionModel<String, Item>> {
        .init(
            configureCell: { _, collectionView, indexPath, _ in
                let cell = collectionView.dequeueReusableCell(ofType: UICollectionViewCell.self, for: indexPath)
                return cell
            },
            configureSupplementaryView: { _, collectionView, kind, indexPath in
                switch kind {
                    case UICollectionView.elementKindSectionHeader:
                        let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, kindType: SectionHeader.self, for: indexPath)
                        return header

                    case UICollectionView.elementKindSectionFooter:
                        let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, kindType: SectionFooter.self, for: indexPath)
                        return footer
                    default:
                        assert(false, "Unexpected element kind")
                }

            })
    }
}

Solution

  • The simple solution, and I feel more elegant, is to use startWith to emit an array of SectionModels that each have 0 items, but have the model contain the right value.