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.