i want know end of reloadTableView
Then I want to scroll down to the bottom of the table view.
Before I used RxSwift
Just after reloadData
It was possible using setContentOffSet or ScrollToRow.
I tried it with the code I found.
never called endUpdates.
var replyList : BehaviorRelay<[Reply]>!
func bind(){
replyViewModel.replyList
.asDriver()
.drive(replyTableView.rx.items){ [weak self] (tableView,row,item) in
return self?.makeReplyCell(tableView: tableView, replyInfo: item) ?? UITableViewCell()
}
.disposed(by: disposeBag)
replyTableView.endUpdatesEvent
.asObservable()
.subscribe({ _ in
print("Scroll To Bottom")
})
.disposed(by: disposeBag)
}
import Foundation
import RxCocoa
import RxSwift
extension UITableView {
/// Reactive wrapper for `UITableView.insertRows(at:with:)`
var insertRowsEvent: ControlEvent<[IndexPath]> {
let source = rx.methodInvoked(#selector(UITableView.insertRows(at:with:)))
.map { a in
return a[0] as! [IndexPath]
}
return ControlEvent(events: source)
}
/// Reactive wrapper for `UITableView.endUpdates()`
var endUpdatesEvent: ControlEvent<Bool> {
let source = rx.methodInvoked(#selector(UITableView.endUpdates))
.map { _ in
return true
}
return ControlEvent(events: source)
}
/// Reactive wrapper for when the `UITableView` inserted rows and ended its updates.
var insertedItems: ControlEvent<[IndexPath]> {
let insertEnded = Observable.combineLatest(
insertRowsEvent.asObservable(),
endUpdatesEvent.asObservable(),
resultSelector: { (insertedRows: $0, endUpdates: $1) }
)
let source = insertEnded.map { $0.insertedRows }
return ControlEvent(events: source)
}
}
UITableView.endUpdates() won't help you here as you are not calling it directly and under the hood it might be not called at all (at least with the rx wrapper).
More legitimate solution would be to observe for invocations of layoutSubviews() method where you can validate cells if they were reloaded at this layout cycle and they are present - then you can do scroll to them for example. If layoutSubviews() were called but the cells are not there yet in the table view then you wait for the next cycle and check it again.
Related
I'm stuck on RxCocoa problem.
I'm gonna implement clear tableView with Rx.
The app using MVVM with RxCocoa needs clear data for initializing tableView with infinite scroll.
But with binding tableView, I dunno how to clear it.
Thanks.
ViewController
self.viewModel.requestData() // request data to Server
self.viewModel.output.hotDealList
.scan(into: [ItemModel]()) { firstPosts, afterPosts in // For Infinite Scroll
return firstPosts.append(contentsOf: afterPosts)
}
.bind(to: self.tableView.rx.items(cellIdentifier: "itemCell", cellType: HotDealTableViewCell.self)) { [unowned self] (index, item, cell) in
self.setCellUI(item: item, cell: cell)
}.disposed(by: self.bag)
ViewModel
struct Output {
let hotDealList = BehaviorSubject<[ItemModel]>(value: [])
}
func requestData(page: String = "0") {
let _ = self.service.requestItemList(["page":page])
.subscribe(
onNext:{ response in
guard let serverModels = response.posts, !serverModels.isEmpty else {
return
}
self.output.hotDealList.onNext(serverModels)
}
).disposed(by: self.bag)
}
The solution here is to expand the state machine that you already have started. A Moore Machine (which is the easiest state machine to understand) consists of a number of inputs, a state, a start state, and a number of outputs. It is expressed in Rx using the scan operator and an Input enum.
You already have the scan operator setup, but you only have one input, hotDealList. You need to include a second input for clearing.
Something like this:
enum Input {
case append([ItemModel])
case clear
}
let state = Observable.merge(
viewModel.output.hotDealList.map { Input.append($0) },
viewModel.output.clear.map { Input.clear }
)
.scan(into: [ItemModel]()) { state, input in
switch input {
case let .append(page):
state.append(page)
case .clear:
state = []
}
}
In Rx, the outputs of the state machine are expressed by bindings. You already have one:
state.bind(to: self.tableView.rx.items(cellIdentifier: "itemCell", cellType: HotDealTableViewCell.self)) { [unowned self] (index, item, cell) in
self.setCellUI(item: item, cell: cell)
}
.disposed(by: bag)
If you need more, be sure to share your state observable.
BTW, using self inside the binder like that is a memory leak. I suggest you move the setCellUI(item:cell:) method into the HotDealTableViewCell class so you don't need self.
I have used RxCollectionViewSectionedReloadDataSource to load my data into UICollectionView.
let dataSource = RxCollectionViewSectionedReloadDataSource<SectionModel<String, WorkGroup>>(
configureCell: { (_, collectionView, indexPath, item) in
guard let cell = collectionView
.dequeueReusableCell(withReuseIdentifier: WorkGroupCell.identifier, for: indexPath) as? WorkGroupCell else {
return WorkGroupCell()
}
cell.viewModel = item
return cell
}
)
viewModel.items.bind(to: tileCollectonView.rx.items(dataSource: dataSource)).disposed(by: disposeBag)
tileCollectonView.rx.setDelegate(self).disposed(by: disposeBag)
Using above code i can display the data. But i want to drag and drop(Reorder) the cell.
Please let me know how can i do that reorder using RxSwift.
Help will be highly appreciated.
As is often the case, you need a state machine.
The logic involved is quite simple:
struct State<Item> {
var items: [Item] = []
}
func state<Item>(initialState: State<Item>, itemMoved: Observable<ItemMovedEvent>) -> Observable<State<Item>> {
itemMoved
.scan(into: initialState) { (state, itemMoved) in
state.items.move(from: itemMoved.sourceIndex.row, to: itemMoved.destinationIndex.row)
}
.startWith(initialState)
}
extension Array
{
mutating func move(from oldIndex: Index, to newIndex: Index) {
if oldIndex == newIndex { return }
if abs(newIndex - oldIndex) == 1 { return self.swapAt(oldIndex, newIndex) }
self.insert(self.remove(at: oldIndex), at: newIndex)
}
}
The above uses the ItemMovedEvent provided by RxCocoa for table views. In order to create something like that for a collection view, you will need to wrap the UICollectionViewDragDelegate in a delegate proxy. An article on how to do that can be found here: Convert a Swift Delegate to RxSwift Observables
I have a BehaviorSubject where my tableview is bound to through RxDataSources.
Besides that, I have a pull to refresh which creates an observable that updates the data and updates the data in the BehaviorSubject so that my UITableView updates correctly.
Now the question is, how do I handle the error handling for whenever my API call fails?
Few options that I have thought of was:
Subscribe to the observer's onError and call the onError of my BehaviorSubject\
Somehow try to concat? or bind(to: ..)
Let another subscriber in my ViewController subscribe besides that my tableview subscribes to the BehaviorSubject.
Any suggestions?
Ideally, you wouldn't use the BehaviorSubject at all. From the Intro to Rx book:
The usage of subjects should largely remain in the realms of samples and testing. Subjects are a great way to get started with Rx. They reduce the learning curve for new developers, however they pose several concerns...
Better would be to do something like this in your viewDidLoad (or a function that is called from your viewDidLoad):
let earthquakeData = Observable.merge(
tableView.refreshControl!.rx.controlEvent(.valueChanged).asObservable(),
rx.methodInvoked(#selector(UIViewController.viewDidAppear(_:))).map { _ in }
)
.map { earthquakeSummary /* generate URLRequest */ }
.flatMapLatest { request in
URLSession.shared.rx.data(request: request)
.materialize()
}
.share(replay: 1)
earthquakeData
.compactMap { $0.element }
.map { Earthquake.earthquakes(from: $0) }
.map { $0.map { EarthquakeCellDisplay(earthquake: $0) } }
.bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: EarthquakeTableViewCell.self)) { _, element, cell in
cell.placeLabel.text = element.place
cell.dateLabel.text = element.date
cell.magnitudeLabel.text = element.magnitude
cell.magnitudeImageView.image = element.imageName.isEmpty ? UIImage() : UIImage(named: element.imageName)
}
.disposed(by: disposeBag)
earthquakeData
.compactMap { $0.error }
.map { (title: "Error", message: $0.localizedDescription) }
.bind { [weak self] title, message in
self?.presentAlert(title: title, message: message, animated: true)
}
.disposed(by: disposeBag)
The materialize() operator turns a Event.error(Error) result into an Event.next(.error(Error)) so that the chain won't be broken down. The .compactMap { $0.element } emits only the successful results while the .compactMap { $0.error } emits only the errors.
The above code is adapted from my RxEarthquake sample.
I'm facing a problem when selecting the table view row on RxSwift. For details, the code on the do(onNext:) function is called twice, thus lead to the navigation pushed twice too. Here is my code in the viewModel, please help me resolve it. Thanks so much.
struct Input {
let loadTrigger: Driver<String>
let searchTrigger: Driver<String>
let selectMealTrigger: Driver<IndexPath>
}
struct Output {
let mealList: Driver<[Meal]>
let selectedMeal: Driver<Meal>
}
func transform(_ input: HomeViewModel.Input) -> HomeViewModel.Output {
let popularMeals = input.loadTrigger
.flatMap { _ in
return self.useCase.getMealList()
.asDriver(onErrorJustReturn: [])
}
let mealSearchList = input.searchTrigger
.flatMap { text in
return self.useCase.getMealSearchList(mealName: text)
.asDriver(onErrorJustReturn: [])
}
let mealList = Observable.of(mealSearchList.asObservable(), popularMeals.asObservable()).merge().asDriver(onErrorJustReturn: [])
let selectedMeal = input.selectMealTrigger
.withLatestFrom(mealList) { $1[$0.row] }
.do(onNext: { meal in
self.navigator.toMealDetail(meal: meal)
})
return Output(mealList: mealList, selectedMeal: selectedMeal)
}
Edit: Here's the implemetation on the ViewController:
func bindViewModel() {
self.tableView.delegate = nil
self.tableView.dataSource = nil
let emptyTrigger = searchBar
.rx.text.orEmpty
.filter { $0.isEmpty }
.throttle(0.1, scheduler: MainScheduler.instance)
.asDriver(onErrorJustReturn: "")
let loadMealTrigger = Observable
.of(emptyTrigger.asObservable(), Observable.just(("")))
.merge()
.asDriver(onErrorJustReturn: "")
let searchTrigger = searchBar.rx.text.orEmpty.asDriver()
.distinctUntilChanged()
.filter {!$0.isEmpty }
.throttle(0.1)
let selectMealTrigger = tableView.rx.itemSelected.asDriver()
let input = HomeViewModel.Input(
loadTrigger: loadMealTrigger,
searchTrigger: searchTrigger,
selectMealTrigger: selectMealTrigger
)
let output = viewModel.transform(input)
output.mealList
.drive(tableView.rx.items(cellIdentifier: MealCell.cellIdentifier)) { index, meal, cell in
let mealCell = cell as! MealCell
mealCell.meal = meal
}
.disposed(by: bag)
output.selectedMeal
.drive()
.disposed(by: bag)
}
Firstly, is this RxSwift?
If so, the .do(onNext:) operator provides side effects when you receive a new event via a subscription; Therefore, two "reactions" will happen when a table row is tapped: 1. subscription method and 2. .do(onNext:) event. Unfortunately, I do not have any further insight into your code, so there may be other stuff creating that error aswell.
Good luck!
I'm new in RxSwift. Some strange thing happens in my code.
I have a collection view and
Driver["String"]
Data for binding.
var items = fetchImages("flower")
items.asObservable().bindTo(self.collView.rx_itemsWithCellIdentifier("cell", cellType: ImageViewCell.self)) { (row, element, cell) in
cell.imageView.setURL(NSURL(string: element), placeholderImage: UIImage(named: ""))
}.addDisposableTo(self.disposeBag)
fetchImages
Function returns data
private func fetchImages(string:String) -> Driver<[String]> {
let searchData = Observable.just(string)
return searchData.observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
.flatMap
{ text in // .Background thread, network request
return RxAlamofire
.requestJSON(.GET, "https://pixabay.com/api/?key=2557096-723b632d4f027a1a50018f846&q=\(text)&image_type=photo")
.debug()
.catchError { error in
print("aaaa")
return Observable.never()
}
}
.map { (response, json) -> [String] in // again back to .Background, map objects
var arr = [String]()
for i in 0 ..< json["hits"]!!.count {
arr.append(json["hits"]!![i]["previewURL"]!! as! String)
}
return arr
}
.observeOn(MainScheduler.instance) // switch to MainScheduler, UI updates
.doOnError({ (type) in
print(type)
})
.asDriver(onErrorJustReturn: []) // This also makes sure that we are on MainScheduler
}
Strange thing is this. First time when I fetch with "flower" it works and return data, but when I add this code
self.searchBar.rx_text.subscribeNext { text in
items = self.fetchImages(text)
}.addDisposableTo(self.disposeBag)
It doesn't work. It doesn't steps in flatmap callback, and because of this, doesn't return anything.
It works in your first use case, because you're actually using the returned Driver<[String]> via a bindTo():
var items = fetchImages("flower")
items.asObservable().bindTo(...
However, in your second use case, you aren't doing anything with the returned Driver<[String]> other than saving it to a variable, which you do nothing with.
items = self.fetchImages(text)
A Driver does nothing until you subscribe to it (or in your case bindTo).
EDIT: To make this clearer, here's how you could get your second use case to work (I've avoided cleaning up the implementation to keep it simple):
self.searchBar.rx_text
.flatMap { searchText in
return self.fetchImages(searchText)
}
.bindTo(self.collView.rx_itemsWithCellIdentifier("cell", cellType: ImageViewCell.self)) { (row, element, cell) in
cell.imageView.setURL(NSURL(string: element), placeholderImage: UIImage(named: ""))
}.addDisposableTo(self.disposeBag)