Search code examples
swiftswift2reactive-programmingreactive-cocoareactive-cocoa-3

How do I observe a signal and immediately receive a `next` event if it has already occured?


I'm trying to wrap an API call that initializes an object after a network request. I don't want the network request to happen for every new observer, so as I understand it, I shouldn't be using SignalProducer. However, by using a single Signal, only the first usage of it will receive a next event, while any newer subscribers will never receive the current value. How should I be doing this? I'm probably doing something fundamentally wrong with RAC.

extension SparkDevice {
    static func createMainDeviceSignal() -> Signal<SparkDevice, NSError> {
        return Signal {
            sink in
            SparkCloud.sharedInstance().getDevices { (sparkDevices: [AnyObject]!, error: NSError!) -> Void in
                if let error = error {
                    sink.sendFailed(error)
                }
                else {
                    if let devices = sparkDevices as? [SparkDevice] {
                        if devices.count > 0 {
                            sink.sendNext(devices[0])
                        }
                    }
                }
            }
            return nil
        }
    }
}

class DeviceAccess {
    let deviceSignal: Signal<SparkDevice, NSError>

    init() {
        self.deviceSignal = SparkDevice.createMainDeviceSignal()
    }
 }

I considered using MutableProperty, but that seems to require a default property, which doesn't seem to make sense for this.

How should I actually be going about this?


Solution

  • What you need is multicasting. However, ReactiveCocoa 3/4 does not offer a simple way to do that (as opposed to Rx) because they often lead to a ton of complexity.

    Sometimes it is really necessary, as in your example, and it can be easily implemented using a PropertyType.

    I'd start by creating the cold signal that makes the request. That has to be a SignalProducer:

    private func createMainDeviceSignalProducer() -> SignalProducer<SparkDevice, NSError> {
        return SignalProducer { observer, _ in
            ....
        }
    }
    

    If you were to expose this as is, the side effects would occur every time this producer is started. To multicast these values you can wrap it in a property and expose the property's producer instead:

    public final class DeviceAccess {
        public let deviceProducer: SignalProducer<SparkDevice, NoError>
        private let deviceProperty: AnyProperty<SparkDevice?>
    
        init() {
            self.deviceProperty = AnyProperty(
               initialValue: nil, // we can use `nil` to mean "value isn't ready yet"         
               producer: SparkDevice.createMainDeviceSignal()
                            .map(Optional.init) // we need to wrap values into `Optional` because that's the type of the property
                            .flatMapError { error in 
                                  fatalError("Error... \(error)") // you'd probably want better error handling
    
                                  return .empty // ignoring errors, but you probably want something better.
                            }
            )
    
            self.deviceProducer = deviceProperty
                  .producer    // get the property producer
                  .ignoreNil() // ignore the initial value
        }
     }
    

    Now DeviceAccess.deviceProducer would replay the values emitted by the underlying producer instead of repeating side-effects. Note however that it's not lazy: the underlying SignalProducer would be started right away.