Reputation: 13675
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:
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.
Upvotes: 3
Views: 1483
Reputation: 3
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.
final class RxTableViewDelegate<Sequence, View: UITableViewHeaderFooterView>: NSObject, UITableViewDelegate where Sequence: Swift.Sequence {
//...
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: identifier) as? View else { return nil }
guard elements.isEmpty == false else { return nil } //<- Here
build(section, elements[section], view)
return view
}
}
Upvotes: 0
Reputation: 33979
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
}
}
Upvotes: 3