Testing Background Uploads in iOS

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 sceneDidEnterBackground(_:) instead to perform any final tasks. UIKit posts a didEnterBackgroundNotification regardless of whether your app uses scenes.

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
sceneDidEnterBackground(_ scene: UIScene)
in your scene delegate.

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.

Leave a Reply

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