Introduction
SwiftUI provides us with AsyncImage to asynchronously load images. However, when it comes to loading multiple images, a ScrollView or a ListView will start deallocating images, forcing the images to load again. This makes your UI look glitchy, and far from smooth to the user. This component will allow you to load images in the background, cache them, and replicating SwiftUI's component.
CachedAsyncImage
The CachedAsyncImage
is a SwiftUI View that encapsulates the logic for asynchronously loading and displaying images. Receives content and placeholder just as a regular AsyncImage
in SwiftUI. Let's take a closer look at the code:
import Foundation
import SwiftUI
struct CachedAsyncImage<Content, Placeholder>: View where Content: View, Placeholder: View {
@ViewBuilder private let content: (Image) -> Content
@ViewBuilder private let placeholder: () -> Placeholder
@ObservedObject var imageLoader: ImageLoader
init(
urlString: String,
@ViewBuilder content: @escaping (Image) -> Content,
@ViewBuilder placeholder: @escaping () -> Placeholder
) {
imageLoader = ImageLoader(urlString: urlString)
self.content = content
self.placeholder = placeholder
}
var body: some View {
if let image = imageLoader.image {
content(Image(uiImage: image))
} else {
placeholder()
}
}
}
ImageLoader
The ImageLoader
class handles the asynchronous loading of images from a given URL. It utilizes Combine framework's Published
property wrapper to update the UI whenever the image is loaded.
import Combine
import UIKit
final class ImageLoader: ObservableObject {
@Published var image: UIImage?
private var urlString: String
private var task: URLSessionDataTask?
init(urlString: String) {
self.urlString = urlString
loadImage()
}
private func loadImage() {
if let cachedImage = ImageCache.shared.get(forKey: urlString) {
self.image = cachedImage
return
}
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { [weak self, urlString] data, response, error in
guard let self, let data = data, error == nil else { return }
DispatchQueue.main.async {
if let image = UIImage(data: data) {
self.image = image
ImageCache.shared.set(image, forKey: urlString)
}
}
}
.resume()
}
}
ImageCache
As the name implies, the ImageCache
class is responsible only for caching images to improve performance and reduce redundant network requests.
import Foundation
import UIKit
final class ImageCache {
static let shared = ImageCache()
private let cache = NSCache<NSString, UIImage>()
private init() {}
func set(_ image: UIImage, forKey key: String) {
cache.setObject(image, forKey: key as NSString)
}
func get(forKey key: String) -> UIImage? {
return cache.object(forKey: key as NSString)
}
}
Putting all together
This example shows how to use CachedAsyncImage
with a content closure for displaying the loaded image and a placeholder closure for displaying a loading indicator. Here's the gist if you want to take a look at the source code.
Thanks for reading. I hope you have enjoyed this small piece of code, and if it was useful for you, don’t be shy to leave a like, and a comment. See you next time.