iosswiftnsurlsessionnsurlsessiondownloadtask

URLSession delegates not working after app resume


I've recently been integrating Background Transfer Service into an application so that the user is able to download files in the background.

Everything works as expected. But my delegate methods stops getting called after sending the application into the background and then re-opening the application.

File is actually being downloaded in the background but I am not receiving any call to my delegate methods. So cant show any progress to the users. So it feels like download is got stuck.

I had to remove our app from the app store as it is hurting our app. I need to resubmit the app as soon as possible. But with this problem, it's not possible.

My download manager code:

import Foundation
import Zip
import UserNotifications

////------------------------------------------------------
//// MARK: - Download Progress Struct
////------------------------------------------------------

public struct DownloadProgress {
    public let name: String
    public let progress: Float
    public let completedUnitCount: Float
    public let totalUnitCount: Float
}


protocol DownloadDelegate: class {
    func downloadProgressUpdate(for progress: DownloadProgress)
    func unzipProgressUpdate(for progress: Double)
    func onFailure()
}

class DownloadManager : NSObject, URLSessionDownloadDelegate {

    //------------------------------------------------------
    // MARK: - Downloader Properties
    //------------------------------------------------------
    static var shared = DownloadManager()
    private lazy var session: URLSession = {
        let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).bookDownloader")
        config.isDiscretionary = true
        config.sessionSendsLaunchEvents = true
        return URLSession(configuration: config, delegate: self, delegateQueue: nil)
    }()

    var delegate: DownloadDelegate?
    var previousUrl: URL?
    var resumeData: Data?
    var task: URLSessionDownloadTask?
    // ProgressHandler --> identifier, progress, completedUnitCount, totalUnitCount
    typealias ProgressHandler = (String, Float, Float, Float) -> ()

    //------------------------------------------------------
    // MARK: - Downloader Initializer
    //------------------------------------------------------
    override private init() {
        super.init()
    }

    func activate() -> URLSession {
        // Warning: If an URLSession still exists from a previous download, it doesn't create a new URLSession object but returns the existing one with the old delegate object attached!
        return session
    }

    //------------------------------------------------------
    // MARK: - Downloader start download
    //------------------------------------------------------

    func startDownload(url: URL) {
        if let previousUrl = self.previousUrl {
            if url == previousUrl {
                if let data = resumeData {
                    let downloadTask = session.downloadTask(withResumeData: data)
                    downloadTask.resume()
                    self.task = downloadTask
                } else {
                    let downloadTask = session.downloadTask(with: url)
                    downloadTask.resume()
                    self.task = downloadTask
                }
            } else {
                let downloadTask = session.downloadTask(with: url)
                downloadTask.resume()
                self.task = downloadTask
            }
        } else {
            let downloadTask = session.downloadTask(with: url)
            downloadTask.resume()
            self.task = downloadTask
        }
    }

    //------------------------------------------------------
    // MARK: - Downloader stop download
    //------------------------------------------------------

    func stopDownload() {
        if let task = task {
            task.cancel { resumeDataOrNil in
                guard let resumeData = resumeDataOrNil else {
                    // download can't be resumed; remove from UI if necessary
                    return
                }
                self.resumeData = resumeData
            }
        }
    }

    //------------------------------------------------------
    // MARK: - Downloader Progress Calculator
    //------------------------------------------------------

    private func calculateProgress(session : URLSession, completionHandler : @escaping ProgressHandler) {
        session.getTasksWithCompletionHandler { (tasks, uploads, downloads) in
            let progress = downloads.map({ (task) -> Float in
                if task.countOfBytesExpectedToReceive > 0 {
                    return Float(task.countOfBytesReceived) / Float(task.countOfBytesExpectedToReceive)
                } else {
                    return 0.0
                }
            })
            let countOfBytesReceived = downloads.map({ (task) -> Float in
                return Float(task.countOfBytesReceived)
            })
            let countOfBytesExpectedToReceive = downloads.map({ (task) -> Float in
                return Float(task.countOfBytesExpectedToReceive)
            })

            if let name = UserDefaults.standard.string(forKey: UserDefaultKeys.OnBookDownload) {
                if name.isEmpty {
                    return self.session.invalidateAndCancel()
                }
                completionHandler(name, progress.reduce(0.0, +), countOfBytesReceived.reduce(0.0, +), countOfBytesExpectedToReceive.reduce(0.0, +))
            }

        }
    }

    //------------------------------------------------------
    // MARK: - Downloader Notifiers
    //------------------------------------------------------

    func postUnzipProgress(progress: Double) {
        if let delegate = self.delegate {
            delegate.unzipProgressUpdate(for: progress)
        }
//        NotificationCenter.default.post(name: .UnzipProgress, object: progress)
    }

    func postDownloadProgress(progress: DownloadProgress) {
        if let delegate = self.delegate {
            delegate.downloadProgressUpdate(for: progress)
        }
//        NotificationCenter.default.post(name: .BookDownloadProgress, object: progress)
    }

    func postNotification() {
        let center = UNUserNotificationCenter.current()
        center.requestAuthorization(options: [.alert, .sound]) { (granted, error) in
            // Enable or disable features based on authorization.
        }
        let content = UNMutableNotificationContent()
        content.title = NSString.localizedUserNotificationString(forKey: "Download Completed".localized(), arguments: nil)
        content.body = NSString.localizedUserNotificationString(forKey: "Quran Touch app is ready to use".localized(), arguments: nil)
        content.sound = UNNotificationSound.default()
        content.categoryIdentifier = "com.qurantouch.qurantouch.BookDownloadComplete"
        // Deliver the notification in 60 seconds.
        let trigger = UNTimeIntervalNotificationTrigger.init(timeInterval: 2.0, repeats: false)
        let request = UNNotificationRequest.init(identifier: "BookDownloadCompleted", content: content, trigger: trigger)

        // Schedule the notification.
        center.add(request)
    }

    //------------------------------------------------------
    // MARK: - Downloader Delegate methods
    //------------------------------------------------------

    // On Progress Update
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        if let name = UserDefaults.standard.string(forKey: UserDefaultKeys.OnBookDownload) {
            if name.isEmpty {
                return self.session.invalidateAndCancel()
            }
        } else {
            return self.session.invalidateAndCancel()
        }
        if totalBytesExpectedToWrite > 0 {
            calculateProgress(session: session, completionHandler: { (name, progress, completedUnitCount, totalUnitCount) in
                let progressInfo = DownloadProgress(name: name, progress: progress, completedUnitCount: completedUnitCount, totalUnitCount: totalUnitCount)
                print(progressInfo.progress)
                self.postDownloadProgress(progress: progressInfo)
            })
        }
    }

    // On Successful Download
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        if let name = UserDefaults.standard.string(forKey: UserDefaultKeys.OnBookDownload) {
            if name.isEmpty {
                return self.session.invalidateAndCancel()
            }
            let folder = URL.createFolder(folderName: "\(Config.bookFolder)\(name)")
            let fileURL = folder!.appendingPathComponent("\(name).zip")

            if let url = URL.getFolderUrl(folderName: "\(Config.bookFolder)\(name)") {
                do {
                    try FileManager.default.moveItem(at: location, to: fileURL)
                    // Download completed. Now time to unzip the file
                    try Zip.unzipFile((fileURL), destination: url, overwrite: true, password: nil, progress: { (progress) -> () in                        
                        if progress == 1 {
                            App.quranDownloaded = true
                            UserDefaults.standard.set("selected", forKey: name)
                            DispatchQueue.main.async {
                                Reciter().downloadCompleteReciter(success: true).done{_ in}.catch{_ in}

                                guard let appDelegate = UIApplication.shared.delegate as? AppDelegate,
                                    let backgroundCompletionHandler =
                                    appDelegate.backgroundCompletionHandler else {
                                        return
                                }
                                backgroundCompletionHandler()
                                self.postNotification()
                            }
                            // Select the book that is downloaded

                            // Delete the downlaoded zip file
                            URL.removeFile(file: fileURL)
                        }
                        self.postUnzipProgress(progress: progress)
                    }, fileOutputHandler: {(outputUrl) -> () in
                    })
                } catch {
                    print(error)
                }
            }
        } else {
            return self.session.invalidateAndCancel()
        }


    }

    // On Dwonload Completed with Failure
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        debugPrint("Task completed: \(task), error: \(error)")
        guard let error = error else {
            // Handle success case.
            return
        }
        let userInfo = (error as NSError).userInfo
        if let resumeData = userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
            self.resumeData = resumeData
        }
        if let delegate = self.delegate {
            if !error.isCancelled {
                delegate.onFailure()
            }
        }
    }

    // On Dwonload Invalidated with Error
    func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
        guard let error = error else {
            // Handle success case.
            return
        }
        if let delegate = self.delegate {
            if !error.isCancelled {
                delegate.onFailure()
            }
        }
    }
}




// MARK: - URLSessionDelegate

extension DownloadManager: URLSessionDelegate {

    // Standard background session handler
    func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
        DispatchQueue.main.async {
            if let appDelegate = UIApplication.shared.delegate as? AppDelegate,
                let completionHandler = appDelegate.backgroundCompletionHandler {
                completionHandler()
                appDelegate.backgroundCompletionHandler = nil
            }
        }
    }

}

And in app delegate:

var backgroundCompletionHandler: (() -> Void)?

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

Solution

  • Finally found a workaround for the issue. Once the application did return from background mode, make sure to call resume on all running tasks. This seems to reactivate callbacks to the delegate.

    func applicationDidBecomeActive(_ application: UIApplication) {
            // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
            DownloadManager.shared.session.getAllTasks(completionHandler: { tasks in
                for task in tasks {
                    task.resume()
                }
            })
    
        }
    

    For more information on this topic, Follow this link: https://forums.developer.apple.com/thread/77666