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.
Related
I am trying to do searching in the table view, with the throttle.
Here I have BehaviorRelay in ViewModel
var countryCodes: BehaviorRelay<[CountryDialElement]> = BehaviorRelay<[CountryDialElement]>(value:[])
Here I have BehaviorRelay for the entered text
var searchText: BehaviorRelay<String> = BehaviorRelay<String>(value: "")
Here I have Table View which is binded with Observable from View Model
self.viewModel.params.countryCodes.bind(to: tableView.rx.items(cellIdentifier: "CountryDialTableViewCell")) { index, model, cell in
let countryCell = cell as! CountryDialTableViewCell
countryCell.configure(model)
}.disposed(by: disposeBag)
Here I have Rx binding to UISearchBar in ViewController
searchBar
.rx
.text
.orEmpty
.debounce(.milliseconds(300), scheduler: MainScheduler.instance)
.distinctUntilChanged()
.subscribe { [weak self] query in
guard
let query = query.element else { return }
self?.viewModel.params.searchText.accept(query)
}
.disposed(by: disposeBag)
Then in ViewModel, I am trying to filter data and push filtered data to dataSource, to update the tableView.
Observable.combineLatest(params.countryCodes, params.searchText) { items, query in
return items.filter({ item in
item.name.lowercased().contains(query.lowercased()) || item.dialCode.lowercased().contains(query.lowercased())
})
}.subscribe(onNext: { resultArray in
self.params.countryCodes.accept(resultArray)
})
.disposed(by: disposeBag)
But I am getting this type of Error
Reentrancy anomaly was detected.
This behavior breaks the grammar because there is overlapping between sequence events.
The observable sequence is trying to send an event before sending the previous event has finished.
Interpretation: This could mean that there is some kind of unexpected cyclic dependency in your code, or that the system is not behaving in an expected way.
I am trying to achieve:
Binded table view with an observable array
Type text in the search bar
Filter observable, array
Update observable, reload tableView.
First thing I noticed... You have two BehaviorRelays defined as var. You should always define them with let.
You haven't posted enough code to demonstrate the error but the fundamental problem, as explained to you in the error message, is that you are breaking the observable grammar because you are pushing data through an Observable while in the middle of pushing data. If allowed, it would form an infinite recursive call that would overflow the stack.
Don't send an event until after the current event is finished sending. It would help a lot if you didn't use so many Relays...
You don't say anything about where the items in the array come from which also makes it hard to help...
Consider something like this:
Observable.combineLatest(
searchBar.rx.text.orEmpty
.debounce(.milliseconds(300), scheduler: MainScheduler.instance)
.distinctUntilChanged()
.startWith(""),
sourceOfItems(),
resultSelector: { searchTerm, items in
items.filter { $0.code.contains(searchTerm) }
}
)
.bind(to: tableView.rx.items(cellIdentifier: "CountryDialTableViewCell")) { index, model, cell in
let countryCell = cell as! CountryDialTableViewCell
countryCell.configure(model)
}
.disposed(by: disposeBag)
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.
Hi have a tableview with sections and I am making API call to populate the tableView. I am also using the MVVm architecture. Now users are able to delete items but I try reloading the sections or tableView but nothing happens as the deleted item still remains in the tableView. Below is my code. Any help is appreciated
My ViewModel
Observable.zip(identiferElements, deviceElements).map {(identifers, devices, _) -> [MyInfoSection] in
var items: [MyInfoSection] = []
let identiferRepository = identifers.map({ (repository) -> MyInfoSectionItem in
let cellViewModel = IdentifiersCellViewModel(with: repository)
return MyInfoSectionItem.identifiersItem(viewModel: cellViewModel)
})
if identiferRepository.isNotEmpty {
items.append(MyInfoSection.setting(title: "Identifier", items: identiferRepository))
}
let deviceRepository = devices.map({ (repository) -> MyInfoSectionItem in
let cellViewModel = DevicesCellViewModel(with: repository)
return MyInfoSectionItem.devicesItem(viewModel: cellViewModel)
})
if deviceRepository.isNotEmpty {
items.append(MyInfoSection.setting(title: "Active Devices", items: deviceRepository))
}
return items
}.bind(to: elements).disposed(by: rx.disposeBag)
deletedEvent.drive(onNext: { (item) in
switch item {
case .identifiersItem(let viewModel):
identiferDeleted.onNext(viewModel.repository)
case .devicesItem(let viewModel):
deviceDeleted.onNext(viewModel.repository)
}
}).disposed(by: rx.disposeBag)
identiferDeleted.asObservable().flatMapLatest({ [weak self] (value) -> Observable<ResponseBase> in
log(value)
guard let self = self, let id = value.id else { return Observable.just(ResponseBase()) }
return self.provider.deleteAddress(id: id)
.trackActivity(self.loading)
.trackError(self.error)
}).subscribe(onNext: { (res) in
log(res)
}).disposed(by: rx.disposeBag)
ViewController
//viewDidLoad
let input = MyInfoViewModel.Input(trigger: refresh, segmentSelection: segmentSelected, selection: tableView.rx.modelSelected(MyInfoSectionItem.self).asDriver(), deleted: tableView.rx.modelDeleted(MyInfoSectionItem.self).asDriver())
let output = viewModel.transform(input: input)
More code would be added based on request. Thanks
Use combineLatest instead of zip. The user can only delete an item out of one section at a time and zip waits until both sections emit a new value before emitting. There might be other problems, but that is one for sure.
I would need to see compilable code for your view model to help further.
I've just started implementing RxSwift.
I've got the following function to dynamically fill a collectionView with users returned from a Firebase observe call, but I'm struggling to then dynamically filter the users based on any potential entires in the searchBar.
Rx collectionView binding:
private func bind() {
viewModel.users.asObservable()
.bind(to: nearbyCollectionView.rx.items(cellIdentifier: "NearbyCell", cellType: NearbyCell.self)) {
row, user, cell in
cell.configureCell(user: user)
}.disposed(by: disposeBag)
}
Should I return to the default collectionView implementations and simply use Rx to dynamically update the collectionView objects, or is there a better way to do this?
My old implementation used the following:
if self.viewModel.inSearchMode {
user = self.viewModel.filteredUsers[indexPath.row]
cell.configureCell(user: user)
} else {
user = self.viewModel.users[indexPath.row]
cell.configureCell(user: user)
}
Thanks a lot for any help!!
You can use combineLatest to filter them out
let searchString = searchTextField.rx.text
let filteredUsersObservable = Observable.combineLatest(searchString, viewModel.users, resultSelector: { string, users in
return users.filter { $0 == string }
})
filteredUsersObservable
.bind(to: nearbyCollectionView.rx.items(cellIdentifier: "NearbyCell", cellType: NearbyCell.self)) {
row, user, cell in
cell.configureCell(user: user)
}.disposed(by: disposeBag)
I'm not sure if the syntax fully correct, but the idea is to get the signal every time there's a change on the text field, make it as an observable and filter with the users observable.
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)