I have an app that needs to download a file which may be rather large (perhaps as large as 20 MB). I've been reading up on URLSession downloadTasks and how they work when the app goes to the background or is terminated by iOS. I'd like for the download to continue and from what I've read, that's possible. I found a blog post here that discusses this topic in some detail.
Based on what I've read, I first created a download manager class that looks like this:
class DownloadManager : NSObject, URLSessionDownloadDelegate, URLSessionTaskDelegate {
static var shared = DownloadManager()
var backgroundSessionCompletionHandler: (() -> Void)?
var session : URLSession {
get {
let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
}
}
private override init() {
}
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
DispatchQueue.main.async {
if let completionHandler = self.backgroundSessionCompletionHandler {
self.backgroundSessionCompletionHandler = nil
completionHandler()
}
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
if let sessionId = session.configuration.identifier {
log.info("Download task finished for session ID: \(sessionId), task ID: \(downloadTask.taskIdentifier); file was downloaded to \(location)")
do {
// just for testing purposes
try FileManager.default.removeItem(at: location)
print("Deleted downloaded file from \(location)")
} catch {
print(error)
}
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
if totalBytesExpectedToWrite > 0 {
let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
let progressPercentage = progress * 100
print("Download with task identifier: \(downloadTask.taskIdentifier) is \(progressPercentage)% complete...")
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
print("Task failed with error: \(error)")
} else {
print("Task completed successfully.")
}
}
}
I also add this method in my AppDelegate:
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
DownloadManager.shared.backgroundSessionCompletionHandler = completionHandler
// if the app gets terminated, I need to reconstruct the URLSessionConfiguration and the URLSession in order to "re-connect" to the previous URLSession instance and process the completed download tasks
// for now, I'm just putting the app in the background (not terminating it) so I've commented out the lines below
//let config = URLSessionConfiguration.background(withIdentifier: identifier)
//let session = URLSession(configuration: config, delegate: DownloadManager.shared, delegateQueue: OperationQueue.main)
// since my app hasn't been terminated, my existing URLSession should still be around and doesn't need to be re-created
let session = DownloadManager.shared.session
session.getTasksWithCompletionHandler { (dataTasks, uploadTasks, downloadTasks) -> Void in
// downloadTasks = [URLSessionDownloadTask]
print("There are \(downloadTasks.count) download tasks associated with this session.")
for downloadTask in downloadTasks {
print("downloadTask.taskIdentifier = \(downloadTask.taskIdentifier)")
}
}
}
Finally, I start my test download like this:
let session = DownloadManager.shared.session
// this is a 100MB PDF file that I'm using for testing
let testUrl = URL(string: "https://scholar.princeton.edu/sites/default/files/oversize_pdf_test_0.pdf")!
let task = session.downloadTask(with: testUrl)
// I think I'll ultimately need to persist the session ID, task ID and a file path for use in the delegate methods once the download has completed
task.resume()
When I run this code and start my download, I see the delegate methods being called but I also see a message that says:
A background URLSession with identifier com.example.testapp.background already exists!
I think this is happening because of the following call in application:handleEventsForBackgroundURLSession:completionHandler:
let session = DownloadManager.shared.session
The getter for the session property in my DownloadManager class (which I took directly from the blog post cited previously) is always trying to create a new URLSession using the background configuration. As I understand it, if my app had been terminated, then this would be the appropriate behavior to "reconnect" to the original URLSession. But since may app is not being terminated but rather just going to the background, when the call to application:handleEventsForBackgroundURLSession:completionHandler: happens, I should be referencing the existing instance of URLSession. At least I think that's what the problem is. Can anyone clarify this behavior for me? Thanks!