2

I'm trying to download multiple files simultaneously using NSURLSession in Swift. I want to merge all the download progress status into one as to show 100% when all the files are downloaded. Currently I get 100% for each file download completion, but I need 100% only if all the files are downloaded. How can I achieve this in Swift ?

Here is my DownloadManager Class :

class DownloadManager : NSObject, URLSessionDelegate, URLSessionDownloadDelegate {

static var shared = DownloadManager()
var task1Progress = 0.00
var task2Progress = 0.00
typealias ProgressHandler = (Float) -> ()

var onProgress : ProgressHandler? {
    didSet {
        if onProgress != nil {
            let _ = activate()
        }
    }
}

override private init() {
    super.init()
}

func activate() -> URLSession {
    let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")

    // 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 URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
}

func calculateProgress(session : URLSession, completionHandler : @escaping (Float) -> ()) {
    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
            }
        })
        completionHandler(progress.reduce(0.0, +))
    }
}


func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {

    if totalBytesExpectedToWrite > 0 {
        if let onProgress = onProgress {
            calculateProgress(session: session, completionHandler: onProgress)
        }

        let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
        debugPrint("Download Progress \(downloadTask) \(progress)")

    }
}

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
    debugPrint("Download finished: \(location)")
    try? FileManager.default.removeItem(at: location)
}


func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
    debugPrint("Task completed: \(task), error: \(String(describing: error))")
}

private func calculateProgress(session : URLSession, completionHandler : @escaping (Float) -> ()) {
    session.getTasksWithCompletionHandler { (tasks, uploads, downloads) in
        let progress = downloads.map({ (task) -> Float in
            if task.countOfBytesExpectedToReceive > 0 {

                if (task.taskIdentifier ==  1) {

                    self.task1Progress = Double(Float(task.countOfBytesReceived) / Float(task.countOfBytesExpectedToReceive))

                } else if (task.taskIdentifier ==  2){

                    self.task2Progress = Double(Float(task.countOfBytesReceived) / Float(task.countOfBytesExpectedToReceive))

                }

                print("pro1 = \(self.task1Progress) pro2 = \(self.task2Progress)")

                if(self.task1Progress>0.0000 && self.task2Progress>0.000) {
                    return Float(min(self.task1Progress  ,self.task2Progress))
                }

                return Float(max(self.task1Progress  ,self.task2Progress))

            } else {

                return 0.0
            }
        })
        completionHandler(progress.reduce(0.0, +))
    }
}

}

Nuibb
  • 1,800
  • 2
  • 22
  • 36

4 Answers4

0

You can use dispatchgroup tp achieve the desired behaviour.

To download file you will call shared method of downloadManager. Dispatch the downloading operation on dispatchGroup and in notify you will get the callback when all files are downloaded.

Please find example below :

func dispatchGroupUsage (completion: CallBack) {
let backgroundQ = DispatchQueue.global(attributes: .qosDefault)
let group = DispatchGroup()

for number in numberOfFilesToBeDownloaded {
  group.enter()
  backgroundQ.async(group: group,  execute: {  
      // call download manager's method to download files
      group.leave()

      })
  }

 group.notify(queue: DispatchQueue.main, execute: {
   print("All Done"); completion(result: fill)
 }) 
}
Mridul Gupta
  • 599
  • 5
  • 18
0

variables for store data and calculate progress

var progress: Float = 0
var expectedContentLength: Int64 = 0
var allData: Data = Data()

create default session:

let defaultConfiguration = URLSessionConfiguration.default
let defaultSession = URLSession(configuration: defaultConfiguration, delegate: self, delegateQueue: nil

download data from url you can call this method as many times as you want

func downlod(from url: URL, session: URLSession) {
    let dataTask = session.dataTask(with: url)
    dataTask.resume();
}

and you need to implement following delegates

URLSessionDelegate, URLSessionDataDelegate 

public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Swift.Void) {

    progress = 0
    expectedContentLength += response.expectedContentLength
    completionHandler(.allow)
}


public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
    allData.append(data)
    progress = Float(allData.count) / Float(expectedContentLength)
    print("progress - \(progress)")
}

don't forget to reset expectedContentLength variable when you starting download, like this

expectedContentLength = 0
Jabson
  • 1,585
  • 1
  • 12
  • 16
0

I had a similar requirement and this is how I managed to fix, for me I had a list of topics and each topic had chapters and each chapter was a file. So when a topic is downloaded, I had to download its chapters. So here is the catch lets say a topic had 15 files ( I get the number from metadata), there will be 15 download objects and 15 download taskes, whenever each task fires the progress I handle it in the below function in my view. The idea is simple each file start from 0.0 to 1.0 to finish, so when all 15 finishes the sum of all progress would be 15.0 that means all download gets finished when the sum of progress == total number of files

    func handleProgress(percentage:Float){
// Array of files
        let totalFileCount = downloadData.count
        totalPercentageProgress += percentage
        DispatchQueue.main.async {
            self.downloadProgressView.progress = self.totalPercentageProgress / Float(totalFileCount)
        }
    }

-----

    public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64){

        if totalBytesExpectedToWrite > 0 {
            let progressPercentage = Float(totalBytesWritten)/Float(totalBytesExpectedToWrite)
            let progressHandler = getProgressHandlerForTask(identifier: downloadTask.taskIdentifier)
            print(progressPercentage)
            delegate?.handleProgress(progressPercentage)
        }

    }
anoop4real
  • 7,598
  • 4
  • 53
  • 56
-1

Just use NSProgress. It allows you to do exactly what you want.

Each NSURLSessionTask has a progress property (if you are targetting an older OS you can create one yourself).

Then just create a parent NSProgress instance and add each task progress as a child.

Finally, observe the fractionCompleted property of NSProgress and update your progress indicator.

pfandrade
  • 2,359
  • 15
  • 25