Делегаты URLSession не работают после возобновления работы приложения - PullRequest
0 голосов
/ 16 мая 2019

Недавно я интегрировал фоновую службу передачи в приложение, чтобы пользователь мог загружать файлы в фоновом режиме.

Все работает как положено. Но мои методы делегата перестают вызываться после отправки приложения в фоновый режим и повторного открытия приложения.

Файл фактически загружается в фоновом режиме, но я не получаю никакого вызова к моим методам делегата. Так что не могу показать какой-либо прогресс для пользователей. Такое ощущение, что загрузка застряла.

Мне пришлось удалить наше приложение из магазина приложений, так как это вредит нашему приложению. Мне нужно повторно отправить приложение как можно скорее. Но с этой проблемой это невозможно.

Мой код менеджера загрузок:

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
    }

Ответы [ 2 ]

1 голос
/ 16 мая 2019

Наконец-то нашли обходной путь для этой проблемы. После того, как приложение вернулось из фонового режима, обязательно вызовите метод возобновления для всех запущенных задач. Кажется, это реактивирует обратные вызовы делегату.

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

    }

Для получения дополнительной информации по этой теме, перейдите по этой ссылке: https://forums.developer.apple.com/thread/77666

0 голосов
/ 16 мая 2019

Вам необходимо возобновить все задачи, как только вы вернетесь в активное состояние.

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