iosswiftrx-swiftrx-cocoarx-binding

Handle Connection Error in UITableView Binding (Moya, RxSwift, RxCocoa)


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()
    }

enter image description here


Solution

  • 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.