Combining Actors with blazing fast lists

Hi,

I really enjoyed both sessions as they helped me improve loading images in a collection view. I am currently experimenting with both codes and I'd like to combine them.

As far as I understand from the "Make blazing fast lists and collection views" session there are key considerations:

  1. In the cell registration get the current asset for your model
  2. If it is a placeholder, download the asset and afterwards reconfigure the cell. This is due to cells potentially being reused already.
  3. If it is not a placeholder, we can just assign it to the cell's image view

From the other talk about "protecting mutable state with Swift actors", I found out about protecting the image cache by using actors and that one should be aware of changed state after re-entry from an await.

Now I am trying to combine the two talks, which is a bit difficult if I want to make sure that I use it correctly, so here are my ideas:

  1. I want an ImageLoader actor, that protects its cache state
  2. By using an actor, both reading from the cache and storing the cache become async operations (since if the cache is currently used, we need to "await" our turn to read from it)
  3. By doing this, in our cell registration, we can no longer fetch the asset synchronously and check if it is a placeholder or not.
  4. This means that even if we already have a cached image, the re-entry might be later and the cell might already be reused and therefore we can't just set the imageView image.
  5. Moreover we cannot call reconfigure of the cell registration (via the data source snapshot) anymore, since this would put us in an infinite loop.

Additionally, preparing the image before displaying it also is an async operation. And since those images are larger in memory, I'd want to avoid caching images - I'd rather prepare them in the cell registration handler, which would make it async as well.

Here is my code:

cell.artworkView.image = UIImage(named: "DefaultArtwork")
            
async {
	let asset: AlbumAsset = await self.assetStore.asset(forURL: artworkURL)

    switch asset {
        case .placeholder:
            await self.assetStore.downloadAsset(forURL: artworkURL)
            self.reconfigureAlbum(albumID)
        case .image(let image):
            let thumbnailImage = await image.byPreparingThumbnail(ofSize: .init(width: 100, height: 100))
            DispatchQueue.main.async {
            	cell.artworkView.image = thumbnailImage // Problem
            }
	}
}

Any help is appreciated as to how I can improve the situation so I can use actors, while still following the rules for updating a cell.

I'd need something like a synchronous way of getting images, while having an asynchronous way of downloading and preparing images, and maintaining mutable state (cache).

Thanks for any help.

Replies

First of all, I'd cache the prepared the thumbnail, possibly in a separate cache or make your asset store layered in a way that you just request a URL and it will supply a prepared image or load + prepare it for you. With separate caches, you can restrict the sizes separately (look into using NSCache for this).

Secondly, a call being async doesn't mean it will suspend 100% of the time when called. For instance, if your asset(forURL:) function doesn't have to await in the code path that returns .image, then the call is essentially synchronous. Similarly, you should mark this view controller and cell as @MainActor so you can remove the dispatch async to main within your async {}. The task will inherit the @MainActor from the context that creates the task.

To get the behavior you want, you need to hold onto the task handle that async {} returns so you can cancel it when the cell is reused.