swiftrx-swiftrxdatasources

How to handle two table view association?


I am a new Swifter, Here is the code of my new company.

Use RxSwift,use RxDataSource, how to handle two table view association?

The left tableView's cell clicked , the right tableView's data changed along.

Organize the right table view's data via the variables of middle state.

Bad code's smell。

Here is the image

one

Here is the code:

private let viewModel = CategoryViewModel()
private var currentListData :[SubItems]?
private var lastIndex : NSInteger = 0
private var currentSelectIndexPath : IndexPath?
private var currentIndex : NSInteger = 0

private func boundTableViewData() {

    var loadCount = 0
    // data source of left table view
    let dataSource = RxTableViewSectionedReloadDataSource<CategoryLeftSection>( configureCell: { ds, tv, ip, item in
    let cell = tv.dequeueReusableCell(withIdentifier: "Cell1", for: ip) as! CategoryLeftCell
    cell.model = item
     if ip.row == 0, !cell.isSelected {
          // in order to give the right table view a start show
             tv.selectRow(at: ip, animated: false, scrollPosition: .top)
             tv.delegate?.tableView!(tv, didSelectRowAt: ip)

        }
       return cell
    })

    vmOutput!.sections.asDriver().drive(leftMenuTableView.rx.items(dataSource: dataSource)).disposed(by: rx.disposeBag)

   // organize the right table view's data via the variables of middle state.

  // bad code's smell
    let listData = leftMenuTableView.rx.itemSelected.distinctUntilChanged().flatMapLatest {
        [weak self](indexPath) ->  Observable<[SubItems]> in
            guard let self = self else { return Observable.just([]) }
            // ...
            self.currentIndex = indexPath.row
            if indexPath.row == self.viewModel.vmDatas.value.count - 1 {
                // ...
           // the self.currentSelectIndexPath was used, because when the left tableView's final cell got clicked, the  UI logic is different.
                self.leftMenuTableView.selectRow(at: self.currentSelectIndexPath, animated: false, scrollPosition: .top)
                return Observable.just((self.currentListData)!)
            }
            if let subItems = self.viewModel.vmDatas.value[indexPath.row].subnav {
                var fisrtSubItem = SubItems()
                fisrtSubItem.url = self.viewModel.vmDatas.value[indexPath.row].url
                fisrtSubItem.name = self.viewModel.vmDatas.value[indexPath.row].banner
                var reult:[SubItems] = subItems
                reult.insert(fisrtSubItem, at: 0)
                self.currentListData = reult
              //  self.currentListData is used to capture the current data of the right table view.
                self.currentSelectIndexPath = indexPath
                return Observable.just(reult)
            }
            return Observable.just([])
    }.share(replay: 1)

    // data source of right table view    
     let listDataSource =  RxTableViewSectionedReloadDataSource<CategoryRightSection>( configureCell: { [weak self]ds, tv, ip, item in
            guard let self = self else { return UITableViewCell() }
            if self.lastIndex != self.currentIndex {
           // to compare the old and new selected index of the left table View ,give a new start to the right table view if changed
                tv.scrollToRow(at: ip, at: .top, animated: false)
                self.lastIndex = self.currentIndex
            }
            if ip.row == 0 {
                let cell = CategoryListBannerCell()
                cell.model = item
                return cell
            } else {
                let cell = tv.dequeueReusableCell(withIdentifier: "Cell2", for: ip) as! CategoryListSectionCell
                cell.model = item
                return cell
            }
     })


     listData.map{ [CategoryRightSection(items:$0)] }.bind(to: rightListTableView.rx.items(dataSource: listDataSource))
            .disposed(by: rx.disposeBag)   
 }     

private var lastIndex : NSInteger = 0, used to compare the old and new selected index of the left table View ,let the right table view start , with currentIndex, if different

the self.currentSelectIndexPath was used, because when the left tableView's final cell got clicked, the UI logic is different.

self.currentListData is used to capture the current data of the right table view, when the left tableView clicked with different row.

self.currentListData is also used in the UITableViewDelegate.

// MARK:- UITableViewDelegate
extension CategoryViewController : UITableViewDelegate {
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        switch indexPath.row {
        case 0 :
            return (mScreenW - 120)/240 * 100;
        default :
            let subItems:SubItems = self.currentListData![indexPath.row]
            if subItems.children.count > 0{
                let lines: NSInteger = (subItems.children.count - 1)/3 + 1
                let buttonHeight = (mScreenW - 136 - 108)/3
                let allButtonHeight = buttonHeight/44 * 63 * CGFloat(lines)
                let other =  (lines - 1)*42 + 56
                let height = allButtonHeight  + CGFloat(other) + 33
                return height
            }
            return 250
        }
    }
}

How to improve the code?

How to eliminate the variables of middle state.

The corresponding model is

class CategoryViewModel: NSObject {

    let vmDatas = Variable<[ParentItem]>([])

    func transform() -> MCBoutiqueOutput {

        let temp_sections = vmDatas.asObservable().map({ (sections) -> [CategoryLeftSection] in
            let count = sections.count
            if count > 0{
                let items = sections[0..<(count-1)]
                return [CategoryLeftSection(items: Array(items))]
            }
            return []
        }).asDriver(onErrorJustReturn: [])

        let output = MCBoutiqueOutput(sections: temp_sections)
        Observable.combineLatest(output.requestCommand, Provider.rx.cacheRequest(.baseUIData)).subscribe({  [weak self]  ( result: Event<(Bool, Response)>) in
            guard let self = self else { return }
            switch result{
            case .next(let response):
                let resultReal = response.1
                // ...
                if resultReal.statusCode == 200 || resultReal.statusCode == 230 {

                    if resultReal.fetchJSONString(keys:["code"]) == "0" {
                        mUserDefaults.set(false, forKey: "categoryVCShowTry")
                        self.vmDatas.value = ParentItem.mapModels(from:
                            resultReal.fetchJSONString(keys:["data","data"]))
                    } 
                }
            default:
                break
            }
        }).disposed(by: rx.disposeBag)
        return output
    }
}

Solution

  • It is easy to get rid of private var currentListData :[SubItems]?, with some encapsulation code, based on the code above.

    Because you have currentSelectIndexPath, then it is easy to get currentListData by calculating.

    private var currentSelectIndexPath = IndexPath(item: 0, section: 0)
    
    private func boundTableViewData() {
        /// list 数据依赖 左侧点击
            let listData = leftMenuTableView.rx.itemSelected.flatMapLatest {
            [weak self](indexPath) ->  Observable<[SubItems]> in
                guard let self = self else { return Observable.just([]) }
                // ...
                if indexPath.row == self.viewModel.vmDatas.value.count - 1 {
                    // ...
                    self.leftMenuTableView.selectRow(at: self.currentSelectIndexPath, animated: false, scrollPosition: .top)
                    return Observable.just(self.getResult(self.currentSelectIndexPath.row))
                }
                let result:[SubItems] = self.getResult(indexPath.row)
                self.currentSelectIndexPath = indexPath
                return Observable.just(result)
    
            }
    
       // ...
    }
    
    
    // MARK:- UITableViewDelegate
    extension CategoryViewController : UITableViewDelegate {
        func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
            guard tableView == self.rightListTableView else {
                return Metric.leftMenuHeight
            }
            switch indexPath.row {
            case 0 :
                return (mScreenW - 120)/240 * 100;
            default :
                let subItems:SubItems = getResult(currentSelectIndexPath.row)[indexPath.row]
                if subItems.children.count > 0{
                    let lines: NSInteger = (subItems.children.count - 1)/3 + 1
                    let buttonHeight = (mScreenW - 136 - 108)/3
                    let allButtonHeight = buttonHeight/44 * 63 * CGFloat(lines)
                    let other =  (lines - 1)*42 + 56
                    let height = allButtonHeight  + CGFloat(other) + 33
                    return height
                }
                return 250
            }
        }
    }
    

    Notice the logic of last row is quite different from the others.

    So it is better to be another section , or a footer in this case.

    With tableView.rx.model to get models, you can get rid of currentSelectIndexPath with the following code:

    private func boundTableViewData() {
            // ...
            let listData = leftMenuTableView.rx.itemSelected.distinctUntilChanged().flatMapLatest {
            [weak self](indexPath) ->  Observable<[SubItems]> in
                guard let self = self else { return Observable.just([]) }
                // ...
                let result:[SubItems] = self.getResult(indexPath.row)
                return Observable.just(result)
            }
    
    
    // MARK:- UITableViewDelegate
    extension CategoryViewController : UITableViewDelegate {
        func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
            switch indexPath.row {
            case 0 :
                return (mScreenW - 120)/240 * 100
            default :
                if let subItems : SubItems = try? tableView.rx.model(at: indexPath), subItems.children.count > 0{
                    let lines: NSInteger = (subItems.children.count - 1)/3 + 1
                    let buttonHeight = (mScreenW - 136 - 108)/3
                    let allButtonHeight = buttonHeight/44 * 63 * CGFloat(lines)
                    let other =  (lines - 1)*42 + 56
                    let height = allButtonHeight  + CGFloat(other) + 33
                    return height
                }
                return 250
            }
        }
    
        func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
            return nil
        }
    
        func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
            return CGFloat.zero
        }
    
        func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
            // ...
            let cell = CategoryLeftCell()
            return cell
        }
    
    
        func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
            // return  ...
        }
    
    }
    

    And the model should be changed a little

    class CategoryViewModel: NSObject {
    
        let vmDatas = Variable<[ParentItem]>([])
    
        func transform() -> MCBoutiqueOutput {
    
            let temp_sections = vmDatas.asObservable().map({ (sections) -> [CategoryLeftSection] in
                let count = sections.count
                if count > 0{
                    let items = sections[0..<(count-1)]
                    return [CategoryLeftSection(items: Array(items))]
                }
                return []
            }).asDriver(onErrorJustReturn: [])
    
     // ...