6 Swift Combine Operators You Should Know

The translation of the article was prepared in anticipation 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<AnyCancellable>()

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


Result: Helloand World! are output in sequential order:







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



func prependPublisherExample() {
    let subject = PassthroughSubject<String, Never>()
    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 similar to the previous one (note that we need to send an event .finishedto subject for the operator to .prependwork):







2. append



The operator .append(literally “add to end”) works similarly .prepend, but in this case we add 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 Helloand World! outputted to the console:







Similar to what we used earlier .prependto add another Publishera, we also have this option for the operator .append:







3.switchToLatest



A more complex operator .switchToLatestallows us to combine a series of publishers into one stream of events:



func switchToLatestExample() {
    let stringSubject1 = PassthroughSubject<String, Never>()
    let stringSubject2 = PassthroughSubject<String, Never>()
    let stringSubject3 = PassthroughSubject<String, Never>()
    
    let subjects = PassthroughSubject<PassthroughSubject<String, Never>, 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 a main object PassthroughSubjectthat dispatches other objects PassthroughSubject.
  • We ship stringSubject1to the main subject.
  • stringSubject1 gets the value A.
  • We dispatch stringSubject2to the main subject, automatically discarding stringSubject1 events.
  • Likewise, we send values ​​to stringSubject2, connect to, stringSubject3and dispatch a completion event to it.


The result is output A, C, Dand G:







For simplicity, the function isAvailablereturns a random value Boolafter some delay.



func switchToLatestExample2() {
    func isAvailable(query: String) -> Future<Bool, Never> {
        return Future { promise in
            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                promise(.success(Bool.random()))
            }
        }
    }
    
    let searchSubject = PassthroughSubject<String, Never>()
    
    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, .switchToLatestwe 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<String, Never>()
    let stringSubject2 = PassthroughSubject<String, Never>()
    
    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



The operator .combineLatestpublishes a tuple containing the latest value of each publisher.



To illustrate this, consider the following real-world example: we have a username, password, UITextFieldsand 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<String, Never>("")
    let passwordTextField = CurrentValueSubject<String, Never>("")
    
    let isButtonEnabled = CurrentValueSubject<Bool, Never>(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")
}


Once usernameTextField and passwordTextFieldreceive user12, and 12345678accordingly, the condition is satisfied, and the button is activated:







6.zip



The operator .zipdelivers 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<Int, Never>()
    let intSubject2 = PassthroughSubject<Int, Never>()
    
    let foundIdenticalPairSubject = PassthroughSubject<Bool, Never>()
    
    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 intSubject1and intSubject2:



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


The latter value is 9not displayed because the intSubject1corresponding value has not yet been published:







Resources



The source code is available on the Gist .



Conclusion



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






All Articles