1

How to enforce minimum interval between events emitted from Combine publisher? With assumption that I want all events from upstream to be emitted but with minimum interval between them, let's say 1s. If the interval between two events in the upstream is > 1s the events should be emitted as they are. So far I've tried something like this:

let subject = PassthroughSubject<Int, Never>()

let result = subject.flatMap(maxPublishers: .max(1)) {
    Just($0).delay(for: 1, scheduler: RunLoop.main)
}

let cancellable = result.sink {
    print("--- value \($0) ---")
}


// Emitting values
subject.send(1)

DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
    subject.send(2)
}

DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
    subject.send(3)
}

DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
    subject.send(4)
}

but the result I get is:

--- value 1 ---
--- value 4 ---

Any idea how to achieve it?

Wujo
  • 1,845
  • 2
  • 25
  • 33

2 Answers2

2

I do not think that combine provides a tool for that, .throttle and .debounce are definitely not what you are looking for. Using flatMap(maxPublishers: .max(1)) will make 2 and 3 vanish as it will prevent the .delay from receive them (there is no storage so you have to add one).

This is a workaround I found using buffer that might do the trick: Add delay between values emitted by publisher in Combine Framework in iOS

I tested using you code and it work properly.

subject
    .buffer(size: Int.max, prefetch: .byRequest, whenFull: .dropOldest)
    .flatMap(maxPublishers: .max(1)) {
          Empty().delay(for: 1, scheduler: RunLoop.main).prepend($0)
    }.sink(receiveValue: { value in
          print(value)
    }.store(&cancellables)

EDIT: tI improved my first answer following @Daniel T comment, applying the delay after popping out the value using .prepend is way better.

xfost
  • 111
  • 5
  • 1
    The only problem with this answer is that it causes a 1 second delay *after* the value enters the flatMap. Better would be to push out the value first and *then* run the delay. You can do that by doing this `Empty().delay(for: 1, scheduler: RunLoop.main).prepend($0)` instead of `Just($0).delay(for: .seconds(1), scheduler: RunLoop.main)`. – Daniel T. Jul 01 '23 at 01:47
  • Thank you, indeed, with the change proposed by @DanielT. it works as expected. Could you maybe edit your reply to include this change? Though, I have to admit that I have problem in understanding why the `buffer` is needed. – Wujo Jul 03 '23 at 09:33
  • @Wujo , using flatMap without condition will create a subPublisher for each of your values and apply the delay for each of them, so, using your sample code: "1" will be printed after 1s, "2" after 1,3s, "3" after 1,6 etc... That's not what you want so you put `maxPublishers: max(1)` to treat them one after an other, thing is that you locking the flatMap pipeline to only one value this way, while 1 is in its 1 second delay, 2 and 3 will come to the beginning of the flatMap but cannot be dispatched so they vanish, that's why you need the `buffer`. – xfost Jul 03 '23 at 10:27
2

@xfost wrote a great answer. Here's another solution:

let subject = PassthroughSubject<Int, Never>()

let result = Publishers.Zip(
    Timer.publish(every: 1, on: RunLoop.main, in: .default).autoconnect().prepend(Date()),
    subject
)
    .map { $1 }

let cancellable = result
    .sink {
        print("--- value \($0) ---")
    }


// Emitting values
subject.send(1)

DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
    subject.send(2)
}

DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
    subject.send(3)
}

DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
    subject.send(4)
}

DispatchQueue.main.asyncAfter(deadline: .now() + 7) {
    subject.send(5)
}
Daniel T.
  • 32,821
  • 6
  • 50
  • 72