Andrew Duncan
Andrew Duncan

Reputation: 3753

Swift Combine - delaying a publisher

TL;DR

I want to delay a publication, but can't figure out how to, er, combine the parts

In Brief

I have a Publisher

let generator = PassthroughSubject<Bool, Never>()

and want somehow to use the modifier

.delay(for: 2, scheduler: RunLoop.main)

so that when I call

generator.send(true)

the message is sent two seconds after the call to send()

Looking at the docs for Publishers.Delay made the type error more clear, but doesn't help me to find the right way to hook things up.

Code

import SwiftUI
import Combine

// Exists just to subscribe.
struct ContainedView : View {
    private let publisher: AnyPublisher<Bool, Never>
    init(_ publisher: AnyPublisher<Bool, Never> = Just(false).dropFirst().eraseToAnyPublisher()) {
        self.publisher = publisher
    }
    var body: some View {
        Rectangle().onReceive(publisher) { _ in print("Got it") }
    }
}

struct ContentView: View {
    let generator = PassthroughSubject<Bool, Never>()
                 // .delay(for: 2, scheduler: RunLoop.main)
                 // Putting it here doesn't work either.

    var body: some View {
        VStack {
            Button("Tap") {

                // Does not compile
                self.generator.delay(for: 2, scheduler: RunLoop.main).send(true)
                // Value of type 'Publishers.Delay<PassthroughSubject<Bool, Never>, RunLoop>' has no member 'send'
                // https://developer.apple.com/documentation/combine/publishers/delay

                // Does not compile
                self.generator.send(true).delay(for: 2, scheduler: RunLoop.main)
                // Value of tuple type '()' has no member 'delay'

                // Just a broken-up version of the first try.
                let delayed = self.generator.delay(for: 2, scheduler: RunLoop.main)
                delayed.send(true)

                // This, of course, builds and works.
                self.generator.send(true)
                print("Sent it")
            }

            ContainedView(generator.eraseToAnyPublisher())
            .frame(width: 300, height: 200)
        }
    }
}

Upvotes: 13

Views: 22496

Answers (3)

AnderCover
AnderCover

Reputation: 2661

let generator = PassthroughSubject<Bool, Never>()
let generated = generator.delay(for: 2, scheduler: RunLoop.main).sink { value in
    print(value.description + " " + Date().timeIntervalSinceReferenceDate.description)
}
print(Date().timeIntervalSinceReferenceDate.description)
generator.send(true)
generator.send(false)

output

641453284.840604
true 641453286.841731
false 641453286.847715

Upvotes: 2

PKS
PKS

Reputation: 141

You can use the debounce property of a publisher to delay the publishing.

$yourProperty
    .debounce(for: 0.8, scheduler: RunLoop.main)
    .eraseToAnyPublisher()

Upvotes: 14

heckj
heckj

Reputation: 7357

.delay(for: 2, scheduler: RunLoop.main) is likely exactly what you need, but it'll be key to see how you're subscribing to fully understand the issue. Delay doesn't delay the sending of the value when using send() with a subject - that's a link for imperative code and sends the data whenever send is invoked, typically against some already existing subscription.

While you have a subscriber in the first bit of code, there isn't one with the subject to pin these together.

For example, if you updated:

Just(false).dropFirst().eraseToAnyPublisher()

to

Just(false).dropFirst().eraseToAnyPublisher().delay(for: 2, scheduler: RunLoop.main)

Then the print statement should trigger ~2 second after the the init() was invoked. Depending on what you're trying to accomplish here, using a closure trigger such as onAppear might make a lot more sense, having that call the subject.send(), which you can then delay as you like in the publisher chain that happens before whatever subscribes to it.

Upvotes: 17

Related Questions