How to map RxSwift Observable and Result

10,428

Solution 1

Updated:

It's a detailed example, hope this may help:

enum RequestError: Error {
    case unknown
}

func requestToken() -> Observable<String> {

    return Observable.create { observer in

        let success = true

        if success {
            observer.onNext("MyTokenValue")
            observer.onCompleted()
        } else {
            observer.onError(RequestError.unknown)
        }

        return Disposables.create()
    }
}

func requestData(token: String) -> Observable<[String: Any]> {

    return Observable<[String: Any]>.create { observer in

        let success = false

        if success {
            observer.onNext(["uid": 007])
            observer.onCompleted()
        } else {
            observer.onError(RequestError.unknown)
        }

        return Disposables.create()
    }
    .map { (data: [String: Any]) in
        var newData = data
        newData["token"] = token
        return newData
    }
}


requestToken()                      // () -> Observable<String>
    .flatMapLatest(requestData)     // Observable<String> -> Observable<[String: Any]>
    .materialize()                  // Observable<[String: Any]> -> Observable<Event<[String: Any]>>
    .subscribe(onNext: { event in
        switch event {
        case .next(let dictionary):
            print("onNext:", dictionary)
        case .error(let error as RequestError):
            print("onRequestError:", error)
        case .error(let error):
            print("onOtherError:", error)
        case .completed:
            print("onCompleted")
        }
    })
    .disposed(by: disposeBag)

Original:

I think it's much easier to achieve it using materialize() with less extra work:

func requestToken() -> Observable<String> { return .empty() }
func requestData(token: String) -> Observable<NSDictionary> { return .empty() }
enum RequestError: Error {}

requestToken()
    .flatMapLatest(requestData)
    .materialize()
    .subscribe(onNext: { event in
        switch event {
        case .next(let dictionary):
            print("onNext:", dictionary)
        case .error(let error as RequestError):
            print("onRequestError:", error)
        case .error(let error):
            print("onOtherError:", error)
        case .completed:
            print("onCompleted")
        }
    })
    .disposed(by: disposeBag)

Hope this may help.

Solution 2

If you use the built in error system, you can save yourself from having to manually pass the error along and all the switches that would entail. You can cast the error at the end.

I would do something more like this:

// this is necessary to handle adding the token to the dictionary.
extension Dictionary {

    /// An immutable version of update. Returns a new dictionary containing self's values and the key/value passed in.
    func updatedValue(_ value: Value, forKey key: Key) -> Dictionary<Key, Value> {
        var result = self
        result[key] = value
        return result
    }
}

// function signatures, note that they don't return Results anymore.
func requestToken() -> Observable<String> { /*...*/ }
func requestData(withToken: String) -> Observable<[String: Any]> { /*...*/ }

requestToken().flatMapLatest {
    requestData(token: $0)
        .map { $0.updatedValue($0, forKey: "token") }
        .map { .success($0) }
}.catchError {
        Observable.just(.failure($0 as! RequestError))
}

With the above, the end result would be an Observable<Result<[String: Any], RequestError>> just like in your case, but the error handling is much cleaner.

If you can't change the signatures of the two functions you are using then I would do this:

    func throwError<T, U: Error>(result: Result<T, U>) throws -> T {
        switch result {
        case .success(let token):
            return token
        case .failure(let error):
            throw error
        }
    }

    requestToken().map {
        try throwError(result: $0)
    }.flatMapLatest {
        requestData(token: $0)
            .map { try throwError(result: $0) }
            .map { $0.updatedValue($0, forKey: "token") }
    }
    .map { .success($0) }
    .catchError {
        Observable.just(.failure($0 as! RequestError))
    }
Share:
10,428

Related videos on Youtube

Rodrigo Ruiz
Author by

Rodrigo Ruiz

Updated on June 04, 2022

Comments

  • Rodrigo Ruiz
    Rodrigo Ruiz almost 2 years

    I have a quick question:

    • I have a network request that returns Observable<Result<String, RequestError>>, let’s call it requestToken
    • if this request succeeds, I want to use the String (token) to do another request that returns Observable<Result<NSDictionary, RequestError>>, let’s call it requestData
    • when that second request comes back, I wanna merge the token into its dictionary
    • in the end I wanna map from Observable<Result<String, RequestError>> to Observable<Result<NSDictionary, RequestError>>

    How can I achieve that without multiple nested levels in my code?

    This is what I have today:

    requestToken()
        .flatMap({ result -> Observable<Result<NSDictionary, RequestError>> in
            switch result {
            case .success(let token):
                return requestData(token: token).map({ $0.map({ $0 + ["token": token] }) })
            case .failure(let error):
                return Observable.of(.failure(error))
            }
        })
    
  • Rodrigo Ruiz
    Rodrigo Ruiz almost 7 years
    I couldn't find materialize() on my Observable. Is it present on the 3.0 version?
  • beeth0ven
    beeth0ven almost 7 years
    Yes it's available since version 3.4.0 github.com/ReactiveX/RxSwift/releases/tag/3.4.0
  • Rodrigo Ruiz
    Rodrigo Ruiz almost 7 years
    I'm not sure I understand what materialize is for, first, my requestToken returns a Result, not a String, so I can't .flatMapLatest(requestData). Second, I need the token merged with the resulting dictionary of requestData . Could you clarify?
  • beeth0ven
    beeth0ven almost 7 years
    Yes, you see requestToken returns a Result, It's difficult to flatMap with a Result. So my code shows you need to change requestToken to return Observable<String> instead of Observable<Result<String>>. So my code's flow is () -> Observable<String> -> Observable<NSDictionary> -> Observable<Event<NSDictionary>>. And Event is something like Result which has three case next , error, completed. What materialize() dose is change Observable<NSDictionary> to Observable<Event<NSDictionary>>. So you can subscribe to the it.
  • Rodrigo Ruiz
    Rodrigo Ruiz almost 7 years
    But then how will I know if requestToken can fail if it's saying it always returns a String?