应用程序恢复后 URLSession 代表不工作

URLSession delegates not working after app resume

我最近一直在将后台传输服务集成到应用程序中,以便用户能够在后台下载文件。

一切正常。但是在将应用程序发送到后台然后重新打开应用程序后,我的委托方法停止被调用。

文件实际上正在后台下载,但我没有收到任何对我的委托方法的调用。因此无法向用户显示任何进度。所以感觉下载卡住了。

我不得不从应用商店中删除我们的应用,因为它正在损害我们的应用。我需要尽快重新提交应用程序。但是有了这个问题,就不行了。

我的下载管理器代码:

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
            }
        }
    }

}

在应用委托中:

var backgroundCompletionHandler: (() -> Void)?

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

终于找到了解决该问题的方法。一旦应用程序从后台模式执行 return,请确保对所有 运行 任务调用 resume。这似乎重新激活了对委托的回调。

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()
            }
        })

    }

有关此主题的更多信息,请关注此 link: https://forums.developer.apple.com/thread/77666

您需要在恢复活动状态后恢复所有任务。

    URLSession.shared.getAllTasks { (tasks) in
        for task in tasks
        {
            task.resume()
        }
    }