RxSwift callback return first before result - ios

I am using Firebase FirAuth API and before the API return result, Disposables.create() has been returned and it's no longer clickable (I know this might due to no observer.onCompleted after the API was called. Is there a way to wait for it/ listen to the result?
public func login(_ email: String, _ password: String) -> Observable<APIResponseResult> {
let observable = Observable<APIResponseResult>.create { observer -> Disposable in
let completion : (FIRUser?, Error?) -> Void = { (user, error) in
if let error = error {
UserSession.default.clearSession()
observer.onError(APIResponseResult.Failure(error))
observer.on(.completed)
return
}
UserSession.default.user.value = user!
observer.onNext(APIResponseResult.Success)
observer.on(.completed)
return
}
DispatchQueue.main.async {
FIRAuth.auth()?.signIn(withEmail: email, password: password, completion: completion)
}
return Disposables.create()
}
return observable
}

You are correct in your assumption that an onError / onCompletion event terminate the Observable Sequence. Meaning, the sequence won't emit any more events, in any case.
As a sidenote to that, You don't need to do .on(.completed) after .onError() , since onError already terminates the sequence.
the part where you write return Disposables.create() returns a Disposable object, so that observable can later be added to a DisposeBag that would handle deallocating the observable when the DisposeBag is deallocated, so it should return immediately, but it will not terminate your request.
To understand better what's happening, I would suggest adding .debug() statements around the part that uses your Observable, which will allow you to understand exactly which events are happening and will help you understand exactly what's wrong :)

I had the same issue some time ago, I wanted to display an Alert in onError if there was some error, but without disposing of the observable.
I solved it by catching the error and returning an enum with the cases .success(MyType) and .error(Error)
An example:
// ApiResponseResult.swift
enum ApiResponseResult {
case error(Error)
case success(FIRUser)
}
// ViewModel
func login(...) -> Observable<ApiResponseResult> {
let observable = Observable.create { ... }
return observable.catchError { error in
return Observable<ApiResponseResult>.just(.error(error))
}
}
// ViewController
viewModel
.login
.subscribe(onNext: { result in
switch result {
case .error(let error):
// Alert or whatever
break
case .success(let user):
// Hurray
break
}
})
.addDisposableTo(disposeBag)

Related

How to move to the next view upon data reception?

I am struggling to trigger the logic responsible for changing the view at the right time. Let me explain.
I have a view model that contains a function called createNewUserVM(). This function triggers another function named requestNewUser() which sits in a struct called Webservices.
func createNewUserVM() -> String {
Webservices().requestNewUser(with: User(firstName: firstName, lastName: lastName, email: email, password: password)) { serverResponse in
guard let serverResponse = serverResponse else {
return "failure"
}
return serverResponse.response
}
}
Now that's what's happening in the Webservices' struct:
struct Webservices {
func requestNewUser(with user: User, completion: #escaping (Response?) -> String) -> String {
//code that creates the desired request based on the server's URL
//...
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async {
serverResponse = completion(nil)
}
return
}
let decodedResponse = try? JSONDecoder().decode(Response.self, from: data)
DispatchQueue.main.async {
serverResponse = completion(decodedResponse)
}
}.resume()
return serverResponse //last line that gets executed before the if statement
}
}
So as you can see, the escaping closure (whose code is in the view model) returns serverResponse.response (which can be either "success" or "failure"), which is then stored in the variable named serverResponse. Then, requestNewUser() returns that value. Finally, the createNewUserVM() function returns the returned String, at which point this whole logic ends.
In order to move to the next view, the idea was to simply check the returned value like so:
serverResponse = self.signupViewModel.createNewUserVM()
if serverResponse == "success" {
//move to the next view
}
However, after having written a few print statements, I found out that the if statement gets triggered way too early, around the time the escaping closure returns the value, which happens before the view model returns it. I attempted to fix the problem by using some DispatchQueue logic but nothing worked. I also tried to implement a while loop like so:
while serverResponse.isEmpty {
//fetch the data
}
//at this point, serverResponse is not empty
//move to the next view
It was to account for the async nature of the code.
I also tried was to pass the EnvironmentObject that handles the logic behind what view's displayed directly to the view model, but still without success.
As matt has pointed out, you seem to have mixed up synchronous and asynchronous flows in your code. But I believe the main issue stems from the fact that you believe URLSession.shared.dataTask executes synchronously. It actually executes asynchronously. Because of this, iOS won't wait until your server response is received to execute the rest of your code.
To resolve this, you need to carefully read and convert the problematic sections into asynchronous code. Since the answer is not trivial in your case, I will try my best to help you convert your code to be properly asynchronous.
1. Lets start with the Webservices struct
When you call the dataTask method, what happens is iOS creates a URLSessionDataTask and returns it to you. You call resume() on it, and it starts executing on a different thread asynchronously.
Because it executes asynchronously, iOS doesn't wait for it to return to continue executing the rest of your code. As soon as the resume() method returns, the requestNewUser method also returns. By the time your App receives the JSON response the requestNewUser has returned long ago.
So what you need to do to pass your response back correctly, is to pass it through the "completion" function type in an asynchronous manner. We also don't need that function to return anything - it can process the response and carry on the rest of the work.
So this method signature:
func requestNewUser(with user: User, completion: #escaping (Response?) -> String) -> String {
becomes this:
func requestNewUser(with user: User, completion: #escaping (Response?) -> Void) {
And the changes to the requestNewUser looks like this:
func requestNewUser(with user: User, completion: #escaping (Response?) -> Void) {
//code that creates the desired request based on the server's URL
//...
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async {
completion(nil)
}
return
}
let decodedResponse = try? JSONDecoder().decode(Response.self, from: data)
DispatchQueue.main.async {
completion(decodedResponse)
}
}.resume()
}
2. View Model Changes
The requestNewUser method now doesn't return anything. So we need to accommodate that change in our the rest of the code. Let's convert our createNewUserVM method from synchronous to asynchronous. We should also ask the calling code for a function that would receive the result from our Webservice class.
So your createNewUserVM changes from this:
func createNewUserVM() -> String {
Webservices().requestNewUser(with: User(firstName: firstName, lastName: lastName, email: email, password: password)) { serverResponse in
guard let serverResponse = serverResponse else {
return "failure"
}
return serverResponse.response
}
}
to this:
func createNewUserVM(_ callback: #escaping (_ response: String?) -> Void) {
Webservices().requestNewUser(with: User(firstName: firstName, lastName: lastName, email: email, password: password)) { serverResponse in
guard let serverResponse = serverResponse else {
callback("failure")
return
}
callback(serverResponse.response)
}
}
3. Moving to the next view
Now that createNewUserVM is also asynchronous, we also need to change how we call it from our controller.
So that code changes from this:
serverResponse = self.signupViewModel.createNewUserVM()
if serverResponse == "success" {
//move to the next view
}
To this:
self.signupViewModel.createNewUserVM{ [weak self] (serverResponse) in
guard let `self` = self else { return }
if serverResponse == "success" {
// move to the next view
// self.present something...
}
}
Conclusion
I hope the answer gives you an idea of why your code didn't work, and how you can convert any existing code of that sort to execute properly in an asynchronous fashion.
This can be achieve using DispatchGroup and BlockOperation together like below:
func functionWillEscapeAfter(time: DispatchTime, completion: #escaping (Bool) -> Void) {
DispatchQueue.main.asyncAfter(deadline: time) {
completion(false) // change the value to reflect changes.
}
}
func createNewUserAfterGettingResponse() {
let group = DispatchGroup()
let firstOperation = BlockOperation()
firstOperation.addExecutionBlock {
group.enter()
print("Wait until async block returns")
functionWillEscapeAfter(time: .now() + 5) { isSuccess in
print("Returned value after specified seconds...")
if isSuccess {
group.leave()
// and firstoperation will be complete
} else {
firstOperation.cancel() // means first operation is cancelled and we can check later if cancelled don't execute next operation
group.leave()
}
}
group.wait() //Waits until async closure returns something
} // first operation ends
let secondOperation = BlockOperation()
secondOperation.addExecutionBlock {
// Now before executing check if previous operation was cancelled we don't need to execute this operation.
if !firstOperation.isCancelled { // First operation was successful.
// move to next view
moveToNextView()
} else { // First operation was successful.
// do something else.
print("Don't move to next block")
}
}
// now second operation depends upon the first operation so add dependency
secondOperation.addDependency(firstOperation)
//run operation in queue
let operationQueue = OperationQueue()
operationQueue.addOperations([firstOperation, secondOperation], waitUntilFinished: false)
}
func moveToNextView() {
// move view
print("Move to next block")
}
createNewUserAfterGettingResponse() // Call this in playground to execute all above code.
Note: Read comments for understanding. I have run this in swift playground and working fine. copy past code in playground and have fun!!!

How to prevent from calling async function many times but call completion for each of them?

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

RxSwift practice to show/hide ActivityIndicator

Let's say we have this pseudocode representing a network request call and show/hide an activity indicator, using RxSwift:
func performRequest() {
isLoading.accept(true)
self.network.executeRequest()
.subscribe(onNext: {
self.isLoading.accept(false)
}, onError: {
self.isLoading.accept(false)
})
}
The function executeRequest returns either an Observable or Single.
I am not feeling comfortable with having to write twice the same code, for onNext/onSuccess and onError, basically doing the same.
I am looking for suggestions to minimize/improve turning off the activity indicator, like for example handling all events of the request in a single statement and avoid using the subscribe function. Or maybe there are other suggestions?
I use ActivityIndicator from RxSwift Example app, which makes it really convenient, especially if your loading multiple things in parallel as it maintains a count of running subscriptions and emit false only when this count is equal to 0:
let isLoading = ActivityIndicator()
func performRequests() {
self.network
.executeFirstRequest()
.trackActivity(isLoading)
.subscribe {
// ...
}
self.network
.executeSecondRequest()
.trackActivity(isLoading)
.subscribe {
// ...
}
}
You can use another method to subscribe, which passes Event in case of Observer or SingleEvent in case of Single:
subscribe(on: (Event<T>) -> Void)
subscribe(observer: (SingleEvent<T>) -> Void)
Observer Example:
func performRequest() {
isLoading.accept(true)
self.network.executeRequest().subscribe {
switch $0 {
case let .error(error):
print(error)
case let .next:
print("good")
case .completed:
print("also good")
}
isLoading.accept(false)
}
}
Single Example:
func performRequest() {
isLoading.accept(true)
self.network.executeRequest().subscribe {
switch $0 {
case let .error(error):
print(error)
case let .next:
print("good")
}
isLoading.accept(false)
}
}

Combine Future block called multiple times when using Flatmap and multiple subscribers

I've been successfully using BrightFutures in my apps mainly for async network requests. I decided it was time to see if I could migrate to Combine. However what I find is that when I combine two Futures using flatMap with two subscribers my second Future code block is executed twice. Here's some example code which will run directly in a playground:
import Combine
import Foundation
extension Publisher {
func showActivityIndicatorWhileWaiting(message: String) -> AnyCancellable {
let cancellable = sink(receiveCompletion: { _ in Swift.print("Hide activity indicator") }, receiveValue: { (_) in })
Swift.print("Busy: \(message)")
return cancellable
}
}
enum ServerErrors: Error {
case authenticationFailed
case noConnection
case timeout
}
func authenticate(username: String, password: String) -> Future<Bool, ServerErrors> {
Future { promise in
print("Calling server to authenticate")
DispatchQueue.main.async {
promise(.success(true))
}
}
}
func downloadUserInfo(username: String) -> Future<String, ServerErrors> {
Future { promise in
print("Downloading user info")
DispatchQueue.main.async {
promise(.success("decoded user data"))
}
}
}
func authenticateAndDownloadUserInfo(username: String, password: String) -> some Publisher {
return authenticate(username: username, password: password).flatMap { (isAuthenticated) -> Future<String, ServerErrors> in
guard isAuthenticated else {
return Future {$0(.failure(.authenticationFailed)) }
}
return downloadUserInfo(username: username)
}
}
let future = authenticateAndDownloadUserInfo(username: "stack", password: "overflow")
let cancellable2 = future.showActivityIndicatorWhileWaiting(message: "Please wait downloading")
let cancellable1 = future.sink(receiveCompletion: { (completion) in
switch completion {
case .finished:
print("Completed without errors.")
case .failure(let error):
print("received error: '\(error)'")
}
}) { (output) in
print("received userInfo: '\(output)'")
}
The code simulates making two network calls and flatmaps them together as a unit which either succeeds or fails.
The resulting output is:
Calling server to authenticate
Busy: Please wait downloading
Downloading user info
Downloading user info <---- unexpected second network call
Hide activity indicator
received userInfo: 'decoded user data'
Completed without errors.
The problem is downloadUserInfo((username:) appears to be called twice. If I only have one subscriber then downloadUserInfo((username:) is only called once. I have an ugly solution that wraps the flatMap in another Future but feel I missing something simple. Any thoughts?
When you create the actual publisher with let future, append the .share operator, so that your two subscribers subscribe to a single split pipeline.
EDIT: As I've said in my comments, I'd make some other changes in your pipeline. Here's a suggested rewrite. Some of these changes are stylistic / cosmetic, as an illustration of how I write Combine code; you can take it or leave it. But other things are pretty much de rigueur. You need Deferred wrappers around your Futures to prevent premature networking (i.e. before the subscription happens). You need to store your pipeline or it will go out of existence before networking can start. I've also substituted a .handleEvents for your second subscriber, though if you use the above solution with .share you can still use a second subscriber if you really want to. This is a complete example; you can just copy and paste it right into a project.
class ViewController: UIViewController {
enum ServerError: Error {
case authenticationFailed
case noConnection
case timeout
}
var storage = Set<AnyCancellable>()
func authenticate(username: String, password: String) -> AnyPublisher<Bool, ServerError> {
Deferred {
Future { promise in
print("Calling server to authenticate")
DispatchQueue.main.async {
promise(.success(true))
}
}
}.eraseToAnyPublisher()
}
func downloadUserInfo(username: String) -> AnyPublisher<String, ServerError> {
Deferred {
Future { promise in
print("Downloading user info")
DispatchQueue.main.async {
promise(.success("decoded user data"))
}
}
}.eraseToAnyPublisher()
}
func authenticateAndDownloadUserInfo(username: String, password: String) -> AnyPublisher<String, ServerError> {
let authenticate = self.authenticate(username: username, password: password)
let pipeline = authenticate.flatMap { isAuthenticated -> AnyPublisher<String, ServerError> in
if isAuthenticated {
return self.downloadUserInfo(username: username)
} else {
return Fail<String, ServerError>(error: .authenticationFailed).eraseToAnyPublisher()
}
}
return pipeline.eraseToAnyPublisher()
}
override func viewDidLoad() {
super.viewDidLoad()
authenticateAndDownloadUserInfo(username: "stack", password: "overflow")
.handleEvents(
receiveSubscription: { _ in print("start the spinner!") },
receiveCompletion: { _ in print("stop the spinner!") }
).sink(receiveCompletion: {
switch $0 {
case .finished:
print("Completed without errors.")
case .failure(let error):
print("received error: '\(error)'")
}
}) {
print("received userInfo: '\($0)'")
}.store(in: &self.storage)
}
}
Output:
start the spinner!
Calling server to authenticate
Downloading user info
received userInfo: 'decoded user data'
stop the spinner!
Completed without errors.

Chaining Network Requests RxSwift

I am relatively new to RxSwift, and functional programming for that matter. I have an app where I need to make a sequential set of network calls, each call depends upon output of the other. How I have architected the app, these calls are not made anywhere close to the view or view controllers - they are handled by data provider classes which perform business logic on the response data. As I understand good Rx design, subscribers should stay at the top of chain, not in classes like these that purely perform business and functional logic on the data.
I am creating a contrived and simplified example for illustration from these requests that return Singles:
func loginRequest(_ data: LoginData) -> Single<LoginResponse> {
return Single.create { observer -> Disposable in
let request = LoginRequestWith(data)
networkClient.send(request) { result in
switch result {
case .success(let response):
observer(.success(response))
case .failure(let error):
observer(.error(error))
}
}
return Disposables.create {
networkClient.cancelRequest()
}
}
}
func userRequest(_ data: UserData) -> Single<UserResponse> {
return Single.create { observer -> Disposable in
let request = InfoRequestWith(data)
networkClient.send(request) { result in
switch result {
case .success(let response):
observer(.success(response))
case .failure(let error):
observer(.error(error))
}
}
return Disposables.create {
networkClient.cancelRequest()
}
}
}
func infoRequest(_ data: InfoData) -> Single<InfoResponse> {
return Single.create { observer -> Disposable in
let request = InfoRequestWith(data)
networkClient.send(request) { result in
switch result {
case .success(let response):
observer(.success(response))
case .failure(let error):
observer(.error(error))
}
}
return Disposables.create {
networkClient.cancelRequest()
}
}
}
These requests are working as intended, however I am calling and handling them improperly using subscriptions / dispose bags in the provider classes to get them to work. What I want to do is chain them together using a FlatMap or more appropriate chain of operators. And this is where I get stuck.
func login() -> Observable<InfoData> {
webServices.loginRequest(loginData)
// verify success?
.flatMap { result in
// Now call userRequest passing the response from the first
// and from that infoRequest passing the response from the second
// Which should return the Observable<InfoData>
}
}
Where I am messing up? I get errors around the FlatMap but I understand that these are best to use with Singles. My errors seem to revolve around not mapping the response correctly to a new observable:
webServices.loginRequest(loginData)
.flatMap { response -> PrimitiveSequence<SingleTrait, Result> in
return response
}
Xcode Error: Reference to generic type 'Result' requires arguments in
<...>
And then how do I chain call into the subsequent request after a previous one. Thank you very much for any assistance.

Resources