I'm following this Apple guide on Downloading Files in the Background and wondering how to properly test that all the steps are working properly. (via unit testing also, if you have resources please share but not the primary focus of this question).
According to the documentation, there are multiple steps for this to work (not listing the standard URLSessionDownloadTask delegates, only background requirements):
URLSessionConfiguration.background(withIdentifier: "MyBackgroundDownload")
handleEventsForBackgroundURLSession
urlSessionDidFinishEvents
didCompleteWithError
However, when testing the app getting terminated by the system I am still getting a successful call to my urlSession:downloadTask:didFinishDownloadingTo
implementation even without implementing steps 2 and 3.
If it's still working without these steps, how do I know they're doing anything at all? Do I even need those steps? This is not giving me confidence.
To get the app killed by the system, I'm using the Fast App Termination developer tool on device, so I am NOT debugging directly in Xcode (since I was concerned that could be keeping the app alive in some way).
You asked:
How can I properly test a background
URLSessionDownloadTask
?
As you noted, you cannot test what happens to a suspended/terminated app from the Xcode debugger (because the debugger will keep the app running). So, just install the app on the device and then launch it directly on the device, not from Xcode.
So that begs the question as to how one confirms what is going on during background execution. Specifically, the question is how one monitors what is going without the benefit of either the Xcode console or the app running in the foreground.
There are a variety of techniques that I use to monitor what is going on during background execution:
I will have the app present a “user notification” when the downloads were done in the background (while testing it, at the very least). This way I get a visual confirmation even though the app is running in the background.
So, when the app is in foreground, I request user notifications permissions:
import UserNotifications
And
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UNUserNotificationCenter.current().requestAuthorization(options: .alert) { granted, error in
print(granted, String(describing: error))
}
}
And then, when the download is done, I post a user notification:
extension BackgroundSession: URLSessionDelegate {
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
UNUserNotificationCenter.current().add(title: "Downloads finished", body: "Hurray")
// then continue doing other stuff; e.g., in this method, I
// would often call the saved completion handler, if any
//
// DispatchQueue.main.async {
// self.savedCompletionHandler?()
// self.savedCompletionHandler = nil
// }
}
}
extension UNUserNotificationCenter {
func add(title: String, body: String) {
let content = UNMutableNotificationContent()
content.title = title
content.body = body
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let request = UNNotificationRequest(identifier: Bundle.main.bundleIdentifier!, content: content, trigger: trigger)
add(request)
}
}
Another approach is to watch iOS logging messages from the macOS Console
. To do this, create a Logger
:
import os.log
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "BackgroundSession")
And then I will have key events log messages.
extension BackgroundSession: URLSessionDelegate {
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
logger.debug(#function)
…
}
}
I can then watch these iOS events on my macOS Console
app. See Logger
discussion in Swift: print() vs println() vs NSLog().
If these downloads take so long that staring at the macOS Console
is impractical, I sometimes log these events to some persistence store (e.g., whether SwiftData/CoreData or just write them to a text or JSON file). Then I can then download this from the Xcode “Devices” window at a later point in time. (For background URLSession
, the aforementioned Console
approach is sufficient, but this technique can be very useful for background events whose timing is less prompt/predictable, such as BGProcessingTask
, BGAppRefreshTask
, etc.)
You mention the “Fast App Termination” developer setting on the device. That is not necessary to test this “relaunch in background” functionality. Yes, it is useful to test the scenario of “what if the app was terminated in the course of its normal lifecycle” workflow (to make sure the background URLSession
is properly re-instantiated). But it is not necessary simply to verify the “relaunch app in background” functionality.
You said that it appeared to work despite not implementing handleEventsForBackgroundURLSession
and urlSessionDidFinishEvents
.
You do not need to implement these if you do not want your app awakened when the background requests finished (i.e., you explicitly set sessionSendsLaunchEvents
to false
).
But if you do want your app to be launched in the background when the network request finish, Apple is quite specific that we should implement these two methods. The idea behind this pattern is that if the app is launched in the background, we want to tell the OS when we are done and the app can be suspended again. The documentation is not always explicit regarding the repercussions of failing to implement all of the necessary delegate methods, but, commonly, apps that fail to fulfill their background execution obligations may become ineligible for background execution in the future. Apple has a history of becoming evermore strict about apps abusing background execution, so I would advise implementing API as advised in the documentation.
You shared a quote from the documentation that says:
Once your resumed app calls the completion handler, the download task finishes its work and calls the delegate’s
urlSession(_:downloadTask:didFinishDownloadingTo:)
method.
This would seem to imply that the didFinishDownloadingTo
is called after you call the closure. That is not the case. As outlined below, urlSessionDidFinishEvents
(which is where we would often call the saved closure) is only called after all the task delegate methods (including didFinishDownloadingTo
) have been called, not before.
Furthermore, the correct sequence of events is outlined earlier in the very same document:
When all events have been delivered, the system calls the
urlSessionDidFinishEvents(forBackgroundURLSession:)
method ofURLSessionDelegate
. At this point, fetch the [saved completion handler] stored by the app delegate.
I just did an empirical test to verify the sequence of events. That confirms that if your app is be awakened when the uploads/downloads are done, the sequence of events is as follows:
While the app is running, create background URLSession
and start downloads. I just leave the app while they are underway to suspend the app. A few observations:
sessionSendsLaunchEvents
configuration setting to false
.The app delegate’s handleEventsForBackgroundURLSession
is called if the app was not running when the downloads finished and the app was therefore relaunched in the background to let you process the tasks. If and when this method is called, we
Save the closure for future reference and will use it at step 4, below.
If the app had previously been terminated, we obviously recreate the the background URLSession
session using the same identifier. If the app had merely been suspended (which is more likely), then obviously we still have the existing background URLSession
running.
Obviously, this is only called if the app is awakened and launched in the background.
Needless to say, if the app happened to be running in the foreground when the downloads finished, this event is not called. But you must still implement this method to support the scenario of the app having been suspended/terminated. (If are not concerned about the app having been suspended/terminated while downloads were in progress, you probably would not be using background URLSession
, at all.)
All the task delegate methods are called (e.g., didFinishDownloadingTo
, didCompleteWithError
, etc.) for the completed background URLSession
tasks.
Only when all of those events are done, will the urlSessionDidFinishEvents
get called. This is often where we would generally call the saved completion handler closure that we received at step 2. (Some may note that if you’ve launched additional asynchronous events as a result of step 3, then you would defer the calling of the closure until all those are done, too, but that is a bit of an edge-case.)
Please note: I have swapped 3 & 4 from your original list, as the task delegate methods are called before urlSessionDidFinishEvents
is.