Article

Downloading Files from Websites

Download files directly to the filesystem.

Overview

For network resources that are already stored as files, like images and documents, you can use download tasks to fetch these items directly to the local filesystem.

For Simple Downloads, Use a Completion Handler

To download files, you create an NSURLSessionDownloadTask from an NSURLSession. If you don’t care about receiving progress updates or other delegate callbacks during the download, you can use a completion handler. The task calls the completion handler when the download ends, either at the end of a successful download or when downloading fails.

Your completion handler may receive a client-side error, indicating a local problem like not being able to reach the network. If there is no client-side error, you also receive an NSURLResponse, which you should inspect to ensure that it indicates a successful response from the server.

If the download is successful, your completion handler receives a URL indicating the location of the downloaded file on the local filesystem. This storage is temporary. If you want to preserve the file, you must copy or move it from this location before returning from the completion handler.

Listing 1 shows a simple example of creating a download task with a completion handler. If no errors are indicated, the completion handler moves the downloaded file to the app’s Documents directory. Start the task by calling resume.

Listing 1

Creating a download task with a completion handler

let downloadTask = URLSession.shared.downloadTask(with: url) {
    urlOrNil, responseOrNil, errorOrNil in
    // check for and handle errors:
    // * errorOrNil should be nil
    // * responseOrNil should be an HTTPURLResponse with statusCode in 200..<299
    
    guard let fileURL = urlOrNil else { return }
    do {
        let documentsURL = try
            FileManager.default.url(for: .documentDirectory,
                                    in: .userDomainMask,
                                    appropriateFor: nil,
                                    create: false)
        let savedURL = documentsURL.appendingPathComponent(
            fileUrl.lastPathComponent)
        try FileManager.default.moveItem(at: fileUrl, to: savedURL)
    } catch {
        print ("file error: \(error)")
    }
}
downloadTask.resume()

To Receive Progress Updates, Use a Delegate

If you want to receive progress updates as the download proceeds, you must use a delegate. Instead of receiving the results in a completion handler, you receive callbacks to your implementations of methods from the NSURLSessionTaskDelegate and NSURLSessionDownloadDelegate protocols.

Create your own NSURLSession instance, and set its delegate property. Listing 2 shows a lazily instantiated urlSession property that sets self as its delegate.

Listing 2

Creating a URL session with a delegate

private lazy var urlSession = URLSession(configuration: .default,
                                         delegate: self,
                                         delegateQueue: nil)

To start downloading, use this NSURLSession to create a NSURLSessionDownloadTask, and then start the task by calling resume, as shown in Listing 3.

Listing 3

Creating and starting a download task that uses a delegate

private func startDownload(url: URL) {
    let downloadTask = urlSession.downloadTask(with: url)
    downloadTask.resume()
    self.downloadTask = downloadTask
}

Receive Progress Updates

Once the download starts, you receive periodic progress updates in the NSURLSessionDownloadDelegate method URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:. You can use the byte counts provided by this callback to update a progress UI in your app.

Listing 4 shows an implementation of this callback method. This implementation calculates the fractional progress of the download, and uses it to update a label that shows progress as a percentage. Because the callback is performed on an unknown Grand Central Dispatch queue, you must explicitly perform the UI update on the main queue.

Listing 4

Using a delegate method to update download progress in a UI

func urlSession(_ session: URLSession,
                downloadTask: URLSessionDownloadTask,
                didWriteData bytesWritten: Int64,
                totalBytesWritten: Int64,
                totalBytesExpectedToWrite: Int64) {
     if downloadTask == self.downloadTask {
        let calculatedProgress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
        DispatchQueue.main.async {
            self.progressLabel.text = self.percentFormatter.string(from:
                NSNumber(value: calculatedProgress))
    }
}

Handle Download Completion or Errors in Your Delegate

When you use a delegate instead of a completion handler, you handle the completion of the download by implementing URLSession:downloadTask:didFinishDownloadingToURL:. Check the downloadTask’s response property to ensure that the server response indicates success. If so, the location parameter provides a local URL where the file has been stored. This location is valid only until the end of the callback. This means you must either read the file immediately, or move it to another location such as the app’s Documents directory before you return from the callback method. Listing 5 shows how to preserve the downloaded file.

Listing 5

Saving the downloaded file in the delegate callback

func urlSession(_ session: URLSession,
                downloadTask: URLSessionDownloadTask,
                didFinishDownloadingTo location: URL) {
    // check for and handle errors:
    // * downloadTask.response should be an HTTPURLResponse with statusCode in 200..<299

    do {
        let documentsURL = try
            FileManager.default.url(for: .documentDirectory,
                                    in: .userDomainMask,
                                    appropriateFor: nil,
                                    create: false)
        let savedURL = documentsURL.appendingPathComponent(
            location.lastPathComponent)
        try FileManager.default.moveItem(at: location, to: savedURL)
    } catch {
        // handle filesystem error
    }
}

If a client-side error occurs, your delegate receives it in a callback to the URLSession:task:didCompleteWithError: delegate method. On the other hand, if the download completes successfully, this method is called after URLSession:downloadTask:didFinishDownloadingToURL: and the error is nil.

See Also

Downloading

Pausing and Resuming Downloads

Allow the user to resume a download without starting over.

Downloading Files in the Background

Create tasks that download files while your app is inactive.