skip to Main Content

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


  1. 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
        }
    }
    
    Login or Signup to reply.
  2. 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
      }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search