简体   繁体   中英

Pull to refresh in a tableview rx datasource

In my mobile application I would like to update the tableView datasource by a pull to refresh request, but I don't know how to insert the new items on top of the tableview datasource.

I see that there is aa method of insertRows such as : self.tableView?.insertRows(at: [indexPath], with: .top) but how do I add the newItems here according to my methods I have?

I have a function called initializedTableView() that initializes the tableView with PublishSubject observable items.

func initializeTableView() {

    viewModel
        .items
        .subscribe(onNext: { items in

            self.tableView?.delegate = nil
            self.tableView?.dataSource = nil

            Observable.just(items)
                .bind(to:(self.tableView?.rx.items(cellIdentifier: 
                 itemCell.Identifier, cellType: itemCell.self))!) { 
                 (index, element, cell) in

                    cell.itemModel = element

                }.disposed(by: self.disposeBag)
        })
        .disposed(by: disposeBag)
}

This function is called once a pull to refresh is requested by user:

func refreshTableView() {

    // get new items
    viewModel
        .newItems
        .subscribe(onNext: { newItems in

            //new
            let new = newItems.filter({ item in
                // items.new == true
            })

            //old
            var old = newItems.filter({ item -> Bool in
                // items.new == false
            })

            new.forEach({item in
                // how to update tableView.rx.datasource here???

            })

 }).disposed(by: disposeBag)
 }

I did something similar with my app since I had issues with tableView.insertRows .

Here is the code:

func loadMoreComments() {
    // call to backend to get more comments
    getMoreComments { (newComments) in
        // combine the new data and your existing data source array
        self.comments = newComments + self.comments
        self.tableView.reloadData()
        self.tableView.layoutIfNeeded()
        // calculate the total height of the newly added cells
        var addedHeight: CGFloat = 0
        for i in 0...result.count {
            let indexRow = i
            let tempIndexPath = IndexPath(row: Int(indexRow), section: 0)
            addedHeight = addedHeight + self.tableView.rectForRow(at: tempIndexPath).height
        }
        // adjust the content offset by how much height was added to the start so that it looks the same to the user
        self.tableView.contentOffset.y = self.tableView.contentOffset.y + addedHeight
    }
}

So, by calculating the heights of the new cells being added to the start and then adding this calculated height to the tableView.contentOffset.y , I was able to add cells to the top of the tableView seamlessly without reworking my tableView . This may look like a jerky workaround, but the shift in tableView.contentOffset isn't noticeable if you calculate the height properly.

struct ViewModel {
    let items: Observable<[Item]>

    init(trigger: Observable<Void>, newItems: @escaping () -> Observable<[Item]>) {
        items = trigger
            .flatMapLatest(newItems)
            .scan([], accumulator: { $1 + $0 })
    }
}

The above doesn't handle errors, nor does it handle resets, but the scan will put the new items at the top of the list.

The situation doesn't feel right though. Normally, the API call returns all the items, how can it possibly know which items are "new"?

struct ViewModel {
    let items: BehaviorRelay<[Item]>

    init() {
        self.items = BehaviorRelay(value: [])
    }

    func fetchNewItems() {
        // This assumes you are properly distinguishing which items are new 
        // and `newItems` does not contain existing items
        let newItems: [Item] = /* However you get new items */

        // Get a copy of the current items
        var updatedItems = self.items.value

        // Insert new items at the beginning of currentItems
        updatedItems.insert(contentsOf: newItems, at: 0)

        // For simplicity this answer assumes you are using a single cell and are okay with a reload
        // rather than the insert animations.
        // This will reload your tableView since 'items' is bound to the tableView items
        //
        // Alternatively, you could use RxDataSources and use the `RxTableViewSectionedAnimatedDataSource`
        // This will require a section model that conforms to `AnimatableSectionModelType` and some
        // overall reworking of this example
        items.accept(updatedItems)
    }
}

final class CustomViewController: UIViewController {

    deinit {
        disposeBag = DisposeBag()
    }

    @IBOutlet weak var tableView: UITableView!

    private var disposeBag = DisposeBag()
    private let viewModel = ViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(CustomTableCell.self, forCellReuseIdentifier: "ReuseID")
        tableView.refreshControl = UIRefreshControl()

        viewModel.items
            .bind(to: tableView.rx.items(cellIdentifier: "ReuseID", cellType: CustomTableCell.self)) { row, item, cell in
                // Configure cell with item
                cell.configure(with: item)
        }
        .disposed(by: disposeBag)

        tableView.refreshControl?.rx.controlEvent(.valueChanged)
            .subscribe(onNext: { [weak self] in
                self?.viewModel.fetchNewItems()
            })
            .disposed(by: disposeBag)
    }
}

Alternative answer using BehaviorRelay and bindings. This way, you are only updating the items relay and it will automatically update the tableView. It also provides a more "Rx" way of handling pull to refresh.

As mentioned in the code comments, this assumes you are determining which items are new and that newItems does not contain any existing items. Either way this should provide a starting point.

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