This is Understanding Combine, written by Matt Neuburg. It is a work in progress. Corrections and suggestions are greatly appreciated (you can comment here). So are donations; please consider keeping me going by funding this work at http://www.paypal.me/mattneub. Or buy my books: the current editions are iOS 13 Programming Fundamentals with Swift and Programming iOS 13. Thank you!


Record

There is one more value publisher to talk about: Record. It is rather like a Sequence publisher, except that it goes one step further; what is hard-coded inside a Record is a sequence of values and a completion. For example:

enum MyError : Error { case tooBig }
let rec = Record(output: [1,2,3,4], completion: .failure(MyError.tooBig))
rec
    .sink(receiveCompletion: {print($0)}, receiveValue: {print($0)})
    .store(in:&self.storage)

That produces exactly the same output as the elaborate flatMap example from the previous section:

1
2
3
4
failure(MyError.tooBig)

Inside a Record publisher is a recording object (Record.Recording). This is actually where the sequence of values and the completion live. The recording object is publicly available through the Record’s recording property, and this means that, having created a Record, we can extract its recording and use it in another Record:

let rec1 = Record(output: [1,2,3,4], completion: .failure(MyError.tooBig))
let recording = rec1.recording
let rec2 = Record(recording: recording)
rec2
    .sink(receiveCompletion: {print($0)}, receiveValue: {print($0)})
    .store(in:&self.storage)
// same result

We can also form a recording interactively — that is, we can actually run a pipeline and form a recording out of the values and completion that pop out the end of the pipeline. To do so, we can call the recording’s receive(_:) (with each successive value) and receive(completion:). Those are both Subscriber methods, but a Recording is not a Subscriber; I find that a little odd. But we can call those methods manually instead; for instance, we can use a sink:

var recording = Record<Int,Error>.Recording()
pipeline
    .sink(receiveCompletion: { recording.receive(completion:$0) }) { 
        recording.receive($0) 
    }
    .store(in:&self.storage)

Our pipeline runs, and the values and completion that it produces are stored in recording, which we can now use to form a Record for purposes of playback. For example, I’ll use the flatMap pipeline that I created earlier:

let pub = (1...10).publisher
    .setFailureType(to: Error.self)
    .flatMap { i -> AnyPublisher<Int,Error> in
        if i < 5 {
            return Just(i).setFailureType(to: Error.self)
                .eraseToAnyPublisher()
        } else {
            return Fail<Int,Error>(error: MyError.tooBig)
                .eraseToAnyPublisher()
        }
    }
var recording = Record<Int,Error>.Recording()
pub.sink(receiveCompletion: { recording.receive(completion:$0) }) { 
    recording.receive($0) 
}.store(in:&self.storage)

We can now embed that recording in a Record and play it back:

Record(recording:recording)
    .sink(receiveCompletion: {print($0)}, receiveValue: {print($0)})
    .store(in:&self.storage)

And sure enough, we get the same results as before:

1
2
3
4
failure(MyError.tooBig)

Actually, we can create the recording and embed it in a Record in a single move, like this:

let pub = // same as before
let rec = Record<Int,Error> { r in
    var recording = Record<Int,Error>.Recording()
    pub
        .sink(receiveCompletion: { recording.receive(completion:$0) }) { 
            recording.receive($0) 
        }.store(in:&self.storage)
    r = recording
}

You can easily imagine that a Record made from a real pipeline would be useful for testing that pipeline, or your app’s usage of it, without actually running the asynchronous operations that constitute the pipeline.


Table of Contents