Core Reactive Primitives

  1. Signal
  2. Event
  3. SignalProducer
  4. Property
  5. Action
  6. Lifetime

Signal: a unidirectional stream of events.

The owner of a Signal has unilateral control of the event stream. Observers may register their interests in the future events at any time, but the observation would have no side effect on the stream or its owner.

A Signal is like a live TV feed — you can observe and react to the content, but you cannot have a side effect on the live feed or the TV station.

let channel: Signal<Program, Never> = tvStation.channelOne
channel.observeValues { program in ... }

See also: The Signal overview, The Signal contract, The Signal API reference

Event: the basic transfer unit of an event stream.

A Signal may have any arbitrary number of events carrying a value, followed by an eventual terminal event of a specific reason.

An Event is like a frame in a one-time live feed — seas of data frames carry the visual and audio data, but the feed would eventually be terminated with a special frame to indicate “end of stream”.

See also: The Event overview, The Event contract, The Event API reference

SignalProducer: deferred work that creates a stream of values.

SignalProducer defers work — of which the output is represented as a stream of values — until it is started. For every invocation to start the SignalProducer, a new Signal is created and the deferred work is subsequently invoked.

A SignalProducer is like an on-demand streaming service — even though the episode is streamed like a live TV feed, you can choose what you watch, when to start watching and when to interrupt it.

let frames: SignalProducer<VideoFrame, ConnectionError> = vidStreamer.streamAsset(id: tvShowId)
let interrupter = frames.start { frame in ... }
interrupter.dispose()

See also: The SignalProducer overview, The SignalProducer contract, The SignalProducer API reference

Property: an observable box that always holds a value.

Property is a variable that can be observed for its changes. In other words, it is a stream of values with a stronger guarantee than Signal — the latest value is always available, and the stream will never fail.

A Property is like the continuously updated current time offset of a video playback — the playback is always at a certain time offset at any time, and it would be updated by the playback logic as the playback continues.

let currentTime: Property<TimeInterval> = video.currentTime
print("Current time offset: \(currentTime.value)")
currentTime.signal.observeValues { timeBar.timeLabel.text = "\($0)" }

See also: The Property overview, The Property contract, The property API reference

Action: a serialized worker with a preset action.

When invoked with an input, an Action applies the input and the latest state to the preset action, and pushes the output to any interested parties.

An Action is like an automatic vending machine — after inserting coins and choosing an option, the machine processes the order and eventually outputs your desired snack. Notice that you cannot have the machine serve two customers concurrently.

// Purchase from the vending machine with a specific option.
vendingMachine.purchase
    .apply(snackId)
    .startWithResult { result
        switch result {
        case let .success(snack):
            print("Snack: \(snack)")

        case let .failure(error):
            // Out of stock? Insufficient fund?
            print("Transaction aborted: \(error)")
        }
    }

// The vending machine.
class VendingMachine {
    let purchase: Action<Int, Snack, VendingMachineError>
    let coins: MutableProperty<Int>

    // The vending machine is connected with a sales recorder.
    init(_ salesRecorder: SalesRecorder) {
        coins = MutableProperty(0)
        purchase = Action(state: coins, enabledIf: { $0 > 0 }) { coins, snackId in
            return SignalProducer { observer, _ in
                // The sales magic happens here.
                // Fetch a snack based on its id
            }
        }

        // The sales recorders are notified for any successful sales.
        purchase.values.observeValues(salesRecorder.record)
    }
}

See also: The Action overview, The Action API reference

Lifetime: limits the scope of an observation

It doesn’t make sense for a Signal or SignalProducer to continue emitting values if there are no longer any observers. Consider the video stream: once you stop watching the video, the stream can be automatically closed by providing a Lifetime:

class VideoPlayer {
  private let (lifetime, token) = Lifetime.make()

  func play() {
    let frames: SignalProducer<VideoFrame, ConnectionError> = ...
    frames.take(during: lifetime).start { frame in ... }
  }
}

See also: The Lifetime overview, The Lifetime API reference