URLSession has a great feature where you can download files while your app is in the background. In this article we’ll go over setting up your app to use this great little feature. We’ll focus on downloading files, but you can use the same principles for uploading files as well. It’s going to be a short article, and hopefully useful 🙂
A Few Words
When you create your background download or upload tasks with URLSession, you’re actually scheduling a download (or upload) with the ‘nsurlsessiond’ which is a daemon service that runs as a separate process. Which means, even if your app gets killed by the system, the scheduled transfer task will keep on running.
You can observe the daemon at work by using the ‘Activity Monitor’ if you’re using the simulator:
Let’s say you’re downloading a large file. Your user will tap the download button, spend a few seconds in the app, and then put the app in the background. Imagine you’re downloading a few gigabytes, you can’t expect your users to keep your app in focus for a few minutes (or even hours). They got better things to do 🙂 With the background transfers, you hand over the downloading of the file to the daemon service, and your file will keep on downloading. If your app is in the foreground, you can even receive progress reports, to update the UI. When your download completes, you will get a link to the temporary file that the daemon saved on the filesystem. You can then do what ever you want with this file, move it to your documents folder, parse it… It’s up to you.
This gives us a little insight into how this process actually works. It’s not magic. The daemon only works with files. If you’re downloading a file from a server, it’s obvious. But, if you’re uploading a file, you can only upload a file from the filesystem, not from memory. Because your apps’ process and your daemons’ process can’t access each others’ memory space the only way the daemon can work with your apps’ data is if it’s persisted on the filesystem. Your app can even be killed by the system, in which case the memory your app occupied will get purged.
If your app is in focus, a background transfer will behave exactly as any other transfer. So if you need to download some large files, you can provide your users with a seamless experience. They can keep your app in focus, see the progress bar moving, check their email, watch a movie, come back to your app…
With all that being said, if a user kills your app (the famous ‘swipe up’ term), all the background transfers associated with your app will get terminated.
Pre-flight
Before jumping into the code, let’s get some of the stuff out of the way. You don’t really have to do much in your project. All you have to do is enable the background modes, so go to the capabilities tab and enable it:
And that’s it. We can code…
The Code
First off, you’ll need to create a background URLSession. Your session will look similar to this one:
// Download session private lazy var bgSession: URLSession = { let config = URLSessionConfiguration.background(withIdentifier: Constant.sessionID.rawValue) //config.isDiscretionary = true config.sessionSendsLaunchEvents = true return URLSession(configuration: config, delegate: self, delegateQueue: nil) }()
Your session identifier is a unique string, you can use your bundle ID for it, for example:
enum Constant: String { case sessionID = "agostini.tech.ATBGTransferDemo.bgSession" }
In the code above, the property ‘isDiscretionary’ is commented out. If you set this property to true, you tell the system that you basically don’t care when the file is downloaded. So the system will try to optimise the download as much as possible, e.g. downloading when you’re connected to WiFi and charging the phone.
Set the ‘sessionSendsLaunchEvents’ to true to have your app woken up by the system when the download completes.
Notice how we set ourselves as a delegate when creating the session, we’ll come to this later.
Start The Download
You have your background session, and you can use it to create your download tasks (and upload tasks as well). So let’s create a download task and start the download:
func startDownload() { if let url = URL(string: "https://speed.hetzner.de/1GB.bin") { let bgTask = bgSession.downloadTask(with: url) bgTask.earliestBeginDate = Date().addingTimeInterval(2 * 60) bgTask.countOfBytesClientExpectsToSend = 512 bgTask.countOfBytesClientExpectsToReceive = 1 * 1024 * 1024 * 1024 // 1GB bgTask.resume() } }
In this function we’re using some test files from a hosting provider ‘Hetzner’. We create a url and use the session to create a download task. We need to set some properties, like expected send and received bytes (if you’re dealing with a range, it’s best to set the upper range). This will allow the system to optimise our download. We can also set the earliest date of the transfer. This only guarantees that the download won’t start before that date. In our example, it can start 2 minutes from now, or 20 minutes from now.
And that’s it, your file will begin downloading in the background.
Setting The Delegates
When your download finishes, your AppDelegate will get notified. You will be provided with a completion handler, you’ll have to keep a reference to this completion handler. Your code will probably look identical to this one:
var bgSessionCompletionHandler: (() -> Void)? func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) { bgSessionCompletionHandler = completionHandler }
You will use this completion handler from our controller. Speaking of which. Remember when we registered as a delegate of that URLSession. Well, when the download completes our delegate methods will get called next, you only need to implement one function:
// MARK: - URLSessionDelegate extension BGTransferController: URLSessionDelegate { func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { DispatchQueue.main.async { guard let appDelegate = UIApplication.shared.delegate as? AppDelegate, let completionHandler = appDelegate.bgSessionCompletionHandler else { return } appDelegate.bgSessionCompletionHandler = nil completionHandler() } } }
Here we’re capturing the completion handler, nilling out the one in AppDelegate, and calling the original handler to notify the system that the download has been handled.
We also implemented the session download delegate, in order to get the location of the temp file and the download progress:
// MARK: - URLSessionDownloadDelegate extension BGTransferController: URLSessionDownloadDelegate { func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { print("Did finish downloading: \(location.absoluteString)") DispatchQueue.main.async { self.onCompleted?(location) } } func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { guard totalBytesExpectedToWrite != NSURLSessionTransferSizeUnknown else { return } let progress = Double(Double(totalBytesWritten)/Double(totalBytesExpectedToWrite)) print("Download progress: \(progress)") DispatchQueue.main.async { self.onProgress?(progress) } } }
The demo app will have a simple progress indicator and a button. When the download completes, it will update the label with the temp file location:
As you can see, the UI is quite complex 🙂 And that’s it. We have our files downloading in the background.
Cleanup
If you’re playing with background transfers and have a fast internet connection like me, you’ll probably be using large files. I was using a 10GB file. And in the process of build-run-debug, rinse and repeat, you’ll be greeted with a funny message from your mac:
Yes, I filled up my mac with garbage 🙂 It might happen to you as well. All you have to do is reset your simulator to reclaim the wasted space:
Conclusion
In this article we learned how to download files in the background. You could download large files or small files, it completely depends on you, and your app. If you need to update the content of your app (especially if you’re using large assets, or a lot of smaller ones), you can easily do this in the background. And when your user starts your app she will have a great experience. Not having to wait for an app to download stuff is always preferable to having to wait 🙂
I hope you had fun with this article and that you learned something new today. You can find the demo app on the GitHub repo. If you need some test files, you can find them on the hetzner site.
Remember when I said this will be a short article… I lied 🙂
Have a nice day 🙂
~D;
First of all congratulations in the article, it was really informative. However, after trying on a real device running iOS 11.3 the system terminates the app after a while and none of the app delegate or nsurlsession methods are ever called.I noticed that it only happens when downloading the 1GB file not the 100MB file from the website. I was wondering why is that happening. Again, thanks for the article.
Thanks a lot, I appreciate it 🙂
It doesn’t matter if the system terminates the app, the download will be started by a separate process. When the system terminates the app, the progress notification delegate callbacks won’t get called, only the app delegate method. Leave it running in the background for an hour or so (depending on your internet connection). If you don’t see the file dowloaded, try setting the ‘isDiscretionary’ property to true, and connecting your phone to a charger. Also, make sure you’re on WiFi. Let me know if this helped you.
Have a nice day Josue and thanks for reading the blog 🙂
~D;
On iOS 12 URLSession breaks when rest api with HTTP request hits and suddenly app enters in the background, is there any way to resolve this issue?
You need to configure the session to run in the background. It’s the first code snippet in the article.
Thank you for your dedication