6

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?

Unikorn
  • 1,140
  • 1
  • 13
  • 27
  • How often do you want to scan? Do you want to stop scanning after a time limit (e.g. 10 seconds), or after a certain number of scans (e.g. after 10 scans, where each is spaced apart by 1s)? – Alexander Oct 29 '21 at 00:20
  • And is there some trigger that should start a rescan? – Daniel T. Oct 29 '21 at 00:31
  • the scan is called once with data coming back every second. I want to stop after 10 seconds. @Alexander – Unikorn Oct 29 '21 at 00:40
  • Are you using combine to trigger the each scan? – Alexander Oct 29 '21 at 00:42
  • @Alexander. No. ie wifiListPublisher.sink... wifiListPublisher is a custom publisher that makes a call and outputs every second with wifi ssid. – Unikorn Oct 29 '21 at 00:49
  • I think writing a version of the Combine Timer without repeating and zip it with another publisher is one way to do it. – Unikorn Oct 29 '21 at 01:00

2 Answers2

8

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) })
Daniel T.
  • 32,821
  • 6
  • 50
  • 72
  • 2
    The timeout publisher can be simplified to something like `Just(1).delay(for: 10, scheduler: DispatchQueue.main)` – Cristik Nov 04 '21 at 06:31
  • 2
    @Cristik Thanks for the improvement. I also wrapped the function in a new operator like you did. – Daniel T. Nov 04 '21 at 11:30
3

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(...)
Cristik
  • 30,989
  • 25
  • 91
  • 127
  • This doesn't work for my scenario as I need to collect the stream of output for the amount of time without erroring out. – Unikorn Oct 31 '21 at 06:15
  • The output values should already be sent by the time the error comes. But if you don't want an error you can `catch` the publisher. Depends on how your wifi publisher emits values. @Unikorn – Cristik Oct 31 '21 at 08:05
  • Timeout doesn't stop if you keep receiving values as in the case of wifi scan. From apple doc: Terminates publishing if the upstream publisher exceeds the specified time interval without producing an element. – Unikorn Nov 02 '21 at 06:35
  • @Unikorn, so you want for the publisher to end after 10 seconds, regardless if new wifis keep being discovered? – Cristik Nov 02 '21 at 06:54
  • I want the publisher to complete in 10 seconds. The publisher will keep publishing wifi list every second indefinitely until request to stop. The prefix(untilOutputFrom:) works well in this situation. – Unikorn Nov 04 '21 at 04:02
  • 1
    @Unikorn yeah, in that case Daniel's solution is the way to go. – Cristik Nov 04 '21 at 07:12