Недавно я интегрировал фоновую службу передачи в приложение, чтобы пользователь мог загружать файлы в фоновом режиме.
Все работает как положено. Но мои методы делегата перестают вызываться после отправки приложения в фоновый режим и повторного открытия приложения.
Файл фактически загружается в фоновом режиме, но я не получаю никакого вызова к моим методам делегата. Так что не могу показать какой-либо прогресс для пользователей. Такое ощущение, что загрузка застряла.
Мне пришлось удалить наше приложение из магазина приложений, так как это вредит нашему приложению. Мне нужно повторно отправить приложение как можно скорее. Но с этой проблемой это невозможно.
Мой код менеджера загрузок:
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
}