Avoid Nested RxSwift Subscriptions - ios

I have to work with 2 database objects which are related to each other. The way I have done it created nested subscriptions. I read a couple of blogs but couldn't find a way to do away with nested subscriptions. Any pointers would be appreciated.
A topic has a list of children. After retrieving all the children, I need to print "topic name" + "child name", after iterating over all the children. This is a simplified version of the problem statement. The only catch is data service returns observable of database objects to which I need to subscribe.
let ds = DatabaseService()
func createBindings() {
_ = ds.getTopics()
.flatMapLatest{self.updateSections(array: $0)}
.subscribe()
}
func updateSections(array: [Topics]) -> Observable<Void> {
for topic in array {
let children = topic.children
for id in children {
_ = ds.getChildWith(id: id).map { lo in
//do business related stuff
}
.subscribe()
}
}
return .never()
}
I want to avoid another subscription in flatMapLatest.

You can try and combine streams created by getChildWith(id: id) with combineLatest operator. (All this streams will need to complete in order to complete the returning stream).
It should look something like:
let ds = DatabaseService()
func createBindings() {
_ = ds.getTopics()
.flatMapLatest{self.updateSections(array: $0)}
.subscribe()
}
func updateSections(array: [Topics]) -> Observable<Void> {
let allStreams = array.flatMap { topic in
topic.children.map { id in
return ds.getChildWith(id: id).map { lo in
//do business related stuff
}
}
}
return Observable.combineLatest(allStreams, { _ in () })
}
P.S. Try to avoid ignoring Disposable you'll end up never releasing that stream.

Related

Dynamically terminate Observable based on another observable | RxSwift

I have array of Observables, say [Observable <WriteTaskResult>]
I want to perform all write tasks keeping their order, and if any one of them fails then I want to perform Observable<ResetTaskResult>
Following function will return observable of type BatchTasksResult for tracking tasks progress.
Sample Code:
enum BatchTasksResult{
case elapsedTime(Double)
case failedFatal
case rolledback
case success
}
func writeBlocks(tasks: [WriteTask]) -> Observable<BatchTasksResult>{
return Observable.create {(observable) -> Disposable in
let allTasks: [Observable<WriteTaskResult>] = self.writeSomewhere(tasks)
Observable.concat(allTasks)
.subscribe { writeTaskResult in
observable.onNext(.elapsedTime(writeTaskResult.totalTime))
}
onError: { (err) in
// Perform Observable<ResetTaskResult>
// if ResetTask was successful then observable.onNext(.rolledback)
// if ResetTask failed then observable.onNext(.failedFatal)
}
onCompleted: {
observable.onNext(.success)
}
.disposed(by: disposeBag)
return Disposables.create()
}
}
How do I trigger rollback logic using Observable from onError of allTasks' observable?
Simple solution seems nested observable, but that's not good practice, I guess? I tried FlatMap, but it can't really solve "If any sinlge task fails, then rollback and reset" Any other solution to this?
There's no need to add the extra level of indirection with the create function. Every Observable operator already creates a new object.
And when you do use Observable.create, do not dispose in an external dispose bag and return a Disposables.create(). Just return the disposable that you just created.
Here's the appropriate way to do what you want:
func writeBlocks(tasks: [WriteTask], resetTask: Single<ResetTaskResult>) -> Observable<BatchTasksResult> {
// create the array of write tasks and concat them. You seem to have that down.
let result = Observable.concat(tasks.map(writeSomewhere(task:)).map { $0.asObservable() })
.share() // the share is needed because you are using the value twice below.
return Observable.merge(
// push out the elapsed time for each task.
result.map { BatchTasksResult.elapsedTime($0.totalTime) },
// when the last one is done, push out the success event.
result.takeLast(1).map { _ in BatchTasksResult.success }
)
.catch { _ in
resetTask // the resetTask will get subscribed to if needed.
.map { _ in BatchTasksResult.rolledback } // if successful emit a rollback
.catch { _ in Single.just(BatchTasksResult.failedFatal) } // otherwise emit the failure.
.asObservable()
}
}
func writeSomewhere(task: WriteTask) -> Single<WriteTaskResult> {
// create a Single that performs the write and emits a result.
}

Get values from two apis into two different Observable and perform some operation

I have two independent observables. I need to perform some operation when both of them are complete and each of them provided an array.
let myObj1Array = myObj1Manager.getMyObj1List()//returns Observable<[MyObj1]>
let myObj2Array = myObj2Manager.getMyObj2List()//returns Observable<[MyObj2]>
Now I need to compare values of myObj1Array and myObj2Array and on the basis of that create another array using values from both arrays. I know how to subscribe 1 variable but not sure how to observe completion of two different arrays.
Edit:
I also tried following but I get values only from first array:
let myObj1Array = myObj1Manager.getMyObj1List()
let myObj2Array = myObj1Array.flatMap { _ in myObj2Manager.getMyObj2List() }
Observable.combineLatest(myObj1Array, myObj2Array)
.subscribe(onNext: { (sss, sds) in
print(sss)
})
.addDisposableTo(disposeBag)
I am actually kind of clueless about how to handle such scenario.
Edit2:
function to get the observables in first array:
func getMyObj1List() -> Observable<[MyObj1]> {
return Observable.create { observer -> Disposable in
self.specialsRest.getMyObj1List { response, error in
if let error = error {
observer.onError(Exception(error))
return
}
guard let saleItems = MyObj1.decode(data: response?.data) else {
observer.onError(Exception("Could not decode specials!"))
return
}
queueBackground.async {
observer.onNext(saleItems)
observer.onCompleted()
}
}
return Disposables.create { self.specialsRest.cancel() }
}
}
DispatchGroup is probably the way to go here.
https://developer.apple.com/documentation/dispatch/dispatchgroup
When all work items finish executing, the group executes its completion handler. You can also wait synchronously for all tasks in the group to finish executing.
var dg:DispatchGroup = DispatchGroup()
//Wherever you start your observables.
//Start Observer1
dg.enter()
//Start Observer2
dg.enter()
...
...
...
//Wherever you retrieve data
SomeAsyncFuncForObserver1 {
//Get Data
dg.leave()
}
SomeAsyncFuncForObserver2 {
//Get Data
dg.leave()
}
dg.notify(queue: .main) {
print("all finished.")
}
I believe you need to use zip instead of combineLatest. From the docs
The CombineLatest operator behaves in a similar way to Zip, but while
Zip emits items only when each of the zipped source Observables have
emitted a previously unzipped item, CombineLatest emits an item
whenever any of the source Observables emits an item (so long as each
of the source Observables has emitted at least one item).
Observable
.zip(myObj1Array, myObj2Array)
.subscribe(onNext: { (sss, sds) in
print(sss)
})
.addDisposableTo(disposeBag)

How can I branch out multiple API calls from the result of one API call and collect them after all are finished with Combine?

So, I have this sequence of API calls, where I fetch a employee details, then fetch the company and project details that the employee is associated with. After both fetching are complete, I combine both and publish a fetchCompleted event. I've isolated the relevant code below.
func getUserDetails() -> AnyPublisher<UserDetails, Error>
func getCompanyDetails(user: UserDetails) -> AnyPublisher<CompanyDetails, Error>
func getProjectDetails(user: UserDetails) -> AnyPublisher<ProjectDetails, Error>
If I do this,
func getCompleteUserDetails() -> AnyPublisher<UserFetchState, Never> {
let cvs = CurrentValueSubject<UserFetchState, Error>(.initial)
let companyPublisher = getUserDetails()
.flatMap { getCompanyDetails($0) }
let projectPublisher = getUserDetails()
.flatMap { getProjectDetails($0) }
companyPublisher.combineLatest(projectPublisher)
.sink { cvs.send(.fetchComplete) }
return cvs.eraseToAnyPublisher()
}
getUserDetails() will get called twice. What I need is fetch the userDetails once and with that, branch the stream into two, map it to fetch the company details and project details and re-combine both.
Is there a elegant(flatter) way to do the following.
func getCompleteUserDetails() -> AnyPublisher<UserFetchState, Never> {
let cvs = CurrentValueSubject<UserFetchState, Error>(.initial)
getUserDetails()
.sink {
let companyPublisher = getCompanyDetails($0)
let projectPublisher = getProjectDetails($0)
companyPublisher.combineLatest(projectPublisher)
.sink { cvs.send(.fetchComplete) }
}
return cvs.eraseToAnyPublisher()
}
The whole idea of Combine is that you construct a pipeline down which data flows. Actually what flows down can be a value or a completion, where a completion could be a failure (error). So:
You do not need to make a signal that the pipeline has produced its value; the arrival of that value at the end of the pipeline is that signal.
Similarly, you do not need to make a signal that the pipeline's work has completed; a publisher that has produced all the values it is going to produce produces the completion signal automatically, so the arrival of that completion at the end of the pipeline is that signal.
After all, when you receive a letter, the post office doesn't call you up on the phone and say, "You've got mail." Rather, the postman hands you the letter. You don't need to be told you've received a letter; you simply receive it.
Okay, let's demonstrate. The key to understanding your own pipeline is simply to track what kind of value is traveling down it at any given juncture. So let's construct a model pipeline that does the sort of thing you need done. I will posit three types of value:
struct User {
}
struct Project {
}
struct Company {
}
And I will imagine that it is possible to go online and fetch all of that information: the User independently, and the Project and Company based on information contained in the User. I will simulate that by providing utility functions that return publishers for each type of information; in real life these would probably be deferred futures, but I will simply use Just to keep things simple:
func makeUserFetcherPublisher() -> AnyPublisher<User,Error> {
Just(User()).setFailureType(to: Error.self).eraseToAnyPublisher()
}
func makeProjectFetcherPublisher(user:User) -> AnyPublisher<Project,Error> {
Just(Project()).setFailureType(to: Error.self).eraseToAnyPublisher()
}
func makeCompanyFetcherPublisher(user:User) -> AnyPublisher<Company,Error> {
Just(Company()).setFailureType(to: Error.self).eraseToAnyPublisher()
}
Now then, let's construct our pipeline. I take it that our goal is to produce, as the final value in the pipeline, all the information we have collected: the User, the Project, and the Company. So our final output will be a tuple of those three things. (Tuples are important when you are doing Combine stuff. Passing a tuple down the pipeline is extremely common.)
Okay, let's get started. In the beginning there is nothing, so we need an initial publisher to kick off the process. That will be our user fetcher:
let myWonderfulPipeline = self.makeUserFetcherPublisher()
What's coming out the end of that pipeline is a User. We now want to feed that User into the next two publishers, fetching the corresponding Project and Company. The way to insert a publisher into the middle of a pipeline is with flatMap. And remember, our goal is to produce the tuple of all our info. So:
let myWonderfulPipeline = self.makeUserFetcherPublisher()
// at this point, the value is a User
.flatMap { (user:User) -> AnyPublisher<(User,Project,Company), Error> in
// ?
}
// at this point, the value is a tuple: (User,Project,Company)
So what goes into flatMap, where the question mark is? Well, we must produce a publisher that produces the tuple we have promised. The tuple-making publisher par excellence is Zip. We have three values in our tuple, so this is a Zip3:
let myWonderfulPipeline = self.makeUserFetcherPublisher()
.flatMap { (user:User) -> AnyPublisher<(User,Project,Company), Error> in
// ?
let result = Publishers.Zip3(/* ? */)
return result.eraseToAnyPublisher()
}
So what are we zipping? We must zip publishers. Well, we know two of those publishers — they are the publishers we have already defined!
let myWonderfulPipeline = self.makeUserFetcherPublisher()
.flatMap { (user:User) -> AnyPublisher<(User,Project,Company), Error> in
let pub1 = self.makeProjectFetcherPublisher(user: user)
let pub2 = self.makeCompanyFetcherPublisher(user: user)
// ?
let result = Publishers.Zip3(/* ? */, pub1, pub2)
return result.eraseToAnyPublisher()
}
We're almost done! What goes in the missing slot? Remember, it must be a publisher. And what's our goal? We want to pass on the very same User that arrived from upstream. And what's the publisher that does that? It's Just! So:
let myWonderfulPipeline = self.makeUserFetcherPublisher()
.flatMap { (user:User) -> AnyPublisher<(User,Project,Company), Error> in
let pub1 = self.makeProjectFetcherPublisher(user: user)
let pub2 = self.makeCompanyFetcherPublisher(user: user)
let just = Just(user).setFailureType(to:Error.self)
let result = Publishers.Zip3(just, pub1, pub2)
return result.eraseToAnyPublisher()
}
And we're done. No muss no fuss. This is a pipeline that produces a (User,Project,Company) tuple. Whoever subscribes to this pipeline does not need some extra signal; the arrival of the tuple is the signal. And now the subscriber can do something with that info. Let's create the subscriber:
myWonderfulPipeline.sink {
completion in
if case .failure(let error) = completion {
print("error:", error)
}
} receiveValue: {
user, project, company in
print(user, project, company)
}.store(in: &self.storage)
We didn't do anything very interesting — we simply printed the tuple contents. But you see, in real life the subscriber would now do something useful with that data.
You can use the zip operator to get a Publisher which emits a value whenever both of its upstreams emitted a value and hence zip together getCompanyDetails and getProjectDetails.
You also don't need a Subject to signal the fetch being finished, you can just call map on the flatMap.
func getCompleteUserDetails() -> AnyPublisher<UserFetchState, Error> {
getUserDetails()
.flatMap { getCompanyDetails(user: $0).zip(getProjectDetails(user: $0)) }
.map { _ in UserFetchState.fetchComplete }
.eraseToAnyPublisher()
}
However, you shouldn't need a UserFetchState to signal the state of your pipeline (and especially shouldn't throw away the fetched CompanyDetails and ProjectDetails objects in the middle of your pipeline. You should simply return the fetched CompanyDetails and ProjectDetails as a result of your flatMap.
func getCompleteUserDetails() -> AnyPublisher<(CompanyDetails, ProjectDetails), Error> {
getUserDetails()
.flatMap { getCompanyDetails(user: $0).zip(getProjectDetails(user: $0)) }
.eraseToAnyPublisher()
}

How do I stop RxSwift ble scanner once it has found a match?

I have a ble scanner that works and looks like this:
func scan(serviceId: String) -> Observable<[BleHandler.BlePeripheral]> {
knownDevices = []
return waitForBluetooth()
.flatMap { _ in self.scanForPeripheral(serviceId: serviceId) }
.map { _ in self.knownDevices }
}
private func waitForBluetooth() -> Observable<BluetoothState> {
return self.manager
.observeState()
.startWith(self.manager.state)
.filter { $0 == .poweredOn }
.take(1)
}
Then in the viewModel class it filters matches from core data:
func scanAndFilter() -> Observable<[LocalDoorCoreDataObject]> {
let persistingDoors: [LocalDoorCoreDataObject] = coreDataHandler.fetchAll(fetchRequest: NSFetchRequest<LocalDoorCoreDataObject>(entityName: "LocalDoorCoreDataObject"))
return communicationService
.scanForDevices(register: false)
.map{ peripherals in
print("🐶 THIS WILL GO ON FOR ETERNITY", peripherals.count)
self.knownDevices = peripherals
return persistingDoors
.filter { door in peripherals.contains(where: { $0.identifier.uuidString == door.dPeripheralId }) }
}
}
And in the view I want to connect when the scan is completed:
private func scanAndConnect(data: LocalDoorCoreDataObject) {
viewModel.scanRelay().subscribe(
onNext: {
print("🐶SCANNED NAME", $0.first?.dName)},
onCompleted: {
print("🐶COMPLETED SCAN")
self.connectToFilteredPeripheral(localDoor: data)
}).disposed(by: disposeBag)
}
It never reaches onCompleted as it will just scan for eternity even after having found and filtered the core data match. In Apple's framework coreBluetooth I could simply call manager.stopScan() after it has found what I want, but that doesn't seem to be available on the Rx counterpart. How does it work for RxSwift
You can create a new Observable that looks for devices and then completes as soon as it finds the device(s) you're looking for. This would be something like:
func scanAndFilter() -> Observable<[LocalDoorCoreDataObject]> {
return Observable.deferred { in
let persistingDoors: [LocalDoorCoreDataObject] = coreDataHandler.fetchAll(fetchRequest: NSFetchRequest<LocalDoorCoreDataObject>(entityName: "LocalDoorCoreDataObject"))
return communicationService
.scanForDevices(register: false)
.filter { /* verify if the device(s) you're looking for is/are in this list */ }
.take(1)
}
}
The filter operator will make sure that only lists that contain the device you're looking for are passed on and the take(1) operator will take the first emitted value and complete immediately.
The deferred call makes sure that the fetch request that is performed in the first line is not executed when you call scanAndFilter() but only when somebody actually subscribes to the resulting Observable.
If you only want one event to exit the filter operator, then just use .take(1). The Observable will shut down after it emits a single value. If the BLE function is written correctly, it will call stopScan() when the Disposable is disposed of.
I have no idea why the other answer says to "always make sure to wrap all function that return Observables into a .deferred. I've been using RxSwift since 2015 and I've only ever needed deferred once. Certainly not every time I called a function that returned an Observable.

RxSwift, how do I chain different observables

I am still a beginner in Reactive programming, and RxSwift in general.
I want to chain two different operation. In my case I simply want to download a zip file from a web server, and then unzip it locally.
I also want, at the same time to show the progress of the downloaded files.
So I started creating the first observable:
class func rx_download(req:URLRequestConvertible, testId:String) -> Observable<Float> {
let destination:Request.DownloadFileDestination = ...
let obs:Observable<Float> = Observable.create { observer in
let request = Alamofire.download(req, destination: destination)
request.progress { _, totalBytesWritten, totalBytesExpectedToWrite in
if totalBytesExpectedToWrite > 0 {
observer.onNext(Float(totalBytesWritten) / Float(totalBytesExpectedToWrite))
}
else {
observer.onNext(0)
}
}
request.response { _, response, _, error in
if let responseURL = response {
if responseURL.statusCode == 200 {
observer.onNext(1.0)
observer.onCompleted()
} else {
let error = NSError(domain: "error", code: responseURL.statusCode, userInfo: nil)
observer.onError(error)
}
} else {
let error = NSError(domain: "error", code: 500, userInfo: nil)
observer.onError(error)
}
}
return AnonymousDisposable () {
request.cancel()
}
}
return obs.retry(3)
}
After that, I create a similar function for the unzip
class func rx_unzip(testId:String) -> Observable<Float> {
return Observable.create { observer in
do {
try Zip.unzipFile(NSURL.archivePath(testId), destination: NSURL.resourceDirectory(testId), overwrite: true, password: nil)
{progress in
observer.onNext(Float(progress))
}
} catch let error {
observer.onError(error)
}
observer.onCompleted()
return NopDisposable.instance
}
}
For now I have this logic on the "View model layer", so I download-> subscribe on completed-> unzip
What I want is to combine the two Observable in one, in order to perform the download first, then on completed unzip the file. Is there any way to do this?
Concat operator requires the same data type
Indeed, the concat operator allows you to enforce the sequence of observables, however an issue you might encounter with using concat is that the concat operator requires that the Observables have the same generic type.
let numbers : Observable<Int> = Observable.from([1,2,3])
let moreNumbers : Observable<Int> = Observable.from([4,5,6])
let names : Observable<String> = Observable.from(["Jose Rizal", "Leonor Rivera"])
// This works
numbers.concat(moreNumbers)
// Compile error
numbers.concat(names)
FlatMap operator allows you to chain a sequence of Observables
Here's an example.
class Tag {
var tag: String = ""
init (tag: String) {
self.tag = tag
}
}
let getRequestReadHTML : Observable<String> = Observable
.just("<HTML><BODY>Hello world</BODY></HTML>")
func getTagsFromHtml(htmlBody: String) -> Observable<Tag> {
return Observable.create { obx in
// do parsing on htmlBody as necessary
obx.onNext(Tag(tag: "<HTML>"))
obx.onNext(Tag(tag: "<BODY>"))
obx.onNext(Tag(tag: "</BODY>"))
obx.onNext(Tag(tag: "</HTML>"))
obx.onCompleted()
return Disposables.create()
}
}
getRequestReadHTML
.flatMap{ getTagsFromHtml(htmlBody: $0) }
.subscribe (onNext: { e in
print(e.tag)
})
Notice how getRequestReadHTML is of type Observable<String> while the function getTagsFromHtml is of type Observable<Tag>.
Using multiple flatMaps can increase emission frequency
Be wary however, because the flatMap operator takes in an array (e.g. [1,2,3]) or a sequence (e.g. an Observable) and will emit all of the elements as an emission. This is why it is known to produce a transformation of 1...n.
If you defined an observable such as a network call and you are sure that there will only be one emission, you will not encounter any problems since its transformation is a 1...1 (i.e. one Observable to one NSData). Great!
However, if your Observable has multiple emissions, be very careful because chained flatMap operators will mean emissions will exponentially(?) increase.
A concrete example would be when the first observable emits 3 emissions, the flatMap operator transforms 1...n where n = 2, which means there are now a total of 6 emissions. Another flatMap operator could again transform 1...n where n = 2, which means there are now a total of 12 emissions. Double check if this is your expected behavior.
You can use concat operator to chain these two Observables. The resulting Observable will send next values from the first one, and when it completes, from the second one.
There is a caveat: you will get progress values ranging from 0.0 to 1.0 from rx_download and then again the progress from rx_unzip will start with 0.0. This might be confusing to the user if you want to show the progress on a single progress view.
A possible approach would be to show a label describing what is happening along with the progress view. You can map each Observable to a tuple containing the progress value and the description text and then use concat. It can look like that:
let mappedDownload = rx_download.map {
return ("Downloading", $0)
}
let mappedUnzip = rx_download.map {
return ("Unzipping", $0)
}
mapped1.concat(mapped2)
.subscribeNext({ (description, progress) in
//set progress and show description
})
Of course, there are many possible solutions, but this is more of a design problem than a coding one.

Resources