Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ReactiveSwift: How to subscribe to SignalProducer?

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 ...
    }
like image 576
ABeard89 Avatar asked Jan 29 '23 01:01

ABeard89


2 Answers

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
    }
}
like image 119
jjoelson Avatar answered Jan 31 '23 22:01

jjoelson


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) }
    }
}
like image 21
Vladimir Vlasov Avatar answered Jan 31 '23 23:01

Vladimir Vlasov