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
Related
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
This is code I am using currently:
typealias ResponseHandler = (SomeResponse?, Error?) -> Void
class LoginService {
private var authorizeTokenCompletions = [ResponseHandler]()
func authorizeToken(withRefreshToken refreshToken: String, completion: #escaping ResponseHandler) {
if authorizeTokenCompletions.isEmpty {
authorizeTokenCompletions.append(completion)
post { [weak self] response, error in
self?.authorizeTokenCompletions.forEach { $0(response, error) }
self?.authorizeTokenCompletions.removeAll()
}
} else {
authorizeTokenCompletions.append(completion)
}
}
private func post(completion: #escaping ResponseHandler) {
// async
completion(nil, nil)
}
}
What is idea of above code?
authorizeToken function may be called as many times as it needs (for example 20 times)
Only one asynchronous request (post) may be pushed at a time.
All completions from called authorizeToken functions should be called with the same parameters as the first one completed.
Usage:
let service = LoginService()
service.authorizeToken(withRefreshToken: "") { a, b in print(a)}
service.authorizeToken(withRefreshToken: "") { a, b in print(a)}
service.authorizeToken(withRefreshToken: "") { a, b in print(a)}
service.authorizeToken(withRefreshToken: "") { a, b in print(a)}
service.authorizeToken(withRefreshToken: "") { a, b in print(a)}
All completions above should be printed with result from the first one which was called.
Is it possible to do this with RxSwift?
PS I will award a bounty of 100 once it is possible for the one who help me with this;)
Is it possible to do this with RxSwift?
Yes it is possible. RxSwift and Handling Invalid Tokens.
The simplest solution:
func authorizeToken(withRefreshToken refreshToken: String) -> Observable<SomeResponse> {
Observable.create { observer in
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
print("async operation")
observer.onNext(SomeResponse())
}
return Disposables.create()
}
}
let response = authorizeToken(withRefreshToken: "")
.share(replay: 1)
response.subscribe(onNext: { print($0) })
response.subscribe(onNext: { print($0) })
response.subscribe(onNext: { print($0) })
response.subscribe(onNext: { print($0) })
response.subscribe(onNext: { print($0) })
The above will only work if all requests (subscribes) are made before the first one completes. Just like your code.
If you want to store the response for use even after completion, then you can use replay instead of share.
let response = authorizeToken(withRefreshToken: "")
.replayAll()
let disposable = response.connect() // this calls the async function. The result will be stored until `disposable.dispose()` is called.
response.subscribe(onNext: { print($0) })
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
response.subscribe(onNext: { print($0) }) // this won't perform the async operation again even if the operation completed some time ago.
}
Answering
is it possible to do this with RxSwift
that's not possible, as every time we trigger the function it gets dispatched and we can't access the callbacks from other threads.
You're creating a race condition, a workaround is to populate the data once in a singleton, and rather than calling the function multiple times use that singleton.
some other approach might also work singleton is just an example.
Race condition: A race condition is what happens when the expected completion order of a sequence of operations becomes unpredictable, causing our program logic to end up in an undefined state
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)
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
I have an app that needs to check a status on a server:
every 30 seconds
whenever the app enters the foreground
I'm doing this by merging two publishers, then calling flatMap the merged publisher's output to trigger the API request.
I have a function that makes an API request and returns a publisher of the result, also including logic to check the response and throw an error depending on its contents.
It seems that once a StatusError.statusUnavailable error is thrown, the statusSubject stops getting updates. How can I change this behavior so the statusSubject continues getting updates after the error? I want the API requests to continue every 30 seconds and when the app is opened, even after there is an error.
I also have a few other points where I'm confused about my current code, indicated by comments, so I'd appreciate any help, explanation, or ideas in those areas too.
Here's my example code:
import Foundation
import SwiftUI
import Combine
struct StatusResponse: Codable {
var response: String?
var error: String?
}
enum StatusError: Error {
case statusUnavailable
}
class Requester {
let statusSubject = CurrentValueSubject<StatusResponse,Error>(StatusResponse(response: nil, error: nil))
private var cancellables: [AnyCancellable] = []
init() {
// Check for updated status every 30 seconds
let timer = Timer
.publish(every: 30,
tolerance: 10,
on: .main,
in: .common,
options: nil)
.autoconnect()
.map { _ in true } // how else should I do this to be able to get these two publisher outputs to match so I can merge them?
// also check status on server when the app comes to the foreground
let foreground = NotificationCenter.default
.publisher(for: UIApplication.willEnterForegroundNotification)
.map { _ in true }
// bring the two publishes together
let timerForegroundCombo = timer.merge(with: foreground)
timerForegroundCombo
// I don't understand why this next line is necessary, but the compiler gives an error if I don't have it
.setFailureType(to: Error.self)
.flatMap { _ in self.apiRequest() }
.subscribe(statusSubject)
.store(in: &cancellables)
}
private func apiRequest() -> AnyPublisher<StatusResponse, Error> {
let url = URL(string: "http://www.example.com/status-endpoint")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
return URLSession.shared.dataTaskPublisher(for: request)
.mapError { $0 as Error }
.map { $0.data }
.decode(type: StatusResponse.self, decoder: JSONDecoder())
.tryMap({ status in
if let error = status.error,
error.contains("status unavailable") {
throw StatusError.statusUnavailable
} else {
return status
}
})
.eraseToAnyPublisher()
}
}
Publishing a failure always ends a subscription. Since you want to continue publishing after an error, you cannot publish your error as a failure. You must instead change your publisher's output type. The standard library provides Result, and that's what you should use.
func makeStatusPublisher() -> AnyPublisher<Result<StatusResponse, Error>, Never> {
let timer = Timer
.publish(every: 30, tolerance: 10, on: .main, in: .common)
.autoconnect()
.map { _ in true } // This is the correct way to merge with the notification publisher.
let notes = NotificationCenter.default
.publisher(for: UIApplication.willEnterForegroundNotification)
.map { _ in true }
return timer.merge(with: notes)
.flatMap({ _ in
statusResponsePublisher()
.map { Result.success($0) }
.catch { Just(Result.failure($0)) }
})
.eraseToAnyPublisher()
}
This publisher emits either .success(response) or .failure(error) periodically, and never completes with a failure.
However, you should ask yourself, what happens if the user switches apps repeatedly? Or what if the API request takes more that 30 seconds to complete? (Or both?) You'll get multiple requests running simultaneously, and the responses will be handled in the order they arrive, which might not be the order in which the requests were sent.
One way to fix this would be to use flatMap(maxPublisher: .max(1)) { ... }, which makes flatMap ignore timer and notification signals while it's got a request outstanding. But it would perhaps be even better for it to start a new request on each signal, and cancel the prior request. Change flatMap to map followed by switchToLatest for that behavior:
func makeStatusPublisher2() -> AnyPublisher<Result<StatusResponse, Error>, Never> {
let timer = Timer
.publish(every: 30, tolerance: 10, on: .main, in: .common)
.autoconnect()
.map { _ in true } // This is the correct way to merge with the notification publisher.
let notes = NotificationCenter.default
.publisher(for: UIApplication.willEnterForegroundNotification)
.map { _ in true }
return timer.merge(with: notes)
.map({ _ in
statusResponsePublisher()
.map { Result<StatusResponse, Error>.success($0) }
.catch { Just(Result<StatusResponse, Error>.failure($0)) }
})
.switchToLatest()
.eraseToAnyPublisher()
}
You can use retry() to get this kind of behaviour or catch it...more info here:
https://www.avanderlee.com/swift/combine-error-handling/