Build your own Combine operators: .publish(every:on:mode:)

Let’s have a look at another custom Combine operator that I had to build. This operator is very similar to the one I talk about in this article.

Use Case

I came up with this operator when I was working on circular progress view, which is a UI component that indicates the progress of a download operation for example. When I was done building this component, I wrote some ugly imperative code to test it out before I remembered Combine existed. I then erased everything and wrote everything using a more declarative syntax.

Let’s start

I started by faking the stream of values that my progress view would receive using a the following publisher but soon realised that my progress view was directly completed.

let values: [Double] = [0.1, 0.3, 0.6, 0.7, 0.9, 1.0]

let subscription = values.publisher
    .assign(to: \.progress, on: progressView)

I had to figure out a way to space out the emission of the values

Timer to the rescue

I was already aware of the Timer.TimerPublisher which repeatedly emits the current date on the given interval. If I could mix the TimerPublisher and the publisher emitting the values, I would get what I want.

let subscription = Timer
    .publish(every: 1.0, on: RunLoop.main, in: .default)
    .autoconnect()
    .sink { print($0) }

We start by creating a TimerPublisher which emits a value every second. TimerPublisher is a ConnectablePublisher which means that it won’t emit a value before the connect() method is called on the publisher. For convenience we call here autoconnect() so that it starts emitting values directly. The call to sink is just here to show the results.

// 2020-11-26 08:10:37 +0000
// 2020-11-26 08:10:38 +0000
// 2020-11-26 08:10:39 +0000
// 2020-11-26 08:10:40 +0000
// 2020-11-26 08:10:41 +0000

Now let’s zip the TimerPublisher and the publisher emitting the progress values.

let subscription = Publishers.Zip(
    values.publisher,
    Timer.publish(every: 1.0, on: RunLoop.main, in: .default).autoconnect()
)
.map(\.0)
.assign(to: \.progress, on: progressView)

Note that since the Zip publisher is returning a (Double, Date) we need to map the Output and only carry the Double before assigning it to the progress property of the progress view.

Finally let’s make this code reusable by moving it to an extension

extension Publisher {
    func publish(
        every interval: TimeInterval,
        on runLoop: RunLoop,
        in mode: RunLoop.Mode
    ) -> AnyPublisher<Self.Output, Self.Failure> {
        Timer.publish(every: interval, on: runLoop, in: mode)
            .autoconnect()
            .eraseToAnyPublisher()
            .setFailureType(to: Self.Failure.self)
            .zip(self)
            .map(\.1)
            .eraseToAnyPublisher()
    }
}

let subscription = [0.1, 0.3, 0.6, 0.7, 0.9, 1.0].publisher
    .publish(every: 1.0, on: .main, in: .default)
    .assign(to: \.progress, on: progressView)

Note that we need to set the Failure type of the TimerPublisher to the same type as the original publisher so make zip happy.

Thanks for reading!

Leave a Reply

Your email address will not be published.