2 // MZDownloadManager.swift
5 // Created by Muhammad Zeeshan on 19/04/2016.
6 // Copyright © 2016 ideamakerz. All rights reserved.
10 fileprivate func < <T : Comparable>(lhs: T?, rhs: T?) -> Bool {
21 fileprivate func > <T : Comparable>(lhs: T?, rhs: T?) -> Bool {
31 @objc public protocol MZDownloadManagerDelegate: class {
32 /**A delegate method called each time whenever any download task's progress is updated
34 @objc func downloadRequestDidUpdateProgress(_ downloadModel: MZDownloadModel, index: Int)
35 /**A delegate method called when interrupted tasks are repopulated
37 @objc func downloadRequestDidPopulatedInterruptedTasks(_ downloadModel: [MZDownloadModel])
38 /**A delegate method called each time whenever new download task is start downloading
40 @objc optional func downloadRequestStarted(_ downloadModel: MZDownloadModel, index: Int)
41 /**A delegate method called each time whenever running download task is paused. If task is already paused the action will be ignored
43 @objc optional func downloadRequestDidPaused(_ downloadModel: MZDownloadModel, index: Int)
44 /**A delegate method called each time whenever any download task is resumed. If task is already downloading the action will be ignored
46 @objc optional func downloadRequestDidResumed(_ downloadModel: MZDownloadModel, index: Int)
47 /**A delegate method called each time whenever any download task is resumed. If task is already downloading the action will be ignored
49 @objc optional func downloadRequestDidRetry(_ downloadModel: MZDownloadModel, index: Int)
50 /**A delegate method called each time whenever any download task is cancelled by the user
52 @objc optional func downloadRequestCanceled(_ downloadModel: MZDownloadModel, index: Int)
53 /**A delegate method called each time whenever any download task is finished successfully
55 @objc optional func downloadRequestFinished(_ downloadModel: MZDownloadModel, index: Int)
56 /**A delegate method called each time whenever any download task is failed due to any reason
58 @objc optional func downloadRequestDidFailedWithError(_ error: NSError, downloadModel: MZDownloadModel, index: Int)
59 /**A delegate method called each time whenever specified destination does not exists. It will be called on the session queue. It provides the opportunity to handle error appropriately
61 @objc optional func downloadRequestDestinationDoestNotExists(_ downloadModel: MZDownloadModel, index: Int, location: URL)
65 open class MZDownloadManager: NSObject {
67 fileprivate var sessionManager: URLSession!
69 fileprivate var backgroundSessionCompletionHandler: (() -> Void)?
71 fileprivate let TaskDescFileNameIndex = 0
72 fileprivate let TaskDescFileURLIndex = 1
73 fileprivate let TaskDescFileDestinationIndex = 2
75 fileprivate weak var delegate: MZDownloadManagerDelegate?
77 open var downloadingArray: [MZDownloadModel] = []
79 public convenience init(session sessionIdentifer: String, delegate: MZDownloadManagerDelegate) {
82 self.delegate = delegate
83 self.sessionManager = backgroundSession(identifier: sessionIdentifer)
84 self.populateOtherDownloadTasks()
87 public convenience init(session sessionIdentifer: String, delegate: MZDownloadManagerDelegate, completion: (() -> Void)?) {
88 self.init(session: sessionIdentifer, delegate: delegate)
89 self.backgroundSessionCompletionHandler = completion
92 fileprivate func backgroundSession(identifier: String) -> URLSession {
93 let sessionConfiguration = URLSessionConfiguration.background(withIdentifier: identifier)
94 let session = Foundation.URLSession(configuration: sessionConfiguration, delegate: self, delegateQueue: nil)
99 // MARK: Private Helper functions
101 extension MZDownloadManager {
103 fileprivate func downloadTasks() -> [URLSessionDownloadTask] {
104 var tasks: [URLSessionDownloadTask] = []
105 let semaphore : DispatchSemaphore = DispatchSemaphore(value: 0)
106 sessionManager.getTasksWithCompletionHandler { (dataTasks, uploadTasks, downloadTasks) -> Void in
107 tasks = downloadTasks
111 let _ = semaphore.wait(timeout: DispatchTime.distantFuture)
113 debugPrint("MZDownloadManager: pending tasks \(tasks)")
118 fileprivate func populateOtherDownloadTasks() {
120 let downloadTasks = self.downloadTasks()
122 for downloadTask in downloadTasks {
123 let taskDescComponents: [String] = downloadTask.taskDescription!.components(separatedBy: ",")
124 let fileName = taskDescComponents[TaskDescFileNameIndex]
125 let fileURL = taskDescComponents[TaskDescFileURLIndex]
126 let destinationPath = taskDescComponents[TaskDescFileDestinationIndex]
128 let downloadModel = MZDownloadModel.init(fileName: fileName, fileURL: fileURL, destinationPath: destinationPath)
129 downloadModel.task = downloadTask
130 downloadModel.startTime = Date()
132 if downloadTask.state == .running {
133 downloadModel.status = TaskStatus.downloading.description()
134 downloadingArray.append(downloadModel)
135 } else if(downloadTask.state == .suspended) {
136 downloadModel.status = TaskStatus.paused.description()
137 downloadingArray.append(downloadModel)
139 downloadModel.status = TaskStatus.failed.description()
144 fileprivate func isValidResumeData(_ resumeData: Data?) -> Bool {
146 guard resumeData != nil || resumeData?.count > 0 else {
151 var resumeDictionary : AnyObject!
152 resumeDictionary = try PropertyListSerialization.propertyList(from: resumeData!, options: PropertyListSerialization.MutabilityOptions(), format: nil) as AnyObject!
153 var localFilePath = (resumeDictionary?["NSURLSessionResumeInfoLocalPath"] as? String)
155 if localFilePath == nil || localFilePath?.count < 1 {
156 localFilePath = (NSTemporaryDirectory() as String) + (resumeDictionary["NSURLSessionResumeInfoTempFileName"] as! String)
159 let fileManager : FileManager! = FileManager.default
160 debugPrint("resume data file exists: \(fileManager.fileExists(atPath: localFilePath! as String))")
161 return fileManager.fileExists(atPath: localFilePath! as String)
162 } catch let error as NSError {
163 debugPrint("resume data is nil: \(error)")
169 extension MZDownloadManager: URLSessionDownloadDelegate {
171 public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
172 for (index, downloadModel) in self.downloadingArray.enumerated() {
173 if downloadTask.isEqual(downloadModel.task) {
174 DispatchQueue.main.async(execute: { () -> Void in
176 let receivedBytesCount = Double(downloadTask.countOfBytesReceived)
177 let totalBytesCount = Double(downloadTask.countOfBytesExpectedToReceive)
178 let progress = Float(receivedBytesCount / totalBytesCount)
180 let taskStartedDate = downloadModel.startTime!
181 let timeInterval = taskStartedDate.timeIntervalSinceNow
182 let downloadTime = TimeInterval(-1 * timeInterval)
184 let speed = Float(totalBytesWritten) / Float(downloadTime)
186 let remainingContentLength = totalBytesExpectedToWrite - totalBytesWritten
188 let remainingTime = remainingContentLength / Int64(speed)
189 let hours = Int(remainingTime) / 3600
190 let minutes = (Int(remainingTime) - hours * 3600) / 60
191 let seconds = Int(remainingTime) - hours * 3600 - minutes * 60
193 let totalFileSize = MZUtility.calculateFileSizeInUnit(totalBytesExpectedToWrite)
194 let totalFileSizeUnit = MZUtility.calculateUnit(totalBytesExpectedToWrite)
196 let downloadedFileSize = MZUtility.calculateFileSizeInUnit(totalBytesWritten)
197 let downloadedSizeUnit = MZUtility.calculateUnit(totalBytesWritten)
199 let speedSize = MZUtility.calculateFileSizeInUnit(Int64(speed))
200 let speedUnit = MZUtility.calculateUnit(Int64(speed))
202 downloadModel.remainingTime = (hours, minutes, seconds)
203 downloadModel.file = (totalFileSize, totalFileSizeUnit as String)
204 downloadModel.downloadedFile = (downloadedFileSize, downloadedSizeUnit as String)
205 downloadModel.speed = (speedSize, speedUnit as String)
206 downloadModel.progress = progress
208 self.downloadingArray[index] = downloadModel
210 self.delegate?.downloadRequestDidUpdateProgress(downloadModel, index: index)
217 public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
218 for (index, downloadModel) in downloadingArray.enumerated() {
219 if downloadTask.isEqual(downloadModel.task) {
220 let fileName = downloadModel.fileName as NSString
221 let basePath = downloadModel.destinationPath == "" ? MZUtility.baseFilePath : downloadModel.destinationPath
222 let destinationPath = (basePath as NSString).appendingPathComponent(fileName as String)
224 let fileManager : FileManager = FileManager.default
226 //If all set just move downloaded file to the destination
227 if fileManager.fileExists(atPath: basePath) {
228 let fileURL = URL(fileURLWithPath: destinationPath as String)
229 debugPrint("directory path = \(destinationPath)")
232 try fileManager.moveItem(at: location, to: fileURL)
233 } catch let error as NSError {
234 debugPrint("Error while moving downloaded file to destination path:\(error)")
235 DispatchQueue.main.async(execute: { () -> Void in
236 self.delegate?.downloadRequestDidFailedWithError?(error, downloadModel: downloadModel, index: index)
240 //Opportunity to handle the folder doesnot exists error appropriately.
241 //Move downloaded file to destination
242 //Delegate will be called on the session queue
243 //Otherwise blindly give error Destination folder does not exists
245 if let _ = self.delegate?.downloadRequestDestinationDoestNotExists {
246 self.delegate?.downloadRequestDestinationDoestNotExists?(downloadModel, index: index, location: location)
248 let error = NSError(domain: "FolderDoesNotExist", code: 404, userInfo: [NSLocalizedDescriptionKey : "Destination folder does not exists"])
249 self.delegate?.downloadRequestDidFailedWithError?(error, downloadModel: downloadModel, index: index)
258 public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
259 debugPrint("task id: \(task.taskIdentifier)")
260 /***** Any interrupted tasks due to any reason will be populated in failed state after init *****/
262 DispatchQueue.main.async {
264 let err = error as NSError?
266 if (err?.userInfo[NSURLErrorBackgroundTaskCancelledReasonKey] as? NSNumber)?.intValue == NSURLErrorCancelledReasonUserForceQuitApplication || (err?.userInfo[NSURLErrorBackgroundTaskCancelledReasonKey] as? NSNumber)?.intValue == NSURLErrorCancelledReasonBackgroundUpdatesDisabled {
268 let downloadTask = task as! URLSessionDownloadTask
269 let taskDescComponents: [String] = downloadTask.taskDescription!.components(separatedBy: ",")
270 let fileName = taskDescComponents[self.TaskDescFileNameIndex]
271 let fileURL = taskDescComponents[self.TaskDescFileURLIndex]
272 let destinationPath = taskDescComponents[self.TaskDescFileDestinationIndex]
274 let downloadModel = MZDownloadModel.init(fileName: fileName, fileURL: fileURL, destinationPath: destinationPath)
275 downloadModel.status = TaskStatus.failed.description()
276 downloadModel.task = downloadTask
278 let resumeData = err?.userInfo[NSURLSessionDownloadTaskResumeData] as? Data
280 var newTask = downloadTask
281 if self.isValidResumeData(resumeData) == true {
282 newTask = self.sessionManager.downloadTask(withResumeData: resumeData!)
284 newTask = self.sessionManager.downloadTask(with: URL(string: fileURL as String)!)
287 newTask.taskDescription = downloadTask.taskDescription
288 downloadModel.task = newTask
290 self.downloadingArray.append(downloadModel)
292 self.delegate?.downloadRequestDidPopulatedInterruptedTasks(self.downloadingArray)
295 for(index, object) in self.downloadingArray.enumerated() {
296 let downloadModel = object
297 if task.isEqual(downloadModel.task) {
298 if err?.code == NSURLErrorCancelled || err == nil {
299 self.downloadingArray.remove(at: index)
302 self.delegate?.downloadRequestFinished?(downloadModel, index: index)
304 self.delegate?.downloadRequestCanceled?(downloadModel, index: index)
308 let resumeData = err?.userInfo[NSURLSessionDownloadTaskResumeData] as? Data
310 if self.isValidResumeData(resumeData) == true {
311 newTask = self.sessionManager.downloadTask(withResumeData: resumeData!)
313 newTask = self.sessionManager.downloadTask(with: URL(string: downloadModel.fileURL)!)
316 newTask.taskDescription = task.taskDescription
317 downloadModel.status = TaskStatus.failed.description()
318 downloadModel.task = newTask as? URLSessionDownloadTask
320 self.downloadingArray[index] = downloadModel
323 self.delegate?.downloadRequestDidFailedWithError?(error, downloadModel: downloadModel, index: index)
325 let error: NSError = NSError(domain: "MZDownloadManagerDomain", code: 1000, userInfo: [NSLocalizedDescriptionKey : "Unknown error occurred"])
327 self.delegate?.downloadRequestDidFailedWithError?(error, downloadModel: downloadModel, index: index)
337 public func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
338 if let backgroundCompletion = self.backgroundSessionCompletionHandler {
339 DispatchQueue.main.async(execute: {
340 backgroundCompletion()
343 debugPrint("All tasks are finished")
347 //MARK: Public Helper Functions
349 extension MZDownloadManager {
351 @objc public func addDownloadTask(_ fileName: String, fileURL: String, destinationPath: String) {
353 let url = URL(string: fileURL as String)!
354 let request = URLRequest(url: url)
356 let downloadTask = sessionManager.downloadTask(with: request)
357 downloadTask.taskDescription = [fileName, fileURL, destinationPath].joined(separator: ",")
358 downloadTask.resume()
360 debugPrint("session manager:\(sessionManager) url:\(url) request:\(request)")
362 let downloadModel = MZDownloadModel.init(fileName: fileName, fileURL: fileURL, destinationPath: destinationPath)
363 downloadModel.startTime = Date()
364 downloadModel.status = TaskStatus.downloading.description()
365 downloadModel.task = downloadTask
367 downloadingArray.append(downloadModel)
368 delegate?.downloadRequestStarted?(downloadModel, index: downloadingArray.count - 1)
371 @objc public func addDownloadTask(_ fileName: String, fileURL: String) {
372 addDownloadTask(fileName, fileURL: fileURL, destinationPath: "")
375 @objc public func pauseDownloadTaskAtIndex(_ index: Int) {
377 let downloadModel = downloadingArray[index]
379 guard downloadModel.status != TaskStatus.paused.description() else {
383 let downloadTask = downloadModel.task
384 downloadTask!.suspend()
385 downloadModel.status = TaskStatus.paused.description()
386 downloadModel.startTime = Date()
388 downloadingArray[index] = downloadModel
390 delegate?.downloadRequestDidPaused?(downloadModel, index: index)
393 @objc public func resumeDownloadTaskAtIndex(_ index: Int) {
395 let downloadModel = downloadingArray[index]
397 guard downloadModel.status != TaskStatus.downloading.description() else {
401 let downloadTask = downloadModel.task
402 downloadTask!.resume()
403 downloadModel.status = TaskStatus.downloading.description()
405 downloadingArray[index] = downloadModel
407 delegate?.downloadRequestDidResumed?(downloadModel, index: index)
410 @objc public func retryDownloadTaskAtIndex(_ index: Int) {
411 let downloadModel = downloadingArray[index]
413 guard downloadModel.status != TaskStatus.downloading.description() else {
417 let downloadTask = downloadModel.task
419 downloadTask!.resume()
420 downloadModel.status = TaskStatus.downloading.description()
421 downloadModel.startTime = Date()
422 downloadModel.task = downloadTask
424 downloadingArray[index] = downloadModel
427 @objc public func cancelTaskAtIndex(_ index: Int) {
428 let downloadInfo = downloadingArray[index]
429 let downloadTask = downloadInfo.task
430 downloadTask!.cancel()
433 @objc public func presentNotificationForDownload(_ notifAction: String, notifBody: String) {
434 let application = UIApplication.shared
435 let applicationState = application.applicationState
437 if applicationState == UIApplicationState.background {
438 let localNotification = UILocalNotification()
439 localNotification.alertBody = notifBody
440 localNotification.alertAction = notifAction
441 localNotification.soundName = UILocalNotificationDefaultSoundName
442 localNotification.applicationIconBadgeNumber += 1
443 application.presentLocalNotificationNow(localNotification)