0
votes

I've been trying to understand rxSwift. I faced with request problem and want to implement this in a good way. Currently, I'm using this code:

enum RequestState<T> {
    case loading
    case loaded(T)
    case error(Error)
}

struct Response<T: Decodable>: Decodable {
    let data: T
    let error: ResponseError?
}

searchBar.rx.text.asObservable()
    .flatMap { self.provider.rx.request(Request(query: $0)) }
    .map({ RequestState<Response<Bool>>.loaded($0) })
    .asDriver(onErrorRecover: { error in
        return Driver.just(.error(error))
    })
    .startWith(.loading)
    .drive(onNext: { state in
        switch state {
        case .loading: ()
        case .loaded(let response): ()
        case .error(let error): ()
        }
    })
    .disposed(by: disposeBag)

This works good but not too convenient to work with data and request state. I saw in rxSwift demo project following code.

struct RequestState<T: Decodable> {
    let isLoading: Bool
    let data: T
    let error: ResponseError?
}

let state = viewModel.requestMethod()

state
    .map { $0.isLoading }
    .drive(self.loaderView.isOffline)
    .disposed(by: disposeBag)

state
    .map { $0.data }
    .drive(tableView.rx.items(dataSource: dataSource))
    .disposed(by: disposeBag)

state
    .map { $0.error }
    .drive(onNext: { error in
        showAlert(error)
    })
    .disposed(by: disposeBag)

And my problem in the following method, I can't understand Rx magic here:

func requestMethod() -> Driver<RequestState> {
    // supper code
}

Can someone advise me what I have to do here?

1
What demo project did you see this in? - Daniel T.

1 Answers

1
votes

Here's what I ended up with while looking at both of your code samples:

First here is the point of use:

    let request = searchBar.rx.text
        .unwrap()
        .map { URLRequest.search(forQuery: $0) }

    let networkRequest = createRequest(forType: MyType.self)
    let state = request
        .flatMap(networkRequest)

    state
        .map { $0.isLoading }
        .bind(to: loaderView.isOffline)
        .disposed(by: bag)

    state
        .map { $0.data }
        .unwrap()
        .bind(to: tableView.rx.items(dataSource: dataSource))
        .disposed(by: bag)

    state
        .map { $0.error }
        .unwrap()
        .subscribe(onNext: showAlert)
        .disposed(by: bag)

Here is the support code for the above:

enum RequestState<T> {
    case loading
    case loaded(T)
    case error(Error)

    var isLoading: Bool {
        guard case .loading = self else { return false }
        return true
    }

    var data: T? {
        guard case let .loaded(t) = self else { return nil }
        return t
    }

    var error: Error? {
        guard case let .error(e) = self else { return nil }
        return e
    }
}

You will see that the above RequestState enum is a bit of an amalgamation of both RequestState types you showed in your example. The enum makes it easy to create the object while the computed properties make it easy to extract the information.

func createRequest<T>(forType type: T.Type, session: URLSession = URLSession.shared) -> (URLRequest) -> Observable<RequestState<T>> where T: Decodable {
    return { request in
        return Observable.create { observer in
            observer.onNext(.loading)
            let disposable = session.rx.data(request: request)
                .subscribe { event in
                    switch event {
                    case let .error(error):
                        observer.onNext(.error(error))
                    case let .next(data):
                        do {
                            let item = try JSONDecoder().decode(type, from: data)
                            observer.onNext(.loaded(item))
                        }
                        catch {
                            observer.onNext(.error(error))
                        }
                    case .completed:
                        observer.onCompleted()
                    }
            }
            return Disposables.create([disposable])
        }
    }
}

The above is a factory function. You use it to create a function that knows how to make network requests for the appropriate type. Recall in the code where it's being used I had let networkRequest = createRequest(forType: MyType.self). This line produces a function networkRequest that takes a URLRequest and returns an Observable that has been specialized for the type in question.

When the Observable from networkRequest is subscribed to, it will immediately push out a .loading case, then make the request. Then it will use the response to either push out a .loaded(T) or an .error(Error) depending on the results.

Personally, I'm more inclined to use something like the ActivityIndicator system from the examples in the RxSwift repository instead.