35

Is there any way to get progress from dataTaskWithURL in swift while the data is downloading?

NSURLSession.sharedSession().dataTaskWithURL(...)

I need to show progress bar while the data is downloading.

Leo
  • 24,596
  • 11
  • 71
  • 92
taffarel
  • 4,015
  • 4
  • 33
  • 61

6 Answers6

73

You can simply observe progress property of the URLSessionDataTask object.

Example:

import UIKit

class SomeViewController: UIViewController {

  private var observation: NSKeyValueObservation?

  deinit {
    observation?.invalidate()
  }

  override func viewDidLoad() {
    super.viewDidLoad()

    let url = URL(string: "https://source.unsplash.com/random/4000x4000")!
    let task = URLSession.shared.dataTask(with: url)

    observation = task.progress.observe(\.fractionCompleted) { progress, _ in
      print("progress: ", progress.fractionCompleted)
    }

    task.resume()
  }
}

Playground example:

import Foundation
import PlaygroundSupport

let page = PlaygroundPage.current
page.needsIndefiniteExecution = true

let url = URL(string: "https://source.unsplash.com/random/4000x4000")!
let task = URLSession.shared.dataTask(with: url) { _, _, _ in
  page.finishExecution()
}

// Don't forget to invalidate the observation when you don't need it anymore.
let observation = task.progress.observe(\.fractionCompleted) { progress, _ in
  print(progress.fractionCompleted)
}

task.resume()
yas375
  • 3,940
  • 1
  • 24
  • 33
  • 3
    This is by far the best answer to the question! Not the only correct answer, but the best answer. Very straightforward, and it works without the use of the delegate (which can cause other problems just by switching to the delegate) – SeanRobinson159 Mar 11 '19 at 18:42
  • 1
    But this needs iOS 11 or more. – Yassine ElBadaoui Jul 02 '19 at 06:08
  • Doesn't work outside a playground context. Probably this needs to be run in some queue? – shawon13 Jul 09 '19 at 18:56
  • 1
    @yas375 this code is not printing progress in 12.4. Would you please confirm if its the same working code? – βhargavḯ Aug 28 '19 at 11:30
  • @βhargavḯ you probably wasn't retaining the `observation`. I just checked on iOS 12.4 and works as expected. I added an example to my answer. – yas375 Aug 29 '19 at 17:02
  • 1
    @shawon13 I've updated my answer to also include an example of the same code used outside of Playgrounds. Hope it helps. – yas375 Aug 29 '19 at 17:04
  • @NiallKiddle see updated answer which now includes an example of the same code used outside of playgrounds. Hope it helps. – yas375 Aug 29 '19 at 17:07
  • Simplicity trumps everything else. I just tried this solution and it works. – Lucas van Dongen Mar 10 '20 at 20:37
  • In this example, is there a good way to make “observation” an appropriate @Published variable for use in SwiftUI? I am trying to pass “observation” into a Text view in SwiftUI and the error is: Instance method 'appendInterpolation' requires that 'NSKeyValueObservation?' conform to '_FormatSpecifiable'. I have no idea if this is possible or how to do it. – d5automations May 14 '20 at 14:31
  • @yas375, how can the progress be tracked in the case of SwiftUI? Any help on this is highly appreciated. Thanks :D – user2580 Sep 21 '20 at 07:26
  • This is a perfect answer. But I have issues understanding one thing: Why does `var observation: NSKeyValueObservation?` have to be on class scope? I'd like to only have it in the functions that initiates the download but that doesn't work – user2875404 Jan 15 '21 at 11:49
  • This should be the accepted answer at this point. Thanks! Delegates are so 2007. – Youssef Moawad Jun 14 '21 at 16:44
45

you can use this code for showing download process with progress bar with its delegate functions.

import UIKit

class ViewController: UIViewController,NSURLSessionDelegate,NSURLSessionDataDelegate{

    @IBOutlet weak var progress: UIProgressView!

    var buffer:NSMutableData = NSMutableData()
    var session:NSURLSession?
    var dataTask:NSURLSessionDataTask?
    let url = NSURL(string:"https://i.stack.imgur.com/b8zkg.png" )!
    var expectedContentLength = 0


    override func viewDidLoad() {
        super.viewDidLoad()
        progress.progress = 0.0
        let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
        let manqueue = NSOperationQueue.mainQueue()
        session = NSURLSession(configuration: configuration, delegate:self, delegateQueue: manqueue)
        dataTask = session?.dataTaskWithRequest(NSURLRequest(URL: url))
        dataTask?.resume()

        // Do any additional setup after loading the view, typically from a nib.
    }
    func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveResponse response: NSURLResponse, completionHandler: (NSURLSessionResponseDisposition) -> Void) {

        //here you can get full lenth of your content
        expectedContentLength = Int(response.expectedContentLength)
        println(expectedContentLength)
        completionHandler(NSURLSessionResponseDisposition.Allow)
    }
    func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {


        buffer.appendData(data)

        let percentageDownloaded = Float(buffer.length) / Float(expectedContentLength)
        progress.progress =  percentageDownloaded
    }
    func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
        //use buffer here.Download is done
        progress.progress = 1.0   // download 100% complete
    }
}
Dharmesh Kheni
  • 71,228
  • 33
  • 160
  • 165
  • 4
    Thanks for a helpful example. How about if I want to isolate NSURLSession logic into a separate Service Access Layer and still have Progress updated, would you have or be able to share any examples for that scenario? Thanks. – AlexVPerl Jan 21 '17 at 00:14
  • Rather than appending the data received to a buffer that you don't do much with and which is using up memory, you could just record the amount of bytes received so far instead, e.g. bytesRecieved += data.length – occulus May 17 '17 at 14:26
  • This works great... But it is overly complicated for just updating a progress bar. Check out yas375 's answer https://stackoverflow.com/a/54204979/5774551 – SeanRobinson159 Mar 11 '19 at 18:45
26

Update for Swift4:
Supports execution of multiple simultaneous operations.

File: DownloadService.swift. Keeps reference to URLSession and tracks executing tasks.

final class DownloadService: NSObject {

   private var session: URLSession!
   private var downloadTasks = [GenericDownloadTask]()

   public static let shared = DownloadService()

   private override init() {
      super.init()
      let configuration = URLSessionConfiguration.default
      session = URLSession(configuration: configuration,
                           delegate: self, delegateQueue: nil)
   }

   func download(request: URLRequest) -> DownloadTask {
      let task = session.dataTask(with: request)
      let downloadTask = GenericDownloadTask(task: task)
      downloadTasks.append(downloadTask)
      return downloadTask
   }
}


extension DownloadService: URLSessionDataDelegate {

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

      guard let task = downloadTasks.first(where: { $0.task == dataTask }) else {
         completionHandler(.cancel)
         return
      }
      task.expectedContentLength = response.expectedContentLength
      completionHandler(.allow)
   }

   func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
      guard let task = downloadTasks.first(where: { $0.task == dataTask }) else {
         return
      }
      task.buffer.append(data)
      let percentageDownloaded = Double(task.buffer.count) / Double(task.expectedContentLength)
      DispatchQueue.main.async {
         task.progressHandler?(percentageDownloaded)
      }
   }

   func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
      guard let index = downloadTasks.index(where: { $0.task == task }) else {
         return
      }
      let task = downloadTasks.remove(at: index)
      DispatchQueue.main.async {
         if let e = error {
            task.completionHandler?(.failure(e))
         } else {
            task.completionHandler?(.success(task.buffer))
         }
      }
   }
}

File: DownloadTask.swift. Lightweight interface just to hide concrete implementation.

protocol DownloadTask {

   var completionHandler: ResultType<Data>.Completion? { get set }
   var progressHandler: ((Double) -> Void)? { get set }

   func resume()
   func suspend()
   func cancel()
}

File: GenericDownloadTask.swift. Concrete implementation of DownloadTask interface.

class GenericDownloadTask {

   var completionHandler: ResultType<Data>.Completion?
   var progressHandler: ((Double) -> Void)?

   private(set) var task: URLSessionDataTask
   var expectedContentLength: Int64 = 0
   var buffer = Data()

   init(task: URLSessionDataTask) {
      self.task = task
   }

   deinit {
      print("Deinit: \(task.originalRequest?.url?.absoluteString ?? "")")
   }

}

extension GenericDownloadTask: DownloadTask {

   func resume() {
      task.resume()
   }

   func suspend() {
      task.suspend()
   }

   func cancel() {
      task.cancel()
   }
}

File: ResultType.swift. Reusable type to keep result or error.

public enum ResultType<T> {

   public typealias Completion = (ResultType<T>) -> Void

   case success(T)
   case failure(Swift.Error)

}

Usage: Example how to run two download tasks in parallel (macOS App):

class ViewController: NSViewController {

   @IBOutlet fileprivate weak var loadImageButton1: NSButton!
   @IBOutlet fileprivate weak var loadProgressIndicator1: NSProgressIndicator!
   @IBOutlet fileprivate weak var imageView1: NSImageView!

   @IBOutlet fileprivate weak var loadImageButton2: NSButton!
   @IBOutlet fileprivate weak var loadProgressIndicator2: NSProgressIndicator!
   @IBOutlet fileprivate weak var imageView2: NSImageView!

   fileprivate var downloadTask1:  DownloadTask?
   fileprivate var downloadTask2:  DownloadTask?

   override func viewDidLoad() {
      super.viewDidLoad()
      loadImageButton1.target = self
      loadImageButton1.action = #selector(startDownload1(_:))
      loadImageButton2.target = self
      loadImageButton2.action = #selector(startDownload2(_:))
   }

}


extension ViewController {

   @objc fileprivate func startDownload1(_ button: NSButton) {
      let url = URL(string: "http://localhost:8001/?imageID=01&tilestamp=\(Date.timeIntervalSinceReferenceDate)")!
      let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 30)
      downloadTask1 = DownloadService.shared.download(request: request)
      downloadTask1?.completionHandler = { [weak self] in
         switch $0 {
         case .failure(let error):
            print(error)
         case .success(let data):
            print("Number of bytes: \(data.count)")
            self?.imageView1.image = NSImage(data: data)
         }
         self?.downloadTask1 = nil
         self?.loadImageButton1.isEnabled = true
      }
      downloadTask1?.progressHandler = { [weak self] in
         print("Task1: \($0)")
         self?.loadProgressIndicator1.doubleValue = $0
      }

      loadImageButton1.isEnabled = false
      imageView1.image = nil
      loadProgressIndicator1.doubleValue = 0
      downloadTask1?.resume()
   }

   @objc fileprivate func startDownload2(_ button: NSButton) {
      let url = URL(string: "http://localhost:8002/?imageID=02&tilestamp=\(Date.timeIntervalSinceReferenceDate)")!
      let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 30)
      downloadTask2 = DownloadService.shared.download(request: request)
      downloadTask2?.completionHandler = { [weak self] in
         switch $0 {
         case .failure(let error):
            print(error)
         case .success(let data):
            print("Number of bytes: \(data.count)")
            self?.imageView2.image = NSImage(data: data)
         }
         self?.downloadTask2 = nil
         self?.loadImageButton2.isEnabled = true
      }
      downloadTask2?.progressHandler = { [weak self] in
         print("Task2: \($0)")
         self?.loadProgressIndicator2.doubleValue = $0
      }

      loadImageButton2.isEnabled = false
      imageView2.image = nil
      loadProgressIndicator2.doubleValue = 0
      downloadTask2?.resume()
   }
}

Bonus 1. File StartPHPWebServer.command. Example script to run 2 Build-in PHP servers to simulate simultaneous downloads.

#!/bin/bash

AWLScriptDirPath=$(cd "$(dirname "$0")"; pwd)
cd "$AWLScriptDirPath"

php -S localhost:8001 &
php -S localhost:8002 &

ps -afx | grep php
echo "Press ENTER to exit."
read
killall php

Bonus 2. File index.php. Example PHP script to implement slow download.

<?php

$imageID = $_REQUEST["imageID"];
$local_file = "Image-$imageID.jpg";

$download_rate = 20.5; // set the download rate limit (=> 20,5 kb/s)
if (file_exists($local_file) && is_file($local_file)) {
    header('Cache-control: private');
    header('Content-Type: image/jpeg');
    header('Content-Length: '.filesize($local_file));

    flush();
    $file = fopen($local_file, "r");
    while(!feof($file)) {
        // send the current file part to the browser
        print fread($file, round($download_rate * 1024));
        flush(); // flush the content to the browser
        usleep(0.25 * 1000000);
    }
    fclose($file);}
else {
    die('Error: The file '.$local_file.' does not exist!');
}

?>

Misc: Contents of directory which simulates 2 PHP servers.

Image-01.jpg
Image-02.jpg
StartPHPWebServer.command
index.php
Vlad
  • 6,402
  • 1
  • 60
  • 74
  • 1
    Instead of the progressHandler using a Double, it could use the Progress class. Did you publish this on Github already? – aleene Jan 12 '18 at 15:47
  • Nothing on GitHub related to this code. No Gist, not Reusable library as the code quite simple .) – Vlad Jan 12 '18 at 15:50
  • 1
    @BorisNikolić URLSession classes are cross platform. UI code (View, ViewController, etc.) need to be replaced with UIKit ones. We are using similar code in our iOS project. – Vlad Mar 26 '18 at 20:28
  • @Vlad Thanks a lot, this piece of code saved me a lot of troubles and time. – Boris Nikolic Apr 13 '18 at 14:04
  • @Vlad If I want to get the location of the download file so that I could move it with the with the following `try FileManager.default.moveItem(at: location, to: destinationUrl)` how can I get that from your example? – MwcsMac Oct 02 '18 at 20:08
  • @MwcsMac To cover this source code needs to be adjusted. Instead of `session.dataTask(with: request)` the `session.downloadTask(with: request)` need to be used. Instead of `URLSessionDataDelegate` the `URLSessionDownloadDelegate` need to be used. Because you need to react on delegate callback `func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL)`. – Vlad Oct 03 '18 at 10:27
  • @vlad Use of unresolved identifier 'DownloadService' when using in ViewController and same for Use of unresolved identifier 'downloadTask1' – Davender Verma Jan 30 '20 at 06:32
  • @Vlad task.expectedContentLength. return -1 always – Davender Verma Feb 04 '20 at 06:39
  • @Vlad why do you suggest using `URLSessionDownloadTask` instead of `URLSessionDataTask`? They have some other differences but both support `Progress`, right? – Legonaftik Nov 16 '20 at 09:07
  • @Legonaftik In one of the previous comments there was a question how to move downloaded file. The `URLSessionDownloadTask` has a delegate call `urlSession(_:downloadTask:didFinishDownloadingTo:)` which contains URL of a temporary file. – Vlad Nov 16 '20 at 10:14
4

in class declare

class AudioPlayerViewController: UIViewController, URLSessionDelegate, URLSessionDataDelegate, URLSessionDownloadDelegate{


         var defaultSession: URLSession!
         var downloadTask: URLSessionDownloadTask!

in download method

            defaultSession = Foundation.URLSession(configuration: .default, delegate: self, delegateQueue: nil)
            downloadProgress.setProgress(0.0, animated: false)
            downloadTask = defaultSession.downloadTask(with: audioUrl)
            downloadTask.Resume()

And add following delegates:

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

    DispatchQueue.main.async {
        self.downloadProgressBar.setProgress(Float(totalBytesWritten)/Float(totalBytesExpectedToWrite), animated: true)
    }
}

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        DispatchQueue.main.async {

//DOWNLOAD SUCCESSFUL AND FILE PATH WILL BE IN URL.

}
}
Krishna Kirana
  • 438
  • 4
  • 10
2

In reference to Dharmesh's work, there have been quite a number of minor changes with URLSessionDelegate and URLSessionDataDelegate delegate methods. Here come the code for Swift 4.2 compatibility.

import UIKit

class ViewController: UIViewController, URLSessionDelegate, URLSessionDataDelegate {
    // MARK: - Variables

    // MARK: - IBOutlet
    @IBOutlet weak var progress: UIProgressView!

    // MARK: - IBAction
    @IBAction func goTapped(_ sender: UIButton) {
        let url = URL(string: "http://www.example.com/file.zip")!
        fetchFile(url: url)
    }

    // MARK: - Life cycle
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    func fetchFile(url: URL) {
        progress.progress = 0.0
        let configuration = URLSessionConfiguration.default
        let mainQueue = OperationQueue.main
        session = URLSession(configuration: configuration, delegate: self, delegateQueue: mainQueue)
        dataTask = session?.dataTask(with: URLRequest(url: url))
        dataTask?.resume()
    }

    var buffer: NSMutableData = NSMutableData()
    var session: URLSession?
    var dataTask: URLSessionDataTask?
    var expectedContentLength = 0

    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
        buffer.append(data)
        let percentageDownloaded = Float(buffer.length) / Float(expectedContentLength)
        progress.progress =  percentageDownloaded
    }

    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: (URLSession.ResponseDisposition) -> Void) {
        expectedContentLength = Int(response.expectedContentLength)
        completionHandler(URLSession.ResponseDisposition.allow)
    }

    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        progress.progress = 1.0
    }
}
El Tomato
  • 6,479
  • 6
  • 46
  • 75
0

for the data been download you need to set the NSURLSessionDownloadDelegate and to implement URLSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)

There is a nice tutorial about it here but is in object-c.

Icaro
  • 14,585
  • 6
  • 60
  • 75