Earlier in this series, I explained how you can use Combine's compactMap
operator to filter out nil
elements emitted by a publisher. While this is a common use case, there are scenarios in which you don't want to filter out nil
elements. Instead you want to replace the nil
elements with a default or fallback value. In this episode of Combine Essentials, you learn about the replaceNil
operator to do just that. Note that I don't recommend developers to use the replaceNil
operator due to its unintuitive behavior. I explain this in more detail in this episode.
Transforming or Replacing Nil Values
Let's start with an example. Fire up Xcode and create a playground by choosing the Blank template from the iOS > Playground section.
Add an import statement for the Combine framework and declare an array of integers. We can create a publisher from the array through the computed publisher
property. Subscribe to the publisher by invoking the sink(_:)
method. In the value handler the sink(_:)
method accepts, we print the value the publisher emits. This should look familiar. The publisher's Output
type is Int
and its Failure
type is Never
.
import Combine
let values = [1, 2, 3].publisher
values.sink { value in
print(value)
}
1
2
3
Update the array of values by inserting a nil
element after the second element. The publisher's Output
type changes to Int?
. Its Failure
type remains Never
.
import Combine
let values = [1, 2, nil, 3].publisher
values.sink { value in
print(value)
}
Optional(1)
Optional(2)
nil
Optional(3)
Notice that the third value the publisher emits is nil
. This is expected. We can use the compactMap
operator to filter out the nil
elements the publisher emits. I explain this in detail in this episode.
import Combine
let values = [1, 2, nil, 3].publisher
values
.compactMap { $0 }
.sink { value in
print(value)
}
1
2
3
We can also use the compactMap
operator to transform or replace nil
elements with a default or fallback value. In this example, we replace every nil
element with 0
. Notice that the publisher's Output
type changes to Int
.
import Combine
let values = [1, 2, nil, 3].publisher
values
.compactMap { $0 ?? 0 }
.sink { value in
print(value)
}
1
2
0
3
The replaceNil
operator operates in a similar fashion. Replace the compactMap
operator with the replaceNil
operator. The replaceNil(with:)
method accepts one argument, the element to use when it encounters a nil
element.
import Combine
let values = [1, 2, nil, 3].publisher
values
.replaceNil(with: 0)
.sink { value in
print(value)
}
Optional(1)
Optional(2)
Optional(0)
Optional(3)
Do you spot the difference with the compactMap
operator? The publisher's Output
type is Int?
, not Int
. Even though the replaceNil
operator replaces nil
elements with an element we provide, the publisher's Output
type doesn't change.
When to Use the ReplaceNil Operator
I usually like APIs that simplify the code I need to write, but the replaceNil
operator is a bit of a special case and I recommend fellow developers to avoid it. The behavior of the replaceNil
operator isn't intuitive. Take a look at the following example.
import Combine
let values = [1, 2, nil, 3].publisher
values
.replaceNil(with: 0)
.sink { value in
print(value)
}
// Output
// Optional(1)
// Optional(2)
// Optional(0)
// Optional(3)
values
.eraseToAnyPublisher()
.replaceNil(with: 0)
.sink { value in
print(value)
}
// Output
// 1
// 2
// 0
// 3
The Output
type of the publisher the replaceNil
operator returns is Int?
in the first example and Int
in the second example. That is confusing. If you would like to understand why the replaceNil
operator behaves this way, I suggest you take a look at this thread on the Swift forums.
Use Map or CompactMap Instead
While the idea of the replaceNil
operator is nice, its unintuitive behavior make that I won't use it. I use map
and compactMap
every single day and it is trivial to replace nil
elements with a default or fallback values. Take a look at this example.
import Combine
let values = [1, 2, nil, 3].publisher
values
.map { $0 ?? 0 }
.sink { value in
print(value)
}
// Output
// 1
// 2
// 0
// 3
values
.compactMap { $0 ?? 0 }
.sink { value in
print(value)
}
// Output
// 1
// 2
// 0
// 3
What's Next?
This example illustrates how important it is to know and understand the tools you use. When I first encountered the replaceNil
operator. I was surprised by its behavior. This example in particular confused me. Why is the Output
type of the publisher the replaceNil
operator returns String
in the second example? This is confusing. Use map
or compactMap
instead is the message.
import Combine
let values = [1, 2, nil, 3].publisher
values
.replaceNil(with: 0)
.sink { value in
print(value)
}
// Output
// Optional(1)
// Optional(2)
// Optional(0)
// Optional(3)
struct List {
// MARK: - Properties
let title: String?
}
[
List(title: "Shopping List"),
List(title: nil),
List(title: "Reading List")
].publisher
.map(\.title)
.replaceNil(with: "No Title")
.sink { value in
print(value)
}
// Output
// Shopping List
// No Title
// Reading List