简体   繁体   中英

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

一

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
    }
}

I get rid of two property , which is used for communication with values.

private let viewModel = CategoryViewModel()
    private var currentListData :[SubItems]?

    private var currentSelectIndexPath : IndexPath?


    func getResult(_ row: Int) -> [SubItems]{
        if viewModel.vmDatas.value.isEmpty == false, let subItems = viewModel.vmDatas.value[row].subnav {
            var fisrtSubItem = SubItems()
            fisrtSubItem.url = self.viewModel.vmDatas.value[row].url
            fisrtSubItem.name = self.viewModel.vmDatas.value[row].banner
            var result:[SubItems] = subItems
            result.insert(fisrtSubItem, at: 0)
            return result
        }
        return []
    }




    private func boundTableViewData() {

        //// left menu 数据源
        let dataSource = MyDataSource<CategoryLeftSection>( configureCell: { ds, tv, ip, item in
            let cell = tv.dequeueReusableCell(withIdentifier: "Cell1", for: ip) as! CategoryLeftCell
            cell.model = item
            return cell
        })
        dataSource.rxRealoded.emit(onNext: { [weak self] in
            guard let self = self else { return }
            self.leftMenuTableView.selectIndexPath()
            self.leftMenuTableView.clickIndexPath()
        }).disposed(by: rx.disposeBag)
        vmOutput!.sections.asDriver().drive(leftMenuTableView.rx.items(dataSource: dataSource)).disposed(by: rx.disposeBag)

        /// list 数据依赖 左侧点击
        let listData = leftMenuTableView.rx.itemSelected.distinctUntilChanged().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.currentListData)!)
            }
            let result:[SubItems] = self.getResult(indexPath.row)
            self.currentListData = result
            self.currentSelectIndexPath = indexPath
            return Observable.just(result)

        }.share(replay: 1)


        let listDataSource = MyDataSource<CategoryRightSection>(configureCell: { ds, tv, ip, item in
            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
            }
        })

        listDataSource.rxRealoded.emit(onNext: { [weak self] in
            guard let self = self else { return }
            self.rightListTableView.scrollToTop(animated: false)
        }).disposed(by: rx.disposeBag)

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

I use the tip of RxDataSources's issue

lastIndex works with currentIndex , to make sure the right table view is always at the top, when the user switches the left tableView's indexPath.

The moment matters, selectRow(at: indexPath, animated: false, scrollPosition: UITableViewScrollPosition.none) .

This should be called when the table View ends Updated.

If this could be done in Rx way, the two variable is useless.

final class MyDataSource<S: SectionModelType>: RxTableViewSectionedReloadDataSource<S> {
    private let relay = PublishRelay<Void>()
    var rxRealoded: Signal<Void> {
        return relay.asSignal()
    }

    override func tableView(_ tableView: UITableView, observedEvent: Event<[S]>) {
        super.tableView(tableView, observedEvent: observedEvent)
        //Do diff
        //Notify update

        relay.accept(())
    }
}

Then handle the tableView.

extension UITableView {
    func hasRowAtIndexPath(indexPath: IndexPath) -> Bool {
        return indexPath.section < numberOfSections && indexPath.row < numberOfRows(inSection: indexPath.section)
    }

    func scrollToTop(animated: Bool) {
        let indexPath = IndexPath(row: 0, section: 0)
        if hasRowAtIndexPath(indexPath: indexPath) {
            scrollToRow(at: indexPath, at: .top, animated: animated)
        }
    }


    func selectIndexPath(indexPath: IndexPath? = nil) {
        let indexPath = IndexPath(row: 0, section: 0)
        if hasRowAtIndexPath(indexPath: indexPath) {
            selectRow(at: indexPath, animated: false, scrollPosition: UITableViewScrollPosition.none)
        }
    }

    func clickIndexPath(indexPath: IndexPath? = nil) {
        let indexPath = IndexPath(row: 0, section: 0)
        if hasRowAtIndexPath(indexPath: indexPath) {
            delegate?.tableView?(self, didSelectRowAt: indexPath)
        }
    }



}

With the same tips, lastIndex is also used to give the right table View the initial data.

The moment also matters.

This could be done after the left table view ends updated, too

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: [])

 // ...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM