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)
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.
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 have the following MVVM-C + RxSwift code.
The problem is that the TableView is not receiving any signals. When I
debug the results I can see that the API call is returning what it should, the objects array is populated with objects but the tableview does not show any results. Here is the console output:
2018-11-13 16:12:08.107: searchText -> Event next(qwerty)
Search something: qwerty
2018-11-13 16:12:08.324: viewModel.data -> Event next([])
Could it be the tableview itself? Maybe wrong custom cell setup?
ViewController.swift:
tableView = UITableView(frame: self.view.frame)
tableView.delegate = nil
tableView.dataSource = nil
tableView.register(SearchResultCell.self, forCellReuseIdentifier: "SearchResultCell")
viewModel.data
.debug("viewModel.data", trimOutput: false)
.drive(tableView.rx.items(cellIdentifier: "SearchResultCell")) { row, object, cell in
cell.name.text = object.name
cell.something.text = object.something
}
.disposed(by: disposeBag)
ViewModel.swift:
let disposeBag = DisposeBag()
var searchText = BehaviorRelay(value: "something to search for")
lazy var data: Driver<[Object]> = {
return self.searchText.asObservable()
.debug("searchText", trimOutput: false)
.throttle(0.3, scheduler: MainScheduler.instance)
.distinctUntilChanged()
.flatMapLatest(searchSomething)
.asDriver(onErrorJustReturn: [])
}()
func searchSomething(query: String) -> Observable<[Object]> {
print("Search something: \(query)")
let provider = MoyaProvider<APIService>()
var objects = [Object]()
provider.rx.request(.search(query: query)).subscribe { event in
switch event {
case let .success(response):
do {
let responseJSON: NSDictionary = try (response.mapJSON() as? NSDictionary)!
objects = self.parse(json: responseJSON["results"] as Any)
} catch(let error) {
print(error)
}
break
case let .error(error):
print(error)
break
}
}
.disposed(by: disposeBag)
let result: Observable<[Object]> = Observable.from(optional: objects)
return result
}
When using flatMap, you do not want to create nested subscriptions. You will create an Observable that returns the expected result, and flatMap will take care of subscribing to it. In the current state of things, searchSomething will always return an empty array, as Observable.from(optional: objects) will be called before the request has a chance to complete.
Since version 10.0 of Moya, provider will cancel the requests it created when deallocated. Here, it will be deallocated when execution exits searchSomething, hence the network request won't have time to finish. Moving provider's declaration to the view model's level solves this issue.
Here's searchSomething(query: String) -> Observable<[Object]> rewritten.
let provider = MoyaProvider<APIService>()
func searchSomething(query: String) -> Observable<[Object]> {
print("Search something: \(query)")
return provider.rx.request(.search(query: query)).map { (response) -> [Object] in
let responseJSON: NSDictionary = try (response.mapJSON() as? NSDictionary)!
return self.parse(json: responseJSON["results"] as Any)
}
}
Instead of doing the transformation in subscribe, it's done in map, which will be called for every next event, being passed the value associated with the event.
I'm using RxCoCoa and RxSwift for UITableView Biding.
the problem is when Connection lost or other connection errors except for Server Errors(I handled them) my app crash because of binding error that mentioned below. my question is how to handle Connection Errors?
fileprivate func getNextState() {
showFullPageState(State.LOADING)
viewModel.getProductListByID(orderGroup: OrderGroup.SERVICES.rawValue)
.do(onError: {
showStatusError(error: $0)
self.showFullPageState(State.CONTENT)
})
.filter {
$0.products != nil
}
.map {
$0.products!
}
.bind(to: (self.tableView?.rx.items(cellIdentifier: cellIdentifier, cellType: ProductCell.self))!) {
(row, element, cell) in
self.showFullPageState(State.CONTENT)
cell.product = element
}
.disposed(by: bag)
self.tableView?.rx.setDelegate(self).disposed(by: bag)
}
and this is my ViewModel :
func getProductListByID(orderGroup: String, page: String = "1", limit: String = "1000") -> Observable<ProductRes> {
return orderRegApiClient.getProductsById(query: getProductQueryDic(stateKey: getNextStateID(product: nextProduct)
, type: orderGroup, page: page, limit: limit)).map {
try JSONDecoder().decode(ProductRes.self, from: $0.data)
}.asObservable()
}
and I use Moya for my Network layer like This:
func getProductsById(query: [String: String]) -> Single<Response> {
return provider.rx.request(.getProductsById(query))
.filterSuccessfulStatusCodes()
}
You aren't handling errors anywhere. I mean you are acknowledging the error in the do operator but that doesn't actually handle it, that just allows it to pass through to the table view, which can't handle an error.
Look up the catchError series of operators for a solution. Probably .catchErrorJustReturn([]) will be all you need.
In a comment, you said:
... I don't want to return empty Array to my table. I want to show the error to customer and customer can retry service
In that case, you should use .catchError only for the success chain and setup a separate chain for the error as done below.
fileprivate func getNextState() {
showFullPageState(State.LOADING)
let products = viewModel.getProductListByID(orderGroup: OrderGroup.SERVICES.rawValue)
.share()
products
.catchError { _ in Observable.never() }
.filter { $0.products != nil }
.map { $0.products! }
.bind(to: tableView!.rx.items(cellIdentifier: cellIdentifier, cellType: ProductCell.self)) {
(row, element, cell) in
self.showFullPageState(State.CONTENT)
cell.product = element
}
.disposed(by: bag)
products
.subscribe(onError: { error in
showStatusError(error: error)
self.showFullPageState(State.CONTENT)
})
.disposed(by: bag)
self.tableView?.rx.setDelegate(self).disposed(by: bag)
}
The way you have the code setup, the only way for the user to retry the service is to call the function again. If you want to let the user retry in a more declarative manor, you would need to tie the chain to an observable that the user can trigger.
I am new to RxSwift and MVVM.
my viewModel has a method named rx_fetchItems(for:) that does the heavy lifting of fetching relevant content from backend, and returns Observable<[Item]>.
My goal is to supply an observable property of the viewModel named collectionItems, with the last emitted element returned from rx_fetchItems(for:), to supply my collectionView with data.
Daniel T has provided this solution that I could potentially use:
protocol ServerAPI {
func rx_fetchItems(for category: ItemCategory) -> Observable<[Item]>
}
struct ViewModel {
let collectionItems: Observable<[Item]>
let error: Observable<Error>
init(controlValue: Observable<Int>, api: ServerAPI) {
let serverItems = controlValue
.map { ItemCategory(rawValue: $0) }
.filter { $0 != nil }.map { $0! } // or use a `filterNil` operator if you already have one implemented.
.flatMap { api.rx_fetchItems(for: $0)
.materialize()
}
.filter { $0.isCompleted == false }
.shareReplayLatestWhileConnected()
collectionItems = serverItems.filter { $0.element != nil }.dematerialize()
error = serverItems.filter { $0.error != nil }.map { $0.error! }
}
}
The only problem here is that my current ServerAPI aka FirebaseAPI, has no such protocol method, because it is designed with a single method that fires all requests like this:
class FirebaseAPI {
private let session: URLSession
init() {
self.session = URLSession.shared
}
/// Responsible for Making actual API requests & Handling response
/// Returns an observable object that conforms to JSONable protocol.
/// Entities that confrom to JSONable just means they can be initialized with json.
func rx_fireRequest<Entity: JSONable>(_ endpoint: FirebaseEndpoint, ofType _: Entity.Type ) -> Observable<[Entity]> {
return Observable.create { [weak self] observer in
self?.session.dataTask(with: endpoint.request, completionHandler: { (data, response, error) in
/// Parse response from request.
let parsedResponse = Parser(data: data, response: response, error: error)
.parse()
switch parsedResponse {
case .error(let error):
observer.onError(error)
return
case .success(let data):
var entities = [Entity]()
switch endpoint.method {
/// Flatten JSON strucuture to retrieve a list of entities.
/// Denoted by 'GETALL' method.
case .GETALL:
/// Key (underscored) is unique identifier for each entity, which is not needed here.
/// value is k/v pairs of entity attributes.
for (_, value) in data {
if let value = value as? [String: AnyObject], let entity = Entity(json: value) {
entities.append(entity)
}
}
// Need to force downcast for generic type inference.
observer.onNext(entities as! [Entity])
observer.onCompleted()
/// All other methods return JSON that can be used to initialize JSONable entities
default:
if let entity = Entity(json: data) {
observer.onNext([entity] as! [Entity])
observer.onCompleted()
} else {
observer.onError(NetworkError.initializationFailure)
}
}
}
}).resume()
return Disposables.create()
}
}
}
The most important thing about the rx_fireRequest method is that it takes in a FirebaseEndpoint.
/// Conforms to Endpoint protocol in extension, so one of these enum members will be the input for FirebaseAPI's `fireRequest` method.
enum FirebaseEndpoint {
case saveUser(data: [String: AnyObject])
case fetchUser(id: String)
case removeUser(id: String)
case saveItem(data: [String: AnyObject])
case fetchItem(id: String)
case fetchItems
case removeItem(id: String)
case saveMessage(data: [String: AnyObject])
case fetchMessages(chatroomId: String)
case removeMessage(id: String)
}
In order to use Daniel T's solution, Id have to convert each enum case from FirebaseEndpoint into methods inside FirebaseAPI. And within each method, call rx_fireRequest... If I'm correct.
Id be eager to make this change if it makes for a better Server API design. So the simple question is, Will this refactor improve my overall API design and how it interacts with ViewModels. And I realize this is now evolving into a code review.
ALSO... Here is implementation of that protocol method, and its helper:
func rx_fetchItems(for category: ItemCategory) -> Observable<[Item]> {
// fetched items returns all items in database as Observable<[Item]>
let fetchedItems = client.rx_fireRequest(.fetchItems, ofType: Item.self)
switch category {
case .Local:
let localItems = fetchedItems
.flatMapLatest { [weak self] (itemList) -> Observable<[Item]> in
return self!.rx_localItems(items: itemList)
}
return localItems
// TODO: Handle other cases like RecentlyAdded, Trending, etc..
}
}
// Helper method to filter items for only local items nearby user.
private func rx_localItems(items: [Item]) -> Observable<[Item]> {
return Observable.create { observable in
observable.onNext(items.filter { $0.location == "LA" })
observable.onCompleted()
return Disposables.create()
}
}
If my approach to MVVM or RxSwift or API design is wrong PLEASE do critique.
I know it is tough to start understanding RxSwift
I like to use Subjects or Variables as inputs for the ViewModel and Observables or Drivers as outputs for the ViewModel
This way you can bind the actions that happen on the ViewController to the ViewModel, handle the logic there, and update the outputs
Here is an example by refactoring your code
View Model
// Inputs
let didSelectItemCategory: PublishSubject<ItemCategory> = .init()
// Outputs
let items: Observable<[Item]>
init() {
let client = FirebaseAPI()
let fetchedItems = client.rx_fireRequest(.fetchItems, ofType: Item.self)
self.items = didSelectItemCategory
.withLatestFrom(fetchedItems, resultSelector: { itemCategory, fetchedItems in
switch itemCategory {
case .Local:
return fetchedItems.filter { $0.location == "Los Angeles" }
default: return []
}
})
}
ViewController
segmentedControl.rx.value
.map(ItemCategory.init(rawValue:))
.startWith(.Local)
.bind(to: viewModel.didSelectItemCategory)
.disposed(by: disposeBag)
viewModel.items
.subscribe(onNext: { items in
// Do something
})
.disposed(by: disposeBag)
I think the problem you are having is that you are only going half-way with the observable paradigm and that's throwing you off. Try taking it all the way and see if that helps. For example:
protocol ServerAPI {
func rx_fetchItems(for category: ItemCategory) -> Observable<[Item]>
}
struct ViewModel {
let collectionItems: Observable<[Item]>
let error: Observable<Error>
init(controlValue: Observable<Int>, api: ServerAPI) {
let serverItems = controlValue
.map { ItemCategory(rawValue: $0) }
.filter { $0 != nil }.map { $0! } // or use a `filterNil` operator if you already have one implemented.
.flatMap { api.rx_fetchItems(for: $0)
.materialize()
}
.filter { $0.isCompleted == false }
.shareReplayLatestWhileConnected()
collectionItems = serverItems.filter { $0.element != nil }.dematerialize()
error = serverItems.filter { $0.error != nil }.map { $0.error! }
}
}
EDIT to handle problem mentioned in comment. You now need to pass in the object that has the rx_fetchItems(for:) method. You should have more than one such object: one that points to the server and one that doesn't point to any server, but instead returns canned data so you can test for any possible response, including errors. (The view model should not talk to the server directly, but should do so through an intermediary...
The secret sauce in the above is the materialize operator that wraps error events into a normal event that contains an error object. That way you stop a network error from shutting down the whole system.
In response to the changes in your question... You can simply make the FirebaseAPI conform to ServerAPI:
extension FirebaseAPI: ServerAPI {
func rx_fetchItems(for category: ItemCategory) -> Observable<[Item]> {
// fetched items returns all items in database as Observable<[Item]>
let fetchedItems = self.rx_fireRequest(.fetchItems, ofType: Item.self)
switch category {
case .Local:
let localItems = fetchedItems
.flatMapLatest { [weak self] (itemList) -> Observable<[Item]> in
return self!.rx_localItems(items: itemList)
}
return localItems
// TODO: Handle other cases like RecentlyAdded, Trending, etc..
}
}
// Helper method to filter items for only local items nearby user.
private func rx_localItems(items: [Item]) -> Observable<[Item]> {
return Observable.create { observable in
observable.onNext(items.filter { $0.location == "LA" })
observable.onCompleted()
return Disposables.create()
}
}
}
You should probably change the name of ServerAPI at this point to something like FetchItemsAPI.
You run into a tricky situation here because your observable can throw an error and once it does throw an error the observable sequence errors out and no more events can be emitted. So to handle subsequent network requests, you must reassign taking the approach you're currently taking. However, this is generally not good for driving UI elements such as a collection view because you would have to bind to the reassigned observable every time. When driving UI elements, you should lean towards types that are guaranteed to not error out (i.e. Variable and Driver). You could make your Observable<[Item]> to be let items = Variable<[Item]>([]) and then you could just set the value on that variable to be the array of items that came in from the new network request. You can safely bind this variable to your collection view using RxDataSources or something like that. Then you could make a separate variable for the error message, let's say let errorMessage = Variable<String?>(nil), for the error message that comes from the network request and then you could bind the errorMessage string to a label or something like that to display your error message.