Background Transfers Using URLSession

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.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;

More resources

5 thoughts on “Background Transfers Using URLSession

  1. Josue Brizuela

    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.

    Reply
    1. Dejan Agostini Post author

      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;

      Reply
  2. Nitesh Kumar Pal

    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?

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.