Search code examples
rx-swift

RxSwift how to skip map depending on previous result?


I am trying to download some json, parse it, check some information in the json and depending one the result continue processing or not.

What's the most RxSwift idiomatic way of doing this?

        URLSession.shared.rx
            .data(request:request)
            .observe(on: ConcurrentDispatchQueueScheduler(qos: .background))
            .flatMap(parseJson) // into ModelObject
            .flatMap(checkModel) // on some condition is there any way to jump into the onCompleted block? if the condition is false then execute processObject
            .map(processObject)
            .subscribe(
                onError: { error in
                print("error: \(error)")
            }, onCompleted: {
                print("Completed with no error")
            })
            .disposed(by: disposeBag)

where parseJsonis something like:

func parseJson(_ data: Data) -> Single<ModelObject>

checkModel does some checking and if some conditions are fullfilled should complete the sequence without ending in processObject

func checkModel(_ modelObject: ModelObject) -> Single<ModelObject> {
  //probably single is not what I want here
}

And finally processObject

func processObject(_ modelObject: ModelObject) -> Completable {
}

Solution

  • This is a bit of a tough question to answer because on the one hand you ask a bog simple question about skipping a map while on the other hand you ask for "most RxSwift idiomatic way of doing this," which would require more changes than simply jumping the map.

    If I just answer the basic question. The solution would be to have checkModel return a Maybe rather than a Single.


    Looking at this code from a "make it more idiomatic" perspective, a few more changes need to take place. A lot of what I'm about to say comes from assumptions based on the names of the functions and expectations as to what you are trying to accomplish. I will try to call out those assumptions as I go along...

    The .observe(on: ConcurrentDispatchQueueScheduler(qos: .background)) is likely not necessary. URLSession already emits on the background.

    The parseJson function probably should not return an Observable type at all. It should just return a ModelObject. This assumes that the function is pure; that it doesn't perform any side effect and merely transforms a Data into a ModelObject.

    func parseJson(_ data: Data) throws -> ModelObject
    

    The checkModel function should probably not return an Observable type. This really sounds like it should return a Bool and be used to filter the model objects that don't need further processing out. Here I'm assuming again that the function is pure, it doesn't perform any side-effect, it just checks the model.

    func checkModel(_ modelObject: ModelObject) -> Bool
    

    Lastly, the processObject function presumably has side effects. It's likely a consumer of data and therefore shouldn't return anything at all (i.e., it should return Void.)

    func processObject(_ modelObject: ModelObject)
    

    Udpdate: In your comments you say you want to end with a Completable. Even so, I would not want this function to return a completable because that would make it lazy and thus require you to subscribe even when you just want to call it for its effects.

    You can create a generic wrap operator to make any side-effecting function into a Completable:

    extension Completable {
        static func wrap<T>(_ fn: @escaping (T) -> Void) -> (T) -> Completable {
            { element in
                fn(element)
                return Completable.empty()
            }
        }
    }
    

    If the above functions are adjusted as discussed above, then the Observable chain becomes:

    let getAndProcess = URLSession.shared.rx.data(request:request)
        .map(parseJson)
        .filter(checkModel)
        .flatMap(Completable.wrap(processObject))
        .asCompletable()
    

    The above will produce a Completable that will execute the flow every time it's subscribed to.

    By setting things up this way, you will find that your base functions are far easier to test. You don't need any special infrastructure, not even RxText to make sure they are correct. Also, it is clear this way that parseJson and checkModel aren't performing any side effects.

    The idea is to have a "Functional Core, Imperative Shell". The imperative bits (in this case the data request and the processing) are moved out to the edges while the core of the subscription is kept purely functional and easy to test/understand.