RxSwift Wait For Observable to Complete - swift

I'm new to rxswift and here's my problem:
Suppose I have observable of actions: Observable.of("do1", "do2", "do3")
Now this observable mapped to function that returns observable:
let actions = Observable.of("do1", "do2", "do3")
func do(action: String) -> Observable<Result> {
// do something
// returns observable<Result>
}
let something = actions.map { action in return do(action) } ???
How can I wait for do1 to complete first, then execute do2, then do3?
Edit: Basically i want to achieve sequential execution of actions. do3 waits for do2 result, do2 waits for do1 result.
Edit2: I've tried using flatmap and subscribe, but all actions runs in parallel.

How can I wait for do1 to complete first, then execute do2, then do3?
I think concatMap solves the problem.
Lets say we have some service and some actions we need to perform on it, for instance a backend against with we'd like to authenticate and store some data. Actions are login and store. We can't store any data if we aren't logged in, so we need to wait login to be completed before processing any store action.
While flatMap, flatMapLatest and flatMapFirst execute observables in parallel, concatMap waits for your observables to complete before moving on.
import Foundation
import RxSwift
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
let service: [String:Observable<String>] = [
"login": Observable.create({
observer in
observer.onNext("login begins")
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: {
observer.onNext("login completed")
observer.onCompleted()
})
return Disposables.create()
}),
"store": Observable.create({
observer in
observer.onNext("store begins")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: {
observer.onNext("store completed")
observer.onCompleted()
})
return Disposables.create()
}),
]
// flatMap example
let observeWithFlatMap = Observable.of("login", "store")
.flatMap {
action in
service[action] ?? .empty()
}
// flatMapFirst example
let observeWithFlatMapFirst = Observable.of("login", "store")
.flatMapFirst {
action in
service[action] ?? .empty()
}
// flatMapLatest example
let observeWithFlatMapLatest = Observable.of("login", "store")
.flatMapLatest {
action in
service[action] ?? .empty()
}
// concatMap example
let observeWithConcatMap = Observable.of("login", "store")
.concatMap {
action in
service[action] ?? .empty()
}
// change assignment to try different solutions
//
// flatMap: login begins / store begins / store completed / login completed
// flatMapFirst: login begins / login completed
// flatMapLatest: login begins / store begins / store completed
// concatMap: login begins / login completed / store begins / store completed
let observable = observeWithConcatMap
observable.subscribe(onNext: {
print($0)
})

I just face the same problem, and finally found the solution.
I expect my devices will do disconnect followed by one another, so I did as follow:
I just create the func like
func disconnect(position: WearingPosition) -> Completable{
print("test run")
return Completable.create { observer in
print("test run 2")
// Async process{
// observer(.complete)
// }
return Disposables.create()
}
}
And use like:
self.disconnect(position: .left_wrist).andThen(Completable.deferred({
return self.disconnect(position: .right_wrist)
})).subscribe(onCompleted: {
// do some things
}) { (error) in
print(error)
}.disposed(by: self.disposeBag)
The key is the usage of " Completable.deferred "
I have tested with the "test run" printed

Use flatMap or flatMapLatest. You can find more about them at reactivex.io.

You can flatMap and subscribe to the Result observable sequence by calling subscribe(on:) on the final output.
actions
.flatMap { (action) -> Observable<Result> in
return self.doAction(for: action)
}
.subscribe(onNext: { (result) in
print(result)
})
func doAction(for action: String) -> Observable<Result> {
//...
}
Read:
https://medium.com/ios-os-x-development/learn-and-master-%EF%B8%8F-the-basics-of-rxswift-in-10-minutes-818ea6e0a05b

Related

Swift Combine collect values until last page is found or until time expires

So I want to collect values until I see the last page, but if the last page never comes I want to send what we have, given a time limit.
I have a way of doing this but it seems rather wasteful. I'm going to be using this to make collections that may have hundreds of thousands of values, so a more space-efficient method would be preferred.
You can copy and paste the following into a playground
import UIKit
import Foundation
import Combine
var subj = PassthroughSubject<String, Never>()
let queue = DispatchQueue(label: "Test")
let collectForTime = subj
.collect(.byTime(queue, .seconds(10)))
let collectUntilLast = subj
.scan([String]()) { $0 + [$1] }
.first { $0.last == "LastPage" }
// whichever happens first
let cancel = collectForTime.merge(with: collectUntilLast)
.first()
.sink {
print("complete1: \($0)")
} receiveValue: {
print("received1: \($0)")
}
print("start")
let strings = [
"!##$",
"ZXCV",
"LastPage", // comment this line to test to see what happens if no last page is sent
"ASDF",
"JKL:"
]
// if the last page is present then the items 0..<3 will be sent
// if there's no last page then send what we have
// the main thing is that the system is not just sitting there waiting for a last page that never comes.
for i in (0..<strings.count) {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(i)) {
let s = strings[i]
print("sending \(s)")
subj.send(s)
}
}
UPDATE
After playing a bit more with the playground I think all you need is:
subj
.prefix { $0 != "LastPage" }
.append("LastPage")
.collect(.byTime(DispatchQueue.main, .seconds(10)))
I wouldn't use collect because under the hood it is basically doing the same thing that scan is doing, you only need another condition in the first closure eg: .first { $0.last == "LastPage" || timedOut } to emit the collected items in case of timeout.
It's unfortunate that collect doesn't offer the API you need but we can create another version of it.
The idea is to combineLatest the scan output with a stream that emits a Bool after a deadline (In reality we also need to emit false initially for combineLatest to start) and || this additional variable inside filter condition.
Here is the code:
extension Publisher {
func collect<S: Scheduler>(
timeoutAfter interval: S.SchedulerTimeType.Stride,
scheduler: S,
orWhere predicate: #escaping ([Output]) -> Bool
) -> AnyPublisher<[Output], Failure> {
scan([Output]()) { $0 + [$1] }
.combineLatest(
Just(true)
.delay(for: interval, scheduler: scheduler)
.prepend(false)
.setFailureType(to: Failure.self)
)
.first { predicate($0) || $1 }
.map(\.0)
.eraseToAnyPublisher()
}
}
let subj = PassthroughSubject<String, Never>()
let cancel = subj
.collect(
timeoutAfter: .seconds(10),
scheduler: DispatchQueue.main,
orWhere: { $0.last == "LastPage" }
)
.print()
.sink { _ in }
I made a small change to your technique
import Foundation
import Combine
var subj = PassthroughSubject<String, Never>()
let lastOrTimeout = subj
.timeout(.seconds(10), scheduler: RunLoop.main )
.print("watchdog")
.first { $0 == "LastPage" }
.append(Just("Done"))
let cancel = subj
.prefix(untilOutputFrom: lastOrTimeout)
.print("main_publisher")
.collect()
.sink {
print("complete1: \($0)")
} receiveValue: {
print("received1: \($0)")
}
print("start")
let strings = [
"!##$",
"ZXCV",
"LastPage", // comment this line to test to see what happens if no last page is sent
"ASDF",
"JKL:"
]
// if the last page is present then the items 0..<3 will be sent
// if there's no last page then send what we have
// the main thing is that the system is not just sitting there waiting for a last page that never comes.
strings.enumerated().forEach { index, string in
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(index)) {
print("sending \(string)")
subj.send(string)
}
}
lastOrTimeout will emit a value when it see's LastPage or finishes because of a timeout (and emits Done).
The main pipeline collects values until the watchdog publisher emits a value and collects all the results.

RxSwift replay the last value of a completed observable

I have a cold observable that may get called multiple times. This observable does an expensive task (a network request) and then completes. I would like this observable to only ever make a single network call and if I need to call it again in the future I would like to get the last emitted value.
If an observable doesn't complete (i.e. just sends a next value without a completed event) I can use the .share(replay: 1, scope: .whileConnected) function to always get the last value. Unfortunately, this doesn't work with observables that completes at the end of the request. Bellow is an example:
let disposeBag = DisposeBag()
let refreshSubject = PublishSubject<Void>()
override func viewDidLoad() {
super.viewDidLoad()
let observable = Observable<String>.create { observer in
let seconds = 2.0
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
observer.onNext("Hello, World")
observer.onCompleted() // <-- Works when commented out
}
return Disposables.create()
}
.share(replay: 1, scope: .whileConnected)
refreshSubject
.flatMap { _ in observable }
.subscribe(onNext: { response in
print("response: ", response)
})
.disposed(by: disposeBag)
}
#IBAction func refreshButtonHandler(_ sender: Any) {
refreshSubject.onNext(())
}
Every time the refreshSubject is triggered it takes 2 seconds for the Hello, World to be printed. If I remove the observer.onCompleted() line however, it only takes 2 seconds the first time and subsequently returns a cached response.
Obviously this is just an example, in the real world I would not have any control if the observable completes or not but I would like to always just replay the last value regardless.
So you don't want the cold observable to be re-subscribed to even when the refresh is triggered. In that case, this is the solution:
Observable.combineLatest(refreshSubject.startWith(()), yourColdObservable)
.map { $0.1 }
.subscribe(onNext: { val in
print("*** val: ", val)
})
Using flatMap means that the observable is re-subscribed to every time an event enters the flatMap. By using combineLatest instead, the cold observable will only be subscribed to once. The combineLatest operator will store the result of the observable internally and emit it again every time the refresh subject emits.
(No share is needed for this method.)
let yourColdObservable = Observable<String>.create { observer in
let seconds = 2.0
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
observer.onNext("Hello, World")
observer.onCompleted()
}
return Disposables.create()
}
let cacheObservable = refreshButton.rx.tap
.startWith(())
.flatMapLatest { _ in yourColdObservable }
.share(replay: 1)
makeRequestWithCacheButton.rx.tap
.flatMapLatest { _ in cacheObservable }
.subscribe(onNext: { response in
print("response: ", response)
})
.disposed(by: disposeBag)
StartWith
emit a specified sequence of items before beginning to emit the items
from the source Observable
Such a fake tap on the refreshButton when starting the sequence
FlatMapLatest
The FlatMap operator transforms an Observable by applying a function
that you specify to each item emitted by the source Observable, where
that function returns an Observable that itself emits items. FlatMap
then merges the emissions of these resulting Observables, emitting
these merged results as its own sequence.
FlatMapLatest is special type of FlatMap, because it cancels previous Observable when refreshButton.rx.tap emit onNext event.

How do I test for flow / order of execution?

I'm trying to add a feature to show loading screen to this code:
func connect(with code: String) {
interactor.connect(with: code)
.subscribe(onNext: { displaySuccessScreenRelay.accept(()) },
onError: { displayErrorScreenRelay.accept(()) } )
.disposed(by: disposeBag)
}
I've made a behavior relay called loadingScreenShownRelay ad I know the proper way to do this is like this:
func connect(with code: String) {
loadingScreenShownRelay.accept(true)
interactor.connect(with: code)
.subscribe(onNext: {
displaySuccessScreenRelay.accept(())
loadingScreenShownRelay.accept(false)
},
onError: {
displayErrorScreenRelay.accept(())
loadingScreenShownRelay.accept(false)
})
.disposed(by: disposeBag)
}
Now the question is how do I rearrange the test so that I can test not only the logic but the order of showLoading -> success -> hideLoading?
I can probably test the displays and loadingScreenShown observables separately into 2 tests (ie. one to test the logic of input-> emit display success / error and one more to test the loadingScreenShown). But how do I know that the order was indeed showLoading -> success -> hideLoading ? If I do the tests without regards to the order, I can also do this and the tests will still go green.
func connect(with code: String) {
loadingScreenShownRelay.accept(true)
loadingScreenShownRelay.accept(false)
interactor.connect(with: code)
.subscribe(onNext: { displaySuccessScreenRelay.accept(()) },
onError: { displayErrorScreenRelay.accept(()) } )
.disposed(by: disposeBag)
}
Thanks in advance.
Imperative code, such as you have, can be difficult to test. You will need to put the entire class under test and you will need a fake Interactor.
A test like this will show that your loading screen relay works:
class ExampleTests: XCTestCase {
func test() {
let scheduler = TestScheduler(initialClock: 0)
let sut = Example(interactor: FakeInteractor(connect: { _ in
scheduler.createColdObservable([.next(10, ()), .completed(10)]).asObservable()
}))
let result = scheduler.createObserver(Bool.self)
_ = sut.loadingScreenShownRelay
.bind(to: result)
sut.connect(with: "foo")
scheduler.start()
XCTAssertEqual(result.events, [.next(0, true), .next(10, false)])
}
}
You can see in the assert that the true event comes before the false event and they are separated by the time units it takes for the interactor to emit.
The above uses the RxTest library.

Observable subscribe alway once issue

I writing async unit tests for RxSwift,this my code,I can't understand subscribe only once
class TestViewModel: NSObject {
let result : Observable<Int>
init(input:Observable<Int>) {
result = input.flatMapLatest({ (value) -> Observable<Int> in
return Observable.create({ (observer) -> Disposable in
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1, execute: {
print("next"+" \(value)")
observer.onNext(value)
})
return Disposables.create()
})
})
}
}
func testCount() {
let expectation = XCTestExpectation(description: "async")
let input = scheduler.createHotObservable([.next(100, 1),.next(200, 10)])
let viewModel = TestViewModel.init(input: input.asObservable())
viewModel.result.subscribe(onNext: { (value) in
print("subscribe"+" \(value)")
}).disposed(by: disposeBag)
scheduler.start()
wait(for: [expectation], timeout: timeout)
}
print info:
next 1
next 10
subscribe 10
I think print info should :
next 1
next 10
subscribe 1
subscribe 10
Someone can give me suggestion?thank
It's how flatMapLatest operator works. Its basically tells "map events into observables but use only recent observable's result". So, you map your events into two observables:
1: --1sec-> 1
10: --1sec-> 10
Most recent observable at the moment is for 10.
Try to use flatMap instead of flatMapLatest.
You should also avoid Observable.create if possible. In your particular case (to delay a value) you could use Observable.timer or Observable.just(...).delay(...).

Dealing with multiple completion handlers

I'm trying to coordinate several completion handlers for each element in an array.
The code is essentially this:
var results = [String:Int]()
func requestData(for identifiers: [String])
{
identifiers.forEach
{ identifier in
service.request(identifier, completion: { (result) in
result[identifier] = result
})
}
// Execute after all the completion handlers finish
print(result)
}
So each element in the Array is sent through a service with a completion handler, and all the results are stored in an array. Once all of these handlers complete, I wish to execute some code.
I attempted to do this with DispatchQueue
var results = [String:Int]()
func requestData(for identifiers: [String])
{
let queue = DispatchQueue.init(label: "queue")
identifiers.forEach
{ identifier in
service.request(identifier, completion: { (result) in
queue.sync
{
result[identifier] = result
}
})
}
// Execute after all the completion handlers finish
queue.sync
{
print(result)
}
}
but the print call is still being executed first, with an empty Dictionary
If I understand what are you are trying to do correctly, you probably want to use a DispatchGroup
Here is an example:
let group = DispatchGroup()
var letters = ["a", "b", "c"]
for letter in letters {
group.enter()
Server.doSomething(completion: { [weak self] (result) in
print("Letter is: \(letter)")
group.leave()
})
}
group.notify(queue: .main) {
print("- done")
}
This will print something like:
b
c
a
// ^ in some order
- done
First, take note that your service.request(...) is processed in asynchronous mode. Another problem is you want to finish all the service request in that loop.
My suggestion is create the function with completion handler and add a counter on each loop done. Your function will be similarly as below.
var results = [String:Int]()
func requestData(for identifiers: [String], callback:#escaping (Bool) -> Void)
{
var counter = 0
var maxItem = identifiers.count
identifiers.forEach
{ identifier in
service.request(identifier, completion: { (result) in
result[identifier] = result
counter += 1
if counter == maxItem {
callback(true) // update completion handler to say all loops request are done
}
// if not, continue the other request
})
}
}
This is how another part of your code will call the function and wait for callback
requestData(for identifiers:yourArrays) { (complete) in
if complete {
print(results)
}
}
Don't forget to manage if errors happened.