Categories
iOS macOS Swift SwiftUI

Image converter with AsyncSequence

WWDC’21 brought us a new protocol named AsyncSequence. As the name stands, it represents a sequence of asynchronous elements. For trying out the new API we’ll build a tiny ThumbnailSequence which takes in a list of image names and by iterating the sequence, we’ll get back scaled thumbnails for those image names one by one. The image scaling runs on a background thread.

The AsyncSequence protocol comes with two associated types: Element and AsyncIterator. Element represents the type which is produced by the sequence, and AsyncIterator is the type responsible for reproducing elements. Same as with sequences, but the main difference is that accessing each of the element is asynchronous. Therefore, for creating a custom type ThumbnailSequence which conforms to AsyncSequence we’ll, set the associated type Element to be equal to UIImage, and implement a custom iterator. ThumbnailSequence initializers takes a list of image names and also defines a max scaled image size. Additionally, we’ll take advantage of the new byPreparingThumbnail(ofSize) async method for scaling the image. Implementation of the async sequence is shown below:

struct ThumbnailSequence: AsyncSequence {
typealias AsyncIterator = Iterator
typealias Element = UIImage
var imageNames: [String]
let maxSize: CGSize
func makeAsyncIterator() -> Iterator {
return Iterator(imageNames: imageNames, maxSize: maxSize)
}
struct Iterator: AsyncIteratorProtocol {
typealias Element = UIImage
var imageNames: [String]
let maxSize: CGSize
mutating func next() async -> UIImage? {
guard !imageNames.isEmpty else { return nil }
guard let image = UIImage(named: imageNames.removeFirst()) else { return nil }
let ratio = image.size.height / maxSize.height // simplified scaling
return await image.byPreparingThumbnail(ofSize: CGSize(width: image.size.width / ratio, height: maxSize.height))
}
}
}

With a simple async sequence created, we can hook it up to a SwiftUI view. WWDC’21 also brought a new task view modifier, which is invoked when the view appears and cancelled when the view is removed. In the task view modifier we’ll loop over the sequence and one by one load UIImages which then are set to a local images array which in turn is connected to a LazyVStack. The flow we’ll get is that we are loading images one by one, and after every image load we’ll add a new item to the stack.

struct ContentView: View {
let imageNamesToLoad = ["Screenshot1", "Screenshot2", "Screenshot3", "Screenshot4", "Screenshot5", "Screenshot6", "Screenshot7", "Screenshot8", "Screenshot9", "Screenshot10"]
@State private var images = [UIImage]()
var body: some View {
ScrollView {
LazyVStack {
ForEach(images, id: \.self) { image in
Image(uiImage: image)
}
}
}
.task {
for await image in ThumbnailSequence(imageNames: imageNamesToLoad, maxSize: CGSize(width: 256, height: 256)) {
print("Loaded \(image)")
self.images.append(image)
}
}
}
}
Screenshot of a iPhone simulator showing a view with vertical list of images.
The final sample app displaying images in a vertical stack.

Summary

In this post, we took a quick look at the AsyncSequence protocol and created a pipeline which converts image names to scaled image instances one by one. After that, we connected the pipeline to a SwiftUI view.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s