How to concatenate SignalProducers with ReactiveCocoa 5

ReactiveCocoa, and functional programming frameworks in general, are sometimes frowned upon for their steep learning curve. This is an undeniable, yet not insurmountable fact. The nice thing is that ReactiveCocoa is opt-in, and it doesn’t have to be all-encompassing. You can take advantage of the bits you like, without impacting the rest of your code.

Development stack: Swift 3, ReactiveSwift 1 (part of ReactiveCocoa 5)
Example project on Github: github.com/jarrroo/Blog-ConcatenatingSignalProducers

Composition is King

Perhaps one of the most important features of ReactiveCocoa is its opinionated stance on composition. It’s expressed in countless APIs and is spread across the whole framework. While there is clearly a lot more to be said on the subject, let’s focus on SignalProducers for the time being.

Composing SignalProducers often involves the .flatMap() or then() operators, but sometimes it’s more efficient to first collect a list of producers and concatenate them in one go. Consider the following hypothetical example:

let users:[User]
let api:APIClient

// getAvatars's type will be inferred to [SignalProducer<UIImage,APIError>] by
// the Swift compiler
let getAvatars = users.map { user in
    return api.download(url: user.avatarUrl)
}

getAvatars now holds a list of SignalProducers that each will go out and fetch the avatar images, as soon as the producer is started. But how do we actually queue them all up in one fell swoop?

It turns out one of SignalProducer‘s initializers conveniently accepts a list of producers (excerpt from ReactiveSwift’s interface):

/// Creates a producer for a Signal that will immediately send the values
/// from the given sequence, then complete.
///
/// - parameters:
/// - values: A sequence of values that a `Signal` will send as separate
///           `value` events and then complete.
public init<S : Sequence where S.Iterator.Element == Value>(values: S)

When applied to our getAvatars example, it translates into:

let users:[User]
let api:APIClient

let getAvatars = users.map { user in
    return api.download(url: user.avatarUrl)
}
// Ick. Y no type inference?!
let producers:SignalProducer<SignalProducer<UIImage,APIError>, APIError> = SignalProducer(values: getAvatars)

producers.startWithResult { result in
    // Handle results however appropriate
}

As you can see, the values: initializer creates a (fairly scary-looking) “producer of producers”. Due to the current state of Swift, the compiler expects an explicit type declaration here. I’m assuming this behavior is actually a bug, though I haven’t put in the time to verify that [yet].

Bummer.. Thankfully, we can extend SignalProducer like so:

extension SignalProducer {
    static func flatten(_ strategy: FlattenStrategy, producers:[SignalProducer<Value,Error>]) -> SignalProducer<Value,Error> {
        let p = SignalProducer<SignalProducer<Value,Error>,Error>(values: producers)
        return p.flatten(strategy)
    }
}

This extension encapsulates all the explicit typing nonsense by referencing the generic value and error types, so you don’t have to. We can now reformulate our example to be much more concise:

let getAvatars = ...

SignalProducer
  .flatten(.concat, producers: getAvatars)
  .flatMap(.concat) { avatar in
    return processAvatar(avatar) // some kind of image processing
  }
  .startWithResult { result in
    switch result {
      case let .failure(error): // do something with errors
      case let .success(avatar): // do something with the UIImage avatars
    }
  }

As expected, the getAvatar(..) methods will be called in succession, effectively running the requests in a serial queue.