In Simple custom Combine operators I spoke about how you can cheaply create your own custom Combine operator without the need to create a new custom Publisher type.

In this post I’d like to show how this kind of “cheap” operators could actually be quite complex and useful; still being created only by creatively combining existing operators.

Let’s look at sample(_)

The sample operator

rxmarbles.com is a marveleous website where you can find visual and interactive presentations of a multitude of reactive operators (based in their implementation in RxJS).

One operator from Rx that I really like but I still don’t see implemented in Combine is sample.

sample is very handy and you can see it in action here: (https://rxmarbles.com/#sample). It allows you to take the values of a publisher only at given times when a second publisher (a source or a trigger) emits.

The second publisher’s values are ignored - it only serves as a “trigger” as to when should the latest value of the first publisher be emitted.

A practical application of sample is having a timer but you only take the current value when a button is pressed like so:

Now let’s look at how to quickly re-implement sample by combining built-in Combine operators.

Re-implementing sample in Combine

First we will start with defining an extension on Publisher - again we will not create a new Publisher implementation but rather extend the protocol with a “cheap” operator.

1
2
3
4
5
6
7
8
extension Publisher {
  func sample<P>(source: P) 
    -> AnyPublisher<Self.Output, Self.Failure>
    where P: Publisher, P.Output: Equatable, 
    P.Failure == Self.Failure {

  }
}

sample will operate on the values of the current publisher. The method takes a single parameter which is the “trigger” or “source” publisher.

The source publisher needs to meet a couple of conditions:

  • first, the values emitted by source need to be, regardless of their actual type, Equatable so we can check for a new value by comparing the current one to the last one.
  • second, the Failure types of source and self need to match as this is a requirement of combineLatest(_) which we will use in the method body.

Now let’s see about the imlementation. First we will start by combinging the two publishers together via combineLatest:

1
combineLatest(source)

That will combine the values of self and source into a single stream of tuples, each containing the latest values of both publishers.

Next we will filter any repeating values of the source publisher by removing duplicates via a custom closure:

1
2
3
4
.removeDuplicates(by: { 
  (tuple1, tuple2) -> Bool in
  tuple1.1 == tuple2.1
})

This will remove any values where the source publisher hasn’t emitted a new value.

Finally we need to get rid of the extra value in the tuple: the source element which is of no interest for this operator. And also erase the operator type to remove some of the complexity of the return type:

1
2
3
4
.map { tuple in
  tuple.0
}
.eraseToAnyPublisher()

And the final implementation is:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func sample<P>(source: P) 
  -> AnyPublisher<Self.Output, Self.Failure>
  where P: Publisher, P.Output: Equatable, 
  P.Failure == Self.Failure {

  combineLatest(source)
    .removeDuplicates(by: { 
      (tuple1, tuple2) -> Bool in
      tuple1.1 == tuple2.1
    })
    .map { tuple in
      tuple.0
    }
    .eraseToAnyPublisher()
}

We recreated sample as implemented in Rx by combinin/g combineLatest, removeDuplicates, and a map. Now we could use sample in all kinds of use-cases (though I personally find it most practical when using UI controls for the source). Let’s try a quick and practical test.

Getting sample out for a spin

A really simple test scenario could be:

  • first publisher is a counter that emits consequtive integer values,
  • second publisher (source, which could be a UI button or a timer) emits periodically to output the current counter value.

Let’s first get the counter going:

1
2
3
4
5
6
let counter = Timer
  .publish(every: 0.17, on: .main, in: .common)
  .autoconnect()
  .scan(0) { result, _ in
    return result + 1
  }

The counter (when subscribed) will continuously count like so:

1
2
3
4
5
...

Then add a timer firing each 5 seconds to use as the “source”:

1
2
3
let trigger = Timer
  .publish(every: 5, on: .main, in: .common)
  .autoconnect()

And finally use sample to use the two publishers together:

1
2
3
4
5
let sub = counter
  .sample(source: trigger)
  .sink { value in
    print(value)
  }

That will print the current value of counter each time trigger fires:

29
58
88
117
147
176
205
...

All works as expected so that’s a wrap for today!

Where to go from here?

You saw how to create more useful and reusable but “cheap” to make Combine operators. Next you’d probably want to look into creating custom operators via implementing new Publisher types!

To learn more about implementing custom operators and publishers check out Combine: Asynchronous programming with Swift - this is where you can see all updates, discuss in the website forums, and more.