6 Swift Combine Operators You Should Know

Translation of the article was prepared on the eve of the start of the advanced course “iOS Developer”.


In this article, we’ll look at six useful Combine operators. We’ll do this with examples, experimenting with each one in the Xcode Playground.

The source code is available at the end of the article.

Well, without further ado, let’s get started.

1.prepend

This group of statements allows us to prepend (literally “prepend”) events, values, or other publishers to our original publisher:

import Foundation
import Combine

var subscriptions = Set()

func prependOutputExample() {
    let stringPublisher = ["World!"].publisher
    
    stringPublisher
        .prepend("Hello")
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
}

Result: Hello and World! are output in sequential order:

Now let’s add another publisher of the same type:

func prependPublisherExample() {
    let subject = PassthroughSubject()
    let stringPublisher = ["Break things!"].publisher
    
    stringPublisher
        .prepend(subject)
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
    
    subject.send("Run code")
    subject.send(completion: .finished)
}

The result is the same as the previous one (note that we need to send an event .finished in subject so that the operator .prepend have worked):

2. append

Operator .append (literally “add to end”) works similarly .prepend, but in this case, we add the values ​​to the original publisher:

func appendOutputExample() {
    let stringPublisher = ["Hello"].publisher
    
    stringPublisher
        .append("World!")
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
}

As a result, we see Hello and World! outputted to the console:

Similar to how we previously used .prepend to add another Publishera, we also have such an opportunity for the operator .append:

3.switchToLatest

More complex operator .switchToLatest allows us to combine a series of publishers into one stream of events:

func switchToLatestExample() {
    let stringSubject1 = PassthroughSubject()
    let stringSubject2 = PassthroughSubject()
    let stringSubject3 = PassthroughSubject()
    
    let subjects = PassthroughSubject, Never>()
    
    subjects
        .switchToLatest()
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
    
    subjects.send(stringSubject1)
    
    stringSubject1.send("A")
    
    subjects.send(stringSubject2)
    
    stringSubject1.send("B") // отброшено
    
    stringSubject2.send("C")
    stringSubject2.send("D")
    
    subjects.send(stringSubject3)
    
    stringSubject2.send("E") // отброшено
    stringSubject2.send("F") // отброшено
    
    stringSubject3.send("G")
    
    stringSubject3.send(completion: .finished)
}

Here’s what’s going on in the code:

  • We create three objects PassthroughSubjectto which we will send values.
  • We create the main object PassthroughSubjectwhich dispatches other objects PassthroughSubject
  • We send stringSubject1 on the main subject.
  • stringSubject1 gets the value A.
  • We send stringSubject2 on the main subject, automatically discarding stringSubject1 events.
  • Likewise, we send values ​​to stringSubject2, connect to stringSubject3 and send it a completion event.

As a result, we see the output A, C, D and G:

For simplicity, the function isAvailable returns a random value Bool after some delay.

func switchToLatestExample2() {
    func isAvailable(query: String) -> Future {
        return Future { promise in
            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                promise(.success(Bool.random()))
            }
        }
    }
    
    let searchSubject = PassthroughSubject()
    
    searchSubject
        .print("subject")
        .map { isAvailable(query: $0) }
        .print("search")
        .switchToLatest()
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
    
    searchSubject.send("Query 1")
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        searchSubject.send( "Query 2")
    }
}

Thanks to the operator .switchToLatest we achieve what we want. Only one Bool value will be displayed:

4.merge (with 🙂

We use .merge(with:) to combine two Publisherss, as if we were getting values ​​from only one:

func mergeWithExample() {
    let stringSubject1 = PassthroughSubject()
    let stringSubject2 = PassthroughSubject()
    
    stringSubject1
        .merge(with: stringSubject2)
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)
    
    stringSubject1.send("A")
    
    stringSubject2.send("B")
    
    stringSubject2.send("C")
    
    stringSubject1.send("D")
}

The result is an alternating sequence of elements:

5.combineLatest

Operator .combineLatest publishes a tuple containing the latest value for each publisher.

To illustrate this, consider the following real-world example: we have a username, password UITextFields and a continue button. We want to keep the button disabled until the username is at least five characters long and the password is at least eight characters. We can easily achieve this using the operator .combineLatest:

func combineLatestExample() {
    let usernameTextField = CurrentValueSubject("")
    let passwordTextField = CurrentValueSubject("")
    
    let isButtonEnabled = CurrentValueSubject(false)
    
    usernameTextField
        .combineLatest(passwordTextField)
        .handleEvents(receiveOutput: { (username, password) in
            print("Username: (username), password: (password)")
            let isSatisfied = username.count >= 5 && password.count >= 8
            isButtonEnabled.send(isSatisfied)
        })
        .sink(receiveValue: { _ in })
        .store(in: &subscriptions)
    
    isButtonEnabled
        .sink { print("isButtonEnabled: ($0)") }
        .store(in: &subscriptions)
    
    usernameTextField.send("user")
    usernameTextField.send("user12")
    
    passwordTextField.send("12")
    passwordTextField.send("12345678")
}

After usernameTextField and passwordTextField will receive user12 and 12345678 accordingly, the condition is satisfied and the button is activated:

6.zip

Operator .zip delivers a pair of matching values ​​from each publisher. Let’s say we want to determine if both publishers have published the same value Int:

func zipExample() {
    let intSubject1 = PassthroughSubject()
    let intSubject2 = PassthroughSubject()
    
    let foundIdenticalPairSubject = PassthroughSubject()
    
    intSubject1
        .zip(intSubject2)
        .handleEvents(receiveOutput: { (value1, value2) in
            print("value1: (value1), value2: (value2)")
            let isIdentical = value1 == value2
            foundIdenticalPairSubject.send(isIdentical)
        })
        .sink(receiveValue: { _ in })
        .store(in: &subscriptions)
    
    foundIdenticalPairSubject
        .sink(receiveValue: { print("is identical: ($0)") })
        .store(in: &subscriptions)
    
    intSubject1.send(0)
    intSubject1.send(1)
    
    intSubject2.send(4)
    
    intSubject1.send(6)
    intSubject2.send(1)
    intSubject2.send(7)
    
    intSubject2.send(9) // Не отображено, потому что его пара еще не отправлена
}

We have the following corresponding values ​​from intSubject1 and intSubject2:

  • 0 and 4
  • 1 and 1
  • 6 and 7

Latest value 9 is not displayed because intSubject1 haven’t posted the corresponding value yet:

Resources

Source code is available at Gist

Conclusion

Interested in other types of Combine operators? Feel free to visit my other articles:

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *