Forcing your users to keep an app open and wait for files to download is like having a tea kettle that only boils water while you stare at it. In this talk, Gwendolyn Weston teaches how to use the iOS Background Transfer Service API to download files in the background, covering common pitfalls and best practices. Learn to easily implement it in your own app to save users time and frustration. 🍵
Introduction (0:00)
Hi, everyone! My name is Gwendolyn Weston. I am a developer at a company called PlanGrid, and today I’m here to talk to you about Background Transfer Services, focusing on background downloading and how to implement it in your own apps. First, I’m going to go through how to download in the foreground for anyone who isn’t familiar with NSURLSession
s. Second, I’m going to go through how to make that request background compatible. Lastly, I’ll demonstrate how to navigate around common pitfalls.
Background Transfer Services — Tea Kettle Analogy (0:14)
Background Transfer Services is an API that was introduced in iOS 7 that allows apps to continue their networking requests (e.g. downloading or uploading) in the background after the app has been suspended or terminated. For example, it is what allows Dropbox to sync files to a device in the background until the sync is finished.
To explain how useful this is, imagine I give you a tea kettle. You pour in the water, push the button to begin boiling, and as you’re about to walk away I stop you and say, “You have to stand near this tea kettle in order for it to boil water.” Although that’s a strange constraint, you oblige. You stand near the tea kettle, take out your phone and start going through Facebook. As you’re about to see all the ways that people are hanging out without you, I stop you again and say, “You have to also watch this tea kettle in order for it to boil water.” This tea kettle sounds like a ridiculous thing to use, and that’s what it’s like to download in the foreground.
Example Usage (2:02)
The company I work for, PlanGrid, could be described as “GitHub for construction blueprints,” providing version control and project management for construction projects.
Here’s a main use case: A contractor is on a site and has to mark up a blueprint. That markup/annotation will sync to other contractors’ devices saving effort, time and money because there’s no longer a need to print out a new set of blueprints every time a change is made.
This means we have users uploading large, high-fidelity blueprints to our repository. Every time someone new joins this project, it literally takes them hours to sync all of the blueprints to their device. We had to tell our users to change their device settings to prevent screen locking and stay in the app to guarantee a complete download. Can you see how that might be a frustrating? It’s also a huge security concern to have an unlocked device containing important blueprints. It would have been a lot easier if we could have told our users to, “Start downloading it and we’ll take care of the rest.”
Downloading without Background Transfer Services is like watching water boil. Whereas downloading with Background Transfer Services is like having a tea kettle, where not only do you not have to stand near and watch it, but you can tell it what kind of tea you want and how long it should steep.
Downloading in the Foreground (4:41)
Here we have our sample code.
let urlstring = "https://remoteteakettle.com/boiledwater.pdf"
let filepath = "Documents/local_teakettle"
if let url = NSURL(string: urlstring) {
let task = NSURLSession.sharedSession().downloadTaskWithURL(url, completionHandler: {
(location:NSURL?, response:NSURLResponse?, error:NSError?) in
if let loc = location, path = loc.path {
try! NSFileManager.defaultManager().moveItemAtPath(path, toPath:filepath)
}
})
task.resume()
}
Let’s go through it line by line. The first line is where our remote file is being served from. We are getting it from remoteteakettle.com and we’re trying to download a file called boiledwater.pdf
. On the second line we have our file path where we intend to store that PDF when we download it. Once we get to the third line you start to see a lot of players from this NSURLSession
framework. Let’s go a little more into detail about that.
We have the NSURLSession
, which vends and manages the request. Then we have this sharedSession
which is a singleton session with some default settings. For example, it’s going to have default cache policies and timeout intervals. We can also create our own custom sessions with custom behaviors, but, the single singleton will work for now. Finally, we have the NSURLSession
download task which is basically our request.
NSURLSessionTask (6:00)
Now, I’m going to pause and eventually get around to going through the rest of this code, but I want to talk about this NSURLSession
download task more. The download task is actually a subclass of this base class, called the NSURLSessionTask
. There are three types:
NSURLSessionDownloadTask
NSURLSessionUploadTask
NSURLSessionDataTask
(for short-lived requests like authorization tokens)
Rather than calling some kind of convenience initializer on the session task in order to get the session tasks back, we get our session tasks through the session object. Namely, by feeding our session URLs.
What does that mean? I like to think of the NSURLSession
as a cookie monster, and our URL as a cookie. NSURLSession
really likes cookies, so every time we give him this cookie, he is so overwhelmed with happiness that he gives us back his love. And this love happens to be this NSURLSessionTask
that we want. We can do this several times over, getting back several session tasks from the same NSURLSession
—creating this one-to-many relationship between the session and the session task. One session might be responsible for several session tasks, but each session task only corresponds back to one session. In code, we have all these methods on the session object to create tasks, such as downloadTaskWithURL(_:completionHandler:)
.
Let’s go back to that and talk about the completion handler. We have three parameters for this completion handler.
- Location (a temporary file path where our download is going to initially be downloaded to)
- Response (where we can get the status code of our requests)
- Error (for anything that goes wrong)
You might have noticed that I do nothing with the error, nor do I catch any of the exceptions. I’m glossing over that and I’m going to pretend that the sample code lives in a just and harmonious universe where there are no errors or exceptions. Thus, all I do is move the file from its temporary file path where it was downloaded to the file path that we just initialized up above.
This works okay, but what if the user switches to another app? We don’t get our boiled water which would be a downright shame.
Downloading in the Background — NSURLSessionConfiguration (9:23)
To do this in the background, we signal to the system that we want these tasks to be background compatible using NSURLSessionConfiguration
. Previously, I said that we could initialize custom sessions so that we could set properties on it. For example, the cache policies and the timeout intervals. And I kind of lied. It wasn’t that we set these things on the session. What we do is we set these properties on a configuration object and then instantiate a session with that configuration. That’s the first step of our recipe.
We create a custom session with something called a background configuration. It’s just another setting on this configuration object. Here’s some code to clarify.
let config = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier("i am the batman")
let session = NSURLSession(configuration: config, delegate: self, delegateQueue: nil)
Line by line, first code is we create our session configuration with this method called backgroundSessionConfigurationWithIdentifier(_:)
. This identifier can be anything. It can be your favorite color, the first chapter of Moby Dick, or as it is here, “i am the batman.” In the second line, we initialize our NSURLSession
with this configuration. We set ourselves as a delegate, so it gets any delegate methods that the session receives. Lastly, we have this delegate queue.
The delegate queue can be any queue that you want all the delegate methods to get called on. But, if you pass a nil
it’ll provide a default one for you. Before we add this code back into our initial sample code, I want to emphasize that these identifiers have to be unique. In order to explain why that is, let’s go into the life cycle of a background request.
The Life of a Background Request (11:02)
We have our Dropbox app and we start this request in the foreground. We start syncing files to our device, then the app dies. Eventually all the requests in a session are going to finish because these requests are background compatible. This is when the system is going to ping back your app back and say, “Hey, all the requests in this session are finished.” It’s going to go through all the same set up that we know. It’s going to call all those UIAppDelegate methods.
The one difference is after it calls that finish loading with options, it’s going to hit a new method called application:handleEventsForBackgroundURLSession
. The only thing that’s going to happen in this method is the system’s going to give you back this identifier. For example, the one we initialized earlier, “I am the Batman.” It’s going to be like, “Hey! The session called ‘I am the Batman’ is done! What are you going do about it?” And what you are going to do about it is if you want to get the errors or the success codes of all your requests, you have to actually recreate the session in this method. As for what that looks like in code, it actually means just creating another background configuration with that identifier, and then creating another session with that configuration.
func application(application: UIApplication, handleEventsForBackgroundURLSession identifier: String,
completionHandler:() -> Void) {
let config = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(identifier)
let session = NSURLSession(configuration: config, delegate: self, delegateQueue: NSOperationQueue.mainQueue())
session.getTasksWithCompletionHandler { (dataTasks, uploadTasks, downloadTasks) -> Void in
// yay! you have your tasks!
}
}
It’s this daisy chain of things you have to do. And then you can get your session tasks back, because the system will be like, “Hey, the session is alive again, and now here are all the tasks that are associated with that session.”
However, because the system only knows which tasks to recreate by the session identifier alone, what happens if you have two sessions with the same identifier? How’s the system going to know which tasks you want recreated? In fact, there is this ominous message in the documentation that says that the behavior of multiple sessions sharing the same identifier is undefined. Let’s not do that. Let’s add the code that we just had for creating our background configuration back in. We have the same code as before except those two new lines.
let urlstring = "https://remoteteakettle.com/boiledwater.pdf"
let filepath = "Documents/local_teakettle"
if let url = NSURL(string: urlstring) {
let config = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier((NSUUID().UUIDString))
let session = NSURLSession(configuration: config, delegate: self, delegateQueue: nil)
let task = NSURLSession.sharedSession().downloadTaskWithURL(url, completionHandler: {
(location:NSURL?, response:NSURLResponse?, error:NSError?) in
if let loc = location, path = loc.path {
try! NSFileManager.defaultManager().moveItemAtPath(path, toPath:filepath)
}
})
task.resume()
}
Pitfall #1: No Completion Handling (13:03)
Unfortunately, this is not to make our request background compatible. When you try to create a task with completion handler for a background task, the console’s going to yell at you that it’s not supported. Instead, we need to use a delegate method. After the app starts back up and you recreate your session in the handleEventsForBackgroundURLSession()
method, the app is going to start calling all of those delegate methods on the session. In particular, the one that we’re interested in is this URLSession(_:downloadTask:didFinishDownloadingToURL:)
. It gives us back:
- the session object
- the task that completed
- the temporary location that the file was downloaded to
Here we are at the second step of our recipe. Let’s move the code from our completion handlers to our delegate method. As for what that looks like in code, here we are.
let urlstring = "https://remoteteakettle.com/boiledwater.pdf"
let filepath = "Documents/local_teakettle"
if let url = NSURL(string: urlstring) {
let config = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier((NSUUID().UUIDString))
let session = NSURLSession(configuration: config, delegate: self, delegateQueue: nil)
let task = session.downloadTaskWithURL(url)
task.resume()
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location:
NSURL) {
if let path = location.path {
try! NSFileManager.defaultManager().moveItemAtPath(path, toPath:filepath)
}
}
Instead of initializing the download task with completion handler, we instead move it into this delegate method. Unfortunately, this doesn’t work either as filepath
is out of the delegate method’s scope…
Pitfall #2: No Auxiliary Request Info (14:49)
Because we have to get the information about the request such as our final file path in a different method, we have to persist it somehow. You could use the taskDescription
for this but the docs claim that “it’s intended to contain human-readable strings that you can then display to the user as part of your app’s interface.” Since you’ll likely want to store more information about a request than just the file path, such as a model UUID or the file name, this is not a good solution. Subclassing NSURLSessionDownloadTask
also doesn’t work, as our cookie monster NSURLSession
only gives back the pre-defined classes, not our own subclasses.
Solution: Persist Request Info (16:43)
If the system doesn’t provide the functionality for us, we’re going to have to do it ourselves. And we’re going to have to persist the data on our own.
public class HalfBoiledWater: NSObject {
public let sessionId: String
public let taskId: Int
public let filepath: String
init(sessionId:String, taskId:Int, filepath:String) {
self.sessionId = sessionId
self.taskId = taskId
self.filepath = filepath
}
func save() {
// save to a persistent data store!
}
}
public func fetchModel(sessionId:String, taskId:Int) -> HalfBoiledWater {
// fetch from a persistent data store!
}
Now, there’s probably many ways that you can persist it. We each have our own favorite flavor of data persistence. It might be FMDB and SQLite, or even Core Data. I’m going to let you decide that and stub in these two methods instead: save()
and fetchModel(_:taskId:)
. Notice that we have the file path on this serializable model that we’re going to put and fetch from the database. We also use our sessionId
and our taskId
as our primary key in order to save this model. Below, we add it in to our code sample:
let urlstring = "https://remoteteakettle.com/boiledwater.pdf"
let filepath = "Documents/local_teakettle"
if let url = NSURL(string: urlstring) {
let config = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier((NSUUID().UUIDString))
let session = NSURLSession(configuration: config, delegate: self, delegateQueue: nil)
let task = session.downloadTaskWithURL(url)
if let sessionId = session.configuration.identifier {
let persistedModel = HalfBoiledWater(sessionId:sessionId, taskId:task.taskIdentifier, filepath:filepath)
persistedModel.save()
}
task.resume()
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location:
NSURL) {
if let sessionId = session.configuration.identifier {
let persistedModel = fetchModel(sessionId, taskId:downloadTask.taskIdentifier)
if let path = location.path {
try! NSFileManager.defaultManager().moveItemAtPath(path, toPath:persistedModel.filepath)
}
}
Right after we create the download task, we also create this model of all the extra information that we need and we persist it. Then, in our delegate method, when we need to get that file path back, so we just fetch the model as keyed by our session ID and our task identifier.
Conclusion (18:35)
This is enough for the most bare-bones implementation of background downloading. To recap, we first created this background configuration to signal to the system that we want all tasks in this session to be background compatible. Second, we moved the code from our completion handler to our delegate method. Third, we persisted the request in a way that any extra information we needed in our delegate method was available to us at the time.
This is only the beginning. There’s a lot more that you can do with this API. You can show progress, cancel, or resume a download right where it left off in case of an error. This also comes with more pitfalls for you to discover.
I hope that this was enough to help you add background downloading into your apps so your users aren’t standing there watching water boil. Thank you.
Receive news and updates from Realm straight to your inbox