Combine's filter
and compactMap
operators share a few similarities and it is possible to use them interchangeably in some scenarios. That said, there are a number of key differences we discuss in today's episode of Combine Essentials.
Similarities Between Filter and CompactMap
It is possible to use Combine's filter
and compactMap
operators interchangeably in some scenarios. Let's take a look at an example.
The reachablePublisher
publisher emits true
if the device has a network connection and false
if the device isn't connected to the network. By applying the filter
operator, the application only fetches a list of episodes from a remote API if the device is reachable.
private func setupBindings() {
reachablePublisher
.filter { $0 }
.sink { [weak self] _ in
self?.fetchEpisodes()
}.store(in: &subscriptions)
}
The filter
operator accepts a closure that returns true
or false
. If the closure returns true
for an element, the filter
operator republishes the element. If the closure returns false
for an element, the filter
operator doesn't republish the element. That is the filter
operator in a nutshell.
The behavior of the compactMap
operator is similar in some ways. Let's update the example by replacing the filter
operator with the compactMap
operator. The resulting behavior is identical.
private func setupBindings() {
reachablePublisher
.compactMap { $0 ? $0 : nil }
.sink { [weak self] _ in
self?.fetchEpisodes()
}.store(in: &subscriptions)
}
The compactMap
operator accepts a closure just like the filter
operator. The difference is that the return value of the closure is T?
, a generic, optional type. The compactMap
operator defines what type it returns.
The compactMap
operator can be used to filter the elements of its upstream publisher because it excludes elements for which its closure returns nil
. What does that mean? Let's analyze the closure that is passed to the compactMap
operator. Let's make the implementation easier to understand by avoiding shorthand argument syntax.
private func setupBindings() {
reachablePublisher
.compactMap { isReachable in
isReachable
? isReachable
: nil
}
.sink { [weak self] _ in
self?.fetchEpisodes()
}.store(in: &subscriptions)
}
If isReachable
is equal to true
, the closure returns the value of isReachable
. If isReachable
is equal to false
, the closure returns nil
. If the closure that is passed to the compactMap
operator returns nil
for an element, then that element isn't republished by the compactMap
operator. That is how the compactMap
operator can be used, and often is used, to filter the elements of an upstream publisher.
More Powerful Filtering
You could see the compactMap
operator as a combination of the map
and filter
operators, but even that comparison isn't accurate. Let's take a look at the example we used in the previous episode of Combine Essentials. We declare a private, variable property, currentSelection
, of type Int?
and apply the Published
property wrapper to it.
@Published private var currentSelection: Int?
We define an enum, TabBarItem
, that defines a case for each tab bar item of the application's tab bar. The raw values for TabBarItem
are defined to be of type Int
. We want to transform the elements emitted by the publisher of currentSelection
to TabBarItem
objects. We apply the filter
operator to exclude nil
values and apply the map
operator to transform the elements to TabBarItem
objects.
@Published private var currentSelection: Int?
var currentSelectionPublisher: AnyPublisher<TabBarItem?, Never> {
$currentSelection
.filter { $0 != nil }
.map { rawValue in
guard let rawValue = rawValue else {
return nil
}
return TabBarItem(rawValue: rawValue)
}
.eraseToAnyPublisher()
}
Notice that the Output
type of currentSelectionPublisher
is TabBarItem?
, an optional type. This is inevitable since the filter
operator doesn't change the Output
type of the upstream publisher and the map
operator doesn't exclude nil
values.
The compactMap
operator is different. It can change the Output
type of the upstream publisher and it excludes nil
values. Take a look at the updated example in which we replace the filter
and map
operators with the compactMap
operator.
@Published private var currentSelection: Int?
var currentSelectionPublisher: AnyPublisher<TabBarItem, Never> {
$currentSelection
.compactMap { rawValue in
guard let rawValue = rawValue else {
return nil
}
return TabBarItem(rawValue: rawValue)
}
.eraseToAnyPublisher()
}
The changes are small. We removed the filter
operator and replaced the map
operator with the compactMap
operator. Notice that the Output
type of currentSelectionPublisher
is TabBarItem
, a non-optional type.
When to Use the Map Operator
You may think that the compactMap
operator is preferred over the map
operator, but that isn't true. First, if the Output
type of the upstream publisher isn't an optional type, then there is no need to apply the compactMap
operator. The map
operator suffices in that scenario. Second, excluding nil
values isn't always what you want or need. nil
defines the absence of a value and that is sometimes desired. Take a look at this example.
@Published private var profile: Profile?
var currentProfilePublisher: AnyPublisher<Profile?, Never> {
$profile
.eraseToAnyPublisher()
}
The Output
type of currentProfilePublisher
is Profile?
, an optional type. If currentProfilePublisher
emits nil
, it indicates the absence of a profile. This could mean the user isn't signed in. That is important information. Whether it makes sense to apply the map
operator or the compactMap
operator is often defined by the context in which they are used.
What's Next?
Even though Combine's filter
and compactMap
operators share a few similarities, they each have their use. Remember that the filter
operator doesn't change the Output
type of the upstream publisher. This isn't true for the compactMap
operator. It can act like the filter
operator, but it has the ability to transform the elements of the upstream publisher and change the optionality of the upstream publisher's Output
type.