How to show an alert to the user inside a completion block in swift 5 - ios

I have an app that makes an API call to a web server. I finally got the API call to work correctly and can now parse the JSON data the server returns, but I am stuck with trying to show an alert to the user if the request fails. For example, my server can return {"success": false, "error": "You didn't ask nicely"} Obviously that error is not real, but a representation of what can be returned. I can only check the error inside the completion block of the URLSession.shared.dataTask, but if I try to show an alert from inside that I get the error that I cannot perform any operation from a background thread on the UI.
The code is rather simple right now...
URLSession.shared.dataTask(with: self.request) { (data, response, error) in
if let error = error {
completion(.failure(error))
return
}
//continue on with processing the response...
completion(.success(fullResponse))
}.resume()
Then in my calling code I have...
connector.connect(pin) { (result) in
switch(result) {
case .success(let response):
if let response = response {
if response.success {
//do things
} else {
self.alert(title: "Error while connecting", message: response.error)
}
}
case .failure(let error):
self.alert(title: "Unable to connect", message: error)
}
}
That is causing the error that I can't do anything on the ui thread from a background thread. If that is the case, how do I let the user know that the API call failed? I have to be able to notify the user. Thank you.

You need to wrap it inside DispatchQueue.main.async as callback of URLSession.shared.dataTask occurs in a background thread
DispatchQueue.main.async {
self.alert(title: "Error while connecting", message: response.error)
}
Same also for
self.alert(title: "Unable to connect", message: error)
but it's better to wrap all code inside alert function inside the main queue to be a single place

Here's a different approach you can use, instead of calling DispatchQueue.main.async on connector.connect(pin)'s callback you could also do it before you call the completion block on your dataTask like this.
URLSession.shared.dataTask(with: self.request) { (data, response, error) in
if let error = error {
DispatchQueue.main.async {
completion(.failure(error))
}
return
}
//continue on with processing the response...
DispatchQueue.main.async {
completion(.success(fullResponse))
}
}.resume()
By doing this your code inside connector.connect(pin) won't be placed in a pyramid of doom, and everything in the completion block is running on the main thread.
connector.connect(pin) { result in
// everything in here is on the main thread now
}

Related

PromiseKit no callback / deallocates? (Alamofire)

My promise chain is broken (maybe deallocated) before it's resolved.
This happens (so far ONLY) when I make Alamofire request fail due to host trust evaluation -> forcing evaluation to fail which results in -999 cancelled etc).
Setup is rather simple:
APIRequest:
func start(_ onSuccess:#escaping SuccessBlock, onError:#escaping ErrorBlock) {
do {
try executeRequest { dataResponse in
self.onSuccess(dataResponse)
}
} catch {
self.onError(error)
}
}
where executeRequest() is:
self.manager.request(request).responseJSON(queue: self.queue) { (response) in
completion(response)
}
Then, there is PromiseKit wrapper defined as APIRequest extension:
(This wrapper callbacks are called correctly in either case)
func start() -> Promise<APIResponse> {
return Promise<APIResponse> { resolver in
self.start({ response in
resolver.fulfill(response)
}) { error in
resolver.reject(error)
}
}
}
And finally, unit test calling the start promise (extension):
( flow never reaches this place in case of Alamofire failing )
request.start().done { response in
}.catch { error in
// not called if request failed
}
Outcome: if request fails -> the extension promise wrapper (catch) block is called, but it's not propagated back to UnitTest promise block.
Simply replacing Alamofire request with mock implementation (which triggers some other error( makes all setup work as expected (Unit Test completes with catch block being called etc) ex:
DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + 2) {
let result = Alamofire.Result { () -> Any in
return try JSONSerialization.data(withJSONObject: [:], options: .fragmentsAllowed)
}
completion(DataResponse<Any>(request: nil, response: nil, data: nil, result: result))
}
Is this something to do with Alamofire? I don't really see how else to handle the promises there ( they shouldn't deallocate anyways... Or is this bug in PromiseKit? Alamofire? I yet have to test this in the app itself ( maybe it's Unit test issue ... )
Looking at related issues -> i'm definitely resolving promises (fulfilling / rejecting) for any flow path.
I don't see how Alamofire request is different from DispatchAsync (where the latter works as expected).
I was only 10 mins short of finding the answer... Problem is also described here:
https://github.com/mxcl/PromiseKit/issues/686
Issue is that '-999 cancelled' error is not treated as 'Error' by PromiseKit. Solution is to use "catch(policy: .allErrors)" - then catch block is called as expected.
func start(_ onSuccess:#escaping SuccessBlock, onError:#escaping ErrorBlock) {
do {
try executeRequest { dataResponse in
onSuccess(dataResponse)
}
} catch {
onError(error)
}
}
You are using self.onSuccess that means its not function parameter block but instance block thats why its not going back to block from you are calling start.

Why calls swiftHTTP the response handler if there is no connection?

I'm using swiftHTTP for requesting to my server and when my internet connection is slow, it goes to response part! I've set the example code below:
HTTP.GET("myURL") { response in
let myResponse = response.data // it comes here after the timeout
if response.statusCode == 200 {
//some code
} else {
do {
let jsonError = try JSON(data: myResponse) // in this line it goes to catch because there is no data in myresponse
} catch{
//alert for not having connection
}
}
Why does it call the response function if there's no response?
My server also says, that no request was sent.
It doesn't "go to response", it tries to make the HTTP request as expected and regardless of success or error it's completion handler is called.
The response object that is returned is an object that contains all of the information you need to determine what happened with the request.
So it will contain a timeout status code (HTTP 408), possibly an error message. If it did not respond at all, your app would not be able to handle these cases.
Say for example your user taps on their profile icon in the app and this sends a request to get the users profile, but it timed out. Do you want the user sat waiting, looking at a blank profile screen? It's much better to catch the error and handle it gracefully. In this case you could present a message to the user telling them that something went wrong and close the empty profile screen
Your response handler will also be called from swiftHTTP, if there's no or a very bad connection.To solve this problem, either check if there is an internet connection or check if the data is nil:
HTTP.GET("myURL") { response in
let myResponse = response.data // it comes here after the timeout
if response.statusCode == 200 || response.data == nil {
//some code
} else {
do {
let jsonError = try JSON(data: myResponse) // in this line it goes to catch because there is no data in myresponse
} catch{
//alert for not having connection
}
}
The important part here is the check if response.data == nil.

RxMoya request using mvvm model always crashes in observer.onError(error)

Following is my code for signing up
self.signedUp = signUpButtonTap.withLatestFrom(userAndPassword).flatMapLatest{
input -> Observable<Response> in
return Observable.create { observer in
let userData = Creator()
userData?.username = input.0
userData?.password = input.1
provider.request(.signIn(userData!)).filter(statusCode: 200).subscribe{ event -> Void in
switch event {
case .next(let response):
observer.onNext(response)
case .error(let error):
let moyaError: MoyaError? = error as? MoyaError
let response: Response? = moyaError?.response
let statusCode: Int? = response?.statusCode
observer.onError(error)
default:
break
}
}
return Disposables.create()
}
}
Following is the binding in the View
self.viewModel.signedUp.bind{response in
self.displayPopUpForSuccessfulLogin()
}
When there is a successful response its works fine.
But when the request times out or I get any other status code than 200, I get the following error "fatalError(lastMessage)" and the app crashes.
When I replace observer.onError(error) with observer.onNext(response) in the case .error it works for response codes other than 200 , but crashes again when the request times out.
I have gone through this link Handling Network error in combination with binding to tableView (Moya, RxSwift, RxCocoa)
Can anyone help me out with what is wrong. I am completely new to RxSwift . Any help will be appreciated. Thank you
If provider.request(.signIn(userData!)) // ... returns results on some background thread, results would be bound to UI elements from a background thread which could cause non-deterministic crashes.
It should be
provider.request(.signIn(userData!))
.observeOn(MainScheduler.instance) // ...
according to RxSwift github tips: Drive

Performing an asynchronous task within a synchronous call

I would like to register a user which is performed asynchronous. However, the calling function behaves synchronous since the program should only continue when a user is created successfully.
The current implementation is:
class SignUp: NSObject {
// ...
func signUpUser() throws -> Bool {
guard hasEmptyFields() else {
throw CustomErrorCodes.EmptyField
}
guard isValidEmail() else {
throw CustomErrorCodes.InvalidEmail
}
createUser( { (result) in
guard result else {
throw CustomErrorCodes.UserNameTaken
}
return true // Error: cannot throw....
})
}
func createUser( succeeded: (result: Bool) -> () ) -> Void {
let newUser = User()
newUser.username = username!
newUser.password = password!
// User is created asynchronously
createUserInBackground(newUser, onCompletion: {(succeed, error) -> Void in
if (error != nil) {
// Show error alert
} else {
succeeded(result: succeed)
}
})
}
}
and in a ViewController the signup is initiated as follows:
do {
try signup.signUpUser()
} catch let error as CustomErrorCodes {
// Process error
}
However, this does not work since createUser is not a throwing function. How could I ensure that signUpUser() only returns true when an new user is created successfully?
You say:
and in a ViewController the signup is initiated as follows:
do {
try signup.signUpUser()
} catch let error as CustomErrorCodes {
// Process error
}
But don't. That's not how asynchronous works. The whole idea is that you do not wait. If you're waiting, it's not asynchronous. That means you're blocking, and that's just what you mustn't do.
Instead, arrange to be called back at the end of your asynchronous process. That's when you'll hear that things have succeeded or not. Look at how a download task delegate is structured:
https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSessionDownloadTask_class/
The download task calls back into the delegate to let it know whether we completed successfully or not. That is the relationship you want to have with your asynchronous task. You want to be like that delegate.
You need to adjust your thinking. Instead of trying to write a synchronous method that we need to wait for an asynchronous event, write a method that takes a completion closure. The method will return immediately, but once the asynchronous process is complete it wild invoke the completion closure. When you call such a method you pass in code in the incompletion closure that gets called once the job is done.

NSURLSession().dataTaskWithRequest(...) can't throw?

I have this function that takes care of an API call (makeAPICall) that I'd like to throw an error for certain API responses and when the httpResponse.statusCode != 200.
The problem is that, as far as I know, NSURLSession().dataTaskWithRequest(...) can't throw. Is this correct and if so, is there some workaround? Or should I do something totally different?
Since dataTaskWithRequest is an asynchronous operation, its error handling is facilitated with a completion handler. If it were to throw, it would be difficult to handle an error at the completion of the operation.
Therefore, you should handle the error condition within the completion handler. If you wanted to throw your own error upon completion, that would be possible but somewhat superfluous.
Instead of throwing an error, have your clients pass in a block, and run that block whenever the request fails (or, for that matter, when it completes successfully).
Actually you you can handle the error if occurs. For instance
let task = session.dataTaskWithRequest(request, completionHandler: {(data, response, error) in
if let error = error {
print(error)
// do whatever you want, there is an error
}
if let data = data{
print("data =\(data)")
}
if let response = response {
print("url = \(response.URL!)")
print("response = \(response)")
let httpResponse = response as! NSHTTPURLResponse
print("response code = \(httpResponse.statusCode)")
}
})
and I showed you how to get the response code as well.

Resources