How to make restartable countdown using Rxswift - ios

I want to create a countdown feature that restart every time I press a button.
However, the code I wrote terminate the subscription when the countdown is completed.
What can I do to ensure that my subscription is not terminated and the countdown is restarted?
fileprivate let counter = 10
fileprivate let startCountDown = PublishRelay<Void>()
startCountDown
.flatMapLatest { _ -> Observable<Int> in
return Observable<Int>.timer(.seconds(0), period: .seconds(1), scheduler: MainScheduler.instance)
}
.take(counter + 1)
.subscribe(onNext: { time in
print(time)
}, onCompleted: {
print("Completed")
})
.disposed(by: rx.disposeBag)

When take(_:) completes, the Observable chain is disposed. However, if the Observable inside the flatMapLatest closure completes, the chain is not disposed because startCountDown hasn't completed yet. The flatMapLatest observable won't complete until all the observables it subscribes to completes. So the solution is to put the take(_:) inside the flatMapLatest.
The View Model should look like this:
func startCountDown(counter: Int, trigger: Observable<Void>, scheduler: SchedulerType) -> Observable<Int> {
trigger
.flatMapLatest {
Observable<Int>.timer(.seconds(0), period: .seconds(1), scheduler: scheduler)
.take(counter + 1)
}
}
You use the above view model in your view controller like this:
startCountdown(counter: 10, trigger: startButton.rx.tap.asObservable(), scheduler: MainScheduler.instance)
.subscribe(onNext: { time in
print(time)
})
.disposed(by: rx.disposeBag)
Strictly speaking, this is a count-up timer. It will go from 0 to counter and then wait until the button is tapped again. If the button is tapped while it is counting, it will restart. If you want it to ignore taps until it's done counting, then use flatMapFirst instead.
Learn more in by reading this article: RxSwift's Many Faces of FlatMap
As a bonus, here's how you can test the view model:
final class CountdownTests: XCTestCase {
func test() {
let scheduler = TestScheduler(initialClock: 0)
let trigger = scheduler.createObservable(timeline: "--V---V-|", values: ["V": ()])
let expected = parseEventsAndTimes(timeline: "---012-0123456789|", values: { Int(String($0))! })
let result = scheduler.start(created: 0, subscribed: 0, disposed: 100) {
startCountDown(counter: 9, trigger: trigger, scheduler: scheduler)
}
XCTAssertEqual(result.events, expected[0])
}
}
The above uses my TestScheduler

Related

How make Combine's flatMap to complete overall stream?

I have some code like this
func a() -> AnyPublisher<Void, Never> {
Future<Void, Never> { promise in
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
print(1)
promise(.success(()))
}
}
.eraseToAnyPublisher()
}
func b() -> AnyPublisher<Void, Never> {
Future<Void, Never> { promise in
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
print(2)
promise(.success(()))
}
}
.eraseToAnyPublisher()
}
var tempBag = Set<AnyCancellable>()
let subject = CurrentValueSubject<Int, Never>(1)
subject
.flatMap({ _ in a() })
.flatMap({ _ in b() })
.print()
.sink(receiveCompletion: { _ in
tempBag.removeAll()
}, receiveValue: { })
.store(in: &tempBag)
So, I have some uncompletable subject in the root of the stream and some completable publishers in flatMap operator. I want the overall stream to complete when the last flatMap's publisher completes. So, I want the console to look like this:
receive subscription: (FlatMap)
request unlimited
1
2
receive value: (())
receive finished
but actual result is
receive subscription: (FlatMap)
request unlimited
1
2
receive value: (())
How can I achieve this?
The problem you are having is that your Subject (the CurrentValueSubject) never finishes so the entire chain never completes. What you need is a publisher that emits a single value then completes at the top of your sequence, and an intermediate that waits until all the publishers it is tracking complete before finishing itself.
You already have a publisher that does one thing then completes... it's returned by a(). To wait until both a() and b() complete you can use combineLatest since a the publisher it creates won't finish until all the publishers it combines finish. The whole thing looks like:
a()
.combineLatest(b())
.print()
.sink(receiveCompletion: { _ in
tempBag.removeAll()
}, receiveValue: { _ in () })
.store(in: &tempBag)
with the output
receive subscription: (CombineLatest)
request unlimited
1
2
receive value: (((), ()))
receive finished

Why is this Combine pipeline not letting items through?

I'm stuck on a Combine problem, and I can't find a proper solution for this.
My goal is to monitor a queue, and process items in the queue until it's empty. If then someone adds more items in the queue, I resume processing. Items needs to be processed one by one, and I don't want to lose any item.
I wrote a very simplified queue below to reproduce the problem. My items are modeled as just strings for the sake of simplicity again.
Given the contraints above:
I use a changePublisher on the queue to monitor for changes.
A button lets me add a new item to the queue
The flatMap operator relies on the maxPublishers parameter to only allow one in-flight processing.
The buffer operator prevents items from being lost if the flatMap is busy.
Additionally, I'm using a combineLatest operator to only trigger the pipeline under some conditions. For simplicity, I'm using a Just(true) publisher here.
The problem
If I tap the button, a first item goes in the pipeline and is processed. The changePublisher triggers because the queue is modified (item is removed), and the pipeline stops at the compactMap because the peek() returns nil. So far, so good. Afterwards, though, if I tap on the button again, a value is sent in the pipeline but never makes it through the buffer.
Solution?
I noticed that removing the combineLatest prevents the problem from happening, but I don't understand why.
Code
import Combine
import UIKit
class PersistentQueue {
let changePublisher = PassthroughSubject<Void, Never>()
var strings = [String]()
func add(_ s: String) {
strings.append(s)
changePublisher.send()
}
func peek() -> String? {
strings.first
}
func removeFirst() {
strings.removeFirst()
changePublisher.send()
}
}
class ViewController: UIViewController {
private let queue = PersistentQueue()
private var cancellables: Set<AnyCancellable> = []
override func viewDidLoad() {
super.viewDidLoad()
start()
}
#IBAction func tap(_ sender: Any) {
queue.add(UUID().uuidString)
}
/*
Listen to changes in the queue, and process them one at a time. Once processed, remove the item from the queue.
Keep doing this until there are no more items in the queue. The pipeline should also be triggered if new items are
added to the queue (see `tap` above)
*/
func start() {
queue.changePublisher
.print("Change")
.buffer(size: Int.max, prefetch: .keepFull, whenFull: .dropNewest)
.print("Buffer")
// NOTE: If I remove this combineLatest (and the filter below, to make it compile), I don't have the issue anymore.
.combineLatest(
Just(true)
)
.print("Combine")
.filter { _, enabled in return enabled }
.print("Filter")
.compactMap { _ in
self.queue.peek()
}
.print("Compact")
// maxPublishers lets us process one page at a time
.flatMap(maxPublishers: .max(1)) { reference in
return self.process(reference)
}
.sink { reference in
print("Sink for \(reference)")
// Remove the processed item from the queue. This will also trigger the queue's changePublisher,
// which re-run this pipeline in case
self.queue.removeFirst()
}
.store(in: &cancellables)
}
func process(_ value: String) -> AnyPublisher<String, Never> {
return Future<String, Never> { promise in
print("Starting processing of \(value)")
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 2) {
promise(.success(value))
}
}.eraseToAnyPublisher()
}
}
Output
Here is a sample run of the pipeline if you tap on the button twice:
Change: receive subscription: (PassthroughSubject)
Change: request max: (9223372036854775807)
Buffer: receive subscription: (Buffer)
Combine: receive subscription: (CombineLatest)
Filter: receive subscription: (Print)
Compact: receive subscription: (Print)
Compact: request max: (1)
Filter: request max: (1)
Combine: request max: (1)
Buffer: request max: (1)
Change: receive value: (())
Buffer: receive value: (())
Combine: receive value: (((), true))
Filter: receive value: (((), true))
Compact: receive value: (3999C98D-4A86-42FD-A10C-7724541E774D)
Starting processing of 3999C98D-4A86-42FD-A10C-7724541E774D
Change: request max: (1) (synchronous)
Sink for 3999C98D-4A86-42FD-A10C-7724541E774D // First item went through pipeline
Change: receive value: (())
Compact: request max: (1)
Filter: request max: (1)
Combine: request max: (1)
Buffer: request max: (1)
Buffer: receive value: (())
Combine: receive value: (((), true))
Filter: receive value: (((), true))
// Second time compactMap is hit, value is nil -> doesn't forward any value downstream.
Filter: request max: (1) (synchronous)
Combine: request max: (1) (synchronous)
Change: request max: (1)
// Tap on button
Change: receive value: (())
// ... Nothing happens
[EDIT] Here is a much more constrained example, which can run in Playgrounds and which also demonstrates the problem:
import Combine
import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
func process(_ value: String) -> AnyPublisher<String, Never> {
return Future<String, Never> { promise in
print("Starting processing of \(value)")
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 0.1) {
promise(.success(value))
}
}.eraseToAnyPublisher()
}
var count = 3
let s = PassthroughSubject<Void, Never>()
var cancellables = Set<AnyCancellable>([])
// This reproduces the problem. Switching buffer and combineLatest fix the problem…
s
.print()
.buffer(size: Int.max, prefetch: .keepFull, whenFull: .dropNewest)
.combineLatest(Just("a"))
.filter { _ in count > 0 }
.flatMap(maxPublishers: .max(1)) { _, a in process("\(count)") }
.sink {
print($0)
count -= 1
s.send()
}
.store(in: &cancellables)
s.send()
Thread.sleep(forTimeInterval: 3)
count = 1
s.send()
Switching combine and buffer fixes the problem.
I am not sure why the pipeline is blocked, but there is no reason to publish when the queue is empty. Fixing this resolved the problem for me.
func removeFirst() {
guard !strings.isEmpty else {
return
}
strings.removeFirst()
if !self.strings.isEmpty {
self.changePublisher.send(self.strings.first)
}
}
Just tried your example. It works as expected if buffer is placed before flatMap. And update removeFirst according to Paulw11's answer below
queue.changePublisher
.print("Change")
// NOTE: If I remove this combineLatest (and the filter below, to make it compile), I don't have the issue anymore.
.combineLatest(Just(true))
.print("Combine")
.filter { _, enabled in return enabled }
.print("Filter")
.compactMap { _ in
self.queue.peek()
}
.print("Compact")
// maxPublishers lets us process one page at a time
.buffer(size: Int.max, prefetch: .keepFull, whenFull: .dropNewest)
.print("Buffer")
.flatMap(maxPublishers: .max(1)) { reference in
return self.process(reference)
}
.sink { reference in
print("Sink for \(reference)")
// Remove the processed item from the queue. This will also trigger the queue's changePublisher,
// which re-run this pipeline in case
self.queue.removeFirst()
print("COUNT: " + self.queue.strings.count.description)
}
.store(in: &cancellables)

Memory leak when using `Publishers.Sequence`

I have a function that create collection of Publishers:
func publishers(from text: String) -> [AnyPublisher<SignalToken, Never>] {
let signalTokens: [SignalToken] = translate(from: text)
var delay: Int = 0
let signalPublishers: [AnyPublisher<SignalToken, Never>] = signalTokens.map { token in
let publisher = Just(token)
.delay(for: .milliseconds(delay), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
delay += token.delay
return publisher
}
return signalPublishers
}
In service class I have to method, one for play():
func play(signal: String) {
anyCancellable = signalTokenSubject.sink(receiveValue: { token in print(token) }
anyCancellable2 = publishers(from: signal)
.publisher
.flatMap { $0 }
.subscribe(on: DispatchQueue.global())
.sink(receiveValue: { [weak self] token in
self?.signalTokenSubject.send(token)
})
}
and one for stop():
func stop() {
anyCancellable?.cancel()
anyCancellable2?.cancel()
}
I've had problem with memory. When collection of publishers is large and I stop() before whole Publishers.Sequence is .finshed memory increase and never release.
Is there a way to completed Publishers.Sequence earlier, before Combine iterate over whole collection?
To reclaim the memory, release the pipelines:
func stop() {
anyCancellable?.cancel()
anyCancellable2?.cancel()
anyCancellable = nil
anyCancellable2 = nil
}
Actually you don't need the cancel calls, because releasing the pipelines does cancel in good order; that is the whole point of AnyCancellable. So you can just say:
func stop() {
anyCancellable = nil
anyCancellable2 = nil
}
Another thing to note is that you are running all your publishers at once. The sequence does not arrive sequentially; the whole sequence is dumped into the flapMap which starts all the publishers publishing simultaneously. Thus cancelling doesn't do you all that much good. You might want to set the maxPublishers: on your flatMap so that backpressure prevents more than some small number of publishers from arriving simultaneously (like for example one at a time).

Execute Combine Future in background thread is not working

If you run this on a Playground:
import Combine
import Foundation
struct User {
let name: String
}
var didAlreadyImportUsers = false
var importUsers: Future<Bool, Never> {
Future { promise in
sleep(5)
promise(.success(true))
}
}
var fetchUsers: Future<[User], Error> {
Future { promise in
promise(.success([User(name: "John"), User(name: "Jack")]))
}
}
var users: AnyPublisher<[User], Error> {
if didAlreadyImportUsers {
return fetchUsers
.receive(on: DispatchQueue.global(qos: .userInitiated))
.eraseToAnyPublisher()
} else {
return importUsers
.receive(on: DispatchQueue.global(qos: .userInitiated))
.setFailureType(to: Error.self)
.combineLatest(fetchUsers)
.map { $0.1 }
.eraseToAnyPublisher()
}
}
users
.receive(on: DispatchQueue.global(qos: .userInitiated))
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
print(value)
})
print("run")
the output will be:
[User(name: "John"), User(name: "Jack")]
run
finished
But I was expecting to get:
run
[User(name: "John"), User(name: "Jack")]
finished
Because the sink should run the code in background thread. What I'm missing here.
Do I need to rin the code:
sleep(5)
promise(.success(true))
in a background thread ? then what's the purpose of
.receive(on: DispatchQueue.global(qos: .userInitiated))
Your Future runs as soon as it's created, so in your case as soon as this property is accessed:
var importUsers: Future<Bool, Never> {
Future { promise in
sleep(5)
promise(.success(true))
}
}
And since the Future runs immediately, that means the closure passed to the promise is executed right away, making the main thread sleep for 5 seconds before it moves on. In your case the Future is created as soon as you access users which is done on the main thread.
receive(on: affects the thread that sink (or downstream publishers) receive values on, not where they are created. Since the futures are already completed by the time you call .sink, the completion and emitted value are delivered to the sink immediately. On a background queue, but still immediately.
After that, you finally hit the print("run") line.
If you replace the sleep(5) bit with this:
var importUsers: Future<Bool, Never> {
Future { promise in
DispatchQueue.global().asyncAfter(deadline: .now() + 5) {
promise(.success(true))
}
}
}
and make some minor tweaks to your subscription code:
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
var cancellables = Set<AnyCancellable>()
users
.receive(on: DispatchQueue.global(qos: .userInitiated))
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
print(value)
}).store(in: &cancellables)
You'll see that the output is printed as expected because that initial future doesn't block the main thread for five seconds.
Alternatively, if you keep the sleep and subscribe like this you would see the same output:
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
var cancellables = Set<AnyCancellable>()
users
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
print(value)
}).store(in: &cancellables)
The reason for that is that you subscribe on a background thread, so the subscription and everything is set up off the main thread asynchronously which causes print("run") to run before receiving the Future's result. However, the main thread still slept for 5 seconds as soon as the users property is accessed (which is on the main thread) because that's when you initialize the Future. So the entire output is printed all at once and not with a 5 second sleep after "run".
There is a convenient way to reach the expected. Combine has a Deferred publisher which waits until subscribe(on:) is receiver. So that the code should be something like this
var fetchUsers: Future<[User], Error> {
return Deferred {
Future { promise in
sleep(5)
promise(.success([User(name: "John"), User(name: "Jack")]))
}
}.eraseToAnyPublisher()
}
var cancellables = Set<AnyCancellable>()
fetchUsers
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
print(value)
}).store(in: &cancellables)
print("run")
Such code won't stop main queue and the output will be as expected
run
[User(name: "John"), User(name: "Jack")]
finished

ReactiveSwift: How to write a Task Scheduler

I am trying to create a scheduler to consume some data.
The scheduler will have to be able to:
send an event each time data should be consumed manually
send an event each time data should be consumed automatically after some time has elapsed after the last consumption
I have modelled the manual consumption with a MutableProperty
let consume = MutableProperty<Void>()
and I am trying to model the automatic consumption with a SignalProducer
let timer = SignalProducer<Void, NoError>
I can get the first time that I need to consume that data by combining the latest values of these two producers like
SignalProducer.combineLatest(consume.producer, timer)
.take(first: 1)
.map() { _ in return () }
That way whichever comes first, a manual consumption or an automatic one the producer will send a value.
I can't figure out how I will be able to do this perpetually.
Can someone help?
You can start a timer using the global timer functions defined in ReactiveSwift
public func timer(interval: TimeInterval, on scheduler: DateSchedulerProtocol) -> SignalProducer<Date, NoError>
To combine the timer with the consume property:
let interval = 10.0
let timerSignal: SignalProducer<Date, NoError> = timer(interval: interval, on: QueueScheduler.main)
let consume = MutableProperty<Void>()
timerSignal.combineLatest(with: consume.producer).startWithValues { (date, v) in
print("triggered at time: \(date)")
}
This way you can trigger the print block manually by setting the value property on consume, or by waiting for the timer event.
You can wrap timer into SignalProducer
func timerProducer() -> SignalProducer<Void, Never> {
return SignalProducer<Void, Never> { observer, disposable in
let timer = Timer(timeInterval: 10, repeats: true) { timer in
if disposable.hasEnded {
timer.invalidate()
} else {
observer.send(value: ())
}
}
RunLoop.main.add(timer, forMode: .common)
}
}
Note: If you starting this producer from NON-main thread, you need add it to RunLoop. Otherwise it will not get triggered.

Resources