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:
Hello
and 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
.finished
to subject for the operator to .prepend
work):
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
Hello
and World
! outputted to the console:
Similar to what we used earlier
.prepend
to add another Publisher
a, we also have this option for the operator .append
:
3.switchToLatest
A more complex operator
.switchToLatest
allows 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
PassthroughSubject
to which we will send values. - We create a main object
PassthroughSubject
that dispatches other objectsPassthroughSubject
. - We ship
stringSubject1
to the main subject. stringSubject1
gets the value A.- We dispatch
stringSubject2
to the main subject, automatically discarding stringSubject1 events. - Likewise, we send values to
stringSubject2
, connect to,stringSubject3
and dispatch a completion event to it.
The result is 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<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,
.switchToLatest
we achieve what we want. Only one Bool value will be displayed:
4.merge (with :)
We use
.merge(with:)
to combine two Publishers
s 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
.combineLatest
publishes a tuple containing the latest value of 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<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 passwordTextField
receive user12
, and 12345678
accordingly, the condition is satisfied, and the button is activated:
6.zip
The 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<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
intSubject1
and intSubject2
:
- 0 and 4
- 1 and 1
- 6 and 7
The latter value is
9
not displayed because the intSubject1
corresponding 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: