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 SignalProducers 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 ... }