How to switch views in the handler of an NSURLSession Request - ios

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.

Related

URLSession request: request Code=-1009 "(null)" UserInfo={_NSURLErrorNWPathKey=unsatisfied (Denied over cellular interface)

Test is performed on iPhone 13 Pro 15.3.1, with Xcode 13.2.1. I have narrow down the issue as below two tests. The test code below just send a simple https request.
I have allowed the network access for the test app on both Wifi and cell. And I'm a paid developer.
All following two tests are passed in simulator but failed on iPhone, so it means something is wrong in my iPhone side, find a post said China brand iPhone may have this issue, still digging...orz
The error exists with WiFi and Cellular. URLSession request is denied on both Wi-Fi interface and cellular interface.
NSURLErrorNWPathKey=unsatisfied...
1st test:
The test code is provided in Apple doc (https://developer.apple.com/documentation/xctest/asynchronous_tests_and_expectations/testing_asynchronous_operations_with_expectations).
func testDownloadWebData() {
// Create an expectation for a background download task.
let expectation = XCTestExpectation(description: "Download apple.com home page")
// Create a URL for a web page to be downloaded.
let url = URL(string: "https://apple.com")!
// Create a background task to download the web page.
let dataTask = URLSession.shared.dataTask(with: url) { (data, _, _) in
// Make sure we downloaded some data.
XCTAssertNotNil(data, "No data was downloaded.")
// Fulfill the expectation to indicate that the background task has finished successfully.
expectation.fulfill()
}
// Start the download task.
dataTask.resume()
// Wait until the expectation is fulfilled, with a timeout of 10 seconds.
wait(for: [expectation], timeout: 10.0)
}
2nd attempt:
I also try my version, but get the same result.
func testDownloadWebData() {
// Create an expectation for a background download task.
let expectation = expectation(description: "Download apple.com home page")
// Create a URL for a web page to be downloaded.
let url = URL(string: "https://apple.com")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
let config = URLSessionConfiguration.default
config.waitsForConnectivity = true
let session = URLSession(configuration: config)
// Create a background task to download the web page.
let dataTask = session.dataTask(with: request) { (data, _, error) in
// put a breakpoint here, no triggered.
guard error == nil else {
print(error!.localizedDescription)
return
}
// Make sure we downloaded some data.
XCTAssertNotNil(data, "No data was downloaded.")
// Fulfill the expectation to indicate that the background task has finished successfully.
expectation.fulfill()
}
// Start the download task.
dataTask.resume()
// Wait until the expectation is fulfilled, with a timeout of 10 seconds.
wait(for: [expectation], timeout: 10.0)
}

Swift 3 - How to handle URLRequest / URLSession errors?

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

NSURLSession, after the data task is converted to download task, it can't download in background

If I run the following code and let the app in background, the download is still continuing. Finally, when the download is finished, I can get the right callback.
let configuration = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(SessionProperties.identifier)
let backgroundSession = NSURLSession(configuration: configuration, delegate: self.delegate, delegateQueue: nil)
let url = NSURLRequest(URL: NSURL(string: data[1])!)
let downloadTask = backgroundSession.downloadTaskWithRequest(url)
downloadTask.resume()
But I have a requirement, that is I have to judge what the server returns to me, if it is a json, I don't do the download, so I want to get the response header first, then if it needs to download, I change the data task to download task, so I did as the following code
let configuration = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(SessionProperties.identifier)
let backgroundSession = NSURLSession(configuration: configuration, delegate: self.delegate, delegateQueue: nil)
let url = NSURLRequest(URL: NSURL(string: data[1])!)
//I change the downloadTaskWithRequest to dataTaskWithRequest
let downloadTask = backgroundSession.dataTaskWithRequest(url)
downloadTask.resume()
Then I can get the response header in the callback, and if it needs to download file, I can change the data task to download task, as following
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveResponse response: NSURLResponse, completionHandler: (NSURLSessionResponseDisposition) -> Void) {
if let response = response as? NSHTTPURLResponse {
let contentType = response.allHeaderFields["Content-Type"] as! String
if contentType == "image/jpeg" {
//change the data task to download task
completionHandler(.BecomeDownload)
return
}
}
completionHandler(.Allow)
}
So far so good. When I run the app in the foreground, the effect is like what I thought. But after the app runs in background, the download is stoped, then when I open the app, the console says "Lost connection to background transfer service".
I thought Apple is so smart, he gives us many useful callbacks, but now, I didn't know where I am wrong, and I also see the source code about the AFNetworking and Alamofire, but I didn't find the referring thing.
I also think it is a common requirement, but I can't find any helpful information on the internet, it is too odd.
So hope you can help me out, thanks a billion.
Enable Background Mode in
Xcode->Target->Capabilities->On Background Mode and select the option Background Fetch.
The main issue I see is that you're calling the completionHandler twice. You need to return out of your content-type conditional like so:
if contentType == "image/jpeg" {
//change the data task to download task
completionHandler(.BecomeDownload)
return
}
Otherwise it appears that you are using the logic correctly. Hope that helps.
The problem is evident from your own answer. It's not a bug, you simply couldn't use data tasks for background transfers just download tasks.
Here is the correct full answer.

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.

Swift iOS how to get the result of NSURLSession

I have a question about NSURLSession
I have downloaded JSON data using NSURLSession
I want to access the JSON enter code hereData variable from outside this block of code so I manipulate it
this is my code
// variable declared in my class
var jsonData = JSON("")
// my function
func loadCategories(){
var url = NSURL(string: "http://localhost:8888/api/v1/getAllCategories")
var request = NSMutableURLRequest(URL:url!)
request.HTTPMethod = "GET"
NSURLSession.sharedSession().dataTaskWithRequest(request, completionHandler: { (data: NSData!, response: NSURLResponse!, errors: NSError!) in
self.jsonData = JSON(data: data)
}).resume()
}
When I try to get jsonData outside the block of NSURLSession I get empty variable
Any help ?
Check if you are accessing the jsonData on the same thread. Maybe you have a race condition here.
The problem is that you don't understand how async functions with completion blocks work.
Take a look at my answer to this thread. I explain what's going on in detail:
Why does Microsoft Azure (or Swift in general) fail to update a variable to return after a table query?
(Don't let the title of the thread mislead you. It has nothing to do with Microsoft Azure.)
The method you are using, dataTaskWithRequest, invokes your completion closure on the URL session's delegate queue. Unless you've provided a background queue as the delegate queue you don't need to bother with dispatch_async.
I have found the solution
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT dispatch_async(dispatch_get_global_queue(priority, 0)) { self.jsonData = JSON(data: data) dispatch_async(dispatch_get_main_queue()) { self.collectionView?reloadData() } }

Resources