I have some misunderstanding in using NSURLSession framework, that's why I decided to write small app from scratch without AFFramework/Alamofire.
I have an API that requires following steps to upload file:
POST file data
Get response (JSON)
Post some json fields to api/save
I have a background session with such config:
let configuration = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier("myBackground")
let session = NSURLSession(configuration: configuration, delegate: self, delegateQueue: nil)
I've implemented 2 methods:
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData)
where I aggregate all data
and
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?)
where I tranform this data to response object. This response object if VERY important for me.
Everything works fine, while app is in foreground, but I have problems in background.
Case 1
App crashed right after I've started to upload data. According to WWDC I need to implement
func application(application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: () -> Void)
and call this handler in didCompleteWithError method. But before calling this method I need to call api/save with data from upload response.
How can I get this data?
Case 2
Mostly similar case. User stops app while upload is in progress. Than loads app in few seconds, while session works with my task. Now session calls didReceiveData, but of course, some of data is missing. What should I do in such case? How to restore response data?
You don't mention implementing URLSessionDidFinishEventsForBackgroundURLSession (a NSURLSessionDelegate method). You really want to implement that, too. The basic process is:
In app delegate's handleEventsForBackgroundURLSession, you should:
start the background NSURLSession (which will start receiving delegate method calls associated with all of the uploads); and
save the completion handler (but do not call it, yet).
Then, in URLSessionDidFinishEventsForBackgroundURLSession (when you're done processing all of the responses), you call the completion handler you saved in handleEventsForBackgroundURLSession. (Make sure to dispatch that to the main queue.)
If you're doing all of that, when the background session is restarted, the didReceiveData calls will come in with the responses to your various uploads.
I just did a quick test, uploading five 20mb images and immediately terminating the app. Then, even though the app wasn't running, I watched the five files slowly show up on my server (obviously handled by the daemon process). When all five were done, by app was transparently restarted in the background, the handleEventsForBackgroundURLSession was called (which restarted the session), it let all of the didReceiveData calls quickly get called, and when that was done, URLSessionDidFinishEventsForBackgroundURLSession was called and my app only then called the saved completion handler.
In terms of why this isn't working for you, there's not enough to diagnose the problem. Possibilities include:
Maybe you terminated the app inappropriately. You can't kill the app by double tapping the home button and terminating the app there; you have to let it naturally terminate on it's own, or for diagnostic/testing purposes, I force it to terminate by calling exit(0) in code.
Maybe you didn't restart the session when handleEventsForBackgroundURLSession was called.
Maybe you called the supplied completion handler too soon (i.e. before URLSessionDidFinishEventsForBackgroundURLSession was called).
It's hard to say, but I suspect that there's something buried inside your implementation that isn't quite right and it's hard to say what it is on the basis of the information provided (assuming it isn't one of the above points). Unfortunately, debugging this background sessions is vexingly complicated because when the app terminates, it is no longer attached to the debugger, so you can't easily debug what happens after the app is restarted automatically by iOS). Personally, I either NSLog messages and just watch the device console (as well as watching what appears on the server), or I build some persistent logging mechanism into the app itself.
For testing Background session code it is recommended to test on a real device. When writing an app that uses NSURLSession’s background session support, it’s easy to get confused by three non-obvious artifacts of the development process:
When you run your app from Xcode, Xcode installs the app in a new container, meaning that the path to your app changes. This can confuse NSURLSession’s background session support.
Note: This problem was fixed in iOS 9; if you encounter a problem with NSURLSession not handling a container path change in iOS 9 or later, please file a bug.
Xcode’s debugging prevents the system from suspending your app. So, if you run your app from Xcode, or you attach to the process some time after launch, and then move your app into the background, your app will continue executing in situations where the system would otherwise have suspended it.
Similarly, the iOS Simulator does not accurately simulate app suspend and resume; this has worked in the past but it does not work in the iOS 8 or iOS 9 simulators.
Source: Apple Developer Forum
Related
I have been using NSURLSession to do background uploading to AWS S3. Something like this:
NSURLSessionConfiguration* configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:#“some.identifier"];
NSURLSession* session = [NSURLSession sessionWithConfiguration:configuration delegate:someDelegate delegateQueue:[NSOperationQueue mainQueue]];
NSURLSessionUploadTask* task = [session uploadTaskWithRequest:request fromFile:[NSURL fileURLWithPath:httpBody]];
[task resume];
In someDelegate, I have implemented didSendBodyData, didCompleteWithError and handleEventsForBackgroundURLSession.
I have three questions:
I have noticed that if I close the app while uploading is in progress, transfer will continue and successfully finish. Is handleEventsForBackgroundURLSession called when the transfer is finished while the app is closed?
Assuming that the answer to the first question is yes, how can I delete httpBody in handleEventsForBackgroundURLSession? This is a temporary file that is not needed once transfer is complete.
I would appreciate it if someone explained, in detail, how background transfer works in iOS. That is when memory is created, which callbacks are called at which states and how the app is woken up once the transfer is completed. Thanks.
When the app delegate's handleEventsForBackgroundURLSession is called, you should:
save the completion handler;
instantiate your background NSURLSession;
let all of your delegate methods to be called;
in your URLSession:task:didCompleteWithError:, you can remove those temp files; and
in URLSessionDidFinishEventsForBackgroundURLSession:, you can call that saved completion handler.
A few additional notes:
There seems to be some confusion about what happens when an app is terminated.
If the app is terminated in the course of its normal lifecycle, the URLSession daemon will keep the background requests going, finishing your uploads, and then wake up your app when it's done.
But manually force-quitting the app (e.g., double tapping on home button, swiping up on the app to force it to quit) is a completely different thing (effectively, the user is saying "stop this app and everything associated with it"). That will stop background sessions. So, yes, background sessions will continue after the app is terminated, but, no, not if the user force-quit the app.
You talk about setting breakpoints and observing this in Xcode. You should be aware that the process of being attached to Xcode will interfere with the normal app life cycle (it keeps it running in background, preventing it from being suspended or, during the normal course of events, terminating).
But when testing background session related code, it's critical to be test the handleEventsForBackgroundURLSession workflow when your app was terminated, so to that end, I'd suggest not using Xcode debugger when testing this dimension of background sessions.
I use the new OSLog unified logging system, because the macOS Console can watch what is logged by the app, while not having Xcode running at all. Then I can write code that starts some download or upload, terminates app and then watch the logging statements I have inserted in order to observe the restarting of the app in background via the macOS console. See Unified Logging and Activity Tracing video for a tutorial of how to watch iOS logs from the macOS Console.
I'm having issues with handling "out of space" / "full disk" errors on ios with NSURLSessionDownloadTask
If the disk is full due to downloads done in the app I get a call to
URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?)
with the error having domain NSPOSIXErrorDomain and error code ENOSPC
But this only happens once for a task, not for all running
Ex: If I have 3 tasks running at the time, I only get this for one of them and the other 2 remain in the state Running
They don't receive any bytes, but they don't fail either.
Moreovever, calling cancel on any of those tasks changes their state from Running to Cancelling and they remain like this indefinitely.
My solution was, when I receive this error the first time, to call invalidateAndCancel for the session and handle failure for all running tasks.
This seems to work when the "full disk" is caused by the downloads made by the app.
But if "full disk" error is caused by external downloads (Ex: iTunes file sharing, downloading Podcasts, other apps downloading) I receive no error in the app
All my download tasks remain in Running, or Cancelling (if I try to cancel them)
They don't download anything, they don't fail with any of the callbacks for NSURLSessionDelegate or NSURLSessionDownloadDelegate
Before starting a download, I check the available space available on device
I also take into consideration the currently Running tasks
But I have no control over other downloads on the device that might end up triggering the "full disk" warning
How can I handle these cases?
Are the download tasks expected to remain in Running state though they are not downloading anymore?
Shouldn't I get a didCompleteWithError call with (NSPOSIXErrorDomain, ENOSPC) for each task, or for the session at least?
Or at least shouldn't I be able to successfully cancel them? and get a didCompleteWithError call anyway?
Is there a delegate call I'm missing, one that would let me know it's time to close all running tasks?
I'm using a shared background session for more background download tasks
The download tasks are created with NSURLSession's:
func downloadTaskWithRequest(_ request: NSURLRequest) -> NSURLSessionDownloadTask
The sesion configuration is created using NSURLSessionConfiguration's:
class func backgroundSessionConfigurationWithIdentifier(_ identifier: String) -> NSURLSessionConfiguration
The seesion is created using, NSURLSession's
init(configuration configuration: NSURLSessionConfiguration,
delegate delegate: NSURLSessionDelegate?,
delegateQueue queue: NSOperationQueue?)
I'm using a NSOperationQueue with a maxConcurrentOperationCount of 3
I have implemented my NSURLSessionDelegate and NSURLSessionDownloadDelegate
Tasks seem to run fine in foreground and background.
Thanks
Ps: using Xocde7, ios9 sdk, tested on an ios9 device
I had also asked Apple about this issue and the answer came back:
Seems like, because the background session uses the disk to persist its state, behaviour when the disk is full is not exactly reliable to return the ENOSPC
And the handling of full disk case is the developer's responsibility for now by:
checking for disk space before you start a download
monitor disk space when your app is downloading — If your app is in the foreground, or your app gets resumed (or relaunched) in the background, actively monitor the disk space available. If it’s below some threshold, suspend your downloads (by calling -cancelByProducingResumeData:, so you can resume them later on).
For a detailed answer check:
https://forums.developer.apple.com/thread/43263
I do not understand the following and hope some does :-)
I am downloading a file from an url (10 minutes downloadtime), which works fine, when the download completes.
BUT:
When I simulate a crash or internet-interruption after a minute and restart the app again, it behaves strange to me.
In this case when restarting my app to download again with same sessionid, it seems that 2 download tasks are working in parallel. I recognizue this with a jumping progressbar from 10% to 0% and back. One, which starts from scratch and one, which I guess continious the old transfer. (not sure). I can restart again and then there is one more in the queue.
Can someone confirm this behaviour and does someone know how I can:
- continue only the interrupted download task (preferred :-) )
- or how can I start from scratch only.
Here my code for downloading, which works fine without any interruption.
func download_file(sURL: String, sToLocation: String) {
println("start downloading ...");
println("sURL : " + sURL + " sToLocation : " + sToLocation);
bytesDownloaded=0;
var delegate = self;
delegate.storePath=sToLocation;
delegate.progressView=progressView;
struct SessionProperties {
static let identifier : String! = "url_session_background_download"
}
var configuration = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(SessionProperties.identifier)
var backgroundSession = NSURLSession(configuration: configuration, delegate: delegate, delegateQueue: nil)
//myURLSession = NSURLSession(configuration: configuration, delegate: delegate, delegateQueue: nil)
var url = NSURLRequest(URL: NSURL(string: sURL)!)
var downloadTask = backgroundSession.downloadTaskWithRequest(url)
//downloadTask.cancel()
downloadTask.resume()
}
Update
When I am using a different sessionid, the Download starts from scratch. The previously started download task runs still in the background. So I am wondering why can't I resume the previously download task by using the old sessionid without starting a new download in parallel ?
The whole point of using a download task is so that downloads can continue even if your app isn't running or if it crashes. You don't need to resume the download. It is actually happening in a separate background daemon. You just need to re-create the session using the same ID.
After you re-create the session, any existing downloads are automatically associated with the new session, and your delegate methods are called whenever the task completes.
There's also a little bit of magic that you need to implement for handling background launches if your app isn't running when the download finishes. See URL Session Programming Guide for details.
In addition to what dgatwood said, remember to have to have your app delegate implement application:handleEventsForBackgroundURLSession:completionHandler:, which is called if the download finishes and your app isn't running at the time. When this method is called (again, only if download finishes when your app wasn't running), you should (a) save the completionHandler; (b) instantiate the background session with the same identifier; (c) let your NSURLSessionDownloadDelegate method be called (at which point which didFinishDownloadingToURL: is called) and when (d) URLSessionDidFinishEventsForBackgroundURLSession is called, if you have a completionHandler saved from when handleEventsForBackgroundURLSession was called, then this is the appropriate time to call this saved completionHandler.
The basic idea is as follows: If your app wasn't already running when the download finishes, the OS seamlessly starts your app in the background (unbeknownst to the end user), providing a reference to this completionHandler, you then have the app do all that it needs to do to move this downloaded file to its new location, and when it's all done, you call the saved completionHandler to let the OS know that you're all done handling the downloaded file in the background execution and the app can safely be suspended again (i.e. to avoid keeping the app running in the background, adversely affecting the UX and battery for the user).
Obviously, if the app happens to be running and has the NSURLSession already instantiated, you won't see these background related events taking place. If the app was running when the download finishes, it behaves much like a foreground NSURLSession and the above is not called upon. But you need that logic in case your app wasn't running when the download finishes.
See Downloading Content in the Background section of The App Programming Guide for iOS, which describes this process. Also refer to WWDC 2013 video in What’s New in Foundation Networking (it's covered later in the video).
I need something similar to Facebook's offline post capabilities. Basically I want users to create content locally on the device regardless of connection state, and whenever internet becomes available it should POST/PUT to the server.
I've searched the internet for a solution and I found that NSURLSessionUploadTask can be used for POST-ing in the background. But I couldn't figure out if the following scenarios are supported:
Will my task remain in the background queue when the user is offline and will the operating system try to execute items in the queue upon reconnecting with a network?
What happens if the application is force-closed by the user or crashes?
What happens if the operation fails?
First of all, background NSURLSession allows file upload only. If that is ok for you:
The task will be in the queue until it receives a server answer.
If your app is force-closed, the task will still be executing. When the request is done, your app will be launched in non-interactive background state and receive application:handleEventsForBackgroundURLSession:completionHandler:. After you process the signal and call the completion handler or 30 second timeout, the app will be closed.
I the operation fails, you will receive URLSession:task:didCompleteWithError:
There is a good tutorial on background NSURLSessions. I suggest you to read all 4 parts of this great article.
If file upload is not an option for you, i suggest you to save information into local database and then wait for internet is reachable. (a good approach here is use of Reachability library, Alamofire allows to do that too). When internet becomes available, simply call your http requests with saved data.
We were running into connectivity issues with our internal apps, so we wrote a Swift framework that allows any network operations to be enqueued and sent whenever the device has access to the internet -
https://cocoapods.org/pods/OfflineRequestManager. You'll still have to handle the network request itself within the object conforming to OfflineRequest, but it sounds like a good fit for your use case.
The simplest use case would look something like the following, though most actual cases (saving to disk, specific request data, etc.) will have a few more hoops to jump through:
import OfflineRequestManager
class SimpleRequest: OfflineRequest {
func perform(completion: #escaping (Error?) -> Void) {
doMyNetworkRequest(withCompletion: { response, error in
handleResponse(response)
completion(error)
})
}
}
///////
OfflineRequestManager.defaultManager(queueRequest: SimpleRequest())
Perhaps I have been reading the wrong stuff, but one thing that all of the literatures that I have been reading seem to agree on is that: iOS does not allow background threads to run for longer than ten minutes. That seems to violate one of the greatest principles of app development: the internet should be invisible to your users. So here is a scenario.
A user is going through a tunnel or flying on an airplane, which causes no or unreliable network. At that instant, the user pulls out my email app, composes an email, and hits the send button.
Question: How do I the developer make sure that the email is sent when network becomes available? Of course I am using email as a general example, but in reality I am dealing with a very much simple http situation where my app needs to send a POST to my server.
Side Note: on android, I use Path’s priority job queue, which allows me to set it and forget it (i.e. as soon as there is network it sends my email).
another Side Note: I have been trying to use NSOperationQueue with AFNetworking, but does not do it.
What you want to achieve can be done using a background NSURLSession. While AFNetworking is based on NSURLSession I’m not quite sure if it can be used with a background session that runs while your app doesn’t. But you don’t really need this, NSURLSession is quite easy to use as is.
As a first step you need to create a session configuration for the background session:
let config = URLSessionConfiguration.background(withIdentifier: "de.5sw.test")
config.isDiscretionary = true
config.waitsForConnectivity = true
The isDiscretionary property allows the system to decide when to perform the data transfer. waitsForConnectivity (available since iOS 11) makes the system wait if there is no internet connection instead of failing immediately.
With that configuration object you can create your URL session. The important part is to specify a delegate as the closure-based callbacks get lost when the app is terminated.
let session = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue.main)
To perform your upload you ask the session to create an upload task and then resume it. For the upload task you first create your URLRequest that specifies the URL and all needed headers. The actual data you want to upload needs to be written to a file. If you provide it as a Data or stream object it cannot be uploaded after your app terminates.
let task = session.uploadTask(with: request, fromFile: fileUrl)
task.resume()
To get notified of success or failure of your upload you need to implement the URLSessionDataDelegate method urlSession(_:task:didCompleteWithError:). If error is nil the transfer was successful.
The final piece that is missing is to handle the events that happened while your app was not running. To do this you implement the method application(_:handleEventsForBackgroundURLSession:completionHandler:) in your app delegate. When the system decides that you need to handles some events for background transfers it launches your app in the background and calls this method.
In there you need first store the completion handler and then recreate your URLSession with the same configuration you used before. This then calls it’s delegate for the events you need to handle as usual. Once it is done with the events it calls the delegate method urlSessionDidFinishEvents(forBackgroundURLSession:). From there you need to call the completion handler that was passed to your app delegate.
The session configuration provides some more options:
timeoutIntervalForResource: How long the system should try to perform your upload. Default is 7 days.
sessionSendsLaunchEvents: If false the app will not be launched to handle events. They will be handled when the user opens the app manually. Defaults is true.
Here is a small sample project that shows how everything fits together: https://github.com/5sw/BackgroundUploadDemo
Your app needs to store the data internally and then you either need something which will cause the app to run in the background (but you shouldn't necessarily add something specially if you don't already have a reason to be doing it) or to wait until the user next brings the app to the foreground - then you can check for a network connection and make the call.
Note that e-mail is very different to a POST, because you can pass an e-mail off to the system mail app to send for you but you can't do exactly the same thing with a POST.
Consider looking also at NSURLSessionUploadTask if you can use it.
In three words: you don't.
And that's actually a good thing. I certainly do not want to have to think and speculate about my last 20 apps, if they are still running in the background, using memory and battery and bandwidth. Furthermore, they would be killed if more memory is needed. How would the user be able to predict if it completed its task successfully? He can't, and need to open the app anyhow to check.
As for the email example, I'd go with showing the email as "pending" (i.e. not sent), until it transferred correctly. Make it obvious to the user that he has to come back later to fulfill the job.
While every developer thinks that his app has an extremely good reason for backgrounding, reality is, for the user in 99% it's just a pain. Can you say "task manager"? ;-)
I wrote a pod that does pretty much this - https://cocoapods.org/pods/OfflineRequestManager. You'd have to do some work listening to delegate callbacks if you want to monitor whether the request is in a pending or completed/failed state, but we've been using it to ensure that requests go out in poor or no connectivity scenarios.
The simplest use case would look something like the following, though most actual cases (saving to disk, specific request data, etc.) will have a few more hoops to jump through:
import OfflineRequestManager
class SimpleRequest: OfflineRequest {
func perform(completion: #escaping (Error?) -> Void) {
doMyNetworkRequest(withCompletion: { response, error in
handleResponse(response)
completion(error)
})
}
}
///////
OfflineRequestManager.defaultManager(queueRequest: SimpleRequest())