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