Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handling Network error in combination with binding to tableView (Moya, RxSwift, RxCocoa)

I am currently using Moya to make my network requests. I have implemented the following from one of the example projects @ https://github.com/DroidsOnRoids/RxSwiftExamples#tutorials

Below I have set up restaurantSearch so that when someone enters text it makes a new request.

var restaurantSearch: Observable<(String)> {
    return searchBar
        .rx_text
        .throttle(0.5, scheduler: MainScheduler.instance)
        .distinctUntilChanged()
}

I have a method that returns an observable of type [Restaurant]

func restaurants() -> Observable<[Restaurant]> {
    return restaurantSearch
        .observeOn(MainScheduler.instance)
        .flatMapLatest { postcode -> Observable<[Restaurant]?> in
            return self.getRestaurants(postcode, cuisine: "", restaurantName: "")
        }.replaceNilWith([])
}

internal func getRestaurants(postcode: String, cuisine: String, restaurantName: String) -> Observable<[Restaurant]?> {
    return self.justEatProvider
        .request(.Restaurant(postcode, cuisine, restaurantName))
        .debug()
        .mapArrayOptional(Restaurant.self, keyPath: "Restaurants")
}

I am calling this method and binding it to a tableView like so:

func setupRx() {

    api = JustEatApi(provider: provider, restaurantSearch: restaurantSearch)

    api
        .restaurants()
        .bindTo(tableView.rx_itemsWithCellIdentifier("RestaurantTableViewCell", cellType: RestaurantTableViewCell.self)) { (row, element, cell) in
            cell.restaurant = element
        }
        .addDisposableTo(disposeBag)
}

This is working fine. If I enter a postcode it does the search and the tableView is populated.

If I turn off the internet and try and change the postcode the tableView remains as is. However when I scroll it crashes my app with the following:

@noreturn func rxFatalError(lastMessage: String) {
    // The temptation to comment this line is great, but please don't, it's for your own good. The choice is yours.
    fatalError(lastMessage)
}

Also if I don't scroll but instead just turn back on the internet and change the postcode nothing happens. It seems like it has lost it's binding.

Firstly I tried adding catchOnError before the bindTo method call but I read here in comment that it shouldn't be handled as part of UIBinding: http://blog.scottlogic.com/2014/05/11/reactivecocoa-tableview-binding.html

I am guessing I should handle it in the method:

func restaurants() -> Observable<[Restaurant]> {
    return restaurantSearch
        .observeOn(MainScheduler.instance)
        .flatMapLatest { postcode -> Observable<[Restaurant]?> in
            return self.getRestaurants(postcode, cuisine: "", restaurantName: "")
        }.replaceNilWith([])
}

So I have 2 questions:

1) Where and how should I handle a network error?

2) Why does the tableView not update after I turn back on the internet?

Any help much appreciated.

like image 962
pls Avatar asked Apr 28 '16 21:04

pls


2 Answers

Just add catchErrorJustReturn([]) before binding your table.

like image 189
dfmarulanda Avatar answered Nov 19 '22 20:11

dfmarulanda


I personally handle network errors in services that make/parse network requests. Good way to do that would be to wrap your network results in some kind of enum (similar to optional, but with error if nil). So you would have something like:

enum APIResult<T> {
    case Success(T)
    case Error(ErrorType)
}

And then your services would return something like this

Observable<APIResult<[Restaurant]>>

If you use MVVM architecture you would then filter only successful results in view model and provide that data to view controller.

Also you should take a look at Driver unit. It is unit that is specifically made for UI bindings, so it subscribes on Main thread only, never errors and shares last result.

To translate Observable to Driver you use one of the three methods:

func asDriver(onErrorJustReturn onErrorJustReturn: Self.E) -> RxCocoa.Driver<Self.E>
func asDriver(onErrorDriveWith onErrorDriveWith: RxCocoa.Driver<Self.E>) -> RxCocoa.Driver<Self.E>
func asDriver(onErrorRecover onErrorRecover: (error: ErrorType) -> RxCocoa.Driver<Self.E>) -> RxCocoa.Driver<Self.E>

This approach would automatically deal with your second question, because when Observable emits an error, all subscribers unsubscribe, ie. it terminates the stream. Try to put .debug() behind your api.restaurants() call and see by yourself that it unsubscribes.

You can read more about Driver and other units here

like image 11
muvaaa Avatar answered Nov 19 '22 20:11

muvaaa