How to populate custom header/footer view when using RxDatasources for datasource

Whirlwind

I am using RxDatasources to create my datasource. Later on, I configure cells in my view controller. The thing is, cause headers/footers has nothing with datasource (except we can set a title, but if we use custom header footer, this title will be overriden).

Now, this is how I configure my tableview cells:

private func observeDatasource(){
    
    let dataSource = RxTableViewSectionedAnimatedDataSource<ConfigStatusSectionModel>(
        configureCell: { dataSource, tableView, indexPath, item in
            if let cell = tableView.dequeueReusableCell(withIdentifier: ConfigItemTableViewCell.identifier, for: indexPath) as? BaseTableViewCell{
                cell.setup(data: item.model)
                return cell
            }

            return UITableViewCell()
        })
    
    botConfigViewModel.sections
        .bind(to: tableView.rx.items(dataSource: dataSource))
        .disposed(by: disposeBag)  
}

now cause

dataSource.titleForHeaderInSection = { dataSource, index in
            return dataSource.sectionModels[index].model
}

... won't work, cause I want to load a custom header and populate it with data from RxDatasource, I wonder what would be a proper way to:

  • get data from my datasource which is defined in my view model
  • populate header, based on a section (I have multiple sections) with correct data, in the way that is always up to date with a datasource.

Here is my view model:

 class ConfigViewModel{
        
        private let disposeBag = DisposeBag()
        let sections:BehaviorSubject<[ConfigStatusSectionModel]> = BehaviorSubject(value: [])
        
        func startObserving(){
            
            let observable = getDefaults()
            
            observable.map { conditions -> [ConfigStatusSectionModel] in
                return self.createDatasource(with: conditions)
            }.bind(to: self.sections).disposed(by: disposeBag)
        }
        
        private func getDefaults()->Observable<ConfigDefaultConditionsModel> {
            
            return Observable.create { observer in
                FirebaseManager.shared.getConfigDefaults { conditions in
                  
                    observer.onNext(conditions!)
    
                } failure: { error in
                    observer.onError(error!)
                }
                return Disposables.create()
            }
        }
        
        private func createDatasource(with defaults:ConfigDefaultConditionsModel)->[ConfigStatusSectionModel]{
            
        
            let firstSectionItems = defaults.start.elements.map{ConfigItemModel(item: $0, data: nil)}
            let firstSection = ConfigStatusSectionModel(model: defaults.start.title, items: firstSectionItems.compactMap{ConfigCellModel(model: $0)})
            
            let secondSectionItems = defaults.stop.elements.map{ConfigItemModel(item: $0, data: nil)}
            let secondSection = ConfigStatusSectionModel(model: defaults.stop.title, items: secondSectionItems.compactMap{ConfigCellModel(model: $0)})
            
            let sections:[ConfigStatusSectionModel] = [firstSection, secondSection]
            
            return sections
        }
    }

Now what I was able to do, is to set a tableview delegate, like this:

tableView.rx.setDelegate(self).disposed(by: disposeBag)

and then to implement appropriate delegate method(s) to create / return custom header:

extension BotConfigViewController: UITableViewDelegate {

    func tableView(_ tableView: UITableView,
                   viewForHeaderInSection section: Int) -> UIView? {
        guard let header = tableView.dequeueReusableHeaderFooterView(
                            withIdentifier: ConfigSectionTableViewHeader.identifier)
                            as? ConfigSectionTableViewHeader
        else {
            return nil
        }
        return header
    }

    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return UITableView.automaticDimension
    }
    
    func tableView(_ tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat {
        return 40
    }
  
}

How to populate my custom header with data from my datasource? I don't want to do things like switch (section){...}, cause then its completely not in sync with a datasource, but rather manually, and if datasource changes, it won't affect on header configuration automatically.

Here are my model structs:

typealias ConfigStatusSectionModel = AnimatableSectionModel<String, ConfigCellModel>

struct ConfigItemData {
    let conditionsLink:String?
    let iconPath:String?
}

struct ConfigItemModel {
    
    let item:OrderConditionModel
    let data:ConfigItemData?
}

struct ConfigCellModel : Equatable, IdentifiableType {
    
    static func == (lhs: ConfigCellModel, rhs: ConfigCellModel) -> Bool {
        
        return lhs.model.item.symbol == rhs.model.item.symbol
    }
    var identity: String {
        return model.item.symbol
    }
    let model: ConfigItemModel
}

I tried to use this but I wasn't able to make it work completely, cause I guess I wasn't providing custom header in a right way/moment.

Daniel T.

The fundamental issue here is that tableView(_:viewForHeaderInSection:) is a pull based method and Rx is designed for push based systems. Obviously it can be done. After all, the base library did it for tableView(_:cellForRowAt:) but it's quite a bit more complex. You can follow the same system that the base library uses for the latter function.

Below is such a system. It can be used like this:

source
    .bind(to: tableView.rx.viewForHeaderInSection(
        identifier: ConfigSectionTableViewHeader.identifier,
        viewType: ConfigSectionTableViewHeader.self
    )) { section, element, view in
        view.setup(data: element.model)
    }
    .disposed(by: disposeBag)

Here is the code that makes the above possible:

extension Reactive where Base: UITableView {
    func viewForHeaderInSection<Sequence: Swift.Sequence, View: UITableViewHeaderFooterView, Source: ObservableType>
    (identifier: String, viewType: View.Type = View.self)
    -> (_ source: Source)
    -> (_ configure: @escaping (Int, Sequence.Element, View) -> Void)
    -> Disposable
    where Source.Element == Sequence {
        { source in
            { builder in
                let delegate = RxTableViewDelegate<Sequence, View>(identifier: identifier, builder: builder)
                base.rx.delegate.setForwardToDelegate(delegate, retainDelegate: false)
                return source
                    .concat(Observable.never())
                    .subscribe(onNext: { [weak base] elements in
                        delegate.pushElements(elements)
                        base?.reloadData()
                    })
            }
        }
    }
}

final class RxTableViewDelegate<Sequence, View: UITableViewHeaderFooterView>: NSObject, UITableViewDelegate where Sequence: Swift.Sequence {
    let build: (Int, Sequence.Element, View) -> Void
    let identifier: String
    private var elements: [Sequence.Element] = []

    init(identifier: String, builder: @escaping (Int, Sequence.Element, View) -> Void) {
        self.identifier = identifier
        self.build = builder
    }

    func pushElements(_ elements: Sequence) {
        self.elements = Array(elements)
    }

    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        guard let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: identifier) as? View else { return nil }
        build(section, elements[section], view)
        return view
    }
}

Collected from the Internet

Please contact [email protected] to delete if infringement.

edited at
0

Comments

0 comments
Login to comment

Related

Binding to a datasource using RxDatasources

configureCell is not triggered when I update a datasource using RxDatasources

RxDataSources - How to add a custom empty cell when there's no data

How to provide custom header/footer to a tableView using RxDatasources

How to populate table view with custom object?

How to populate datasource for listview using api response in react native?

Add image to HeaderFooter using Axlsx

how to add custom item in dropdown while using sql datasource

Inflate exception occurs when using custom view

View is not update when using ControllerAs and custom directive

Android Room how to search by text when using DataSource.Factory?

How to get play datasource instance when using slick

How to capture datasource name when using Azure Search indexer

How to configure datasource when using authenticationProvider in spring boot security?

breezejs how to using as datasource

How to populate array in Rails view using Slim syntax

How to parse this JSON response using RETROFIT and populate in Recylcer view Android

How can I Populate a ListView in JavaFX using Custom Objects?

How to populate a table valued parameter using a custom DbDataReader?

How to pre-populate select options custom directive using angularjs

"Undefined is not a function" error when using a KendoUI custom download containing only the grid with asp.mvc wrapper and Datasource

How to specify the view location in asp.net core mvc when using custom locations?

How to set width and heigh of UIButton that is added as custom view of UIBarButtonItem when using SVG images?

Angular custom directive not repainting when the datasource changes

How to draw the bitmap by using custom view

How to display a text message at the Center of the view when the List datasource is empty in SwiftUI?

How to populate a picker view with a dictionary?

How to populate "ArrayYear" in picker view?

How to Populate dropdown in view Page?