What is the most elegant way to transform my ReactiveSwift's SignalProducer<A, NetworkError>
into a Signal<A, NoError>
?
Most of the time, my signal producer is the result of a network call, so I want to split the results into two cases:
Signal<A, NoError>
Signal<String, NoError>
with the error's localized description(why? because i'm trying to be as MVVM as possible)
So far, I end up writing a lot of boilerplate like the following:
let resultsProperty = MutableProperty<SearchResults?>(nil)
let alertMessageProperty = MutableProperty<String?>(nil)
let results = resultsProperty.signal // `Signal<SearchResults?, NoError>`
let alertMessage = alertMessageProperty.signal // `Signal<String?, NoError>`
// ...
searchStrings.flatMap(.latest) { string -> SignalProducer<SearchResults, NetworkError> in
return MyService.search(string)
}
.observe { event in
switch event {
case let .value(results):
resultsProperty.value = results
case let .failed(error):
alertMessageProperty.value = error
case .completed, .interrupted:
break
}
}
ie:
MutableProperty
instances, that I have to set as optional to be able to initialize themAny help on (A) keeping my signals non optional and (B) splitting them into 2 NoError
signals elegantly would be greatly appreciated.
I will try to answer all your questions / comments here.
The errors = part doesn't work as flatMapError expects a SignalProducer (ie your sample code works just because searchStrings is a Signal string, which coincidently is the same as the one we want for errors: it does not work for any other kind of input)
You are correct, this is because flatMapError
does not change the value
type. (Its signature is func flatMapError<F>(_ transform: @escaping (Error) -> SignalProducer<Value, F>) -> SignalProducer<Value, F>
). You could add another call to map
after this if you need to change it into another value type.
the results = part behaves weirdly as it terminates the signal as soon as an error is met (which is a behavior I don't want) in my real-life scenario
Yes, this is because the flatMap(.latest)
forwards all errors to the outer signal, and any error on the outer signal will terminate it.
Okay so here's an updated version of the code, with the extra requirements that
errors
should have different type than searchStrings
, let's say Int
MyService.search($0)
will not terminate the flowI think the easiest way to tackle both these issues is with the use of materialize()
. What it does is basically "wrap" all signal events (new value, error, termination) into a Event
object, and then forward this object in the signal. So it will transform a signal of type Signal<A, Error>
into a Signal<Event<A, Error>, NoError>
(you can see that the returned signal does not have an error anymore, since it is wrapped in the Event
).
What it means in our case is that you can use that to easily prevent signals from terminating after emitting errors. If the error is wrapped inside an Event
, then it will not automatically terminate the signal who sends it. (Actually, only the signal calling materialize()
completes, but we will wrap it inside the flatMap
so the outer one should not complete.)
Here's how it looks like:
// Again, I assume this is what you get from the user
let searchStrings: Signal<String, NoError>
// Keep your flatMap
let searchResults = searchStrings.flatMap(.latest) {
// Except this time, we wrap the events with `materialize()`
return MyService.search($0).materialize()
}
// Now Since `searchResults` is already `NoError` you can simply
// use `filterMap` to filter out the events that are not `.value`
results = searchResults.filterMap { (event) in
// `event.value` will return `nil` for all `Event`
// except `.value(T)` where it returns the wrapped value
return event.value
}
// Same thing for errors
errors = searchResults.filterMap { (event) in
// `event.error` will return `nil` for all `Event`
// except `.failure(Error)` where it returns the wrapped error
// Here I use `underestimatedCount` to have a mapping to Int
return event.error?.map { (error) in
// Whatever your error mapping is, you can return any type here
error.localizedDescription.characters.count
}
}
Let me know if that helps! I actually think it looks better than the first attempt :)
Do you need to access the state of you viewModel or are you trying to go full state-less? If state-less, you don't need any properties, and you can just do
// I assume this is what you get from the user
let searchStrings: Signal<String, NoError>
// Keep your flatMap
let searchResults = searchStrings.flatMap(.latest) {
return MyService.search($0)
}
// Use flatMapError to remove the error for the values
results = searchResults.flatMapError { .empty }
// Use flatMap to remove the values and keep the errors
errors = searchResults.filter { true }.flatMapError { (error) in
// Whatever you mapping from error to string is, put it inside
// a SignalProducer(value:)
return SignalProducer(value: error.localizedDescription)
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With