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.

How to Use Combine's ReplaceNil Operator

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