URLSessionDownloadDelegate didFinishDownloadingTo not called in background when download is started from action extension

I start a download in an action extension (`ActionRequestHandler`) like this:


    private lazy var urlSession: URLSession = {
        let config = URLSessionConfiguration.background(withIdentifier: "de.stefantrauth.Downloader")
        config.sharedContainerIdentifier = "group.de.stefantrauth.Downloader"
        return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue.main)
    }()
   
    private func initiateDownloadOfFileFrom(url: URL) {
        urlSession.downloadTask(with: url).resume()
        completeRequest() // this tells the system the action extension is done with its work
    }


Then the download is processed by iOS in the background.


I now want to handle the finished download in my main application `AppDelegate`, because that is what iOS calls when the download has finished.


    func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
        print("handleEventsForBackgroundURLSession")
        urlSessionBackgroundCompletionHandler = completionHandler
    }


This method gets called in background after some time as expected.


My `AppDelegate` also implements `URLSessionDelegate` and `URLSessionDownloadDelegate` to process updates for the download.


Especially interesting are


    func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
        DispatchQueue.main.async {
            print("urlSessionDidFinishEvents")
            self.urlSessionBackgroundCompletionHandler?()
            self.urlSessionBackgroundCompletionHandler = nil
        }
    }


and


    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        print("download finished to \(location.absoluteString)")
        do {
            let documentsURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
            let savedURL = documentsURL.appendingPathComponent(location.lastPathComponent)
            try FileManager.default.moveItem(at: location, to: savedURL)
            print("moved file to: \(savedURL.absoluteString)")
        } catch {
            print ("file error: \(error)")
        }
    }


Both `urlSessionDidFinishEvents` and `didFinishDownloadingTo` are not being called after `handleEventsForBackgroundURLSession` got called in background. Only after relaunching the app into foreground the delegate methods get called.


Why are they not getting called and what can I do to fix that?


I tried creating the `URLSession` in `handleEventsForBackgroundURLSession`like this:


    private func initUrlSessionWith(identifier: String) {
        let config = URLSessionConfiguration.background(withIdentifier: identifier)
        config.sharedContainerIdentifier = "group.de.stefantrauth.Downloader"
        urlSession = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue.main)
    }


    func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
        print("handleEventsForBackgroundURLSession")
        initUrlSessionWith(identifier: identifier)
        urlSessionBackgroundCompletionHandler = completionHandler
    }


However this did not fix the problem.


Before you ask: Yes I am testing this on a real device because the simulator has problems with background task handling.

Accepted Reply

Dealing with background sessions in a short-lived app extension is quite tricky. You can find some general guidelines in my Networking in a Short-Lived Extension.

It’s hard to say what’s going on here but there’s a couple of things I suggest:

  • Implement

    -URLSession:didBecomeInvalidWithError:
    in both the app and the app extension so you can learn about invalidation.
  • Debug via logging, rather than with the debugging, for the reason explained in the above-mentioned post.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Replies

Dealing with background sessions in a short-lived app extension is quite tricky. You can find some general guidelines in my Networking in a Short-Lived Extension.

It’s hard to say what’s going on here but there’s a couple of things I suggest:

  • Implement

    -URLSession:didBecomeInvalidWithError:
    in both the app and the app extension so you can learn about invalidation.
  • Debug via logging, rather than with the debugging, for the reason explained in the above-mentioned post.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thank you for putting me into the right direction. I implemented logging and watched my code working in the Console app instead of Xcode debug mode. Now it works as expected.

Hey, I've got the same problem. I'm scheduling background download task but with earliestBeginDate(in the app, not extension). It's called correctly on my scheduled date, but didFinishDownloadingTo: is called after I'm entering foreground. I'm not debugging with Xcode, here's the prints of method trace:

20.06.18 20:35:12: application(_:handleEventsForBackgroundURLSession:completionHandler:)

20.06.18 20:35:12: iOS urlSession(_:task:willBeginDelayedRequest:completionHandler:) with task identifier: 17

20.06.18 20:35:12: iOS urlSessionDidFinishEvents(forBackgroundURLSession:), deferred completion is nil: false

20.06.18 20:37:51: iOS urlSession(_:downloadTask:didFinishDownloadingTo:) - did finish downloading URLSessionDownloadTask with earliest begin date:20:35


As you can see at the end, 20:37:51 is the date when I opened the app. This should've been called at 20:35 and >12 seconds. I've tried everything, I have the proper code(same as above). Don't mind that logs above shows wrong order of method invokation, they were called at the same second, and logs misplaced it a little bit.