Search code examples
reactive-programmingrx-swift

RxSwift How to split progress and result observables?


I need to make a long async calculation based on a String input and return a big Data instance. I use Single trait to achieve this:

func calculateData(from: String) -> Single<Data>

This example is simple and works. But I also need to track progress — a number between 0 and 1. I'm doing something like this:

func calculateData(from: String) -> Observable<(Float, Data?)>

where I get the following sequence:

next: (0, nil)
next: (0.25, nil)
next: (0.5, nil)
next: (0.75, nil)
next: (1, result data)
complete

I check for progress and data to understand if I have a result, it works, but I feel some strong smell here. I want to separate streams: Observable with progress and Single with a result. I know I can return a tuple or structure with two observables, but I don't like this as well.

How can I achieve this? Is it possible?


Solution

  • What you have is fine although I would name the elements in the tuple

    
    func calculateData(from: String) -> Observable<(percent: Float, data: Data?)>
    
    let result = calculateData(from: myString)
        .share()
    
    result
        .map { $0.percent }
        .subscribe(onNext: { print("percent complete:", $0) }
        .disposed(by: disposeBag)
    
    result
        .compactMap { $0.data }
        .subscribe(onNext: { print("completed data:", $0) }
        .disposed(by: disposeBag)
    

    Another option is to use an enum that either returns percent complete OR the data:

    enum Progress {
        case incomplete(Float)
        case complete(Data)
    }
    
    func calculateData(from: String) -> Observable<Progress>
    

    However, doing that would make it harder to break the Observable up into two streams. To do so, you would have to extend Progress like so:

    extension Progress {
        var percent: Float {
            switch self {
            case .incomplete(let percent):
                return percent
            case .complete:
                return 1
            }
        }
    
        var data: Data? {
            switch self {
            case .incomplete:
                return nil
            case .complete(let data):
                return data
            }
        }
    }
    

    And as you see, doing the above essentially turns the enum into the tuple you are already using. The nice thing about this though is that you get a compile time guarantee that if Data emits, the progress will be 1.

    If you want the best of both worlds, then use a struct:

    struct Progress {
        let percent: Float
        let data: Data?
    
        init(percent: Float) {
            guard 0 <= percent && percent < 1 else { fatalError() }
            self.percent = percent
            self.data = nil
        }
    
        init(data: Data) {
            self.percent = 1
            self.data = data
        }
    }
    
    func calculateData(from: String) -> Observable<Progress>
    

    The above provides the compile time guarantee of the enum and the ease of splitting that you get with the tuple. It also provides a run-time guarantee that progress will be 0...1 and if it's 1, then data will exist.