NSURLSession with background configuration and app killed by user - ios

This is the scenario:
NSURLSession with background Configuration
Download or upload task start with Bad or No Internet Connection.
User close the App.
If iOS get Internet Connection will star session task. However,
With task still waiting for Internet.
User kills the App
System cancel all pending tasks
The Question
It is possible to know when the user opens the app again that the tasks were cancelled?
If yes, where?
This Answer says yes, it is possible, but I can not get any callback returning me an error.
I'm using Alamofire to handle all my Networking calls. However, I doubt that Alamofire will change the behavior.
Edit 1
/// Networking manager with Background Session Configuration
static var backgroundManager: Alamofire.Manager = {
let configuration = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier("com.xxx.NetworkingManager.Identifier")
configuration.HTTPAdditionalHeaders = Manager.defaultHTTPHeaders
let backgroundManager = Alamofire.Manager(configuration: configuration)
return backgroundManager
}()

Kudos to Rob because he showed me the right path.
So after the user kills the app, the system cancels all the pending tasks.
You can see that with the system.log:
Simulator/Debug/Open System Log...
How to catch what was already ongoing?
Instantiate again your Background NSURLSession. Do it elsewhere, but I'll do it in AppDelegate for this example.
The system knows (thanks to the identifier) that it is the same Background Session that before so it maps the pending tasks.
Then retrieve all the tasks. The canceled tasks are still there
The tasks will have a error that you can check.
Error Domain=NSURLErrorDomain Code=-999 "(null)"
UserInfo={NSErrorFailingURLStringKey=http://your.api.com/url,
NSURLErrorBackgroundTaskCancelledReasonKey=0,
NSErrorFailingURLKey=http://your.api.com/url}
Also, with the tasks, you will get the Request URL, so you can map your app requests and do something.
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// This is the code for Swift 2.x. In Swift 3.x this call is a bit different.
NetworkingManager.backgroundManager.session.getTasksWithCompletionHandler { (data, upload, download) in
for task in data {
NSLog("\(task.error)")
}
for task in upload {
NSLog("\(task.error)")
}
for task in download {
NSLog("\(task.error)")
let reason = task.error?.userInfo[NSURLErrorBackgroundTaskCancelledReasonKey] as? Int
let code = task.error?.code
if reason == NSURLErrorCancelledReasonUserForceQuitApplication &&
code == NSURLErrorCancelled {
NSLog("\(task.originalRequest)")
NSLog("\(task.currentRequest?.URL)")
}
}
}
}
NSURLErrorCancelledReasonUserForceQuitApplication -> The operation was canceled because the user forced the app to quit.
So we are on the right track.
If someone has a better solution, please share! I do not really like the mapping solution of my requests urls.

Related

iOS - Running a network request in the background with app closed

I'm currently working on a cross-platform (native Java and Swift/Objective-C) mobile app where the users may frequently have limited access to a network connection, but we want to record and send up statistics when a network becomes available, even if our app has been closed. We’ve already done this on Android fairly easily with their AndroidX.WorkManager library like so:
String URL = "https://myexampleurl.com";
Data inputData = new Data.Builder()
.putString("URL", URL)
.build();
Constraints constraints = new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build();
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(NetworkWorker.class)
.setInputData(inputData)
.setConstraints(constraints)
.build();
WorkManager.getInstance(context).enqueueUniqueWork("com.lkuich.exampleWorker", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest);
This works great, when our app invokes this method, our request is added to a queue, and even if the user is offline and closes our app, when they reconnect to a network our request still gets made.
I’ve been struggling however to find an iOS equivalent to this API, and I am fairly new to native iOS development. We've enabled the Background Task capability and registered our identifiers in Plist.info in our project.
First we’ve tried the URLSession "background" API like so:
class NetworkController: NSObject, URLSessionTaskDelegate {
func buildTask() -> URLSession {
let config = URLSessionConfiguration.background(withIdentifier: "com.lkuich.exampleWorker")
config.waitsForConnectivity = true
config.shouldUseExtendedBackgroundIdleMode = true
return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue.main)
}
func makeNetworkRequest(session: URLSession) {
let url = URL(string: "https://myexampleurl.com")!
let task = session.dataTask(with: url)
task.resume()
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
print("We're done here")
}
}
This works well when the WiFi is disabled and re-enabled, but if the app is closed the request never gets sent it seems.
So we tried the BGTaskScheduler instead, but I'm having a hard time testing if it's working in the background with the app closed, since it can often take hours to actually run (forcing it to run immediately with a network connection works):
static let bgIdentifier = "com.lkuich.exampleWorker"
// Invoked on app start (didFinishLaunchingWithOptions)
static func registerBackgroundTasks() {
BGTaskScheduler.shared.register(forTaskWithIdentifier: bgIdentifier, using: nil) { (task) in
task.expirationHandler = {
task.setTaskCompleted(success: false)
}
// Make my network request in BG
let url = URL(string: "https://myexampleworker.com")!
let t = URLSession.shared.dataTask(with: url, completionHandler: { data, response, error in
// Print the response
print(response)
task.setTaskCompleted(success: true)
})
t.resume()
}
}
static func makeRequest() {
do {
let bgRequest = BGProcessingTaskRequest(identifier: bgIdentifier)
bgRequest.requiresNetworkConnectivity = true
bgRequest.requiresExternalPower = false
try BGTaskScheduler.shared.submit(bgRequest)
print("Submitted task request")
} catch {
print("Failed to submit BGTask")
}
}
Is it possible to schedule a network request to be run in the background when a network is available, even if the invoking app is closed? And if so, how would I pass input data (like URL args for example) to my background task, since my task has to be registered on app start?
There are two possible states a closed app can be in. It's either in the background, or it is killed. App can be killed by the system, for various reasons, or by the user, who can kill it from the app switcher.
This is important because when the app is killed, you can't do anything in the background. Only VoIP apps are exception (using PushKit, which you are now allowed to be using if the app doesn't have VoIP functionality, you won't get pass the AppStore), and watchOS complications. But for all other apps, when the app is killed you have no options at all. That is just Apple's standard, they don't want apps that are not running to be able to run code for security reasons.
You can register some tasks to be done, when the app is still in the background, using the BGTask API you mentioned. I used that same API for one of my apps, and found it extremely volatile. Sometimes a task is done every 30 minutes, and then it won't be executed until tomorrow morning, then at one time it would just stop getting executed at all.
Therefore I think that silent push notifications are the best way out, but they also only work when the app is still in the background, not killed. Unless your app has VoIP functionality, in which case you can use PushKit.

nw_read_request_report [C9] Receive failed with error "Software caused connection abort"

I got this error with application connection lost. While redirecting from another app to my app I face this issue.This issue triggered only on live app, getting error with connection lost and while debugging with Xcode getting error but redirected to specific view controller successfully I used deep linking with url scheme for handling response from another app. Still not getting clarity to what exact issue is there because not able to debug live app issue.
Working on iOS 13.2
In AppDelegate:
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true)
let params = components?.queryItems
signedResponse = (params?.first(where:{$0.name == "signedResponse"})?.value)!
self.decodedMsgString = String(data:Data(base64Encoded: signedResponse)!,encoding:.utf8)!
print("decodedMsgString : \(decodedMsgString)")
//Call API here
return true
}
I also ran into this issue, maybe this could give you an insight?
https://forums.developer.apple.com/thread/106838
From one of the replies
Following up, we determined that the issue was caused because our app continued to issue new NSURLConnection requests after going into the background and wasn't explicitly making them background tasks. As we didn't need background syncing, putting in code to prevent new requests from going out once the app was in the background eliminated this error.
There was the same problem, after returning to the application, it was necessary to make a short pause before requesting data from the deep link.
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
YourNetworkResponse
}

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.

NSURLSession Performance - Probable race conditions or blocked threads?

I've ran into a bit of a performance issue with my iOS app, this is my first time working with NSURLSession and NSURLRequest, and although I've tried to inform myself as much as I can, I've hit a wall trying to debug a performance issue I'm facing.
So here's what I got: I've got an iOS 9 app written in Swift 2, I'm communicating with a NodeJS/Express server through Get, Post and Put Http requests, utilizing NSURLRequest and NSURLMutableRequest. I'm sending requests to fetch a group of objects (All together no more than 12000 bytes), however the requests are taking a significant amount of time (sometimes up to a minute). I've added logging to the nodeJs server and I can see that the requests take no longer than 30 milliseconds to be processed.
Note: I'm unsure if this is relevant, but I'm using a singleton "helper "class to make all my api requests and parse the results (saving authentication tokens, parsing JSON objects and saving them to Core Data, saving user preferences to NSUserDefaults, etc), I'm using a singleton so i can access it statically and I'm parsing all the data without saving anything in singleton's properties other than the URL of the server and the NSURLSession.
Here's what my code looks like.
//On initialization of the helper class
private let session = NSURLSession.sharedSession()
func getAllObjects() {
let route = "api/someRoute"
let request = getRequest(route)
request.timeoutInterval = httpTimeout
session.dataTaskWithRequest(request, completionHandler: ResultingObjects).resume()
}
The getRequest method returns a formatted NSMutableURLRequest, shown
here:
func getRequest(route: String) -> NSMutableURLRequest {
let request = NSMutableURLRequest()
request.URL = NSURL(string: "\(serverUrl)/\(route)")!
request.HTTPMethod = "GET"
request.addValue("Bearer \(self.AuthenticationToken()!)", forHTTPHeaderField: "Authorization")
return request
}
The completion handler will parse the objects returned and notify
the main thread with the resulting parsed objects, as so:
private func ResultingObjects(data: NSData?, response: NSURLResponse?, error: NSError?) {
if let d = data {
if !isAuthorized(d){
return
}
do {
if let JSON = try NSJSONSerialization.JSONObjectWithData(d, options: []) as? NSDictionary {
if let message = JSON["message"] as? String {
if message == "Empty result" {
//- Return notification to be handled in main thread
notifyMainThread(NoObjectsFetched, payload: nil)
return
}
}
if let objcts = JSON["SomeObjects"] as? NSArray {
if let SomeObjects = parseResultingObjects(objcts) {
//- Return notification to be handled in main thread
notifyMainThread(ObjectsFetched, payload: ["payload": SomeObjects])
}
return
}
}
}
catch {
print("Error getting resulting objects")
}
}
else if let e = error {
print("\(e), could not process GET request")
}
}
I've also tried parsing the resulting objects on the main thread but
that doesn't seem to make a difference.
If you are curious, this is how I'm sending data to the main thread:
private func notifyMainThread(notification: String, payload: AnyObject?) {
dispatch_async(dispatch_get_main_queue(), {
if let p = payload {
NSNotificationCenter.defaultCenter().postNotificationName(notification, object: nil,
userInfo: p as! [String: [MYMODEL]])
}
else {
NSNotificationCenter.defaultCenter().postNotificationName(notification, object: nil)
}
});
}
What I've found out:
Nothing makes sense! I've attempted debugging this but I cant really pin point what the issue is, When the debugger hits my "getAllObjects" method, it can take a good few seconds (up to 45 seconds) before the server logs that it received and processed the request (which it usually takes around 30 milliseconds). As far as I can tell, this happens for all requests types. Also, once the application gets the data back (super fast), it takes a long time (around 4 seconds) to parse it, and its only around 11kbs.
I've also attempted to change the requests cache policy in case the application was checking the validity of the cached records with the server, I used ReloadIgnoringLocalAndRemoteCachedData which didn't work either.
Now, this sounds like a memory leak
If I pause the application at any point (after using it for a few minutes), I can see a concerning number of threads. I'm honestly not too familiar with IOS so I'm unsure if this threads are from the simulator or if they all belong to the app, the app streams video contents (which has no latency issues) usin the AVPlayer class and I believe that many of this threads are related to this, however I'm unsure if this is normal, here's a screenshot of what I mean (Note the scroll bar T_T) Screenshot
Could it be that I've got a memory leak or some zombie threads considerably slowing the performance of my app? The only really noticeable delay happens only on HTTP requests which is quite odd, no other part of my UI lags, and no other feature in my app suffers from performance issues (even streaming video contents from a url).
What would be the best way to profile this performance issues to pin point the source of the problem?
UPDATE 1:
Thanks to the suggestions by Scriptable, I've managed to address the threading issue (caused by multiple AVPlayers doing their thing). The performance of the requests however are not solved.
Its worth pointing out, the server is physically in the same country as where I'm making the requests from, when making requests from the browser or form the Command Line the requests are almost immediate.
Also, when I randomly pause the app (while I wait to the request to happen) I can see a 'mach_msg_trap' in some of the threads, I'm not familiar with this but I believe this might be a race condition? or a deadlock?

Repeat an asynchronous request if it fails?

I am writing an ios app that relies on being able to tell when a user is connected to wifi and, when he or she connects or disconnects, send an asynchronous request using alamo fire.
The first time I connect, my asynchronous succeeds.
However, after I first connect, any toggling of the wifi results in 404s.
I suspect this is because I am sending the request as soon as the user connects/disconnects, meaning that for a brief moment he or she has no internet service.
My question is, can I repeat the request if it fails or is it possible to "cache" the requests I want to make and wait until the user has internet connection to make them?
There are many solutions to solve this. One is to call the download method recursively again and so implementing an automatic retry mechanism on errors:
func downloadSomething() {
Alamofire.request(.GET, "https://httpbin.org/get", parameters: ["foo": "bar"])
.response { request, response, data, error in
if let error = error {
log(error)
self.downloadSomething() // recursive call to downloadSomething
} else {
// do something on success
}
}
}
You can extend this by:
showing the user also an altert view asking him if he want's to retry
the download or not before retrying the download. (depending on your
UI strategy on network errors)
a specified count of automatic re-trys and then ask the user.
checking the error status code and then depending on the code do
different network error handling strategies...
etc...
I think there is no needed to re-invented apple code like reachability or this swift reachability porting. You can able to check if a user is connected to the net or wifi very easily:
class func hasConnectivity() -> Bool {
let reachability: Reachability = Reachability.reachabilityForInternetConnection()
let networkStatus: Int = reachability.currentReachabilityStatus().rawValue
return networkStatus != 0
}
For a Wi-Fi connection:
(reachability.currentReachabilityStatus().value == ReachableViaWiFi.value)

Resources