How to Cancel download in Queue using Swift - ios

I have an app where user can download multiple files, sequentially. I have followed Mr. Rob's solution for the sequential downloads. However, I have problem when I try to cancel the download.
There are two situation when I try to cancel the download.
I want to cancel the current downloading file. When I cancel that file, the download can proceed to the next file in the queue
I want to cancel the file that are currently in the queue. The queue has cancelAll() method which will cancel all file in the queue.
Here is the codes
DownloadManager.swift
class DownloadManager: NSObject, NSURLSessionTaskDelegate, NSURLSessionDownloadDelegate
{
/// Dictionary of operations, keyed by the `taskIdentifier` of the `NSURLSessionTask`
internal var delegate : DownloadVC!
private var operations = [Int: DownloadOperation]()
/// Serial NSOperationQueue for downloads
let queue: NSOperationQueue = {
let _queue = NSOperationQueue()
_queue.name = "download"
_queue.maxConcurrentOperationCount = 1
return _queue
}()
/// Delegate-based NSURLSession for DownloadManager
lazy var session: NSURLSession = {
let sessionConfiguration = NSURLSessionConfiguration.defaultSessionConfiguration()
return NSURLSession(configuration: sessionConfiguration, delegate: self, delegateQueue: nil)
}()
/// Add download
///
/// - parameter URL: The URL of the file to be downloaded
///
/// - returns: The DownloadOperation of the operation that was queued
func addDownload(URL: NSURL) -> DownloadOperation
{
print("url in download manager: \(URL)")
let operation = DownloadOperation(session: session, URL: URL)
operations[operation.task.taskIdentifier] = operation
queue.addOperation(operation)
return operation
}
/// Cancel all queued operations
func cancelAll()
{
queue.cancelAllOperations()
}
// func cancelOne()
// {
// operations[identifier]?.cancel()
// print("identifier : \(identifier)")
// //queue.operations[identifier].cancel()
// // cancelAll()
// }
// MARK: NSURLSessionDownloadDelegate methods
func URLSession(session: NSURLSession,
downloadTask: NSURLSessionDownloadTask,
didFinishDownloadingToURL location: NSURL)
{
print("downloadTask.taskIdentifier \(downloadTask.taskIdentifier)")
operations[downloadTask.taskIdentifier]?.delegate = delegate
operations[downloadTask.taskIdentifier]?.URLSession(session, downloadTask: downloadTask, didFinishDownloadingToURL: location)
}
func URLSession(session: NSURLSession,
downloadTask: NSURLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64)
{
operations[downloadTask.taskIdentifier]?.delegate = delegate
operations[downloadTask.taskIdentifier]?.URLSession(session, downloadTask: downloadTask, didWriteData: bytesWritten, totalBytesWritten: totalBytesWritten, totalBytesExpectedToWrite: totalBytesExpectedToWrite)
}
// MARK: NSURLSessionTaskDelegate methods
func URLSession(session: NSURLSession,
task: NSURLSessionTask,
didCompleteWithError error: NSError?)
{
let key = task.taskIdentifier
operations[key]?.URLSession(session, task: task, didCompleteWithError: error)
operations.removeValueForKey(key)
}
}
DownloadOperation.swift
class DownloadOperation : AsynchronousOperation
{
let task: NSURLSessionTask
var percentageWritten:Float = 0.0
var delegate : DataDelegate!
init(session: NSURLSession, URL: NSURL)
{
task = session.downloadTaskWithURL(URL)
super.init()
}
override func cancel() {
task.cancel()
super.cancel()
}
override func main() {
task.resume()
}
// MARK: NSURLSessionDownloadDelegate methods
func URLSession(session: NSURLSession,
downloadTask: NSURLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten write: Int64,
totalBytesExpectedToWrite expect: Int64)
{
//download bar progress
percentageWritten = Float(write) / Float(expect)
//progressBar.progress = percentageWritten
let pW = Int(percentageWritten*100)
delegate.didWriteData(pW)
}
// using cocoa Security for encryption and decryption
func URLSession(session: NSURLSession,
downloadTask: NSURLSessionDownloadTask,
didFinishDownloadingToURL location: NSURL)
{
let documentsDirectoryURL = NSFileManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first as NSURL?
print("Finished downloading!")
print(documentsDirectoryURL)
let downloadLocation = "\(location)".stringByReplacingOccurrencesOfString("file://", withString: "")
let fileData = NSFileManager().contentsAtPath(downloadLocation)
delegate.didFinishDownloadingToUrl(fileData!)
}
// MARK: NSURLSessionTaskDelegate methods
func URLSession(session: NSURLSession,
task: NSURLSessionTask,
didCompleteWithError error: NSError?)
{
completeOperation()
if error != nil {
print(error)
}
}
}
AsynchronousOperation.swift
class AsynchronousOperation : NSOperation
{
override var asynchronous: Bool { return true }
private var _executing: Bool = false
override var executing: Bool
{
get
{
return _executing
}
set {
if (_executing != newValue)
{
self.willChangeValueForKey("isExecuting")
_executing = newValue
self.didChangeValueForKey("isExecuting")
}
}
}
private var _finished: Bool = false
override var finished: Bool
{
get
{
return _finished
}
set
{
if (_finished != newValue)
{
self.willChangeValueForKey("isFinished")
_finished = newValue
self.didChangeValueForKey("isFinished")
}
}
}
func completeOperation()
{
if executing {
executing = false
finished = true
}
}
override func start()
{
if (cancelled) {
finished = true
executing = false
return
}
executing = true
main()
}
}
DownloadViewController.swift
This is where I want to cancel the download.
if cell.downloadLabel.currentTitle == "CANCEL"
{
print("cancel button was pressed")
downloadManager.cancelAll()
// I want to put the codes here
let defaults: NSUserDefaults = NSUserDefaults.standardUserDefaults()
let key = cell.stringId
print("key : \(key)")
defaults.setBool(false, forKey: key)
defaults.synchronize()
cell.accessoryType = UITableViewCellAccessoryType.None
cell.downloadLabel.backgroundColor = UIColor.redColor()
cell.downloadLabel.setTitle("DOWNLOAD", forState: .Normal)
}
For situation 1, I have tried to use do
queue.operations[identifier].cancel()
but it crash saying
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]: index 1 beyond bounds [0 .. 0]'
For situation 2, I have tried to put all downloaded urls in an array, add into queue, once the cancel button is clicked, it will cancelAll() queue, remove the cancel one from queue and re-insert the remaining urls back into the array. However, it won't work
Can anybody help me to satisfy both situation?

You need to keep a reference to the NSOperation instance. Then you can call cancel on it.

Related

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
}
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
You need to resume all tasks once you come back to active state.
URLSession.shared.getAllTasks { (tasks) in
for task in tasks
{
task.resume()
}
}

Download with URLSession and Operation queue does not start next operation when application is in background

Use case : Download file from a server via one API which will give the download server url(download server url vality is for 10 secs only).
I have enabled the background capabilities.
Then created one Download Manager which holds the OperationQueue for download data task.
In Operation class when main() function calls, I have makes the API call which will return the download server URL and started a download.
Works fine in debug mode but does not work in release mode.
Download Manager :
final class SCDownloadManager: NSObject {
#objc static var shared = SCDownloadManager()
private override init() {
super.init()
NotificationCenter.default.addObserver(self, selector: #selector(SCDownloadManager.networkDidChange(_:)), name: NSNotification.Name.init(NetworkConnectionChanged), object: nil)
}
/// Dictionary of operations, keyed by the `Download URL` of the `URLSessionTask`
fileprivate var operations = [URL: SCDownloadOperation]()
fileprivate var serverURLRequestMap = [URL: SCDownloadOperation]()
/// Serial NSOperationQueue for downloads
private let queue: OperationQueue = {
let _queue = OperationQueue()
_queue.name = "SCDownloadManagerQueue"
_queue.maxConcurrentOperationCount = 3
return _queue
}()
/// Delegate-based NSURLSession for DownloadManager
lazy var downloadSession: URLSession = {
let configuration = URLSessionConfiguration.background(withIdentifier: "com.xyz.background.download")
let sessionQueue = OperationQueue()
return URLSession(configuration: configuration, delegate: self, delegateQueue: sessionQueue)
}()
lazy var dataSession: URLSession = {
let configuration = URLSessionConfiguration.background(withIdentifier: "com.xyz.background.url")
let sessionQueue = OperationQueue()
return URLSession(configuration: configuration, delegate: self, delegateQueue: sessionQueue)
}()
#discardableResult
#objc func addDownload(_ downloadRecord: DownloadRecord) -> SCDownloadOperation {
let operation = SCDownloadOperation(withDownloadRecord: downloadRecord)
operation.delegate = self
operations[downloadRecord.downloadUrl] = operation
queue.addOperation(operation)
return operation
}
/// Cancel all queued operations
#objc func cancelAll() {
queue.cancelAllOperations()
}
#objc func networkDidChange(_ notification: Notification) {
if let reachability = notification.object as? Reachability {
if reachability.currentReachabilityStatus() == NotReachable {
cancelAll()
}
}
}
}
//MARK:- Download operation delegate
extension SCDownloadManager: SCDownloadOperationDelegate {
func getServerDownloadUrl(forRequest request: URLRequest, ofOperation operation: SCDownloadOperation) {
if let url = request.url {
serverURLRequestMap[url] = operation
}
self.dataSession.downloadTask(with: request).resume()
}
}
// MARK: URLSessionDownloadDelegate methods
extension SCDownloadManager: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
guard let sourceURL = downloadTask.originalRequest?.url else { return }
if session == dataSession {
if FileManager.default.isReadableFile(atPath: location.path) {
if let data = FileManager.default.contents(atPath: location.path) {
if let downloadOperation = serverURLRequestMap[sourceURL] {
if let serverUrl = String(data: data, encoding: .utf8), let downloadServerUrl = URL(string: serverUrl) {
self.operations.changeKey(from: downloadOperation.downloadRecord.downloadUrl, to: downloadServerUrl)
downloadOperation.downloadRecord.downloadUrl = downloadServerUrl
if let url = downloadOperation.downloadRecord.downloadUrl {
downloadOperation.downloadRecord.downloadTask = downloadSession.downloadTask(with: url)
downloadOperation.downloadRecord.downloadTask.resume()
}
}
}
do {
try FileManager.default.removeItem(at: location)
} catch {
print("error while removing file from default location")
}
}
}
} else {
operations[sourceURL]?.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location)
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
if session == self.downloadSession {
guard let sourceURL = downloadTask.originalRequest?.url else { return }
operations[sourceURL]?.urlSession(session, downloadTask: downloadTask, didWriteData: bytesWritten, totalBytesWritten: totalBytesWritten, totalBytesExpectedToWrite: totalBytesExpectedToWrite)
}
}
}
// MARK: URLSessionTaskDelegate methods
extension SCDownloadManager: URLSessionTaskDelegate {
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
if downloadSession == session {
DispatchQueue.main.async {
if let completionHandler = Constants.appDelegate.backgroundTransferCompletionHandler {
Constants.appDelegate.backgroundTransferCompletionHandler = nil
completionHandler()
}
}
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard let sourceURL = task.originalRequest?.url else { return }
if session == dataSession {
if error == nil {
} else {
print("got error while doing API on download task")
}
} else {
operations[sourceURL]?.urlSession(session, task: task, didCompleteWithError: error)
operations.removeValue(forKey: sourceURL)
}
}
}
Operation Class :
class SCDownloadOperation: SCAsynchronousOperation {
let downloadRecord: DownloadRecord
var getServerDownloadUrl: ((URLRequest)->Void)?
weak var delegate: SCDownloadOperationDelegate?
init(withDownloadRecord dr: DownloadRecord) {
downloadRecord = dr
super.init()
}
override func cancel() {
if let task = downloadRecord.downloadTask{
task.cancel()
}
super.cancel()
}
override func main() {
switch downloadRecord.downloadDataType {
case DOWNLOAD_DATA_TYPE_CONTENT:
let configuration = RequestConfiguration(withMethodType: .GET, urlString: downloadRecord.downloadUrl.absoluteString)
if let request = NetworkManager.sharedInstance.urlRequest(forRequestConfiguration: configuration) {
delegate?.getServerDownloadUrl(forRequest: request, ofOperation: self)
}
default: break
}
}
}
// MARK: NSURLSessionDownloadDelegate methods
extension SCDownloadOperation: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
downloadRecord.progress = 1
switch downloadRecord.downloadDataType {
case DOWNLOAD_DATA_TYPE_CONTENT:
processDownloadFinish(atLocation: location)
default: break
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
downloadRecord.isExecuting = true
downloadRecord.isDownloadProgressAvailable = true
let downloadProgress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
downloadRecord.progress = downloadProgress
downloadRecord.percentComplete = ((Float(totalBytesWritten) / Float(totalBytesExpectedToWrite))*100)
downloadRecord.downloadProgress = downloadProgress
switch downloadRecord.downloadDataType {
case DOWNLOAD_DATA_TYPE_CONTENT:
updateWrittenData()
default: break
}
}
}
// MARK: NSURLSessionTaskDelegate methods
extension SCDownloadOperation: URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
completeOperation()
if error != nil {
print("\(String(describing: error))")
switch downloadRecord.downloadDataType {
case DOWNLOAD_DATA_TYPE_CONTENT:
contentDownloadFail()
default: break
}
}
}
}

Swift 3 : How to sequential process download files and open ViewController

I want to click a button, then download a number of files sequentially and after complete download, then open a webview to display. But I encounter download files not completed the webview already opened. I saw some approach in days and don't know how to fix the problem. Can anyone help?
Many Thanks.
DownloadManager Class
class DownloadManager: NSObject {
/// Dictionary of operations, keyed by the `taskIdentifier` of the `URLSessionTask`
fileprivate var operations = [Int: DownloadOperation]()
/// Serial NSOperationQueue for downloads
private let queue: OperationQueue = {
let _queue = OperationQueue()
_queue.name = "download"
_queue.maxConcurrentOperationCount = 1 // I'd usually use values like 3 or 4 for performance reasons, but OP asked about downloading one at a time
return _queue
}()
/// Delegate-based NSURLSession for DownloadManager
lazy var session: URLSession = {
let configuration = URLSessionConfiguration.default
return URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}()
/// Add download
///
/// - parameter URL: The URL of the file to be downloaded
///
/// - returns: The DownloadOperation of the operation that was queued
#discardableResult
func addDownload(_ url: URL) -> DownloadOperation {
let operation = DownloadOperation(session: session, url: url)
operations[operation.task.taskIdentifier] = operation
queue.addOperation(operation)
return operation
}
/// Cancel all queued operations
func cancelAll() {
queue.cancelAllOperations()
}
}
// MARK: URLSessionDownloadDelegate methods
extension DownloadManager: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
operations[downloadTask.taskIdentifier]?.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location)
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
operations[downloadTask.taskIdentifier]?.urlSession(session, downloadTask: downloadTask, didWriteData: bytesWritten, totalBytesWritten: totalBytesWritten, totalBytesExpectedToWrite: totalBytesExpectedToWrite)
}
}
// MARK: URLSessionTaskDelegate methods
extension DownloadManager: URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
let key = task.taskIdentifier
operations[key]?.urlSession(session, task: task, didCompleteWithError: error)
operations.removeValue(forKey: key)
}
}
/// Asynchronous Operation subclass for downloading
class DownloadOperation : AsynchronousOperation {
let task: URLSessionTask
init(session: URLSession, url: URL) {
task = session.downloadTask(with: url)
super.init()
}
override func cancel() {
task.cancel()
super.cancel()
}
override func main() {
task.resume()
}
}
// MARK: NSURLSessionDownloadDelegate methods
extension DownloadOperation: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
do {
let manager = FileManager.default
let destinationURL = try manager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent(downloadTask.originalRequest!.url!.lastPathComponent)
if manager.fileExists(atPath: destinationURL.path) {
try manager.removeItem(at: destinationURL)
}
try manager.moveItem(at: location, to: destinationURL)
} catch {
print("\(error)")
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
print("\(downloadTask.originalRequest!.url!.absoluteString) \(progress)")
}
}
// MARK: NSURLSessionTaskDelegate methods
extension DownloadOperation: URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
completeOperation()
if error != nil {
print("\(error)")
}
}
}
/// Asynchronous operation base class
///
/// This is abstract to class performs all of the necessary KVN of `isFinished` and
/// `isExecuting` for a concurrent `Operation` subclass. You can subclass this and
/// implement asynchronous operations. All you must do is:
///
/// - override `main()` with the tasks that initiate the asynchronous task;
///
/// - call `completeOperation()` function when the asynchronous task is done;
///
/// - optionally, periodically check `self.cancelled` status, performing any clean-up
/// necessary and then ensuring that `completeOperation()` is called; or
/// override `cancel` method, calling `super.cancel()` and then cleaning-up
/// and ensuring `completeOperation()` is called.
public class AsynchronousOperation : Operation {
override public var isAsynchronous: Bool { return true }
private let stateLock = NSLock()
private var _executing: Bool = false
override private(set) public var isExecuting: Bool {
get {
return stateLock.withCriticalScope { _executing }
}
set {
willChangeValue(forKey: "isExecuting")
stateLock.withCriticalScope { _executing = newValue }
didChangeValue(forKey: "isExecuting")
}
}
private var _finished: Bool = false
override private(set) public var isFinished: Bool {
get {
return stateLock.withCriticalScope { _finished }
}
set {
willChangeValue(forKey: "isFinished")
stateLock.withCriticalScope { _finished = newValue }
didChangeValue(forKey: "isFinished")
}
}
/// Complete the operation
///
/// This will result in the appropriate KVN of isFinished and isExecuting
public func completeOperation() {
if isExecuting {
isExecuting = false
}
if !isFinished {
isFinished = true
}
}
override public func start() {
if isCancelled {
isFinished = true
return
}
isExecuting = true
main()
}
}
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this sample’s licensing information
Abstract:
An extension to `NSLock` to simplify executing critical code.
From Advanced NSOperations sample code in WWDC 2015 https://developer.apple.com/videos/play/wwdc2015/226/
From https://developer.apple.com/sample-code/wwdc/2015/downloads/Advanced-NSOperations.zip
*/
extension NSLock {
/// Perform closure within lock.
///
/// An extension to `NSLock` to simplify executing critical code.
///
/// - parameter block: The closure to be performed.
func withCriticalScope<T>(block: () -> T) -> T {
lock()
let value = block()
unlock()
return value
}
}
Then, I call below code when click download button.
func downloadFiles() {
do {
var localJson: JSON?
let serverJson = self.serverJson
let str = serverJson?.description
let data = str?.data(using: .utf8)
let fileManager = FileManager.default
let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! as URL
let oldManifestUrl = documentsUrl.appendingPathComponent("manifest.json")
let oldManifestPath = oldManifestUrl.path
if fileManager.fileExists(atPath: oldManifestPath) {
let jsonData = NSData(contentsOfFile:oldManifestPath)
localJson = JSON(data: jsonData! as Data)
}
var totalCount = 0
let downloadManager = DownloadManager()
for (index, subJson): (String, JSON) in serverJson! {
for (_, subJson): (String, JSON) in subJson {
let filepath = subJson["path"].stringValue
let nUpdated = subJson["updated"].stringValue
if let oUpdated = localJson?[index].array?.filter({ $0["path"].string == filepath}).first?["updated"].stringValue {
if (oUpdated == nUpdated) { continue }
}
var absPath = filepath
let strIdx = absPath.index(absPath.startIndex, offsetBy: 2)
if (absPath.hasPrefix("./"))
{
absPath = absPath.substring(from: strIdx)
}
let sourceUrl = URL(string: self.sourceUrl.appending(absPath))
downloadManager.addDownload(sourceUrl!)
}
}
// Remove temp json file first if exists.
if fileManager.fileExists(atPath: oldManifestPath) {
try? fileManager.removeItem(atPath: oldManifestPath)
}
// Write temp json file to local.
try data?.write(to: oldManifestUrl)
self.defaults.set(hashes, forKey: "LastHash")
let alertController = UIAlertController(title: "Information", message: "Download completed", preferredStyle: UIAlertControllerStyle.alert)
alertController.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: nil))
self.present(alertController, animated: true, completion: {
self.openWebView()
})
} catch {
print("Write JSON data to file failed, \(error.localizedDescription)")
}
}
What you need is a promise library that offers code execution on certain completion conditions. Currently, you make the request and open the webview straight away. The requests take time to load but your code doesn't tell the device to wait for the requests to finish before opening up the webview. You can use PromiseKit for this purpose. With this library, you can send multiple requests asynchronously and then open the webview when all of the requests have been processed. You will have to explore it in order to understand how it works. Basically, your code structure will look something like this
var promiseArray = [Promise<Void>]()
for WHATEVER CONDITION YOU WANT TO USE FOR THE LOOP {
let promise = YOUR REQUEST GOES HERE
promiseArray.append(promise)
}
_ = when(fulfilled: promiseArray).then {
OPEN WEBVIEW HERE
}

Swift cancellation of NSOperationQueue does not work

I'm trying to cancel NSOperationQueue process using method cancelAllOperations() using swift but it does not work.
I am downloading a file from url when i cancel the download it doesn't work.
here is the code i am using:
var s = NSOperationQueue.mainQueue()
lazy var session : NSURLSession = {
let config = NSURLSessionConfiguration.ephemeralSessionConfiguration()
config.allowsCellularAccess = false
let session = NSURLSession(configuration: config, delegate: self, delegateQueue: self.s)
return session
}
override func viewDidLoad() {
progressBar.setProgress(0.0, animated: true) //set progressBar to 0 at start
}
#IBAction func cancel(sender: AnyObject) {
s.cancelAllOperations()
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten writ: Int64, totalBytesExpectedToWrite exp: Int64) {
print("downloaded \(100*writ/exp)")
dispatch_async(dispatch_get_main_queue(), {
self.counter = Float(100*writ/exp)
return
})
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
// unused in this example
}
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
print("completed: error: \(error)")
if(error != nil){
}
}
// this is the only required NSURLSessionDownloadDelegate method
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
let path = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)
let documentDirectoryPath:String = path[0]
let fileManager = NSFileManager()
let destinationURLForFile = NSURL(fileURLWithPath: documentDirectoryPath.stringByAppendingString("Music.mp3"))
if fileManager.fileExistsAtPath(destinationURLForFile.path!){
print("File Already Exists")
}
else{
do {
try fileManager.moveItemAtURL(location, toURL: destinationURLForFile)
print("File doesn't exists")
}catch{
print("An error occurred while moving file to destination url")
}
}
}

swift 2 NSURLSessionDownloadTask completes even when there are still files left to be downloaded

I am trying to download 30+ images using NSURLSessionDownloadTask, the problem is that it won't finish downloading everything. Sometimes, only 26 images are download but other times it is more or less. I've checked my url's and all of them exist but I don't understand why it's doing this. I have tried downloading using the json urls directly from the server and it crashes because of an empty optional which is caused I think by a connection timeout. I understand that happens because the internet here is not that stable. But when I tried to download the whole json file and saved it in my document directory and tried to download using the json that is saved locally, it doesn't crash anymore since I won't need to get the urls from the server. However, what happens is that my progress bar completes but not all images are downloaded. It's like the other urls are being ignored.
import UIKit
class ViewController: UIViewController, NSURLSessionDownloadDelegate {
var taskProgress = [NSURL: (written: Int64, expected: Int64)]()
#IBOutlet weak var progressBar: UIProgressView!
#IBOutlet weak var progressCount: UILabel!
var images = [String]()
var task : NSURLSessionTask!
var pics = ["http://pillar.foundationu.com/wp-content/uploads/2016/03/banner.jpg", "http://pillar.foundationu.com/wp-content/uploads/2016/03/Abad-Edmerl.jpg", "http://pillar.foundationu.com/wp-content/uploads/2016/03/Abellana-Deniz-Dawn-1.jpg", "http://pillar.foundationu.com/wp-content/uploads/2016/03/Abequibel-Arneth.jpg", "http://pillar.foundationu.com/wp-content/uploads/2016/03/Abilla-Harriette.jpg", "http://pillar.foundationu.com/wp-content/uploads/2016/03/Pasko-Maria-Katrina.jpg", "http://pillar.foundationu.com/wp-content/uploads/2016/03/Pastias-Grace.jpg"]
var counter:Float = 0.0 {
didSet {
let fractionalProgress = Float(counter) / 100.0
let animated = counter != 0
progressBar.setProgress(fractionalProgress, animated: animated)
progressCount.text = ("\(counter)%")
}
//The didSet is called immediately after the new value is stored. The fractionalProgress constant keeps track of the progress.
}
lazy var session : NSURLSession = {
let config = NSURLSessionConfiguration.ephemeralSessionConfiguration()
config.allowsCellularAccess = false
let session = NSURLSession(configuration: config, delegate: self, delegateQueue: NSOperationQueue.mainQueue())
return session
}()
override func viewDidLoad() {
progressBar.setProgress(0.0, animated: true) //set progressBar to 0 at
start
}
#IBAction func doElaborateHTTP (sender:AnyObject!) {
progressCount.text = "0%"
if self.task != nil {
return
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
let taskID = self.beginBackgroundUpdateTask()
let file = FileHelperClass()
var url = NSURL!()
var req = NSMutableURLRequest()
var task: NSURLSessionDownloadTask!
for s in file.parseJsonFromFile() /* fund returning images url */ {
let url = NSURL(string: s)
req = NSMutableURLRequest(URL:url!)
task = self.session.downloadTaskWithRequest(req)
self.task = task
task.resume()
}
// Do something with the result
self.endBackgroundUpdateTask(taskID)
})
}
func beginBackgroundUpdateTask() -> UIBackgroundTaskIdentifier {
return UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler({})
}
func endBackgroundUpdateTask(taskID: UIBackgroundTaskIdentifier) {
UIApplication.sharedApplication().endBackgroundTask(taskID)
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten writ: Int64, totalBytesExpectedToWrite exp: Int64) {
dispatch_async(dispatch_get_main_queue()) {
self.taskProgress[downloadTask.originalRequest!.URL!] = (writ, exp)
// Update your views, you may separate it into a different function
let totalWritten = self.taskProgress.reduce(0) { $0 + $1.1.written }
let totalExpected = self.taskProgress.reduce(0) { $0 + $1.1.expected }
let progress = Float(totalWritten) / Float(totalExpected)
self.progressBar.setProgress(progress, animated: true)
self.progressCount.text = "\(progress * 100)%"
}
}
func URLSession(session: NSURLSession, downloadTask:
NSURLSessionDownloadTask, didResumeAtOffset fileOffset: Int64,
expectedTotalBytes: Int64) {
// unused in this example
}
func URLSession(session: NSURLSession, task: NSURLSessionTask,
didCompleteWithError error: NSError?) {
// print("completed: error: \(error)")
}
// this is the only required NSURLSessionDownloadDelegate method
func URLSession(session: NSURLSession, downloadTask:
NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
// print(downloadTask.response!.suggestedFilename!)
images.append(downloadTask.response!.suggestedFilename!)
print("\n \(images)")
}
}

Resources