Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you run a Swift Combine Publisher for a certain amount of time?

I have a publisher when the sink, scans for a list of wifi. I only want to scan for about 10 seconds and stop.

Is there a way to do this within the publisher chain of calls?

like image 895
Unikorn Avatar asked Oct 29 '21 00:10

Unikorn


People also ask

How do you stop a timer in Publisher?

To stop the timer, you have two choices as well: Cancel the subscriber. If you called connect() on the timer publisher, that call returned a Cancellable object. If you kept a reference to that object, you can send cancel() to it.

What is PassthroughSubject?

A PassthroughSubject broadcasts elements to downstream subscribers and provides a convenient way to adapt existing imperative code to Combine. As the name suggests, this type of subject only passes through values meaning that it does not capture any state and will drop values if there aren't any subscribers set.

What is a publisher combine?

Combines elements from this publisher with those from another publisher of the same type, delivering an interleaved sequence of elements. func merge<P>(with: P) -> Publishers. Merge<Self, P> Combines elements from this publisher with those from another publisher, delivering an interleaved sequence of elements.


2 Answers

This operator will do the trick.

import PlaygroundSupport
import Foundation
import Combine

let page = PlaygroundPage.current
page.needsIndefiniteExecution = true

extension Publisher {
    func stopAfter<S>(_ interval: S.SchedulerTimeType.Stride, tolerance: S.SchedulerTimeType.Stride? = nil, scheduler: S, options: S.SchedulerOptions? = nil) -> AnyPublisher<Output, Failure> where S: Scheduler {
        prefix(untilOutputFrom: Just(()).delay(for: interval, tolerance: tolerance, scheduler: scheduler, options: nil))
            .eraseToAnyPublisher()
    }
}

let source = Timer.publish(every: 1, tolerance: nil, on: RunLoop.main, in: .default, options: nil)
    .autoconnect()
    .eraseToAnyPublisher()

let cancellable = source
    .stopAfter(10, scheduler: DispatchQueue.main)
    .sink(receiveValue: { print($0) })
like image 109
Daniel T. Avatar answered Sep 27 '22 20:09

Daniel T.


You can use the timeout() operator:

Terminates publishing if the upstream publisher exceeds the specified time interval without producing an element.

wifiScannerPublisher
    .timeout(.seconds(waitTime), scheduler: DispatchQueue.main, options: nil, customError:nil)
    .sink(
        receiveCompletion: { print("completion: \($0), at: \(Date())") },
        receiveValue: { print("wifi: \($0)") }
     )

If, however your publisher keeps regularly emitting events, and you just want to stop it after an amount of time passes, then Daniel's answer is probably the way to go.

Nonetheless, I'll add a solution on my own, via a Publisher extension that uses timeout() and scan():

extension Publisher {
    func stopAfter(_ interval: TimeInterval) -> AnyPublisher<Output, Failure> {
        self
            .timeout(.seconds(interval), scheduler: DispatchQueue.main)
            .scan((Date()+interval, nil)) { ($0.0, $1) }
            .prefix(while: { Date() < $0.0 })
            .map { $0.1! }
            .eraseToAnyPublisher()
    }
}

The above publisher will carry the timeout date, and once that date is reached, it will stop. The map is needed to discard the extra Date carried along the items.

Usage:

wifiListPublisher.stopAfter(10)
    .sink(...)
like image 42
Cristik Avatar answered Sep 27 '22 21:09

Cristik