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

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

Related

How to async await empty response using Alamofire [duplicate]

This question already has answers here:
Alamofire Response Serialization Failed
(2 answers)
Closed 6 months ago.
I have an API where I PUT stuff to. I need to make sure to wait until I get an http 200 response from the server, but I don't know how to await that using Alamofire because my response itself if empty. So it's just an http 200 with no content.
I only can find async functions that e.g. serialize a String or Data or a Decodable, but they don't work if my response is empty.
Is there a way to await something like that in Alamofire?
I know that your question is about async/await from Alamofire, but is good to know that the http status codes 204 and 205 are exactly for this. Which means that if you have access to the server code you could send the empty responses with the http status code 204 and 205 instead of 200 and then Alamofire would not generate any errors. But assuming you don't have access to the server code and you need to parse an empty response as correct then you could use the following code:
func testRequestWithAlamofire() {
let dataResponseSerializer = DataResponseSerializer(emptyResponseCodes: [200, 204, 205]) // Default is [204, 205] so add 200 too :P
AF.request("http://www.mocky.io/v2/5aa696133100001335e716e0", method: .put).response(responseSerializer: dataResponseSerializer) { response in
switch response.result {
case .failure(let error):
print(error)
case .success(let value):
print(value)
}
}
}
And for a real and complete example of how async/await from Alamofire or any other async context look this code:
// This function get report from API and save to a local JSON to be readed by the app
func updateReport() {
Task {
guard let session = self.sessionRepository.getSession(WithUser: Defaults.lastLoggedUsername!) else { return }
guard let company = session.profile?.companies.first else { return }
self.apiManager.configure(WithToken: session.accessToken)
do {
let dateA = Date().dateAtStartOf(.year)
//let dateB = Date().dateAtEndOf(.month)
let dateB = Date() // Just now
let report = try await self.apiManager.report(CompanyId: company._id, DateA: dateA, DateB: dateB, ChartPeriodicity: .month)
self.currentReport = report
// Save data to disk to be read later
self.reportManager.saveReportToDisk(report: report!, withProfileId: session.profile!._id)
} catch {
print("Error getting report: \(error)")
}
}
}
// Get personal report from a given date range
func report(CompanyId companyId: String, DateA dateA: Date, DateB dateB: Date, ChartPeriodicity chartPeriodicity: ChartPeriodicity) async throws -> CDReport? {
try await withCheckedThrowingContinuation { continuation in
self.contappApi.request(.report(companyId: companyId, dateA: dateA, dateB: dateB, chartPeriodicity: chartPeriodicity)) { result in
switch result {
case let .success(response):
// Check status code
guard response.statusCode == 200 else {
continuation.resume(throwing: ContappNetworkError.unexpected(code: response.statusCode))
return
}
// Decode data
do {
//let report = try JSONDecoder().decode(CDReport.self, from: response.data)
let report = try CDReport(data: response.data)
continuation.resume(returning: report)
} catch {
continuation.resume(throwing: ContappNetworkError.cantDecodeDataFromNetwork)
}
case .failure(_):
continuation.resume(throwing: ContappNetworkError.networkError)
}
}
}
}
Alamofire already supports this, you just need to choose a form. Your biggest issue will be accepting a 200 with no data, as that's technically invalid since only 204 or 205 are supposed to be empty.
All Alamofire responses require some sort of payload type, but Alamofire provides an Empty type to fill this role for Decodable. So the simplest way is to use the
await AF.request(...)
.serializingDecodable(Empty.self, emptyResponseCodes: [200])
.response
Note, if you already have an Empty type or are importing Combine in the same file as this code, you may need to disambiguate by using Alamofire.Empty.
If Alamofire does not provide a method for your purpose, then you will have wrap the old Alamofire methods that uses closures as below:
func myRequest() async throws {
try await withUnsafeThrowingContinuation { continuation in
myAlamofireRequest {
continuation.resume()
}
}
}

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

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
}

Cast value to enum that inherits from Error

I have a service class that offers several methods that make calls to the backend, and a common theme for those methods is that I can pass callbacks for the success and error cases:
func makeCall(onSuccess: #escaping APIResponse, onError: #escaping APIError)
The type APIError is defined like so:
typealias APIError = (Error) -> Void
Furthermore I have an enum APICallError like this:
enum APICallError: Error {
case insufficientCredentials
case malformedResponse
case forbidden
}
So in my service methods I can call onError(response.result.error!) if the result contained an error object (the force unwrap is just for brevity, I'm not really doing that) and also pass my own enum value if the HTTP result is not specific enough, like onError(APICallError.insufficientCredentials).
This works fine.
The problem is, I can't figure out how to evaluate in my client code whether the error parameter that's coming in is of type APICallError and which one of those specifically. If I do:
if let callError = error as? APICallError {
if callError == .forbidden {
// ...
}
}
execution doesn't make it past the typecast. if error is APICallError also does not seem to work.
How can I cast the error parameter to my APICallError enum value that I know is in there, because when I print(error) it gives me myapp.APICallError.forbidden?
I tried to simulate what you have posted in your question in Playground, and what you are already doing should work fine for you.
if error is APICallError is also working. One possibility why the if let condition fails might be due to the error being nil. Check if that is the case by using breakpoints.
typealias APIError = (Error) -> Void
//The error type used in your question
enum APICallError: Error {
case insufficientCredentials
case malformedResponse
case forbidden
}
//A different error type for comparison
enum AnotherError: Error {
case error1
case error2
case error3
}
let apiCallError: Error = APICallError.insufficientCredentials
let anotherError: AnotherError = AnotherError.error1
//Closure definition
var onError: APIError? = { inputError in
if let apiError = inputError as? APICallError {
print("APICallError")
}
if let anotherError = inputError as? AnotherError {
print("AnotherError")
}
}
//Above defined closure is called here below...
onError?(apiCallError)
onError?(anotherError)
Console Output (Works as expected):
APICallError
AnotherError
You need to use the enum rawValue constructor.
if let callError = APICallError(rawValue: error) {
if callError == .forbidden {
...
}
} else {
// was not an APICallError value
}

How to describe error from Alamofire using switch case in Swift?

I want to give info to the user about the error that occurred while sending a request to the server. I use Alamofire.
The code is like below:
Alamofire.request(url, method: methodUsed, parameters: parameters).responseData { (response) in
switch response.result {
case .failure(let error) :
// I want to the describe the error in here
case .success(let value) :
let json = JSON(value)
completion(.success(json))
}
}
I have tried but I can't switch the error. I want something similar to this to be placed in the code above:
switch error {
case .NoSignal : // give alert to the user about the signal
case .ServerError : // give alert to the user about server error
}
For some case I want to inform the user to take some action on the alert but I don't know what the available cases are and I don't know the syntax that has to be used.
As per Jayesh Thanki says you can identify the server error using status code and for internet connectivity you can use NetworkReachabilityManager of Alamofire. Write following code in viewDidLoad():
var networkManager: NetworkReachabilityManager = NetworkReachabilityManager()!
networkManager.startListening()
networkManager.listener = { (status) -> Void in
if status == NetworkReachabilityManager.NetworkReachabilityStatus.notReachable {
print("No internet available")
}else{
print("Internet available")
}
You can identify error using status code. response.response.statusCode.
There is lots of HTTP status code and using them you can inform end user with alert.
Here is wikipedia link for list of status code.
For example is status code is 200 OK then its successful HTTP request
and status code is 500 Internal Server Error then its server related error.
You can also provide error description using response.result.error.localizedDescription if error is available.

How to handle the response of all types of requests in one handler, but also uniquely handle every request with Alamofire and Moya

In my app I use Moya and Alamofire (And also Moya/RxSwift and Moya-ObjectMapper) libraries for all network requests and responses.
I would like to handle the response of all types of requests in one handler, but also uniquely handle every request.
For example for any request I can get the response "Not valid Version", I would like to avoid to check in every response if this error arrived.
Is there an elegant way to handle this use case with Moya?
Apparently that is very simple, You just should create your own plugin. And add it to your Provider instance (You can add it in the init function)
For example:
struct NetworkErrorsPlugin: PluginType {
/// Called immediately before a request is sent over the network (or stubbed).
func willSendRequest(request: RequestType, target: TargetType) { }
/// Called after a response has been received, but before the MoyaProvider has invoked its completion handler.
func didReceiveResponse(result: Result<Moya.Response, Moya.Error>, target: TargetType) {
let responseJSON: AnyObject
if let response = result.value {
do {
responseJSON = try response.mapJSON()
if let response = Mapper<GeneralServerResponse>().map(responseJSON) {
switch response.status {
case .Failure(let cause):
if cause == "Not valid Version" {
print("Version Error")
}
default:
break
}
}
} catch {
print("Falure to prase json response")
}
} else {
print("Network Error = \(result.error)")
}
}
}
I suggest to use generic parametrized method.
class DefaultNetworkPerformer {
private var provider: RxMoyaProvider<GitHubApi> = RxMoyaProvider<GitHubApi>()
func performRequest<T:Mappable>(_ request: GitHubApi) -> Observable<T> {
return provider.request(request).mapObject(T.self)
}
}
DefaultNetworkPerformer will handle all requests from you Moya TargetType. In my case it was GitHubApi. Example usage of this implementation is:
var networkPerformer = DefaultNetworkPerformer()
let observable: Observable<User> = networkPerformer.performRequest(GitHubApi.user(username: "testUser"))
here you 'inform' network performer that response will contain User object.
observable.subscribe {
event in
switch event {
case .next(let user):
//if mapping will succeed here you'll get an Mapped Object. In my case it was User that conforms to Mappable protocol
break
case .error(let error):
//here you'll get MoyaError if something went wrong
break
case .completed:
break
}
}

Resources