Image Caching in Swift

Image Caching with Kingfisher

Image Caching in Swift
1 Cancelling Image Requests 10:19
2 Caching Images in Memory Plus 09:44
3 Caching Images on Disk Plus 09:27
4 Asynchronously Reading Data from Disk Plus 10:41
5 Limiting the Cache on Disk Plus 12:41
6 Image Caching with Kingfisher 10:48
Download Your Free Copy of
The Missing Manual
for Swift Development

The Guide I Wish I Had When I Started Out

Join 20,000+ Developers Learning About Swift Development

Download Your Free Copy

In the previous episodes, we implemented a service to fetch and cache remote images. Even though the service we built is pretty flexible, some applications require a more powerful solution and more options to fit their needs.

This episode focuses on Kingfisher, a popular, open source library to fetch and cache remote images. You learn how to integrate Kingfisher in a project using CocoaPods. I show you how Kingfisher differs from the service we created earlier in this series and we take a peek under the hood to learn how Kingfisher does its magic.

Starter Project

Fire up Xcode and open the starter project of this episode if you want to follow along. The starter project of this episode is identical to that of the first episode of this series.

Remember that the project has a few shortcomings. First, the application doesn't cache the remote images it fetches. It fetches the same image multiple times as the user scrolls the table view. Second, the application suffers from a race condition. Fetching a remote image is an asynchronous operation that takes time. Multiple requests are initiated as the user scrolls the table view and it isn't possible to predict which requests finish when. It can happen that a request for a table view cell that is no longer visible completes after a request for a table view cell that is visible completes. In other words, it is possible that a table view cell displays the wrong image. Kingfisher can help us address these issues.

Installing Kingfisher Using CocoaPods

You can integrate Kingfisher into a project using CocoaPods, Carthage, or the Swift Package Manager. In this episode, I show you how to install Kingfisher using CocoaPods. Open Terminal and navigate to the root of the project. Make sure CocoaPods is installed and run the pod init command to create a Podfile for the project.

pod init

Open the project's Podfile, set the deployment target for the iOS platform to 12.0, and add Kingfisher as a dependency of the Landscapes target.

platform :ios, '12.0'

target 'Landscapes' do
  use_frameworks!

  pod 'Kingfisher'
end

Close the project and run the pod install command to install the project's dependencies. Open the workspace CocoaPods created for us.

Integrating Kingfisher

Open LandscapesViewController.swift and add an import statement for the Kingfisher library at the top.

import UIKit
import Kingfisher

final class LandscapesViewController: UIViewController {

    ...

}

Navigate to the tableView(_:cellForRowAt:) method. We no longer put the landscapes view controller in charge of fetching the remote image for each table view cell. Let's move that responsibility to Kingfisher. We first create a builder object by invoking the url(_:cacheKey:) method on the KF enum. The KF enum defines no cases. It acts as a namespace for a number of public, static methods. The url(_:cacheKey:) method returns the builder object. We invoke the set(to:) method on the builder object, passing in the thumbnail image view of the table view cell.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: LandscapeTableViewCell.reuseIdentifier, for: indexPath) as? LandscapeTableViewCell else {
        fatalError("Unable to Dequeue Landscape Table View Cell")
    }

    // Fetch Landscape
    let landscape = landscapes[indexPath.row]

    // Configure Cell
    cell.configure(title: landscape.title)

    // Fetch Image for Landscape
    KF.url(landscape.imageUrl)
        .set(to: cell.thumbnailImageView)

    return cell
}

Build and run the application. Scroll the table view up and down to see the result of this simple change. Kingfisher takes care of fetching and caching the remote images the landscapes view controller displays in its table view. We addressed the first issue. Let's focus on the second issue next.

Cancelling Image Requests

To address the race condition, the application needs to cancel the image request for a table view cell that is no longer visible. You learned how to do that earlier in this series. We need to make a few changes.

Open LandscapeTableViewCell.swift and add an import statement for the Kingfisher library at the top.

import UIKit
import Kingfisher

final class LandscapeTableViewCell: UITableViewCell {

    ...

}

Update the configure(title:) method to also accept the URL of the landscape's image. In the body of the configure(title:imageUrl:) method, we use Kingfisher to fetch the remote image and update the table view cell's thumbnail image view.

// MARK: - Public API

func configure(title: String, imageUrl: URL) {
    // Configure Title Label
    titleLabel.text = title

    // Animate Activity Indicator View
    activityIndicatorView.startAnimating()

    // Fetch Image for Landscape
    KF.url(imageUrl)
        .set(to: thumbnailImageView)
}

The set(to:) method of the builder object returns an object of type DownloadTask?. If Kingfisher is able to return the remote image from cache, the set(to:) method returns nil. If Kingfisher needs to make a request to fetch the remote image, it returns an object of type DownloadTask. The DownloadTask struct defines a cancel() method to cancel the request to fetch the remote image.

Revisit LandscapeTableViewCell.swift and define a private, variable property, downloadTask, of type DownloadTask?.

private var downloadTask: DownloadTask?

In the configure(title:imageUrl:) method, we assign the DownloadTask object the builder returns to the downloadTask property. Because the downloadTask property has an optional type, the table view cell doesn't need to unwrap the return value of the set(to:) method.

// MARK: - Public API

func configure(title: String, imageUrl: URL) {
    // Configure Title Label
    titleLabel.text = title

    // Animate Activity Indicator View
    activityIndicatorView.startAnimating()

    // Fetch Image for Landscape
    downloadTask = KF.url(imageUrl)
        .set(to: thumbnailImageView)
}

To cancel the download task, the table view cell invokes the cancel() method of the DownloadTask object that is stored in the downloadTask property in its prepareForReuse() method.

// MARK: - Overrides

override func prepareForReuse() {
    super.prepareForReuse()

    // Reset Thumbnail Image View
    thumbnailImageView.image = nil

    // Cancel Download Task
    downloadTask?.cancel()
}

Revisit LandscapesViewController.swift and remove the import statement for the Kingfisher library at the top.

import UIKit

final class LandscapesViewController: UIViewController {

    ...

}

Navigate to the tableView(_:cellForRowAt:) method and pass the URL of the landscape's image to the configure(title:imageUrl:) method of the table view cell. We can also remove the fetchImage(with:completion:) method. We no longer need it.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: LandscapeTableViewCell.reuseIdentifier, for: indexPath) as? LandscapeTableViewCell else {
        fatalError("Unable to Dequeue Landscape Table View Cell")
    }

    // Fetch Landscape
    let landscape = landscapes[indexPath.row]

    // Configure Cell
    cell.configure(title: landscape.title,
                   imageUrl: landscape.imageUrl)

    return cell
}

Open LandscapeTableViewCell.swift. Because the landscape table view cell uses Kingfisher to request the landscape's image, the thumbnail image view no longer needs to be exposed. We can declare it privately.

@IBOutlet private var thumbnailImageView: UIImageView! {
    didSet {
        // Configure Thumbnail Image View
        thumbnailImageView.contentMode = .scaleAspectFill
    }
}

I want to point out that the Kingfisher library exposes or leaks some of its implementation. Its set(to:) method returns an object of type DownloadTask?. By returning nil, Kingfisher communicates that the remote image is cached. This is a design decision that adds flexibility. Remember that the ImageService class doesn't leak its implementation by not exposing this information.

Associated Objects

There is another option to fetch and display a remote image in an image view using the Kingfisher library. This option is more convenient and it is the option I usually use. The Kingfisher library defines an extension for UIImageView in a kf namespace. The result is a simple, straightforward API. To display a remote image in the image view, we invoke the setImage(_:) method, passing in the URL of the remote image. Both solutions, the builder and the extension for UIImageView, have plenty of options to meet the requirements of your project.

// MARK: - Public API

func configure(title: String, imageUrl: URL) {
    // Configure Title Label
    titleLabel.text = title

    // Animate Activity Indicator View
    activityIndicatorView.startAnimating()

    // Builder
    // downloadTask = KF.url(imageUrl)
    //     .set(to: thumbnailImageView)

    // Extension
    thumbnailImageView.kf.setImage(with: imageUrl)
}

To cancel the download task, we invoke the cancelDownloadTask() method in the table view cell's prepareForReuse() method.

// MARK: - Overrides

override func prepareForReuse() {
    super.prepareForReuse()

    // Reset Thumbnail Image View
    thumbnailImageView.image = nil

    // Builder
    // downloadTask?.cancel()

    // Extension
    thumbnailImageView.kf.cancelDownloadTask()
}

Even though Kingfisher is a Swift library, it relies on the Objective-C runtime to make this possible. The Kingfisher library uses associated objects to associate a download task to an image view. As you know, it isn't possible to add properties to types using extensions. Objective-C's associated objects work around this limitation. I don't recommend this approach, though. Associated objects can result in severe problems when used incorrectly.

Caching Images

The most compelling feature of Kingfisher is caching. Each image is fetched once and that is why scrolling the table view is fast and performant. Kingfisher caches images in memory for fast access, but it also writes images to disk. Its behavior is similar to that of the ImageService class we created earlier in this series. Writing images to disk is useful to avoid fetching the same image every time the application launches. It also prevents Kingfisher from using too much memory.

Kingfisher's defaults are fine for most applications, but know that it is possible to configure the library's caching strategy. Open AppDelegate.swift and add an import statement for the Kingfisher library at the top.

import UIKit
import Kingfisher

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    ...

}

We access Kingfisher's cache through the default static property of the ImageCache class. The in-memory cache can be accessed through the memoryStorage property. The on-disk cache can be accessed through the diskStorage property.

// MARK: - Application Life Cycle

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Configure Kingfisher's Cache
    let cache = ImageCache.default

    return true
}

The library exposes a rich API to query and configure Kingfisher's cache. Let's limit the in-memory cache to ten megabytes. We access the in-memory cache through the memoryStorage property and set totalCostLimit of the config property to ten megabytes. Limiting the in-memory cache can be useful for lightweight applications, such as watchOS applications or application extensions.

// MARK: - Application Life Cycle

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Configure Kingfisher's Cache
    let cache = ImageCache.default

    // Constrain In-Memory Cache to 10 MB
    cache.memoryStorage.config.totalCostLimit = 1024 * 1024 * 10

    return true
}

We can do the same for the on-disk cache by setting sizeLimit of the config property.

// MARK: - Application Life Cycle

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Configure Kingfisher's Cache
    let cache = ImageCache.default

    // Constrain Memory Cache to 10 MB
    cache.memoryStorage.config.totalCostLimit = 1024 * 1024 * 10

    // Constrain Disk Cache to 100 MB
    cache.diskStorage.config.sizeLimit = 1024 * 1024 * 100

    return true
}

Scratching the Surface

We only scratched the surface in this episode. Even though Kingfisher claims to be a lightweight library, it is packed with features. The beauty of Kingfisher is that it is straightforward to get started with, but there are countless options to configure its behavior to meet the needs of your project.

What's Next?

Kingfisher is a popular, open source library for fetching and caching remote images and it makes sense to include it in a project that needs these features. That said, it is a dependency and that always introduces risk. If you only need a basic caching strategy, then it may make more sense to roll your own solution.

Download Your Free Copy of
The Missing Manual
for Swift Development

The Guide I Wish I Had When I Started Out

Join 20,000+ Developers Learning About Swift Development

Download Your Free Copy