fragon
fragon

Reputation: 3471

Combine - Delay publisher's send

What's the best way, to delay a moment when a publisher sends some data in Swift Combine? Let's assume following situation:

private var publisher: PassthroughSubject<Progress, Error>

// closure called every second:
startWithProgress() { [weak self] progress in
    self.publisher.send(.init(progress: progress))

    // How to call this 0.5 second after the above `send`:
    self.publisher.send(.init(progress: progress + 0.5))
}

I checked Delay API, but it seems I'd need to create another publisher to make use of it, which is suboptimal in my case. I also checked throttle and debounce, but those also don't allow me to send 2 updates one after another, with a given delay between them.

Upvotes: 0

Views: 3945

Answers (3)

Rich
Rich

Reputation: 27

I haven't tested this, it might make more sense to use a timer. You will need to store cancellable in an instance variable so it doesn't disappear when the function returns.

var cancellable = self.publisher
    .delay(for: .seconds(0.5), scheduler: RunLoop.main )
    .sink(receiveCompletion:{ _ in
    // do something with the error or completion
}, receiveValue:{ [unowned self] progress in
        // you don't show any code that actually updates progress, without that this will just keep sending the initial value + 0.5.
        self.publisher.send(.init(progress: progress + 0.5))
    })
self.publisher.send(.init(progress: progress))

Upvotes: 1

New Dev
New Dev

Reputation: 49620

There's a difference between delaying the act of sending the values - which you can do with DispatchQueue.asyncAfter - and creating a combine pipeline that delays upstream values.

You haven't specified any details about what you are actually trying to accomplish, so it's hard to give a definitive answer.

If I was to generalize, it looks like that you want a pipeline that for each upstream value it emits the value, then emits the value + 0.5 again, but delayed. This could be done like below, as an example:

let duplicateAndDelay = publisher
   .flatMap { [($0, 0), ($0 + 0.5, 0.5)].publisher } // duplicate
   .flatMap { (progress, delay) in 
      Just(progress)
         .delay(for: .seconds(delay), scheduler: RunLoop.main) // delay
   }

Then you can just send once:

startWithProgress() { [weak self] progress in
   self?.publisher.send(progress)
}

and return the duplicateAndDelay publisher, instead of the publisher publisher, to be subscribed to.

Upvotes: 3

AnderCover
AnderCover

Reputation: 2671

Judging from what you shared, I would not use Combine. DispatchQueue.asyncAfter(deadline:execute:) seems to be enough.

That being said if you must, you can use Publisher.delay(for:tolerance:scheduler:options:) :

let subject = PassthroughSubject<Progress, Error>()
subject.delay(for: .seconds(5), scheduler: RunLoop.main)
    .replaceError(with: .init())
    .sink { progress in
        self.publisher.send(.init(progress: progress + 0.5))
    }
    .store(in: &cancellables)

or even Publisher.publish(every:tolerance:on:in:options:)

Timer
    .publish(every: 5, on: .main, in: .default)
    .autoconnect()
    .first()
    .sink { _ in
        self.publisher.send(.init(progress: progress + 0.5))
    }

or, if it makes sense in your case, have progress in a @Published variable and use it to start a pipeline

Upvotes: 2

Related Questions