Continue NSURLSession Data Task when app enters background - ios

I have an application that retrieves JSON data from my server and enters it in to my Core Data model. I have this working reasonably well, minus a few issues with random EXC_BAD_ACCESS errors that I haven't figured out yet. During this dataTask, it can retrieve large amounts of data so it can take a while to complete.
I'm looking for a way to let the user suspend the app the dataTask will continue to retrieve the data. I seen NSURLSession has a background mode, but seen it only supports upload and download tasks.
Is there a way to support this?
My dataTask function:
class func Request(file: String, withData: String?, completion: (NSData -> Void)) {
let url = NSURL(string: "\(root)\(file)")!
let request = NSMutableURLRequest(URL: url)
if let sentData = withData {
request.HTTPMethod = "POST"
request.HTTPBody = sentData.dataUsingEncoding(NSUTF8StringEncoding)
}
let dataTask = session.dataTaskWithRequest(request) {
data, response, error in
if error != nil {
if error?.domain == NSURLErrorDomain && error?.code == NSURLErrorTimedOut {
print("Data task timed out")
}
} else {
let httpResponse : NSHTTPURLResponse = response as! NSHTTPURLResponse
if httpResponse.statusCode == 200 {
completion(data!)
} else {
print("Request failed with status code: \(httpResponse.statusCode)")
}
}
}
dataTask.resume()
}

Related

How to define a fallback case if a remote GET request fails?

I recently started with iOS development, and I'm currently working on adding new functionality to an existing app. For this feature I need to obtain a JSON file from a web server. However, if the server is unreachable (no internet/server unavailable/etc), a local JSON needs to be used instead.
In my current implementation I tried using a do catch block, but if there's no internet connection, the app just hangs instead of going to the catch block. JSON parsing and local data reading seem to work fine, the problem is likely in the GET method, as I tried to define a callback to return the JSON data as a separate variable, but I'm not sure if that's the correct way.
What is the best way to handle this scenario?
let url = URL(string: "https://jsontestlocation.com") // test JSON
do {
// make a get request, get the result as a callback
let _: () = getRemoteJson(requestUrl: url!, requestType: "GET") {
remoteJson in
performOnMainThread {
self.delegate.value?.didReceiveJson(.success(self.parseJson(jsonData: remoteJson!)!))
}
}
}
catch {
let localFile = readLocalFile(forName: "local_json_file")
let localJson = parseJson(jsonData: localFile!)
if let localJson = localJson {
self.delegate.value?.didReceiveJson(.success(localJson))
}
}
getRemoteJson() implementation:
private func getRemoteJson(requestUrl: URL, requestType: String, completion: #escaping (Data?) -> Void) {
// Method which returns a JSON questionnaire from a remote API
var request = URLRequest(url: requestUrl) // create the request
request.httpMethod = requestType
// make the request
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
// check if there is any error
if let error = error {
print("GET request error: \(error)")
}
// print the HTTP response
if let response = response as? HTTPURLResponse {
print("GET request status code: \(response.statusCode)")
}
guard let data = data else {return} // return nil if no data
completion(data) // return
}
task.resume() // resumes the task, if suspended
}
parseJson() implementation:
private func parseJson(jsonData: Data) -> JsonType? {
// Method definition
do {
let decodedData = try JSONDecoder().decode(JsonType.self, from: jsonData)
return decodedData
} catch {
print(error)
}
return nil
}
If you don't have to use complex logic with reachability, error handling, request retry etc. just return nil in your completion in case of data task, HTTP and No data errors:
func getRemoteJson(requestUrl: URL, requestType: String, completion: #escaping (Data?) -> Void) {
var request = URLRequest(url: requestUrl)
request.httpMethod = requestType
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
// Task error
guard error == nil else {
print("GET request error: \(error!)")
completion(nil)
return
}
// HTTP error
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
print("GET request failed: \(response!.description)")
completion(nil)
return
}
// No data
guard let data = data else {
completion(nil)
return
}
completion(data)
}
task.resume()
}
let url = URL(string: "https://jsontestlocation.com")!
getRemoteJson(requestUrl: url, requestType: "GET") { remoteJson in
if let json = remoteJson {
print(json)
...
}
else {
print("Request failed")
...
}
}
func NetworkCheck() -> Bool {
var isReachable = false
let reachability = Reachability()
print(reachability.status)
if reachability.isOnline {
isReachable = true
// True, when on wifi or on cellular network.
}
else
{
// "Sorry! Internet Connection appears to be offline
}
return isReachable
}
Call NetworkCheck() before your API request. If It returns false, read your local json file. if true do remote API call.
Incase after remote API call, any failure check with HTTP header response code.
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 {
}
I think you need to stop the request from hanging when it’s waiting for a response. The app might be running on a poor connection and be able to get some but not all the data in which case you likely want to failover to the local JSON.
I think you can roughly use what you have but add a timeout configuration on the URLSession as described here: https://stackoverflow.com/a/23428960/312910

Trouble with calling API in Swift

Please check if there's something wrong
I have a block of code that use API to get a list of film but nothing happens.
typealias JSONDictHandler = (([String : Any]?) -> Void)
let session = URLSession(configuration: .default)
var request = URLRequest(url: URL(string: "http://dev.bsp.vn:8081/training-movie/movie/list")!)
let token = "dCuW7UQMbdvpcBDfzolAOSGFIcAec11a"
request.httpMethod = "GET"
request.setValue(token, forHTTPHeaderField: "app_token")
let dataTask = session.dataTask(with: request) { (data, response, error) in
// if nothing wrong
if error == nil {
if let httpResponse = response as? HTTPURLResponse {
switch httpResponse.statusCode {
case 200: // successful case
if let data = data {
do {
let jsonDict = try JSONSerialization.jsonObject(with: data, options: .mutableContainers)
print(jsonDict)
} catch let error as NSError {
print("Error: \(error.localizedDescription)")
}
}
default:
print("HTTP Response Code: \(httpResponse.statusCode)")
}
}
// there's something wrong
} else {
print("Error: \(String(describing: error?.localizedDescription))")
}
}
dataTask.resume()
Besides, there're some parameters I have to insert to the url, how can I do this?
Hope this is helpful!!
Allow arbitrary load to yes

getting data from defaultSession completion handlers

I am new to swift and ios programming, I have the following block of code which queries the url for some songs and I should get the songs back.
let defaultSession = URLSession(configuration: URLSessionConfiguration.default)
var dataTask: URLSessionDataTask?
if dataTask != nil {
dataTask?.cancel()
}
let expectedCharSet = CharacterSet.urlQueryAllowed
let searchTerm = term.addingPercentEncoding(withAllowedCharacters: expectedCharSet)!
let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=\(term)")
print("URL: ", url)
// building up a dataTask computation
dataTask = defaultSession.dataTask( with: url!)
{(maybe_data, response, error) in
if error != nil {
print("Error: ", error!.localizedDescription)
// closure(nil)
return
} else if let httpResponse = response as? HTTPURLResponse {
print("no error")
if httpResponse.statusCode == 200 {
if let data = maybe_data {
// closure(songData)
print("songData: ", data)
} else {
print("no song data")
return
}
}
}
}
// does htis run the data task? is it a promise that's build?
dataTask?.resume()
The problem is that I want to get the data out of the completion handler, so ideally I'd write
let music = dataTask?.resume()
However, I am not sure how to output the value.

NSURLSession doesn't return data in first call

In general, it is necessary to implement a class for the network. This is a class that will take a URL, and to give data. All this is done in order not to score an extra logic controllers. I encountered such a problem that when you first create a View, the data do not come. That's Network Class:
private static var dataTask: NSURLSessionDataTask?
private static var dataJSON: NSData?
private static var sessionConfig: NSURLSessionConfiguration = {
var configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
configuration.allowsCellularAccess = false
configuration.HTTPMaximumConnectionsPerHost = 2
configuration.HTTPAdditionalHeaders = ["Accept": "application/json"]
configuration.timeoutIntervalForRequest = 30.0
configuration.timeoutIntervalForResource = 60.0
return configuration
}()
static func getListObjectsBy(url: String?) -> NSData? {
let session = NSURLSession(configuration: sessionConfig)
log.debug("DataTask start")
dataTask = session.dataTaskWithURL(NSURL(string: url!)!) { (data, response, error) in
log.debug("if error = error")
if let error = error {
print(error.localizedDescription)
} else if let httpResponse = response as? NSHTTPURLResponse {
log.debug("if httpResponse")
if httpResponse.statusCode == 200 {
dataJSON = data
} else {
print("Bad request")
}
}
}
dataTask?.resume()
log.debug("DataTask Resume")
return dataJSON
}
Method viewDidLoad in my main controller:
let response = Network.getListObjectsBy("http://lb.rmc.su/api-dev/v2/wc/5")
print(String(response))
My log say me, that data return nil. Notes, i'm switch between controllers with help SWRevealViewController. When reloading the main view controller, the data is returned. What me do?
enter image description here
You seem to be misunderstanding that this is an asynchronous call.
static func getListObjectsBy(url: String?) -> NSData? {
let session = NSURLSession(configuration: sessionConfig)
log.debug("DataTask start")
dataTask = session.dataTaskWithURL(NSURL(string: url!)!) { (data, response, error) in
// Everything in this block is happening on a separate thread.
log.debug("if error = error")
if let error = error {
print(error.localizedDescription)
} else if let httpResponse = response as? NSHTTPURLResponse {
log.debug("if httpResponse")
if httpResponse.statusCode == 200 {
// this won't happen until the data comes back from the remote call.
dataJSON = data
} else {
print("Bad request")
}
}
}
// This code here does not wait for the response from the remote.
// The call to the remote is sent then this code
// is immediately executed WITHOUT WAITING
dataTask?.resume()
log.debug("DataTask Resume")
// dataJSON will be nil until the remote answers.
return dataJSON
}
When you do this:
let response = Network.getListObjectsBy("http://lb.rmc.su/api-dev/v2/wc/5")
print(String(response))
The remote has not answered yet so you will get nil.
Your next question might be "what do I do about this?". The answer isn't clear without knowing everything else that you are doing.
Threads
Multiple threads of execution is like two programs running at the same time. Think of 2 people working on two different tasks at the same time. In order to keep the interface very responsive, iOS uses one thread of execution for updating the screen. If a process has to run that take a long time, we would not want the screen to wait until that is done. Let's say you have to fetch data from some remote system and that remote system is slow, your device would sit there frozen until the response came back. To avoid this, activities like calls to remote systems are done in another thread. The request is sent to the operating system itself and the operating system is told to call back when the operation is done.
This is what is happening here.
Sets up the request to send to the operating system.
dataTask = session.dataTaskWithURL(NSURL(string: url!)!)
Tells the operating system to start doing the work.
dataTask?.resume()
This block is the callback AKA the closure. iOS will run this code when the remote call is done.
dataTask = session.dataTaskWithURL(NSURL(string: url!)!) {
// Closure starts here
// Gets called when the remote has sent a response.
(data, response, error) in
// Everything in this block is happening on a separate thread.
log.debug("if error = error")
etc
}
This means you must wait until the response has come back before printing your output. You can use a closure in your function to do this.
public typealias CompletionHandler = (data: NSData?, error: NSError?) -> Void
static func getListObjectsBy(url: String?, completion: CompletionHandler) {
let session = NSURLSession(configuration: sessionConfig)
log.debug("DataTask start")
dataTask = session.dataTaskWithURL(NSURL(string: url!)!) {
(data, response, error) in
// Everything in this block is happening on a separate thread.
log.debug("if error = error")
if let error = error {
print(error.localizedDescription)
} else if let httpResponse = response as? NSHTTPURLResponse {
log.debug("if httpResponse")
if httpResponse.statusCode == 200 {
// this won't happen until the data comes back from the remote call.
} else {
print("Bad request")
}
}
// Call your closure
completion(data, error)
}
// This code here does not wait for the response from the remote.
// The call to the remote is sent then this code
// is immediately executed WITHOUT WAITING
dataTask?.resume()
log.debug("DataTask Resume")
}
In your calling code you would do this:
Network.getListObjectsBy("http://lb.rmc.su/api-dev/v2/wc/5") {
(data, error) in
if let data == data {
print(data)
}
}

NSHTTPURLResponse Status Code 200 in Airplane Mode

I have an interesting problem: when my device is in Airplane Mode, the HTTP Status Code that the NSHHTPURLResponse provides is 200 "OK".
Shouldn't it fail with an error, or at least not respond that it has a valid connection with code 200?
Here is a snipit of my code:
let url = NSURL(string: "http://apple.com");
let task = NSURLSession.sharedSession().dataTaskWithURL(url!) {(data, response, error) in
if(error == nil){
let statusCode = (response as! NSHTTPURLResponse).statusCode;
if(statusCode == 200){
println("All Good");
}
}
}
task.resume();
In Airplane Mode, "All Good" is printed
Don't test error, test the data returned. Error (NSError, ErrorType) is used to return errors from the callback (inout).
The code below works for me.
I edited it with idiomatic Swift syntax:
let urlPath = "https://www.google.com/"
let url = NSURL(string: urlPath)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(url!) { data, response, error in
if data != nil {
NSLog("%#", NSString(data: data!, encoding: NSUTF8StringEncoding)!) // EDIT
let res = response as? NSHTTPURLResponse
if res?.statusCode == 200 {
NSLog("All Good")
}
}
}
task!.resume()
Seems you may be getting an cached response. Check this article out.
.reloadIgnoringLocalCacheData
should solve your issue if this is the cause.
cachepolicy

Resources