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.