Alamofire Network error exceptions handling - ios

I am currently developing an application for iOS. Most of the features that I wanted implemented I have already finished, but there is one feature in particular that I really need to have - Network Errors handling.
So for example: A user is trying to refresh his data inside my application. Instead of my app crashing or simply not doing anything, I would love for that exception to be caught, identified and then display a corresponding message on screen using AlertDialogs. for example:
Network Error - title;
Unreachable host, please check your network connectivity and try again - Message;
OK - button;
I was able to have this working in my Android application and it's quite useful, however, I am quite new to Swift and iOS development, so, please help me out here and point me in the right direction.
I am currently using latest Alamofire for sending HTTP Requests, here is my example of HTTP Request that I have implemented inside my application.
func loadProfile() {
let url = Constants.profileURL
let headers: HTTPHeaders = ["Cookie": "username; password"]
AF.request(url, method: .post, headers: headers).response {response in
if let data = response.data, let dataString = String(data: data, encoding: .utf8) {
if dataString.contains(Constants.loginSuccess) {
//Do something
}
}
}
}

Alamofire's DataResponse (and all response types) contain the Result of serialization. You can use that value to check whether serialization, and the request in general, succeeded or failed. You can then pass the result value to completion handlers to be dealt with as you wish.

Related

network requests being saved in memory?

This has got to be one of the strangest thing I've come across...
Simple network request using alamofire
manager.request(PostRouter.readPosts(pn: pn)).validate().responseJSON {
response in
switch response.result {
case .success(let json):
if let json = json as? JSON,
let postsArray = json["posts"] as? [JSON],
let posts = Post.build(from: postsArray) {
completion(posts, nil)
}
else {
completion(nil, NetworkingError.jsonParsingError)
}
case .failure(let error):
completion(nil, error)
}
}
The same thing happens whether running from the simulator or mobile.
make request in airplane mode -> error of no internet connection.
make request again with internet -> loads posts
make request again in airplane mode -> loads posts without internet. Returns a success case with json value.
Even if in step 3 I make the request with an internet connection and have since modified the value of the data between steps 2 and 3, it will still return the exact same data from step 2 instead of fetching new data. It's almost like the whole request, including the data is saved in memory.
The only way I can actually make a new request to fetch the new data is by removing it from my phone and installing it again. The issue then repeats itself.
This happens to all the requests I make from the app. Also, The issue isn't linked to Alamofire because without it the issue still occurs
It's probably not an issue but a feature. Foundation will automatically cache responses. Check the response's cache headers (like Expires, Cache-Control, ETag etc.) if the server enables caching.

Pattern for retrying URLSession dataTask?

I'm fairly new to iOS/Swift development and I'm working on an app that makes several requests to a REST API. Here's a sample of one of those calls which retrieves "messages":
func getMessages() {
let endpoint = "/api/outgoingMessages"
let parameters: [String: Any] = [
"limit" : 100,
"sortOrder" : "ASC"
]
guard let url = createURLWithComponents(endpoint: endpoint, parameters: parameters) else {
print("Failed to create URL!")
return
}
do {
var request = try URLRequest(url: url, method: .get)
let task = URLSession.shared.dataTask(with: request as URLRequest) { (data, response, error) in
if let error = error {
print("Request failed with error: \(error)")
// TODO: retry failed request
} else if let data = data, let response = response as? HTTPURLResponse {
if response.statusCode == 200 {
// process data here
} else {
// TODO: retry failed request
}
}
}
task.resume()
} catch {
print("Failed to construct URL: \(error)")
}
}
Of course, it's possible for this request to fail for a number of different reasons (server is unreachable, request timed out, server returns something other than 200, etc). If my request fails, I'd like to have the ability to retry it, perhaps even with a delay before the next attempt. I didn't see any guidance on this scenario in Apple's documentation but I found a couple of related discussions on SO. Unfortunately, both of those were a few years old and in Objective-C which I've never worked with. Are there any common patterns or implementations for doing something like this in Swift?
This question is airing on the side of opinion-based, and is rather broad, but I bet most are similar, so here goes.
For data updates that trigger UI changes:
(e.g. a table populated with data, or images loading) the general rule of thumb is to notify the user in a non-obstructing way, like so:
And then have a pull-to-refresh control or a refresh button.
For background data updates that don't impact the user's actions or behavior:
You could easily add a retry counter into your request result depending on the code - but I'd be careful with this one and build out some more intelligent logic. For example, given the following status codes, you might want to handle things differently:
5xx: Something is wrong with your server. You may want to delay the retry for 30s or a minute, but if it happens 3 or 4 times, you're going to want to stop hammering your back end.
401: The authenticated user may no longer be authorized to call your API. You're not going to want to retry this at all; instead, you'd probably want to log the user out so the next time they use your app they're prompted to re-authenticate.
Network time-out/lost connection: Retrying is irrelevant until connection is re-established. You could write some logic around your reachability handler to queue background requests for actioning the next time network connectivity is available.
And finally, as we touched on in the comments, you might want to look at notification-driven background app refreshing. This is where instead of polling your server for changes, you can send a notification to tell the app to update itself even when it's not running in the foreground. If you're clever enough, you can have your server repeat notifications to your app until the app has confirmed receipt - this solves for connectivity failures and a myriad of other server response error codes in a consistent way.
I'd categorize three methods for handling retry:
Reachability Retry
Reachability is a fancy way of saying "let me know when network connection has changed". Apple has some snippets for this, but they aren't fun to look at — my recommendation is to use something like Ashley Mill's Reachability replacement.
In addition to Reachability, Apple provides a waitsForConnectivity (iOS 11+) property that you can set on the URLSession configuration. By setting it, you are alerted via the URLSessionDataDelegate when a task is waiting for a network connection. You could use that opportunity to enable an offline mode or display something to the user.
Manual Retry
Let the user decide when to retry the request. I'd say this is most commonly implemented using a "pull to refresh" gesture/UI.
Timed/Auto Retry
Wait for a few second and try again.
Apple's Combine framework provides a convenient way to retry failed network requests. See Processing URL Session Data Task Results with Combine
From Apple Docs: Life Cycle of a URL Session (deprecated)... your app should not retry [a request] immediately, however. Instead, it should use reachability APIs to determine whether the server is reachable, and should make a new request only when it receives a notification that reachability has changed.

How to reduce boilerplate in responseJSON just like I use URLRequestConvertible to group related web calls

I use URLRequestConvertible to groups my web calls, and to reduce the boilerplate code. But in each responseJSON I still have boilerplate to process my JSON response. They all look like these,
Check response.result.isSuccess
Check response.result.value as? the type data I expect (mostly a dictionary)
Check for the success indicator in dictionary
If succeed then retrieve the data I need.
And because I group the related calls in one URLRequestConvertible, their responses have the similar format that I actually have the 5th step to further retrieve the "real" data I am looking for.
So is there any way to reduce these boilerplate codes in responseJSON?
BTW, I actually come up with a kludge solution for it. But I was wondering is there any common practice for that?
I raised the same question at alamofire forum #2099 and got the answer to use ResponseSerializer
But after check the ResponseSerializer document I realize my home-made solution was not as crappy as I thought (using ResponseSerializer seems rather complicated)
So my solution is to add a static verify method to my Router and let it do the basic verification work (from Step #1 to Step #5)
static func verify(json:DataResponse<Any>, request:Router) -> result //needs the 2nd
parameter b/c is a static method
Now my calling method changed to these,
var result = CallResult.fail
Alamofire.request(Router.Callback(input))
.responseJSON { response in
result = Router.verify(json:response,request:Router.Callback(input))
}
.responseJSON { _ in //AS I already parsed response into my result
//process the result now
}

Intercepting EVERY response with Alamofire

I'm just exploring using Alamofire and it is excellent but I'd like to do something that I feel is possible just not sure how.
Our authentication with the server uses one-time-use bearer tokens. So for every request made I have to store the new token sent down with that request.
What I'd like to do is intercept every response that comes back and check the Authorisation header. Save it to disk and then forward to the place waiting for the actual data.
Is this possible with Alamofire?
If so, please could you point me in the right direction.
Thanks
OK, after a bit of searching the github and head scratching I decided to create a new response serialiser by extending the Request type.
I created a new saveAuth() block like so...
extension Request {
public static func AuthSaver() -> ResponseSerializer<Bool, NSError> {
return ResponseSerializer { request, response, data, error in
guard error == nil else { return .Failure(error!) }
if let auth = response?.allHeaderFields["Authorization"] as? String {
Router.OAuthToken = auth // this uses a didset on the Router to save to keychain
}
return .Success(true)
}
}
public func saveAuth() -> Self {
return response(responseSerializer: Request.AuthSaver()) {_ in}
}
}
I can call it like...
Alamofire.request(Router.Search(query: query))
.validate()
.responseSaveAuth() // this line
.responseJSON {
response in
// ...
}
It still requires adding in each place that I want to strip out the newly sent auth token but it means I can choose when not to do it also and it's a single line of code.
It's maybe not the most elegant code in the extension (I'm still getting to grips with it all) but it makes it much easier to save the authentication each time.
I have solved this by only having one place in my app that sends network requests. Basically, I have a "network manager" that builds up NSURLRequests and pipes them to one function that actually sends the request (in my case it's an NSOperation sub class). That way I have only one location that I'm reading responses from.

POST Queries in Swift for given website

I am trying to make queries to get the fuel type and consumption of a specified car (the user enters both make and model) for an iOS app written in Swift.
The app is targeted for Spain, and I have found a website that allows the user to enter make and model, and it returns the details for that car (http://coches.idae.es/portal/BaseDatos/MarcaModelo.aspx). I have seen using the tool WireShark, that the query is based on POST instead of GET. But I am not quite sure how I can make the requests within the app I am developing, or how to handle the info that is sent to me back from the sender.
Is there any way to make those requests to the given website? If so, I would really appreciate some help on the subject, I am new in iOS development and am looking forward to learning as much as possible.
Thanks :)
Many people prefer to use AFNetworking for making HTTP requests. However you don't need to do that. You said that its a POST request. Setting that up is easy even without AFNetworking using NSMutableURLRequest. I'm assuming you have a link to the API and not just to the aspx page. My Spanish is pretty weak so I can't look up the API reference for you but here's how you can make the request and receive data from the server. You will have to put the correct values and parse the responses:
let request = NSMutableURLRequest(URL: NSURL(string: "/* Paste URL here */")!)
request.HTTPMethod = "POST"
// Do this as many times are required for filling in the headers.
request.addValue("/* The value for the HTTP header */", forHTTPHeaderField: "/*The header field like Accept-Type, etc..*/")
// If you need an HTTP body too then make the JSONObj as a dictionary or array or whatever and then
let data = NSJSONSerialization.dataWithJSONObject(JSONObj, options: [])
request.HTTPBody = data // This needs to be NSData.
// Now make the request.
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request, { (data, response, error) -> Void in
if error == nil
{
assert(data != nil)
let JSON = NSJSONSerialization.JSONObjectWithData(data!, options: []) as? [NSObject: AnyObject]
// If you are using swift 2 this needs to be in a do try catch statement.
// TODO: Use JSON for whatever.
}
else
{
print(error!.localizedDescription)
}
}
task?.resume()
Let me know if you have any other questions or if the API doesn't use JSON or is completely different.

Resources