Leave a rating/review
The first type of operators we’ll focus on are Transforming operators. This style of operator is the most common, and is used to manipulate values coming from publishers into a format that is suitable for subscribers downstream. As we discuss this type of operator, you’ll see many similarities (and differences!) with operators from the Swift standard library such as map and flatMap.
Let’s start with the collect operator. This operator takes a stream of individual values from a publisher and turns them into an array of values. The marble diagram you see on screen here demonstrates how this operator works. The top line represents the publisher, the box in the middle represents the operator - which we’ve labeled collect(), and the bottom line represents the subscriber, or more specifically what a subscriber would receive from the operator.
Here, the individual values 1, 2, and 3 have been collected into an array of integers, with 1, 2, and 3 inside it. Remember that since operators can act as subscribers as well as publishers, the subscriber represented by the bottom line could be another operator.
Inside the starter playground for this part of the course, add this code:
example(of: "collect") {
["A", "B", "C", "D", "E"].publisher
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
.store(in: &subscriptions)
}
This set of code initializes the array of strings, creates a publisher from it, and then subscribes to the publisher with a sink subscriber. If you run it now, you’ll see that it simply outputs the array elements, one by one.
There’s no operator in the pipeline, so let’s add a collect() operator
["A", "B", "C", "D", "E"].publisher
.collect()
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
.store(in: &subscriptions)
You’ll see this code block style repeated throughout the course - a publisher, one or more operators, with a sink subscriber and a store at the end of the pipeline.
Running the playground now, you can see that the values are collected into a single array, which is received by the sink subscriber, and printed to the console.
You may have noticed that there is nothing that restricts the number of items that the collect operator can work on, so be careful with large datasets - you could run into memory issues. If you want to limit the number of items that get collected you can use something like collect(2). Let’s update our code and see what happens.
Since collect is now limited to only collect 2 items, several arrays get generated, including a partial one with only one member, since the publisher ran out of values, and collect did the best it could with the values given.
Sometimes you’ll want to transform the incoming data from a publisher in some way before sending it on to a subscriber. That’s where map comes into play. The marble diagram shows that values coming into the operator, represented by the $0 in the closure body of the operator, get manipulated and sent to the subscriber. For every value sent by the publisher, there is a corresponding mapped value sent to the subscriber. Let’s look at that in a playground.
Add the following to your playground
example(of: "map") {
// 1
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
// 2
[123, 4, 56].publisher
// 3
.map {
formatter.string(for: NSNumber(integerLiteral: $0)) ?? ""
}
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
}
Here you create a number formatter (which will spell out the numbers into a string). Then you have an array of numbers and genereate a publisher from it.Finally you inser a map operator between the publisher and the sink subscriber. The body of the map operator is a closure that takes in the passed in value and processes it through the number formatter.
When you run the playground, you see a list of values - one for each value in the original array - representing the string versions of the numbers.
What happens if you experience a nil in your upstream values? You could use something like the nil-coalescing operator to provide a default, but you can use an operator instead - replaceNil, in fact. The marble diagram here shows that anytime a nil is encountered in the publisher stream, it’s replaced with the value specified in the operator’s closure. Let’s take a look at that in an example.
Build the usual publisher-operator-sink block with an arry of strings, one of which is a nil, acting as the publisher. For the operator, use replaceNil(with:) and pass in a string with a dash character as the argument.
example(of: "replaceNil") {
// 1
["A", nil, "C"].publisher
.replaceNil(with: "-") // 2
.sink(receiveValue: { print($0) }) // 3
.store(in: &subscriptions)
}
Running the pipeline shows that the nil has been replaced with the dash, giving you a result of A-C. Notice that the Playground shows a warning
After calling replaceNil we can safely assume that each of the values is not nil, so we can explictly unwrap it before printing it, using a simple map before the sink statement
example(of: "replaceNil") {
["A", nil, "C"].publisher
.replaceNil(with: "-")
.map { $0! }
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
}
Now the warning is gone.
replaceEmpty handles the case where the publisher fails to emit any values before completing. In this case, replaceEmpty inserts the specified value into the pipeline for the downstream consumer to use. Note here that your publisher must send a completion event for this operator to work - otherwise it doesn’t know if the publisher is done emitting values!
For this example, use a special publisher - Empty, which has a Int value and never returns an error.
example(of: "replaceEmpty(with:1)") {
// 1
let empty = Empty<Int, Never>()
The operator use in our publisher-operator-sink block is replaceEmpty, with an argument of 1
// 2
empty
.replaceEmpty(with: 1)
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
.store(in: &subscriptions)
}
Running the pipeline, you see that the 1 gets inserted into the pipeline and emitted. If you comment that operator out, no values get emitted.
In this episode, we took a look at some of basic transforming operators Combine has to offer: collect takes individual values from a publisher and transforms them into an array of those values, and map transforms values from the publisher based on a closure you pass into the map operator, sending those transformed values downstream, replaceNil and replaceEmpty replace nils and empty publishers with specified values
In the next episode we’ll up the complexity a bit and introduce scan and flatMap into your Combine toolbelt.