Pickel 1.0.0: a small, dependency-free image loader built for Swift 6 concurrency

Hi everyone :waving_hand:

I just released Pickel 1.0, a lightweight image loading library for SwiftUI and UIKit. I’d love feedback from people who’ve used or built image loaders.

Why another image loader?
I wanted something small that was written from the start for async/await and Swift 6 strict concurrency, instead of having concurrency added later. Pickel compiles in Swift 6 language mode, has zero data-race safety errors on the Swift Package Index and has no dependencies.

What it does

  • Memory and disk caching (each image downloads once; requests for the same URL share one download)
  • Downsampling to the size the image is shown at, so large photos don’t take up a lot of memory
  • Processors: resize, rounded corners, circle
  • Prefetching for lists and grids
  • Basic safety: only http/https/file URLs, a 50 MB limit per response, and a 24-megapixel limit when decoding
  • iOS 17+, macOS 14+, tvOS 17+, visionOS 1+

SwiftUI

PickelImage(url: url) { image in
    image.resizable().scaledToFill()
} placeholder: {
    ProgressView()
} failure: {
    Image(systemName: "photo")
}

UIKit (old loads are cancelled when cells are reused)

imageView.pickel.setImage(with: url, placeholder: UIImage(named: "placeholder"))

async/await

let image = try await ImagePipeline.shared.image(for: url)

Links

Feedback I’m looking for

  • What does the API get wrong or leave out compared with what you use today?
  • Which features would you need before using it in a real app? (Animated GIF/WebP and an NSImageView extension are what I’m considering next.)
  • Anything about how the pipeline handles concurrency that looks wrong to you

Thanks for taking a look!

1 Like

One edge case I'd want to test before using this for authenticated images is switching between user accounts.

Looking at ImageRequest.swift, it seems dataCacheKey is based on url.absoluteString. And ImageDataLoader uses that key for coalescing in-flight downloads.

Since ImageRequest also accepts a URLRequest with custom headers, wouldn't two requests to the same URL but with different Authorization headers end up sharing the same cache key?

For example, an endpoint like /me/avatar could return different images depending on the logged-in user. After switching accounts, the memory or disk cache might still contain the previous user's image. Two simultaneous requests could potentially share a download as well.

Would it make sense to support an application-defined cache namespace or request identity, so authenticated resources can be separated without putting actual credentials into cache keys?

A regression test using the same URL with two different authentication contexts would be useful here. Disabling disk caching alone wouldn't necessarily address the in-flight request sharing.

3 Likes

Good catch, thanks. Confirmed: the memory, disk and coalescing keys are all URL-only, so different Authorization contexts for the same URL can share cached images and downloads.

The fix adds ImageRequest.cacheNamespace (e.g. an account ID, never a token). Every cache layer derives its key from it, so all three cases are covered. Regression tests check the same URL with two users for memory, disk and concurrent downloads. It defaults to nil, so existing caches aren't affected. Until per-namespace clearing lands, call removeAllCachedImages() on logout.

I'll update this thread when it's released.