In the world of responsive programming, we need a data sieve. This analogy is too good. The data we really want is the data that has been stripped of impurities.

compactMap/tryCompactMap

Compact means compact, so it’s easy to understand that compactMap can compress data in a data stream, as shown in the figure above. It can automatically filter empty data, like a sieve, leaving non-empty data.

_ = ["a".nil."c"]
    .publisher
    .compactMap { value -> String? in
        value
    }
    .sink(receiveCompletion: { _ in
        print("It's over.")
    }, receiveValue: { someValue in
        print(someValue)
    })
Copy the code

The closure in its argument supports the return of an optional type of argument, so we can simply return the data passed in, implementing null-value filtering in a single operation.

If you think compactMap is nothing more than that, don’t ignore the Map keyword. It not only filters empty data, but also retains the full functionality of Map.

_ = ["123".nil."456"]
    .publisher
    .compactMap { value -> Int? in
        guard let v = value else {
            return nil
        }
        return Int(v)
    }
    .sink(receiveCompletion: { _ in
        print("It's over.")
    }, receiveValue: { someValue in
        print(someValue)
    })
Copy the code

The essence of compactMap is that it allows you to return optional types, which you can consider when you have a scenario where the data might be nil.

TryCompactMap builds on compactMap to allow exceptions to be thrown in closures, which I won’t go into here.

filter/tryFilter

The filter is so simple and powerful that its name gives us an idea of what it can do when we need a data sieve.

Filter takes a closure that returns a value of type Bool, and only data that results in true is saved.

Imagine a scenario where we need to screen out hotels with prices less than 300 yuan, which can be easily achieved by using filter.

In this case, we filter all results with numbers greater than 8, as follows:

_ = [5.10.25]
    .publisher
    .filter { value in
        value > 8
    }
    .sink(receiveCompletion: { _ in
        print("It's over.")
    }, receiveValue: { someValue in
        print(someValue)
    })
Copy the code

removeDuplicates/tryRemoveDuplicates

In principle, data flowing through a Pipline can be of any type, although it can be duplicated. When de-duplicates these data, removeDuplicates should be used.

As shown in the figure above, duplicate data is filtered out. By default, when using. RemoveDuplicates (), data output from Publisher must implement the Equatable protocol, which uses == to determine whether the two data are equal.

_ = [1.1.1.2.2.3]
    .publisher
    .removeDuplicates()
    .sink(receiveCompletion: { _ in
        print("It's over.")
    }, receiveValue: { someValue in
        print(someValue)
    })
Copy the code

Of course, the application can be flexible, and it can be if the output data from Publisher does not implement the Equatable protocol, as shown below:

let students = [Student(age: 20, score: 80),
                Student(age: 20, score: 90),
                Student(age: 21, score: 90)]

_ = students
    .publisher
    .removeDuplicates(by: { first, second in
        first.age = = second.age
    })
    .sink(receiveCompletion: { _ in
        print("It's over.")
    }, receiveValue: { someValue in
        print(someValue)
    })
Copy the code

When an Operator argument is a closure, it is flexible. It is declarative. In the above code, we used age to cancel weights.

TryRemoveDuplicates allow exceptions to be thrown in closures, which we won’t explain:

_ = students
    .publisher
    .tryRemoveDuplicates(by: { first, second in
        if first.age = = 21 {
            throw MyError.customError
        }
        return first.age = = second.age
    })
    .sink(receiveCompletion: { _ in
        print("It's over.")
    }, receiveValue: { someValue in
        print(someValue)
    })
Copy the code

replaceEmpty/replaceError/replaceNil

The core idea of replaceEmpty is to provide a default value for empty data streams.

So what is an empty data stream?

When the pipline finally receives the.Finish event, if no data has previously flowed through the pipe, the situation is called empty data flow.

As shown in the figure above, the black bar represents the end event, and when the data stream above receives the end event, there is no data flow, so replaceEmpty is fired and 100 is returned.

Let’s write a simple program to verify:

class ReplaceViewViewObservableObject: ObservableObject {
    var publiser = PassthroughSubject<Int.Never> ()var cancellables = Set<AnyCancellable> ()@Published var number: Int = 0

    init(a) {
        publiser
            .replaceEmpty(with: 100)
            .assign(to: \.number, on: self)
            .store(in: &cancellables)
    }

    func stop(a) {
        publiser.send(completion: Subscribers.Completion.finished)
    }
    
    func getRandomValue(a) {
        publiser.send(Int.random(in: 50..<1000))}}Copy the code
HStack(spacing: 20) {
      Text("Current value:\(myObject.number)")

      Button("Random value") {
          myObject.getRandomValue()
      }

      Button("The end") {
          myObject.stop()
      }
  }
  .padding(20)
  .border(Color.red)
Copy the code

The purpose of choosing PassthroughSubject as Publisher is to proactively control when data and events are sent.

The running effect is as follows:

Clicking on the random value button generates a random integer, and clicking on the End button does not change the number to 100, indicating that replaceEmpty will not fire as long as there has been data flowing through the pipe.

Now look at the following effect:

When you click the end button first, you can see that the value changes to 100 and then it doesn’t change after that because clicking the end button sends the.Finish event and the data flow terminates.


As shown in the figure above, replaceError catches any exception in the Pipline and returns a default value. Consider using replaceError in scenarios where our data flow model does not want to accept any errors.

The same functionality of replaceError can be achieved with catch:

.catch { err in
    return Just(0)}Copy the code

ReplaceError is obviously easier to write, but there is another use case where we can only use catch if we want to return a new Publisher after listening for an Error.


ReplaceNil is simple enough to replace empty data in a data stream. If you want to filter out data, consider using compactMap or tryCompactMap.

Operators that implement the replace function should be placed as far back in the pipline as possible.