Swift — 11 Useful Combine Operators You Need to Know

It’s time to discover how to manipulate the data in your Combine code
Apr 19 2022 · 6 min read

Background

Hello guys, You may already be aware of the Combine framework which provides a declarative Swift API for processing values over time.

Today we will explore the 11 very important Combine operators with examples and graphical representation.

We are what we repeatedly do. Excellence, then, is not an act, but a habit. Try out Justly and start building your habits today!

What is an Operator in Combine?

Combine declares publishers to expose values that can change over time, and subscribers to receive those values from the publishers. And the methods that perform an operation on values coming from a publisher are called operators.

Each operator returns a publisher that, receives the values, manipulates them, and then sends them to its subscribers.

Let’s get started!

1. Prepend

This operator prepends or adds values at the beginning of your original publisher.

For example, here we have an array publisher and are performing a prepend operation to add value at the beginning of the array.

var subscriptions = Set<AnyCancellable>()
let publisher = [5, 6].publisher
publisher
    .prepend(3, 4)
    .sink(receiveValue: { print($0) }
    .store(in: &subscriptions)

Output:
3, 4, 5, 6

Here, the result must be the same type as the items i.e if you have the publisher of Integer, you can not get String as a result.

Similarly, we have the Append operator which works the same as Prepend with the difference that it appends values after the original publisher.

2. ReplaceNil

It replaces nil items with the values we specify.

For example, we have one publisher that contains a nil value and we want to replace that nil value with our defined custom value.

[1, 2, nil, 6].publisher
    .replaceNil(with: 5)
    .sink(receiveValue: { print($0!) }
    .store(in: &subscriptions)

Output:
1, 2, 5, 6

As a result, we can see that the nil value is replaced with the given number 5.

An important difference between ?? and replaceNil(with:) is that the former can return another optional, while the latter can’t.

3. switchToLatest

It is a more powerful and complex operator as it switches the entire publisher subscriptions stream along with canceling the pending publisher subscription to the latest one as a single stream.

We can only use it on publishers that themselves emit publishers.

Let’s understand it with an example.

let subject1 = PassthroughSubject<Int, Never>()
let subject2 = PassthroughSubject<Int, Never>()
let subject3 = PassthroughSubject<Int, Never>()
let subjects = PassthroughSubject<PassthroughSubject<Int, Never>, Never>()

subjects.switchToLatest()
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
        
subjects.send(subject1)
subject1.send(1)
subjects.send(subject2)
subject1.send(2)
subject2.send(3)
subject2.send(4)
subjects.send(subject3)
subject2.send(5)
subject2.send(6)
subject3.send(7)

Output:
1
3
4
7

What happens is that each time you send a publisher through publishers, you switch to the new one and cancel the previous subscription.

Consider a real-world example where we have a search text field that is used to detect if an item is available. Once the user inputs something, we want to trigger a request. Our goal is to cancel the previous request if the user has inputted a new value. This can be done with the help of .switchToLatest.

4. merge(with:)

This operator combines two Publishers as if we’re receiving values from just one.

Let’s understand it with an example.

let stringSubject1 = PassthroughSubject<String, Never>()
let stringSubject2 = PassthroughSubject<String, Never>()
stringSubject1
    .merge(with: stringSubject2)
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
stringSubject1.send("Hello")
stringSubject2.send("World")
stringSubject2.send("Good")
stringSubject1.send("Morning")

Output:
Hello
World
Good
Morning

As shown here, we can see that all values print as a single-stream publisher.

Note: Here, it requires that both publishers must be of the same type.

5. CombineLatest

It combines different publishers of different value types. The emitted output is a tuple with the latest values of all publishers whenever any of them emit a value.

Before .combineLatest emits something, each publisher must emit at least a value.

Let’s understand it with an example.

let subject1 = PassthroughSubject<Int, Never>()
let subject2 = PassthroughSubject<String, Never>()

subject1
    .combineLatest(subject2)
    .sink(receiveValue: { print("\($0) = \($1)") })
    .store(in: &subscriptions)
subject1.send(1)
subject1.send(2)
subject2.send("A")
subject2.send("B")
subject1.send(3)
subject2.send("C")

Output:
2 = A
2 = B
3 = B
3 = C

As shown here, the original publisher combines with the latest value of another publisher and makes combinations accordingly.

Consider a real-world example where we have a phone number and country name UITextFields and a button to allow us to go ahead with the process. We want to disable that button until we have enough digits of a phone number and the correct country. We can easily achieve this by using the .combineLatest operator.

6. zip

It works similarly to the .combineLatest but in this case, it emits a tuple of paired values in the same indexes only after each publisher has emitted a value at the current index.

Let’s understand it with an example.

let subject1 = PassthroughSubject<String, Never>()
let subject2 = PassthroughSubject<String, Never>()

subject1
    .zip(subject2)
    .sink(receiveValue: { (string1, string2) in
        print("String1: \(string1), String2: \(string2)")
    })
    .store(in: &subscriptions)
subject1.send("Hello")
subject1.send("World")
subject2.send("Nice")
subject1.send("Cool")
subject2.send("Rock")
subject2.send("Cool")
subject2.send("Awesome")

Output:
String1: Hello, String2: Nice
String1: World, String2: Rock
String1: Cool, String2: Cool

As shown here, only two zipped items are emitted from the resulting publisher as subject1 has only 3 items. That’s why the Awesome value is not printed, because there’s no item to pair it with in subject1.

7. map

As the name suggests, this operator maps or transforms the elements a publisher emits.

It uses a closure to transform each element it receives from the upstream publisher. We can use map(_:) to transform from one kind of element to another.

Let’s understand it with an example.

[10, 20, 30]
   .publisher
   .map { $0 * 2 }
   .sink { print($0) }

Output:
20
40
60

Here, we can see that publisher values are mapped one by one and are multiplied by two as we specified.

8. collect

It is a powerful operator that allows us to receive all items at once from a publisher. It collects all received elements and emits a single array of the collection when the upstream publisher finishes.

Let’s understand it with an example.

[1, 2, 3, 4].publisher
    .collect()
    .sink { (output) in
        print(output)
    }.store(in: &subscriptions)

Output:
[1, 2, 3, 4]

Here, we can see that publisher values are displayed as a single element.

Note: If we don’t use .collect() operator here then it will simply print as a separate element in a new line instead of a single array.

9. Reduce

This operator iteratively accumulates a new value based on the emissions of the upstream publisher.

It returns the publisher that applies the closure to all received elements and produces an accumulated value when the upstream publisher finishes.

Let’s understand it with an example.

let publisher = ["Hello!", " ", "How ", "Are ", "You?"].publisher

publisher
    .reduce("👋🏻 ", +)
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)

Output:
👋🏻 Hello! How Are You?

Here we can see that it reduces the separate array elements in a single element by prepending all values.

10. Debounce

It waits for a specific period from the emission of the last value before emitting the last value received.

For example, we are implementing search in our app and we only want to fire a search query if the user does not type for 500ms, otherwise, we may have too many unwanted query call continuously along with the user type any single character in the search box.

So, we can do this as given in the following example,

let bounces: [(Int, TimeInterval)] = [
    (0, 0),
    (1, 0.25),  // 0.25s interval since last index
    (2, 1),     // 0.75s interval since last index
    (3, 1.25),  // 0.25s interval since last index
    (4, 1.5),   // 0.25s interval since last index
    (5, 2)      // 0.5s interval since last index
]

let subject = PassthroughSubject<Int, Never>()
subject
    .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
for bounce in bounces {
    DispatchQueue.main.asyncAfter(deadline: .now() + bounce.1) {
        subject.send(bounce.0)
    }
}

Output:
1
4
5

Here, some indexes are waiting until the debounce period ends and after its completion, they again start publishing the index.

11. Throttle

Throttle works similar to Debounce, but it waits for the specified interval repeatedly, and at the end of each interval, it will emit either the first or the last of the values depending on what is passed for its latest parameter.

In short, throttle does not pause after receiving values.

Let’s understand it with an example.

let bounces: [(Int, TimeInterval)] = [
    (0, 0), (1, 1), (2, 1.1), (3, 1.2), (4, 1.3), (5, 2)
]

let subject = PassthroughSubject<Int, Never>()
subject
    .throttle(for: .seconds(0.5), scheduler: RunLoop.main, latest: false)
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
for bounce in bounces {
    DispatchQueue.main.asyncAfter(deadline: .now() + bounce.1) {
        subject.send(bounce.0)
    }
}

Output:
0
1
2
5

Here, values of index 2, 3, and 4 are emitted in the given interval, so it took the very first emitted value from three of them which is 2, and then took the next value which is of the next interval.

After each given interval, throttle sends the first value it received during that interval.

Related Popular Article


amisha-i image
Amisha Italiya
iOS developer @canopas | Sharing knowledge of iOS development


amisha-i image
Amisha Italiya
iOS developer @canopas | Sharing knowledge of iOS development

background-image

Get started today

Let's build the next
big thing!

Let's improve your business's digital strategy and implement robust mobile apps to achieve your business objectives. Schedule Your Free Consultation Now.

Get Free Consultation
footer
Subscribe Here!
Follow us on
2024 Canopas Software LLP. All rights reserved.