4

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'

  1. Found this candidate (ReactiveSwift.SignalProducer< Value, NoError >)
  2. 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 ...
    }
ABeard89
  • 911
  • 9
  • 17

2 Answers2

8

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
    }
}
jjoelson
  • 5,771
  • 5
  • 31
  • 51
  • 1
    I also asked this on the GitHub support page [here](https://github.com/ReactiveCocoa/ReactiveSwift/issues/574): andersio correctly pointed out that `startWithResult` is unavailable because of the error type. And now I see that `extension ... where` clause in the source. But thank you for pointing out the philosophy of explicitly handling and transforming errors. I was trying to work on success cases before going back and handling the error cases, so I didn't know **why** it was set up that way. – ABeard89 Dec 15 '17 at 02:04
  • Also, I just didn't actually understand how to use signal providers. Thanks to your and andersio's comments, I'm starting to understand the correct way to think about `Signal`s, `SignalProvider`s, and FRP in general. – ABeard89 Dec 15 '17 at 02:07
2

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) }
    }
}
Vladimir Vlasov
  • 1,860
  • 3
  • 25
  • 38