I am trying to build a sequential download manager where the user can initiate up to 1000 download tasks at once but only 2 will actually download and the rest will be put on hold, until one of the two finishes so that another one of the 998 can start.
Because there is no way to add a downloadTask then have that task wait for others to complete then automatically start, I have built a download queue myself and will only create new downloadTask when the old one completes.
I have read the documentations and am well aware of the mechanism of iOS's handling of background download event, and what methods I need to implement in order to handle them correctly. However, I am unable to find anything on whether it is safe and reliable to start a new downloadTask when the old one completes IN BACKGROUND.
The Main Question:
When iOS relaunches my app in background to inform me that my download tasks are finished, can I reliably create a new downloadTask and add it to the current session? If so, when that task finishes as well, will the system relaunch my app AGAIN to tell me it's finished? If so, then I can create an infinite loop of adding new tasks when the old backgroundTask finish.
Code Example
Would the following code reliable to get the entire download queue downloaded(say, 1000 items) if I download ONLY one item at a time and meanwhile the app stays entirely in background?
extension MyClass: URLSessionDelegate {
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
// This is called when background tasks are done
// Let's add a new background tasks WHILE app has just been
// relaunched IN BACKGROUND
if let nextURL = myURLQueue.removeFirst() {
session.downloadTask(with: URLRequest(url: nextURL))
}
DispatchQueue.main.async {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate,
let completionHandler = appDelegate.backgroundSessionCompletionHandler {
appDelegate.backgroundSessionCompletionHandler = nil
completionHandler()
}
}
}
}
You are creating the URLSessionDownloadTask, but never starting it with resume().
FWIW, even if you fix the above, you should recognize that requests that are submitted while the app is running in background mode will always be submitted in discretionary mode. As the docs for isDiscretionary say:
For transfers started while your app is in the background, the system always starts transfers at its discretion—in other words, the system assumes this property is true and ignores any value you specified.
Also, recognize that having the system fire up your app every time a request is done in order to submit the next request is both inefficient (because your app will constantly fire up and go back into background) and slow (because concurrent requests reduce latency effects). It’s generally better to just submit all of the requests up front, while the app is active, and let the background session manage how many it will allow to run concurrently.
Related
My iOS app receives notification to refresh its state from our main service. Right now, we are using Alamofire to fetch the latest state and we play a continues sound when there is updated data. Our devices are locked in guided mode to stop them from turning off and provide kiosk experience.
We are making changes such that the device can go to sleep after xx minutes of inactivity. However, we ran into a problem where the device was not getting results back from Alamofire even though the request was sent successfully (based on our logs on the api side).
As I am using Alamofire 4, I have setup a singleton with backgroundsessionmanager which is how AF requests are sent now. But the challenge is that requests are sent intermittently and fail most of the time when the device is sleeping with this error:
Domain=NSURLErrorDomain Code=-997 "Lost connection to background transfer service"
Here is my code for singleton (and I have associated code in AppDelegate):
class Networking {
static let sharedInstance = Networking()
public var sessionManager: Alamofire.SessionManager // most of your web service clients will call through sessionManager
public var backgroundSessionManager: Alamofire.SessionManager // your web services you intend to keep running when the system backgrounds your app will use this
private init() {
self.sessionManager = Alamofire.SessionManager(configuration: URLSessionConfiguration.default)
self.backgroundSessionManager = Alamofire.SessionManager(configuration: URLSessionConfiguration.background(withIdentifier: "com.test.app"))
}
}
Here is my code for sending the request:
let NetworkManager = Networking.sharedInstance.backgroundSessionManager
DispatchQueue.main.async(execute: {
NetworkManager.request(self.api_url, method: .post, parameters: params, encoding: JSONEncoding.default, headers: headers).responseJSON{ response in
switch (response.result){
case .success:
if let jsonResp = response.result.value{
print("got the response")
print(jsonResp)
// parse results
}
case .failure:
print("Network Error: \(response.error)")
}
}
})
I am hoping to get some help to resolve this issue as I am unable to rootcause the inconsistent behavior. I have read in some places that Apple/iOS will only allow upload/download when the app is in background mode instead of requests.
Yes, background sessions only permit upload and download tasks, not data tasks. They also only permit delegate-based requests, not completion handler-based requests. This answer outlines many of the considerations when doing this in conjunction with Alamofire.
But this begs the question as to whether you really want to use a background session at all. When your app is awaken for background fetch, if you’re able to finish you request within a reasonable amount of time (e.g. 30 seconds), you should probably consider a standard session, not a background session. It’s a lot simpler.
Do not conflate an app running in the “background” with a “background” URLSessionConfiguration: They’re completely different patterns. Just because your app is running in the background, that doesn’t mean you have to use background URLSessionConfiguration. If your app is running (whether in foreground or in the background), then a standard session is fine. You only need background session if you want it to continue after the app is suspended (or is terminated) and you’re willing to encumber yourself with all the extra overhead that background sessions entail.
Background sessions are not intended for requests performed while the app is running in the background. They’re intended for requests that will continue after your app is suspended (and even if it eventually is terminated in the course of its natural lifecycle). That means that background sessions are ideal for slow requests that cannot be completed in a reasonable amount of time, e.g., downloading video asset, downloading many large image assets or documents, etc.
But if you’re just performing a routine GET/POST request that will complete in a reasonable amount of time, consider not using background URLSessionConfiguration, but just do normal request and call the background fetch completion handler when your request is done (i.e., in your network request’s completion handler).
I'm using NSURLSessionDownloadTask to download files. At one point in the application it is necessary to pause the download for a short time and then resume it. I tried using the suspend method for pausing the download but the task continuous to download. Apple documentation for NSURLSessionTask's method suspend states:
A task, while suspended, produces no network traffic and is not subject to timeouts. A download task can continue transferring data at a later time. All other tasks must start over when resumed.
This seems to indicate that what I'm doing is right, but the result is not as expected. I was able to reproduce it with the following code in the Playground.
import UIKit
let backgroundSessionConfiguration = URLSessionConfiguration.background(withIdentifier: "some-identifier")
let backgroundSession = Foundation.URLSession(configuration: backgroundSessionConfiguration, delegate: nil, delegateQueue: nil)
let request = URLRequest(url: URL(string: "http://google.com")!)
let task = backgroundSession.downloadTask(with: request)
task.state.rawValue
task.resume()
task.state.rawValue
task.suspend()
task.state.rawValue
sleep(10)
task.state.rawValue
The expected result for task states would be 1, 0, 1, 1 (0 - downloading, 1 - suspended, 2 - cancelled, 3 - done). Actual result is 1, 0, 1, 3. This means that although the task is suspended it still continues to download and marks itself done once the download is finished.
The setup in the app is more complicated and the task states cannot always be replicated (in the Playground the results seem always the same) but the result after suspending the task is always that the file is successfully downloaded (including that the progress callback is called).
The question:
Am I doing something wrong? Have I misunderstood the Apple docs and it's not possible to pause a download task this way?
My first intuition when faced with this issue in the app was that I was calling resume somewhere else that resulted in the download to continue, but this does not seem to be the case after debugging the code + the Playground findings support this as well.
Probably not the answer you're looking for, but an Apple Dev Relations representative has confirmed in their dev forums that background downloads can in some situations not be really suspended by using suspend:
[...] a suspended task can still be active on the wire; all that the suspend does is prevent it making progress internally, issuing callbacks, and so on.
They go on by suggesting to use cancel(byProducingResumeData:) instead:
if you're implementing a long-term pause (for example, the user wants to pause a download), you'd be better off calling -cancelByProducingResumeData:.
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
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).
Im trying to upload images from the Photo app through Action Extension. I use NSURLSession to upload it in background. Here is the code i use.
var configName = "com.myapp.uploadImage"
var config = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(configName)
config.sharedContainerIdentifier = "group.myApp.sample"
var session = NSURLSession(configuration: config)
let task = session.dataTaskWithRequest(request)
task.resume()
self.extensionContext!.completeRequestReturningItems(self.extensionContext!.inputItems, completionHandler: nil)
It works fine.
The question is when i upload an image and dismiss the view once and then again try uploading a second image while the initial process is still running in the background, the initial NSURLSession isn't completed. Only the second process gets completed. In short, the second session overcomes the first session.
I tried using NSOperationQueue. But action extension once dismissed and opened again for the second session, it just creates a new NSOperationQueue and hence the problem still persists.
Any suggestion will be helpful. Thanks in advance.
Make sure you don't attempt to instantiate a second background session with the same identifier while the first is still running. Save your background session so you can use it later.
As the "Background Session Considerations" of the URL Loading System Programming Guide: Using NSURLSession says:
Note: You must create exactly one session per identifier (specified when you create the configuration object). The behavior of multiple sessions sharing the same identifier is undefined.
Note, this document also mentions that one "must" specify and implement the delegate. (If nothing else, how will you know about failure if you don't do that?) The example provided in the Performing Uploads and Downloads section of the App Extension Programming Guide specifies the delegate, too.
Also, has your main app's app delegate implemented the handleEventsForBackgroundURLSession method? You have to capture the completionHandler and call it when the NSURLSessionDelegate method URLSessionDidFinishEventsForBackgroundURLSession is called.
Finally, I notice that you're using data task. The NSURLSession documentation is specific that one should not be using data tasks with background sessions, only upload/download tasks. I always assumed that was just so you don't try to use didReceiveData delegate method, but I might try using upload task just in case there's some other issue associated with data tasks with background sessions.