Implementing background upload queue with Swift
I’m working on an app that lets users select a bunch of photos, upload and share them with other users. Sounds simple enough: user selects photos, app uploads photos, user shares the album. More realistically though: user selects photos, app starts to upload photos, user figures out this might take some time, user leaves the app to do something else, iOS suspends the app (or even worse terminates it), user comes back to find out the job is not done :( We don’t want that, do we?
How I did it on android
Some months back I implemented this feature in the android version of the same app. It was fairly straightforward, especially since I did similar stuff many times over the years (I’ve been creating android apps since 2010, feel old yet?). Basically I created a service to do the work, made it a foreground service so the OS wouldn’t kill it even if it’s in a background and voila. After creating Uber clones, music players and voice operated apps background service was a no brainer.
Your android app must put up an ongoing notification if it’s running a foreground service. It can and should use it to give user an overview of the progress and a way to stop the work.
How can this be achieved on iOS
In regard to the background work iOS and android started from two very different points but converged over the years. In the early days of android an app could declare itself doing “important” work in the background and OS would not kill it to reclaim the memory. It shouldn’t come as a surprise that developers abused this, so in recent years Google has been tightening this up in order to increase the device battery life. iOS on the other hand was very strict about this and gradually opened up APIs for background work.
One of those APIs is
expirationHandler: function. Basically you ask the OS to let you finish some task even if the app is moved to background. How much time do you get to do it? Not sure, I haven't found it in the docs but people say it can be up to 10 minutes. That should be plenty enough for a reasonable number of uploads but I don't like imposing artificial constraints like this to the users. Also this work is done opportunistically, meaning it can be in chunks at times OS is doing something else.
Another API let you declare the app doing important work in the background and the OS will respect this and not suspend it. Hey, this sounds exactly like the android foreground service! The gotcha is only specific types of apps are permitted to do this: apps that play audible content, apps that record audio, location apps, VoIP apps, etc. Unfortunately my app doesn’t qualify for this :/
And the winner API is…
… something Apple calls “downloading content in the background”. Can it be used to upload content in the background? Yes, it can. Basically, you ask OS to execute any number of network tasks for you, OS runs with it, letting you know when it’s done or something is wrong. It does so on another process so it doesn’t really matter if your app gets suspended/terminated. You can ask OS to wake the app once it’s done.
The way you go about this is by using
URLSession object created with a background
URLSessionConfiguration. You give that session a unique name and you'll use this name later to reconnect to the session to get the results in case app was suspended/terminated. This is pretty straightforward and can be found explained on many blog posts online so I'll skip talking about it here. Instead I'll talk about some important details that can be pain in the ass unless you get it just right.
Background URLSession tips and tricks
- Do not use Alamofire
- Alamofire does not have a very strong support for background downloads. Don’t take my word for it, I quoted the guy with by far the most commits to the library.
Alamofire is a great library but developers concentrated on more general features so far, and rightfully so.
2. Use Alamofire
Wait, what? We can still use some of the hard work this guys were so nice to do for us all. In this particular case we want to upload files to server, right? That means making a
multipart/form data request. Since we're handling the work over to the OS to do when we might not be around we need to provide it with files containing the multipart/form data. This file needs to be properly structured with boundaries, params and image data properly presented etc. Implementing creation of these files properly and efficiently would take some time and effort. It would be nice if we could use Alamofire for this part of the job, and we can.
3. Only create one background URLSession with the same name per app lifecycle
Originally I had the photo album view controller create this session as its property every time it’s instantiated. I thought that by using the same name for background session I’d reconnect to the correct session and continue to get updates for the time this VC is around. I was vaguely aware about a warning somewhere in the docs that creating more than one session with the same name is not a good idea, but this is how it made sense to me. Well, it didn’t work. Not only that it didn’t work but it caused a memory leak.
4. Avoid memory leak via strong reference cycle between URLSession and it’s delegate
Photo album VC has a
URLSession property and it sets itself as a delegate to it. You see where this is going, right? User opens photo album screen and an instance of photo album view controller is created (instance PAVC1). User starts some uploads and leave the photo album screen by going back. Later it comes back to it, creating another instance of the view controller (instance PAVC2). In my mind when this instance (re)created session with the correct name everything would be peachy. In reality, PAVC2 wasn't getting updates about the jobs PAVC1 started. Charles Proxy was telling me that the upload were indeed still happening and after some debugging I discovered that PAVC1 was still around and it was actually still receiving updates from the session, the session held on to it via a strong reference to its delegate. So I took away the session instances from the VCs and put them in a singleton "upload manager" type of object.
I usually keep request timeouts to something like 30 seconds for “regular” API request, such as fetching some data or making a simple
POST. I wanted to increase this value a bit in this case since the app is uploading several megabytes to the server. I couldn't really do that here though. There are two things you can configure on the
timeoutIntervalForResource. I personally found the docs about these two lacking to say the least. Here's what I discovered:
timeoutIntervalForRequest is not designed to be used for background sessions so you can just leave it alone.
timeoutIntervalForResource is designed to be used for background sessions and its default value is 7 days??? Basically the OS will retry the failed tasks as many time as it takes for them to succeed or until this timeout value is up. I couldn't figure out a sensible value for this property. I'd much rather ask OS to retry 3 times and let me know if it fails so I can let the user know something is wrong but alas...
6. When it’s all said and done
When the uploads finish OS will wake your app by calling the
application(handleEventsForBackgroundURLSession: completionHandler:) on your app delegate. When you create the
URLSession you'll get the results of all the tasks. My delegate was called on the background queue and I needed to go on the main thread to pop up a local notification letting user know the tasks are done (OS wakes your app but does not show UI of it). This was working a bit inconsistently with notification sometimes popping out but sometimes not. It looks like OS calls the session delegate methods and terminates the app again sometimes so soon that a closure posted on main thread has no time to execute. Solution for this was an API I actually talked about earlier -
expirationHandler:. I needed to ask OS for just a couple milliseconds more so I could go to main thread and post a notification.
7. License to kill
If you ever implement something like this a useful trick to know is how to make iOS terminate your app so you can test that the downloads/uploads are still done and your app is woken up. Press and hold power button on your iPhone. Power off screen will come up. DO NOT slide to power off, DO NOT tap cancel button. Press and hold home button. After 5 seconds or so you’ll be back on the home screen and your app will be terminated.
Hope you find some of this helpful. Over and out.