I have a network request called login that returns an Observable<UserInfo>. I need to make another API call from that result based on whether the data returned from login has a count > 1, otherwise, I just need to go to a different view controller. I’m trying to use flatMapLatest to do the check for the first request login and make the next network call jobStates (which returns an Observable<JobState>, but I don’t think I’m arranging them correctly. Any ideas? Is there a better / easier way to do this?
Here's what it looks like:
I would expect to see something like this:
func login() {
let loginResult = networkService
.login(login: usernameTextField.text!, password: passwordTextField.text!)
.share()
loginResult
.filter { $0.count > 1 }
.subscribe(onNext: { userInfo in
// stop here and go to a different view with userInfo data
})
.disposed(by: disposeBag)
let networkService = self.networkService // so you don't have to capture self below
loginResult
.filter { $0.count <= 1 }
.flatMapLatest { networkService.jobStates(locationId: $0.locationId) }
.subscribe(
onNext: { data in
// do whatever with data from second request
},
onError: { error in
// if either request errors, you will end up here.
})
.disposed(by: disposeBag)
}
When you have two different possible outcomes, you need two different subscribes.
Related
I have a logic problem. The premise of what I would like to achieve is to combine two sources of data and in case of failure, I would like to run retry only on one observable. The logic flow goes like this: I try and fail to get local data from StorageManager class, afterwards I try to get the data from an API. In case that API call fails, I would like to retry only that API call a certain number of times. Is there a nice way of doing this? By just running retry like in the code below, the local data observable gets triggered.
class func loginUser(user: User, replaySubject: ReplaySubject<User>){
let savedUserObs = StorageManager.readCachedModelData(requestType: .LOGIN, modelType: User.self)
let apiUserObs = NetworkHelper.makeRequest(requestType: .LOGIN).map { response -> User in
guard let user = response.user?.first else { throw TempErrors.wasNotAbleToExtractUser }
return user
}
savedUserObs.concat(apiUserObs)
.retry { errorObs in
errorObs.scan(0) { attempt, error in
let max = 5
if attempt == max { throw TempErrors.tooManyRetries }
return attempt + 1
}
}
.subscribe { user in
replaySubject.onNext(user)
StorageManager.saveData(requestType: .LOGIN, data: user)
} onError: { error in
print("onerror error: \(error)")
} onCompleted: {
print("completed")
} onDisposed: {
print("disposed")
}.disposed(by: disposeBag)
}
I need to call a sequences of function to get all the information I need for a notification. First subscribe which opens up the session, then queryNotification to listen on all the incoming notifications, and once a notification is received, need to call getNotificationAttrs with the notificationId returned in queryNotification, then call getAppAttributes with appIdentifier returned in getNotificationAttrs and I need the combined result of queryNotification, getNotificationAttrs and getAppAttributes. How the functions look like are below:
func subscribeNotification() -> Single<Info>
func queryNotification() -> Observable<Notification>
func getNotificationAttrs(uid: UInt32, attributes: [Attribute]) -> Single<NotificationAttributes>
func getAppAttributes(appIdentifier: String, attributes: [AppAttribute]) -> Single<NotificationAppAttributes>
The tricky part is that queryNotification returns Observable and both getNotificationAttrs and getAppAttributes return Single. What I have in mind of chaining them together is like:
subscribeNotification()
.subscribe(onSuccess: { info in
queryNotification()
.flatMap({ notification in
return getNotificationAttributes(uid: notification.uid, attributes: [.appIdentifier, .content])
})
.flatMap({ notifAttrs
return getAppAttributes(appIdentifier: notifAttrs.appIdentifier, attributes: [.displayName])
})
.subscribe {
// have all the result from last two calls
}
})
Is this doable? Any direction is appreciated! Thanks!
The most obvious and IMHO correct solution is to promote your Single into an Observable. Also, I'm not a fan of the first subscribe where it is. You end up with an indentation pyramid.
I'm following your comments about needing the values from all of queryNotification(), getNotificationAttrs(did:attributes:) and getAppAttributes(appIdentifier:attributes:)...
let query = subscribeNotification()
.asObservable()
.flatMap { _ in queryNotification() }
.share(replay: 1)
let attributes = query
.flatMap { getNotificationAttrs(uid: $0.uid, attributes: [.appIdentifier, .content]) }
.share(replay: 1)
let appAttributes = attributes
.flatMap { getAppAttributes(appIdentifier: $0.appIdentifier, attributes: [.displayName]) }
Observable.zip(query, attributes, appAttributes)
.subscribe(onNext: { (query, attributes, appAttributes) in
})
The above will follow the steps you outlined and the subscribe will get called every time a new notification is emitted.
Also notice how the above reads quite a bit like synchronous code would (just with some extra wrapping.)
I need to make multiple calls.
1. Delete Document Upload
2. Image 1 & server returns URL
3. Upload Image 2 & server returns URL
4. Create Document API contains both URLs & extra
parameters.
The code which I tried to write is in RxSwift,& MVVM.
let resultOfDocumentUpdateWithDelete =
donepressed
.filter{ $0 }
.withLatestFrom(self.existingDocumentIDChangedProperty)
.flatMapLatest {id in
let deleted_document = apiClient.deleteDocument(id).asObservable().materialize()
let upload_frontImage = deleted_document
.withLatestFrom(self.frontImageNameChangedProperty)
.flatMapLatest {image in
apiClient.uploadImage(image: image!).asObservable().materialize()
}
let upload_backImage = upload_frontImage
.withLatestFrom(self.backImageChangedProperty)
.flatMapLatest {image in
apiClient.uploadImage(image: image!).asObservable().materialize()
}
let upload_document = upload_backImage
.withLatestFrom(self.parametersChangedProperty)
.flatMapLatest {parameters in
apiClient.uploadDocument(parameters: parameters)
}
return upload_document.materialize()
}
.share(replay: 1)
Make sure, two responses of server are input in last API, so all of these will be called in a sequence.
how to do in RxSwift.
This was an interesting one! The take-away here is that when you are in doubt, go ahead and make your own operator. If it turns out that you later figure out how to do the job using the built-in operators, then you can replace yours. The only thing with making your own is that they require a lot more testing.
Note, to use the below, you will have to combineLatest of your observables and then flatMap and pass their values into this function.
// all possible results from this job.
enum ProcessResult {
case success
case deleteFailure(Error)
case imageFailue(Error)
case backImageFailure(Error)
case documentFailure(Error)
}
func uploadContent(apiClient: APIClient, documentID: Int, frontImage: UIImage, backImage: UIImage, parameters: Parameters) -> Single<ProcessResult> {
// instead of trying to deal with all the materializes, I decided to turn it into a single process.
return Single.create { observer in
// each api call happens in turn. Note that there are no roll-back semantics included! You are dealing with a very poorly written server.
let deleted = apiClient.deleteDocument(id: documentID)
.asObservable()
.share()
let imagesUploaded = deleted
.flatMap { _ in Observable.zip(apiClient.uploadImage(image: frontImage).asObservable(), apiClient.uploadImage(image: backImage).asObservable()) }
.share()
let documentUploaded = imagesUploaded
.flatMap { arg -> Single<Void> in
let (frontURL, backURL) = arg
var updatedParams = parameters
// add frontURL and backURL to parameters
return apiClient.uploadDocument(parameters: updatedParams)
}
.share()
let disposable = deleted
.subscribe(onError: { observer(.success(ProcessResult.deleteFailure($0))) })
let disposable1 = imagesUploaded
.subscribe(onError: { observer(.success(ProcessResult.imageFailue($0))) })
let disposable2 = documentUploaded
.subscribe(
onNext: { observer(.success(ProcessResult.success)) },
onError: { observer(.success(ProcessResult.documentFailure($0))) }
)
return Disposables.create([disposable, disposable1, disposable2])
}
}
I want to chain following operations
createUserandVerify
Create Anonymous User (user)
Verify User -> verifiedUser
If verification successful return verifiedUser else return user
Get stuff with coredata getStuff
If stuff.count > 0 Upload stuff with user credentials uploadStuff
Finally report the result of all operations
I wrote createUserandVerify as below. I wonder how should I write uploadStuff in reactive way. Upload function depends on user credentials. Therefore It must only run after createUserandVerify. I know I could just check count of array inside uploadStuff and return empty but I wonder the best practices.
func createUserandVerify() -> Single<User> {
return Service.sharedInstance.generateAnonUser()
.flatMap{ user in
if Service.sharedInstance.isOldRegisteredUser {
print("It is old user")
// We need to verify the receipt
return Service.sharedInstance.verifyReceipt()
.flatMap { verifiedUser in
print("Returning Verified new user [Verification Success]")
return Single.just((verifiedUser))
}.catchError{ error ->Single<User> in
print("Returning firstly created user [Verification Failed]")
print("Error Type: \(error)")
return Single.just(user)
}
} else {
//Normal anonymous old user
print("Returning firstly created user [Anonymous]")
return Single.just(user)
}
}
}
Assumptions (since I have not worked with Single I changed them to Observable):
func createUserandVerify() -> Observable<User>
func getStuff() -> [Stuff]
func uploadStuff(_ user: User) -> Observable<String>
createUserandVerify() should publish errors with onError so uploadStuff will not be called if something goes wrong.
Possible solution:
enum CustomError: Error {
case instanceMissing
case notEnoughStuff
}
createUserandVerify()
.flatMap { [weak self] (user) -> Observable<String> in
guard let strongSelf = self else { throw CustomError.instanceMissing }
guard strongSelf.getStuff().count > 0 else { throw CustomError.notEnoughStuff }
return strongSelf.uploadStuff(user)
}
.subscribe(
onNext: { stringResult in
// print result from 'uploadStuff'
print(stringResult)
},
onError: { error in
// will be reached if something goes
// wrong in 'createUserandVerify' or 'uploadStuff'
// or if one of your custom errors in 'flatMap' are thrown
print(error)
})
.disposed(by: disposeBag)
You could also make getStuff reactive by returning an Observable or Single and also include it in the chain via flatMap.
I'm combining a viewDidAppear and filter Drivers with RxSwift. And they work great. But when I introduce a third Driver, it stops calling flatMapLatest on the latest combine.
In my View Controller, I have these Drivers:
let filter: Driver<String>
let viewDidAppear: Driver<Void>
let refresh: Driver<Void>
And in my view model:
// On viewDidAppear, I download a list of portfolios
let viewDidAppearPortfolios = viewDidAppear
.flatMapLatest({ (_) -> Driver<Result<[PortfolioModel]>> in
return networkService.request(Router.portfolios)!
.responseCollections()
.trackActivity(fetching)
.asDriver(onErrorJustReturn: .failure(NSError()))
})
.flatMapLatest({ (result: Result<[PortfolioModel]>) -> Driver<[PortfolioModel]> in
switch result {
case .success(let value): return Driver.just(value)
case .failure(_): return Driver.just([])
}
})
// Then I combine with a filter from my search bar.
self.portfolios = Driver.combineLatest(viewDidAppearPortfolios, filter)
.flatMapLatest { (portfolios: [PortfolioModel], filter: String) -> Driver<[PortfolioModel]> in
if filter.isEmpty {
return Driver.just(portfolios)
}
return Driver.just(portfolios.filter({ (portfolio) -> Bool in
portfolio.portfolio.localizedCaseInsensitiveContains(filter)
}))
}
The above works!
The network requests a list of portfolios, and I'm able to filter those results as I type, client side.
However, I'd like for the user to pull to refresh, and trigger the network request again! And so, I combine with my refresh driver.
And this:
Driver.combineLatest(viewDidAppearPortfolios, filter)
Becomes this:
Driver.combineLatest(viewDidAppearPortfolios, filter, refresh)
Problem!
After combining with refresh the flatMapLatest is no longer called on viewDidAppear! Only if I manually pullToRefresh.
Driver.combineLatest(viewDidAppearPortfolios, filter, refresh).flatMapLatest { _,_,_ in
// No longer get's called on viewDidAppear after combining with refresh
}
The viewDidAppearPortfolios still executes, so the network request
is getting called!
Only if I manually pull to refresh do I get the
list of portfolios that I previously requested...
Any idea why?
Thank you!
It looks like your refresh didn't emit a single event yet and so the combineLatest is not computed.
I tried this code to test:
let one = Driver.just(1)
let two = Driver.just(2)
let three: Driver<Int> = .just(3)
let result = Driver.combineLatest(one, two, three)
.flatMapLatest {
return Driver.just($0 + $1 + $2)
}
result.drive(onNext: {
print($0)
})
This prints 6 but if you use let three: Driver<Int> = .empty() this is not printing anything. So I guess you need a way to set an initial value to refresh stream.