D.Yasiru Jayatissa
D.Yasiru Jayatissa

Reputation: 21

Add delay between values emitted by publisher in Combine Framework in iOS

I have one publisher let subject = PassthroughSubject<Human,Error>() that emits the following objects,

subject.send(Human(fName: "John", lName: "Abraham"))
subject.send(Human(fName: "Jane", lName: "Anne"))

I need there to be a delay between these two objects, when they are being received.

example(of: "delayy(with:)"){
    
    struct Human {
        let fName: String
        let lName: String
    }

    let subject = PassthroughSubject<Human,Error>()
    let delayInSeconds = 4
    var isLoading = false
    
    subject
        .delay(for: .seconds(delayInSeconds), scheduler: DispatchQueue.main)
        subject.sink(
            receiveCompletion:{
                print("Received Completion", $0)
                isLoading = true
                print("isLoading =", isLoading)
            },
            receiveValue: {

                print("Value Received:" ,$0)
            })

        subject.send(Human(fName: "John", lName: "Abraham"))
        subject.send(Human(fName: "Jane", lName: "Anne"))
        subject.send(completion: .finished)
         
}

The First value the publisher is going to be emitting here is (fName: "John", lName: "Abraham"). The second value emitted is going to be (fName: "Jane", lName: "Anne").

I want there to be a delay between these two objects when the values are received here in print("Value Received:" ,$0)

I tried adding delay but it does not seem to be working. Any help is appreciated. (I am a beginner to combine)

fyi: The above code snippet is from playground

Upvotes: 2

Views: 1284

Answers (1)

burnsi
burnsi

Reputation: 7744

There are several issues with your code.


You do not use the modifier properly.

subject
    .delay(for: .seconds(delayInSeconds), scheduler: DispatchQueue.main)

subject.sink(

Here you assign the .delay modifier to your subject. But this will return an new publisher that you simply don´t use. With the next line subject.sink you are subscirbing to the original publisher without the delay modifier.

Proper syntax:

subject
    .delay(for: .seconds(delayInSeconds), scheduler: DispatchQueue.main)
    .sink(

Next, you are not storing the AnyCancellable that is returned by your .sink modifier. If you don´t store it, the subscription will go out of scope and you won´t receive new values.

let cancellable = subject
                     .delay...
                     .sink...

would be the proper way to store it. If you use this in a function or class instead of a Playground take care your cancellable var does not go out of scope untill you don´t need it anymore.


With these simple issues out of the way lets look at the real problem.

The .delay modifier won´t help you here. It just delays the entire stream of emitted values by the delay value given. This means, after sending your values the delay function will wait for the specified time and then emit all values at once.

I tried different approaches and the only thing that worked was a combination of a .buffer and a .flatMap modifier.

let cancellable = subject
    .buffer(size: 20000, prefetch: .byRequest, whenFull: .dropOldest)
    .flatMap(maxPublishers: .max(1)) { Just($0).delay(for: .seconds(delayInSeconds), scheduler: RunLoop.main) }
    .sink(
    receiveCompletion:{
        print("Received Completion", $0, Date())
        isLoading = true
        print("isLoading =", isLoading)
    },
    receiveValue: {
        print("Value Received:" ,$0, Date())
    })

The buffer is required to store the values somewhere. The .flatMap creates new publishers from the values given but emits only one at a time. This publisher is then delayed by the time you give.

Example input:

subject.send(Human(fName: "John", lName: "Abraham"))
subject.send(Human(fName: "Jane", lName: "Anne"))
subject.send(Human(fName: "John2", lName: "Abraham"))
subject.send(Human(fName: "John3", lName: "Abraham"))

subject.send(completion: .finished)

Output:

Value Received: Human(fName: "John", lName: "Abraham") 2023-02-10 10:58:48 +0000
Value Received: Human(fName: "Jane", lName: "Anne") 2023-02-10 10:58:52 +0000
Value Received: Human(fName: "John2", lName: "Abraham") 2023-02-10 10:58:56 +0000
Value Received: Human(fName: "John3", lName: "Abraham") 2023-02-10 10:59:00 +0000
Received Completion finished 2023-02-10 10:59:00 +0000
isLoading = true

This solution has some drawbacks.

  • you need to specify the buffer size explicitly in order to avoid losing values
  • the completion fires immediately after the last value (don´t know if this is an issue or not)

Upvotes: 4

Related Questions