I'm trying to learn ReactiveSwift and ReactiveCocoa. I can use Signal
and Property
pretty well, but I'm having trouble with SignalProducer
.
As I understand it, SignalProducer
is ideal for things like network requests. I set up my API layer to create and return a signal provider, which the caller can start.
class ApiLayer {
func prepareRequest(withInfo info: RequestInfo) -> SignalProducer<ModelType, ErrorType> {
return SignalProducer<ModelType, ErrorType> { (observer, lifetime) in
// Send API Request ...
// In Completion Handler:
let result = parseJson(json)
observer.send(value: result)
observer.sendCompleted()
}
}
}
But how am I supposed to listen for the results?
I've tried something like this, but I get an error, so I must be doing / thinking about this wrong.
apiLayer.prepareRequest(withInfo: info)
.startWithValues { (resultModel) in
// Do Stuff with result ...
}
Here's the error I get:
Ambiguous reference to member 'startWithValues'
- Found this candidate (ReactiveSwift.SignalProducer< Value, NoError >)
- Found this candidate (ReactiveSwift.SignalProducer< Never, NoError >)
EDIT
I tried being more explicit to help the compiler identify the proper method, like so. But the error still remains.
apiLayer.prepareRequest(withInfo: info)
.startWithValues { (resultModel: ModelType) in // Tried adding type. Error remained.
// Do Stuff with result ...
}
EDIT 2
After getting help over at the GitHub support page and thinking about the provided answer here, here's what I ended up with.
One key difference from my earlier attempts is that the caller doesn't manually start the returned SignalProducer
. Rather, by creating it inside/in response to another signal, it's implicitly started inside the chain.
I had previously (incorrectly) assumed that it was necessary to extract and explicitly subscribe to the Signal
that a SignalProducer
"produced".
Instead, I now think about SignalProducer
s simply as deferred work that is kickstarted in response to stimuli. I can manually subscribe to the SignalProvider
or I can let another Signal
provide that stimulus instead. (The latter used in my updated sample below. It seems fairly clean and much more FRP-esque than manually starting it, which I had carried over from my imperative mindset.)
enum ErrorType: Error {
case network
case parse
}
class ApiLayer {
func prepareRequest(withInfo info: RequestInfo) -> SignalProducer<ModelType, ErrorType> {
let producer = SignalProducer<ResultType, NoError> { (observer, lifetime) in
sendRequest(withInfo: info) { result in
observer.send(value: result)
observer.sendCompleted()
}
}
return producer
.attemptMap { result throws -> ResultType in
let networkError: Bool = checkResult(result)
if (networkError) {
throw ErrorType.network
}
}
.retry(upTo: 2)
.attemptMap { result throws -> ModelType in
// Convert result
guard let model: ModelType = convertResult(result) else {
throw ErrorType.parse
}
return model
}
// Swift infers AnyError as some kind of error wrapper.
// I don't fully understand this part yet, but to match the method's type signature, I needed to map it.
.mapError { $0.error as! ErrorType}
}
}
// In other class/method
// let apiLayer = ApiLayer(with: ...)
// let infoSignal: Signal<RequestInfo, NoError> = ...
infoSignal
.flatMap(.latest) { (info) in
apiLayer.prepareRequest(withInfo: info)
}
.flatMapError { error -> SignalProducer<ModelType, NoError> in
// Handle error
// As suggested by the ReactiveSwift documentation,
// return empty SignalProducer to map/remove the error type
return SignalProducer<ModelType, NoError>.empty
}
.observeValues { model in
// Do stuff with result ...
}
ReactiveSwift
's philosophy is that it shouldn't be easy for users to ignore errors. So startWithValues
is only available if the producer's error type is NoError
, which ensures that no error can ever be sent. If your producer can send an error, you need to use a function like startWithResult
which will allow you to handle it:
apiLayer.prepareRequest(withInfo: info).startWithResult { result in
switch result {
case let .success(model):
// Do stuff with model
case let .failure(error):
// Handle error
}
}
Ignoring errors isn't a good idea but in some cases, it's possible to treat them as nil values with such an extension:
public extension SignalProducer {
func skipErrors() -> SignalProducer<Value?, NoError> {
return self
.flatMap(.latest, { SignalProducer<Value?, NoError>(value: $0) })
.flatMapError { _ in SignalProducer<Value?, NoError>(value: nil) }
}
}
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