Swift 3 - How to handle URLRequest / URLSession errors? - ios

I want to know how you guys handle errors when using a URLRequest in your app. How do you go about notifying your users that an error has occurred? Do you even notify your users at all? Do you try and reload the URLRequest again? Do you tell your users to close the current screen and open it again with an alert box? I have no clue.
Once there's an error, your app stops. So what do you do when this happens and you have a network issue, bad Json data?
What do you do when you get a "Bad Network Connection (The server is down)" or the URLSession comes back with an error and the internet connection is fine?
Please look at the code below and help me figure out what needs to be done when an error occurs.
let url = URL(string:"http://example/jsonFile.php")
var request = URLRequest(url:url!)
request.httpMethod = "POST"
let postingString = "id=\(id)"
request.httpBody = postingString.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: request as URLRequest){(data, response, error) -> Void in
if error != nil {
print("error \(error)")
// *****
// What do you do here? Do you tell your users anything?
// *****
return
}
// Check for Error
if let urlContent = data {
do{
let jsonResult = try JSONSerialization.jsonObject(with: urlContent, options: .allowFragments) as! [String: AnyObject]
print("jsonResult \(jsonResult)")
}
catch{
print("JSON serialization failed")
// *****
// What do you do here? Do you tell your users anything?
// *****
}
}
}
task.resume()

It is often a bad idea to hide the errors and do nothing (see Error Hiding Anti-Pattern). Unfortunately, handling errors is not easy and can be tedious some times. Pretty code examples quickly become "uglier" when exceptions and errors have to be handled. It takes time to learn to do it well.
In the case of network connections, you would want to wrap your requests into asynchronous methods that take a completion callback as parameter. This callback will often receive a successful result or an error, sometimes wrapped in an enum to remove ambiguity (see Result as an example).
Once you know a network request has failed, you can present that information to the user in a clean, informational but non-obstructive way. To find out what other applications do in this scenario, turn on Airplane Mode in your phone and poke around.
Here are some examples:
Apple Music
Facebook Messenger

Related

Data contentsOf yields nil, but the URL is valid

I am using this code
let url = URL(string: "http://image.tmdb.org/t/p/w185" + movie.poster_path!) // https://www.themoviedb.org/talk/568e3711c3a36858fc002384
print(url!)
DispatchQueue.global().async {
let data = try? Data(contentsOf: url!) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
DispatchQueue.main.async {
self?.movieImage.image = UIImage(data: data!)
}
}
from this stack overflow post. I have a URL with an image on it, I would like to use that URL to bring the image into my app and have it show up in a
#IBOutlet weak var movieImage: UIImageView!
but for some reason, I am getting an error saying that data is nil. Why would data be nil if the URL is valid? Is this an issue with the contentsOf function or am I doing something wrong here?
If you try changing your URL declaration to be: let url = URL(string: "http://image.tmdb.org/t/p/w185//nBNZadXqJSdt05SHLqgT0HuC5Gm.jpg") it works as expected. So perhaps you are not assembling the URL correctly?
I would print whatever URL you're creating and try visiting the website to see if it is actually correct
I’d suggest not using try? (which discards any meaningful error data) and instead use try wrapped in a do-catch block, and in the catch block, examine what the error is. Right now, you’re flying blind.
Or, better, use URLSession.shared.dataTask(with:) and look at the error in the completion handler.
You asked:
... but why is this such a bad thing [to use Data(contentsOf:)] if it is the background thread?
Yes, by dispatching this to a global queue you’ve mitigated the “don’t block the main thread” problem. But Data(contentsOf:) doesn’t provide much diagnostic information about why it failed. Also, it ties up one of the very limited number of worker threads that GCD draws upon. If you exhaust the worker thread pool, then GCD won’t be able to do anything else until it’s freed up. Using URLSession offers the chance to do more meaningful diagnostics and avoids blocking GCD worker threads.
So, I would suggest removing all of those ! forced unwrapped operators and not using Data(contentsOf:). Thus, I might suggest something like:
guard
let path = movie.poster_path,
let baseURL = URL(string: "http://image.tmdb.org/t/p/w185")
else {
print("problem getting path/URL")
return
}
let url = baseURL.appendingPathComponent(path)
URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
guard
let data = data,
let response = response as? HTTPURLResponse,
error == nil
else {
print("network error:", error ?? "Unknown error")
return
}
guard 200..<300 ~= response.statusCode else {
print("invalid status code, expected 2xx, received", response.statusCode)
}
guard let image = UIImage(data: data) else {
print("Not valid image")
return
}
DispatchQueue.main.async {
self?.movieImage.image = image
}
}.resume()
Then, by displaying the error, if any, we’ll see what the problem was. FWIW, the above network request identifies three types of errors, which might be helpful for diagnostic purposes:
Basic network errors
HTTP errors
Content errors (not an image)

When to use do-catch block using Swift

In the following scenario when reading JSON data from a file, I have the following block of code:
// Fetch URL
let url = Bundle.main.url(forResource: "sampleJSON", withExtension: "json")!
// Load Data
let data = try! Data(contentsOf: url)
// Deserialize JSON
let json = try! JSONSerialization.jsonObject(with: data, options: [])
Is this block of code correct on its own, or should would it be better practice to include it inside a do-catch block? I'm asking because I have seen scenarios when pulling data from the web using URLSession, where developers do the JSONSerialization inside of a do-catch block. Is there a reason why for doing it when using URLSession, and not when simply pulling the JSON data from a file? What is best practice for something like this?
1 - Is this block of code correct on its own, or should would it be better practice to include it inside a do-catch block?
A: This code is correct. It will work if your sampleJSON.json file is in your bundle AND the data in your JSON file is correctly formated AND the JSONSerialization succeds parsing the data provided.
2 - I'm asking because I have seen scenarios when pulling data from the web using URLSession, where developers do the JSONSerialization inside of a do-catch block. Is there a reason why for doing it when using URLSession, and not when simply pulling the JSON data from a file? What is best practice for something like this?
A: The do-catch statement is seen more often when consuming data(JSON in this case) from the web because the API might break for any reason(wrong specification of the data that must be shown, error in the web application itself, etc) and if this happens we do not want our application to crash.
I say CRASH because you used the ! which do not propagate the error to the upper layer of your application, it tries to force the operation and if fails would crash the app.
At this point you probably realized that the reason you don't see do-catch statement when consuming data from your bundle is because the app developer himself provided the JSON so I'd assume you are sure about the content of the file, but I'd still use the do-catch statement since something could go wrong and don't want my app to crash due to a silly thing like this.
TL;DR
I recommend to ALWAYS use error propagation with throws/rethrows or even the ? so you can test for nil results.
I have written a small article here with some tips and how it works in Swift 2.1, not much have changed in Swift 3.1 so you can use to study the do-catch statement.
I would rewrite the code you provided like this:
WARNING: UNTESTED CODE
enum JSONFromFileError: Error {
case fileNotInBundle(String)
case deserializationError
case getDataError(URL)
}
func json(from file: String) throws -> Any {
// Fetch URL in Bundle
guard let url = Bundle.main.url(forResource: file, withExtension: "json") else {
throw JSONFromFileError.fileNotInBundle(file)
}
// Load Data from url
guard let data = try? Data(contentsOf: url) else {
throw JSONFromFileError.getDataError(url)
}
// Deserialize JSON
guard let json = try? JSONSerialization.jsonObject(with: data, options: []) else {
throw JSONFromFileError.deserializationError
}
return json
}
do {
let myJsonObject = try json(from: "sampleJSON")
print(myJsonObject)
} catch let error {
print(error)
}
You should in general use a try-catch block for each function that is throwable. With your current code, if either of your try blocks fail (either the data can't be downloaded from the url or it is not a valid json), the forced trys will fail and result in a runtime exception.
let url = Bundle.main.url(forResource: "sampleJSON", withExtension: "json")!
do {
let data = try Data(contentsOf: url)
let json = try JSONSerialization.jsonObject(with: data, options: [])
} catch {
//handle error
}
If you don't care about the error that would be thrown be the function, you can use try? which returns a nil when the function would throw an error. This way your code won't crash.
guard let data = try? Data(contentsOf: url) else {return}
guard let json = try? JSONSerialization.jsonObject(with: data, options: []) else {return}
Firstly, It is recommended that we wrap any functions that throws (with try) into a do-catch block. In my own opinion, I will also perform a do-catch block in case I modify the sampleJSON file without me knowing, that causes the format of the JSON to be disturbed.
Secondly,it is definitely a good practise to never use force unwrap at all in the code.
Lastly, we should always catch for serialization exceptions and throw to the caller, which eventually will inform the view to throw an error dialog.

How to read HTTP requests without flooding my API with Swift 3

I'm trying to make a simple messaging app using an API. Right now, I have a thread that checks a request each second and see if the number of messages have changed, but this causes so much trouble, the RAM is constantly going up and the API becomes unresponsive because of the large number of requests. At the moment my code looks like this :
var request = URLRequest(url: URL(string: "URL")!)
let session = URLSession.shared
public func thread()
{
DispatchQueue.global(qos: .background).async {
while(true)
{
self.request.httpMethod = "GET"
self.session.dataTask(with: self.request) {data, response, err in
let json = try! JSONSerialization.jsonObject(with: data!, options: []) as? [String: Any]
let data = json?["data"] as? [[String: Any]]
if((data?.count)! > self.nbMessages)
{
self.messages.removeAll()
for message in data! {
let text = message["message_body"] as? String
let creator = message["creator_id"] as? Int
self.messages.append([text!, String(creator!)])
}
DispatchQueue.main.async {
self.nbMessages = (data?.count)!
self.TableView.reloadData()
let scrollPoint = CGPoint(x: 0, y: self.TableView.contentSize.height - self.TableView.frame.size.height)
self.TableView.setContentOffset(scrollPoint, animated: false)
}
}
}.resume()
usleep(2000)
}
}
}
This works fine, I can send messages and see messages sent to me (with a decent delay), but my logic with the request at every 2 second is way off and I acknowledge it. I'm still learning Swift so I'd really appreciate some advises on this matter, thanks!
In comments you provide elaboration, saying that you are implementing a messenger. For that purpose simple HTTP requests are not appropriate approach. Instead, you want to introduce so-called socket connection. I dare quote myself from another relevant thread:
It's called socket-connections, at a glance it looks like a channel, that hosts on a server side, and any clients (devices) can join this channel (two or more, whatever you want). If device send a message to the server, it should broadcast the message toward all other participants (it can broadcast the message even to the sender itself, but with some meta-information so we can distiguish our own messages and ignore them).
Thereby first of all you need server with socket connection established, then you can implement any of already existing solutions (e.g. https://github.com/daltoniam/Starscream for iOS). Also you may want to take a look at AWS https://aws.amazon.com, as it has the socket connection service out of the box for the server side and required SDK for both Android and iOS platforms.

iOS NSURLSession with Wi-Fi Assist turned on results in crashing app

I'm performing an HTTP request using iOS's NSURLSession. My code looks like this:
let session = NSURLSession.sharedSession()
let url = NSURL(string:"www.example.com")
guard let url = url else {
return
}
let request = NSURLRequest(URL: url)
var task = session.dataTaskWithRequest(request){
(data, response, error) -> Void in
//do stuff with the data
}
task.resume()
(sorry if my code isn't 100% correct I just typed it real quickly inside my browser, you get the idea)
When Wi-Fi Assist is turned off it works fine, but when Wi-Fi Assist is turned on the app crashes.
I found this but the discussion never got an answer.
Apart from the fact that I want to fix the problem I am very curious WHY this is happening.

How to switch views in the handler of an NSURLSession Request

I have a Login View Controller, and an Other View Controller. What I'd like to do is: when the user hits login, it sends their credentials to the remote server. The remote server returns a response indicating whether the credentials were good or not, and if they were good, the app redirects to the Other View Controller.
The code below crashes at the call to .performSegueWithIdentifier.
The crash gives an error code of EXC_BAD_ACCESS(code=1, address=0xbbadbeef)
Question: what is the swifty way of doing this?
var request = NSMutableURLRequest(URL: NSURL(string: "http://url.to/my/login/handler")!)
var session = NSURLSession.sharedSession()
request.HTTPMethod = "POST"
//user initialized earlier
bodyData = "email=\(user.username)&password=\(user.password)"
request.HTTPBody = bodyData.dataUsingEncoding(NSUTF8StringEncoding);
var task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
// check that log in was successful by looking in 'response' arg
// if login was successful
self.performSegueWithIdentifier("SegueToOtherController", sender: self)
}
task.resume()
}
If it's crashing, you should share the details the crash in order to identify why. Likely problems include that it didn't find a segue of that identifier as the "storyboard id" from the current view controller to the next scene. But it's impossible to say without details on the precise error.
Having said that, there is another problem here: The completion block may not run on the main thread, but all UI updates must happen on the main thread. So make sure to dispatch that back to the main queue, e.g.
let request = NSMutableURLRequest(URL: NSURL(string: "http://url.to/my/login/handler")!)
let session = NSURLSession.sharedSession()
request.HTTPMethod = "POST"
//user initialized earlier
bodyData = "email=\(user.username)&password=\(user.password)"
request.HTTPBody = bodyData.dataUsingEncoding(NSUTF8StringEncoding);
let task = session.dataTaskWithRequest(request) {data, response, error in
// check that log in was successful by looking in 'response' arg
// if login was successful
dispatch_async(dispatch_get_main_queue()) {
self.performSegueWithIdentifier("SegueToOtherController", sender: self)
}
}
task.resume()
Note, I also changed all of those var references to let (as a general rule, use let wherever possible). Also, I haven't tackled it here, but you really should be percent escaping the username and password properties. If, for example, the password included any reserved characters like + or &, this would fail. There are lots of ways of doing that, e.g. something like the method discussed here: https://stackoverflow.com/a/26317562/1271826 or https://stackoverflow.com/a/25154803/1271826.

Resources