Most applications need to fetch data from a remote server and downloading images is a very common task applications need to perform. In this series, I show you how to download images using Swift. We take a look at several solutions. I show you the pros and cons of each solution and, most importantly, which pitfalls to avoid.

Downloading an Image From a URL

This post focuses on the basics, that is, How does an application download an image from a URL in Swift? Let's start with a blank Xcode project. Select New > Project... from Xcode's File menu and choose the Single View App template from the iOS > Application section.

Setting Up the Project in Xcode 11

Name the project Images and set User Interface to Storyboard. Leave the checkboxes at the bottom unchecked. Tell Xcode where you would like to save the project and click the Create button.

Setting Up the Project in Xcode 11

We keep the application simple. Open ViewController.swift and create an outlet with name imageView of type UIImageView!, an implicitly unwrapped optional. The idea is simple. The view controller downloads an image from a URL and displays it in its image view.

import UIKit

class ViewController: UIViewController {

    // MARK: - Properties

    @IBOutlet var imageView: UIImageView!

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

Open Main.storyboard, click the Library button in the top right, and add an image view to the View Controller scene. Pin the image view to the edges of the view controller's view.

Creating the User Interface in Xcode 11

Select the view controller in the storyboard and open the Connections Inspector on the right. Connect the imageView outlet to the image view in the View Controller scene.

Before we move on, we need to configure the image view. Select the image view, open the Attributes Inspector on the right, and set Content Mode to Aspect Fit.

Downloading Image Data

With the user interface in place, we can focus on downloading an image and displaying it in the image view. Open ViewController.swift and navigate to the viewDidLoad() method. When it's appropriate for an application to download remote resources, such as images, differs from application to application. We download an image in the view controller's viewDidLoad() method, but this is usually not what you want.

We start by creating a URL object that points to the remote image. Your application usually obtains the URL of the remote image from an API of some sort. You should not hard code the URL of a remote resource in the project if you can avoid it.

Notice that we forced unwrap the result of the initialization. This is an example so we are not focused on safety.

// MARK: - View Life Cycle

override func viewDidLoad() {
    super.viewDidLoad()

    // Create URL
    let url = URL(string: "https://cdn.cocoacasts.com/cc00ceb0c6bff0d536f25454d50223875d5c79f1/above-the-clouds.jpg")!
}

Strategy 1: Using the Data Struct to Download Images

I want to show you two strategies to download an image from a remote server. The first strategy is as simple as it gets. We initialize a Data object by invoking the init(contentsOf:) initializer. Because the initializer is throwing, we use the try? keyword and optional binding to safely access the result of the initialization.

// MARK: - View Life Cycle

override func viewDidLoad() {
    super.viewDidLoad()

    // Create URL
    let url = URL(string: "https://cdn.cocoacasts.com/cc00ceb0c6bff0d536f25454d50223875d5c79f1/above-the-clouds.jpg")!

    // Fetch Image Data
    if let data = try? Data(contentsOf: url) {
        // Create Image and Update Image View
        imageView.image = UIImage(data: data)
    }
}

In the body of the if statement, the view controller uses the Data object to create a UIImage instance. It assigns the UIImage instance to the image property of its image view. That's it. Build and run the application in the simulator to see the result.

Download an Image From a URL in Swift

This looks fine, but there is one important problem. The image data is fetched synchronously and the operation blocks the main thread. Wait. What? This simply means that the execution of the application is interrupted as long as the image data is being downloaded. This may not seem like a big problem, but I can assure you that this is something you need to avoid at any cost. If the device suffers from a slow network connection, then this subtle issue turns into a major problem. As long as the application is downloading the remote resource, the user isn't able to interact with the application. That's a problem. Right?

Strategy 2: Using the URLSession API to Download Images

Let's take a look at the second strategy. We use the URLSession API to fetch the image data. This is a bit more involved, but it isn't complex. We obtain a reference to the shared URLSession instance through the shared class method. We invoke the dataTask(with:completionHandler:) method on the URLSession instance, passing in the URL object as the first argument and a closure as the second argument. We store the result of dataTask(with:completionHandler:) in a constant with name dataTask.

// MARK: - View Life Cycle

override func viewDidLoad() {
    super.viewDidLoad()

    // Create URL
    let url = URL(string: "https://cdn.cocoacasts.com/cc00ceb0c6bff0d536f25454d50223875d5c79f1/above-the-clouds.jpg")!

    // Create Data Task
    let dataTask = URLSession.shared.dataTask(with: url) { (data, _, _) in

    }
}

The completion handler, a closure, is executed when the request to fetch the image data completes, successfully or unsuccessfully. The closure accepts three arguments. We are interested in the first argument for now, the Data object that holds the image data. In the closure, we safely unwrap the Data object and use it to create a UIImage instance. We use the UIImage instance to update the image property of the view controller's image view.

The closure keeps a strong reference to self, the view controller. That isn't what we want, though. We use a capture list to weakly reference self in the closure.

// MARK: - View Life Cycle

override func viewDidLoad() {
    super.viewDidLoad()

    // Create URL
    let url = URL(string: "https://cdn.cocoacasts.com/cc00ceb0c6bff0d536f25454d50223875d5c79f1/above-the-clouds.jpg")!

    // Create Data Task
    let dataTask = URLSession.shared.dataTask(with: url) { [weak self] (data, _, _) in
        if let data = data {
            // Create Image and Update Image View
            self?.imageView.image = UIImage(data: data)
        }
    }
}

To start the data task, we invoke resume() on the URLSessionDataTask instance.

// MARK: - View Life Cycle

override func viewDidLoad() {
    super.viewDidLoad()

    // Create URL
    let url = URL(string: "https://cdn.cocoacasts.com/cc00ceb0c6bff0d536f25454d50223875d5c79f1/above-the-clouds.jpg")!

    // Create Data Task
    let dataTask = URLSession.shared.dataTask(with: url) { [weak self] (data, _, _) in
        if let data = data {
            // Create Image and Update Image View
            self?.imageView.image = UIImage(data: data)
        }
    }

    // Start Data Task
    dataTask.resume()
}

There is one last problem we need to resolve. The completion handler we pass to the dataTask(with:completionHandler:) method is executed on a background thread. You probably know that the user interface should always be updated from the main thread. Fortunately, the solution is simple. We use Grand Central Dispatch to update the image view on the main thread.

// MARK: - View Life Cycle

override func viewDidLoad() {
    super.viewDidLoad()

    // Create URL
    let url = URL(string: "https://cdn.cocoacasts.com/cc00ceb0c6bff0d536f25454d50223875d5c79f1/above-the-clouds.jpg")!

    // Create Data Task
    let dataTask = URLSession.shared.dataTask(with: url) { [weak self] (data, _, _) in
        if let data = data {
            DispatchQueue.main.async {
                // Create Image and Update Image View
                self?.imageView.image = UIImage(data: data)
            }
        }
    }

    // Start Data Task
    dataTask.resume()
}

Like I said, the second strategy is more involved, but it is more robust and doesn't block the main thread. We can fix the issue we encountered using the first strategy by creating the Data object on a background thread, using Grand Central Dispatch. The solution looks something like this.

// MARK: - View Life Cycle

override func viewDidLoad() {
    super.viewDidLoad()

    // Create URL
    let url = URL(string: "https://cdn.cocoacasts.com/cc00ceb0c6bff0d536f25454d50223875d5c79f1/above-the-clouds.jpg")!

    DispatchQueue.global().async {
        // Fetch Image Data
        if let data = try? Data(contentsOf: url) {
            DispatchQueue.main.async {
                // Create Image and Update Image View
                self.imageView.image = UIImage(data: data)
            }
        }
    }
}

Notice that we still update the image view from the main thread using Grand Central Dispatch. It's a small but important detail.