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.
2
Answers
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 fortableView(_: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:
Here is the code that makes the above possible:
Daniel T.’s answer works fine in Swift 5 as well.
However, in my case, a crash occurred when there was empty data.
So I added a
guard else
to return the header view so that it is not visible when the elements are empty.If you want to see an empty header view, just return view instead of nil.
I hope this helps.