Tracking Download Progress with URLSessionDownloadDelegate

Tracking Download Progress with URLSessionDownloadDelegate

How do you show download progress to your users?

Downloading ....

You've this cool app that has to download something from the internet, be it images, data or pdf files or whatever. While your app downloads the required resources, your user has to wait. Now this isn't an issue if the resources are small or can be fetched really fast. What if the process isn't fast enough or the resource is very large (say size in MB in double or even triple digits!)? Your users will have to stare at a blank screen with a probably unresponsive app (depending on how you handled resource loading) which is a deal breaker and may lead users away from your app. You must do something to assure the users that the required resource is indeed loading and the app isn't stuck at some deadlock. Show them some progress. Animation, text, percentage labels - whatever goes with your app. Question is, how do you track the download progress to show to your users?

URLSessionDownloadDelegate

You can track download progress in your Swift app using URLSessionDownloadDelegate functions. What you have to do is -

  • Create an URLSession with a .default configuration
  • Using the created session, init a downloadTask
  • Track the progress of that downloadTask with URLSessionDownloadDelegate function -
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)
  • Yes you've to implement the function
  • There's another function you'll have to implement, which fires up when your download task is complete -
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL)

Let's start!

We start by creating a single view iOS application in Xcode. What this app is going to do is: download an image from a url, show the progress in a label with percentage and finally set the downloaded image to an image view (so that we can be sure that the image actually downloaded!). In the end it should look like this -

final state of the app

Interface

Open up storyboard and add 3 controls - an imageview, a label and a button, using any constraints you like. (Constraints aren't necessary for our discussion at the moment) Take the image below for reference.

reference storyboard interface

Create the necessary outlets and add an action for the button (ctrl + drag).

class ViewController: UIViewController, URLSessionDownloadDelegate {
    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var percentageLabel: UILabel!
    @IBOutlet weak var startButton: UIButton!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    // MARK: event handler
    @IBAction func startButtonTapped(_ sender: Any) {
		// code goes here
    }
}

We also have to set the content mode for imageview so that the downloaded image fits in properly. You can either do it from the storyboard or just with code inside viewDidLoad like I did.

override func viewDidLoad() {
    super.viewDidLoad()
	// fit image to the view
    imageView.contentMode = .scaleAspectFill
}

Now for the main course

Creating the downloadTask

Let's add a function named download to our ViewController class. It's going to take the url of the image as the only parameter.

// MARK: fetch image from url
func download(from url: URL) {
    let configuration = URLSessionConfiguration.default
    let operationQueue = OperationQueue()
    let session = URLSession(configuration: configuration, delegate: self, delegateQueue: operationQueue)
        
    let downloadTask = session.downloadTask(with: url)
    downloadTask.resume()
}

Let's check what we've done here. We've created a default config for the URLSession. Next we've created an OperationQueue so that our downloadTask can run asynchronously without locking up the app (i.e. causing a deadlock). (To know more, read on GCD in docs). This will ensure that the other parts of our app will be accessible to the users while the app downloads the image. Finally we're creating the download task using the session and asking it to resume / start operation. For image url, we're going to use this one - https://upload.wikimedia.org/wikipedia/commons/thumb/a/aa/Polarlicht_2.jpg/1920px-Polarlicht_2.jpg?1568971082971. \

Now we have a slight problem. Our function requires an URL but we've that as String. So, we need to convert the string to a URL.

// MARK: prepare url from string
func getURLFromString(_ str: String) -> URL? {
    return URL(string: str)
}

Starting the download

We'll be calling the download function when a user taps the button.

// MARK: event handler
@IBAction func startButtonTapped(_ sender: Any) {
    let url = "https://upload.wikimedia.org/wikipedia/commons/thumb/a/aa/Polarlicht_2.jpg/1920px-Polarlicht_2.jpg?1568971082971"
        
    if let imageURL = getURLFromString(url) {
        download(from: imageURL)
    }
}

Where's the progress at?

We do have a progressLabel, and we can update its text property every time downloadTask makes progress. But the question is where do we update it? We've two delegate functions to implement right? Let's implement the first one and put the label updating code in there.

For download task, URLSessionDownloadDelegate lets you track the amount of bytes to be received and the amount of bytes being received currently. Using these two metrics, we can calculate the percentage.

// MARK: protocol stub for tracking download progress
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        
    let percentDownloaded = totalBytesWritten / totalBytesExpectedToWrite
        
    // update the percentage label
    DispatchQueue.main.async {
        self.percentageLabel.text = "\(percentDownloaded * 100)%"
    }
}

That'll take care of tracking download progress, what we've chasing so far. But wait, we're not done yet.

Side Concerns - download done, how to set the image then?

By default, resources downloaded with downloadTask end up in the documents bundle of the app which can be accessed using FileHandle. For that, we need the URL to the file, where do we get that? Remember the second protocol function? That'll give us the URL of the file.

// MARK: protocol stub for download completion tracking
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        
    // get downloaded data from location
        
    // set image to imageview
}

Accessing downloaded data

We'll be adding a function readDownloadedData which reads the downloaded resource as raw data and returns it. The function returns an optional for safety.

// MARK: read downloaded data
func readDownloadedData(of url: URL) -> Data? {
    do {
        let reader = try FileHandle(forReadingFrom: url)
        let data = reader.readDataToEndOfFile()
            
        return data
    } catch {
        print(error)
        return nil
    }
}

Now that we've the raw data, we can use that to create a UIImage to set to our imageview.

UIImage from Data?

func getUIImageFromData(_ data: Data) -> UIImage? {
    return UIImage(data: data)
}

Setting the image

// MARK: set image to image view
func setImageToImageView(from data: Data?) {
    guard let imageData = data else { return }
    guard let image = getUIImageFromData(imageData) else { return }
        
    DispatchQueue.main.async {
        self.imageView.image = image
    }
}

Completing the protocol function

// MARK: protocol stub for download completion tracking
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        
    // get downloaded data from location
    let data = readDownloadedData(of: location)
        
    // set image to imageview
    setImageToImageView(from: data)
}

And we're done! When you run the app in Simulator or your Device, expect to see similar results like the gif I showed earlier.

Aaaaand......

And that's how you track download progress in your Swift app using URLSessionDownloadDelegate without adding any extra dependencies to your app. You can also do the same thing with libs like Alamofire, with less code and headache but that'll add and extra dependency which you may or may not want!

Code - Where at?

You can find all the code for this post @URLSessionProgressTrackerExample

Some other posts you may also like...

The curious genericness of Associated Types

The curious genericness of Associated Types

To associate or not to associate is the question

Swift - PAT or Generics?

Swift - PAT or Generics?

Both do the same job, so, which one to use then?

Loading images from URL in Swift

Loading images from URL in Swift

Like they do in HTML?