When implementing background uploads on iOS, I found this incredibly helpful article by Antoine van der Lee. Any feature that relies on uncontrollable iOS system behavior is a tough one to implement, and his article was my map through the void. Since it was published, some things have changed slightly in iOS, so I’m writing this post to add some corrections about implementing background uploads in iOS.
Invalidating URLSession
One tip it gives at the end is to invalidate the URLSession on app launch so that you can start from a clean slate:
Starting from scratch
First of all, it’s good to start with a clean slate. We can do this by executing the following piece of code on launch to invalidate our URLSession and its tasks:
#if DEBUG if debuggingBackgroundTasks { /// Cancel any running tasks and invalidate the session. URLSession.shared.invalidateAndCancel() } #endif
I’m not sure this is a good idea, or at the very least requires relaunching your app after invalidating all tasks. The issue is that this is invalidating URLSession.shared
, not the background URLSession
we created to handle background uploads.
The Apple Documentation for URLSession.invalidateAndCancel()
states:
Calling this method on the session returned by the
shared
method has no effect.
If we invalidate our background URLSession
instead, we run into a new problem: Only a single URLSession for a given configuration identifier will ever be created. This means that if we invalidate and then re-create our background URLSession
, we actually get back the same URLSession
that we just invalidated!
backgroundSession.invalidateAndCancel() let config = URLSessionConfiguration.background(withIdentifier: Self.backgroundSessionIdentifier) config.sessionSendsLaunchEvents = true config.isDiscretionary = false backgroundSession = URLSession(configuration: config) // <--- This returns the same `backgroundSession` that we just invalidated!
The Apple Documentation for URLSession.invalidateAndCancel()
states:
Once invalidated, references to the delegate and callback objects are broken. After invalidation, session objects cannot be reused.
This does mention we shouldn’t use the same session, but doesn’t mention that the exact same URLSession
will be returned from the init(configuration:)
, which I found surprising.
Instead of invalidating the session to clear out old upload tasks, simply iterate through the tasks and cancel them individually:
let tasks = await yourBackgroundURLSession.allTasks for task in tasks { task.cancel() }
Forcing App Background
One other note, in the article Antoine recommends calling exit(0)
from applicationDidEnterBackground(
:_:
)
Make your app suspend sooner
Waiting for your app to suspend is a waste of time. By using
exit(0)
we can force our app to suspend:func applicationDidEnterBackground(_ application: UIApplication) { #if DEBUG if debuggingBackgroundTasks { /// Exit to make our app suspend directly. exit(0) } #endif }
However, the Apple documentation for that method notes that applicationDidEnterBackground(_:)
will not be called for applications using scenes:
If you’re using scenes (see Scenes), UIKit will not call this method. Use
scene
instead to perform any final tasks. UIKit posts aDid Enter Background(_:) did
regardless of whether your app uses scenes.Enter Background Notification
Oops! I’d completely forgotten about this, and I was left wondering why my app delegate method wasn’t call. The fix is easy, I switched to use
in your scene delegate.
sceneDidEnterBackground(_ scene: UIScene)
Conclusion
If you want to cancel all background tasks and start from scratch, call cancel()
on your background URLSession.allTasks
. Otherwise, if you invalidate your background session, the background session will not be able to be re-used in the same app session that it was invalidated in. To start new background tasks, you would need to relaunch the app and not invalidateAndCancel()
a second time.