应用程序恢复后 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()
}
}
我最近一直在将后台传输服务集成到应用程序中,以便用户能够在后台下载文件。
一切正常。但是在将应用程序发送到后台然后重新打开应用程序后,我的委托方法停止被调用。
文件实际上正在后台下载,但我没有收到任何对我的委托方法的调用。因此无法向用户显示任何进度。所以感觉下载卡住了。
我不得不从应用商店中删除我们的应用,因为它正在损害我们的应用。我需要尽快重新提交应用程序。但是有了这个问题,就不行了。
我的下载管理器代码:
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()
}
}