Persist Filtering Logic With Swift Combine

Data-driven Combine

Kevin Cheng
Jan 12 · 4 min read

In the previous episode, we successfully modeled a stream of values and had one simple operator (delay) attached to each value.

In this piece, we’re going to look into a few more operators, make them Codeable, and finally translate them into the Combine publisher at runtime.


Types of Operators

The ReactiveX website categorizes them roughly into 10 types: creating, transforming, filtering, combining, error handling, utility, conditional, mathematical/aggregate, backpressure, connectable-observable, and operators-to-convert observables. If you’re interested, ReactiveX has a good explanation for each type and operator.

Note: If you’re not familiar with RxSwift, Observable in RxSwift is equivalent to Publisher in Combine.

In the previous piece, we mentioned the delay operator, which is of the utility type. Today we’ll focus on persisting two operators in the filtering type.


The Filtering Operator

dropFirst

enum Operator {
case delay(seconds: Double)
case dropFirst(count: Int)
}

We can also easily convert this enum case into Publisher.

extension Operator {func applyPublisher<T>(_ publisher: AnyPublisher<T, Never>) -> AnyPublisher<T, Never> {  
switch
self {
case .dropFirst(let count):
return publisher.dropFirst(count).eraseToAnyPublisher()
//skip the rest of cases
}}}

Now, the dropFirst operator can be persisted and displayed in the list of operators.

Persisting dropFirst seems similar to the and delay operator. Perhaps, filtering is not so different from utility operators. Well, let’s try one more operator before we jump to that conclusion.

Filter

Let’s look closer at the filter method.

func filter(_ isIncluded: @escaping (Self.Output) -> Bool) -> Publishers.Filter<Self>

Its closure, isIncluded, takes a generic type and returns a boolean.

Is there anything in Foundation that represents logical conditions and returns a boolean? Ring any bells?

Filter with NSPredicate

Let’s go ahead and add filter in the enum.

enum Operator {
case delay(seconds: Double)
case dropFirst(count: Int)
case filter(expression: String)
}

All we need here is to filter expressions like %d !=3 or %@ != “D”; therefore, expression is our associate type. Likewise, we need to be able to move the filter enum into Publisher.

extension Operator {
func applyPublisher<T>(_ publisher: AnyPublisher<T, Never>) -> AnyPublisher<T, Never> {
switch
self {
case .filter(let expression):
return publisher.filter { value in
NSPredicate(format: expression,
argumentArray: [value])
.evaluate(with: nil) }.eraseToAnyPublisher()

//skip the rest of cases
}}}

As planned, we send the expression to NSPredicate along with the value sent upstream from Publisher.

Note that NSPredicate takes an array of arguments. Therefore, with some modification, it should work even when values are in tuple format, which is very common in reactive scenarios. We’ll get to that in the future when we talk about combined operators.

As you can see, the Filter Stream is added into this persisted array of operators and is converted into Publisher in order to filter the number 3 out of the upstream values.


Next Episode: Persist-Transforming Operators, Map, and Scan

You can find the source code here in this combine-magic-swifui repo under the combine-playground folder.

Better Programming

Advice for programmers.

Kevin Cheng

Written by

iOS engineer / entrepreneur / freelancer / San Francisco

Better Programming

Advice for programmers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade