Cached Image
AsyncImage drop-in with a memory + disk cache, so re-created views show remote images on the first frame instead of flashing the placeholder
Copies a short instruction with your private download link. Paste it into your agent: it downloads the whole skill (the source code, the setup steps and the mistakes we already made for you) and builds the screen.
Works with
What it does
SWCachedImage has the same shape as AsyncImage(url:content:placeholder:) and is meant to replace it everywhere a remote image is shown: avatars, covers, feed photos, attachments.
AsyncImage keeps no in-memory cache. Whenever its view identity is re-created it starts over from the empty phase, so the placeholder shows for at least one frame before the image comes back. Identity is re-created more often than it looks: a ForEach whose ids change, a parent that re-renders under a 1 Hz countdown, a lazy stack scrolling an item back in, a card that flips. A row of avatars rebuilt once per second flickers between photo and placeholder, and users report it as a bug.
SWCachedImage keeps decoded images in memory and looks its URL up synchronously while the view is being built, so a re-created view is on the photo in its very first frame. Original bytes go to a dedicated disk cache, so a relaunch reads from disk instead of the network.
What you get
- ✓241 lines of source code, ready to paste
- ✓Features
- ✓Source Code
- ✓Usage
- ✓Integration Checklist
- ✓Pitfalls
Features
- Same API shape as
AsyncImage:url, optionalscale,contentandplaceholderclosures - Memory cache of decoded
CGImages in anNSCachepriced by byte size (200 MB default, purged by the system under memory pressure) - Synchronous lookup while the view is built: a cached image never shows the placeholder, even for one frame
- Dedicated disk cache (
URLCachein Caches/SWImageCache, 200 MB) with.returnCacheDataElseLoad: cached images load offline and never revalidate - One request per URL at a time, shared by every view that asks for it
- 404 / 410 remembered for the session; network errors are not, so the next appearance retries
- Background decoding through ImageIO, with EXIF orientation applied and the longest side capped at 2400 px
- Cross-platform: renders through
Image(decorative:scale:), so the same file builds for iOS and macOS ImageRenderercan draw a cached remote image (share cards, receipts), whereAsyncImageonly ever renders its placeholder
Usage
// 1. Avatar with a monogram fallback (no URL, loading, or failed)
SWCachedImage(url: user.avatarURL) { image in
image.resizable().scaledToFill()
} placeholder: {
Circle()
.fill(.blue.opacity(0.15))
.overlay { Text(user.initial).font(.headline) }
}
.frame(width: 40, height: 40)
.clipShape(Circle())
// 2. Feed photo inside a fixed frame (overlay keeps scaledToFill from widening the layout)
Color.clear
.frame(height: 220)
.overlay {
SWCachedImage(url: post.photoURL) { image in
image.resizable().scaledToFill()
} placeholder: {
Color.secondary.opacity(0.15)
}
}
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
// 3. Lists: give rows a stable id so they are not rebuilt on every render
ForEach(people, id: \.userID) { person in
SWCachedImage(url: person.avatarURL) { $0.resizable().scaledToFill() } placeholder: { Color.gray }
.frame(width: 32, height: 32)
.clipShape(Circle())
}
// 4. Warm the cache as soon as data arrives, so the first appearance skips the placeholder too
for url in snapshot.avatarURLs {
Task { _ = await SWImageCache.shared.image(for: url) }
}Integration Checklist
- Drop
SWCachedImage.swiftinto the project and replace everyAsyncImagethat shows a remote image - Make image URLs immutable: a new upload gets a new key (for example a UUID file name). If your backend overwrites files in place, append a version to the URL (
?v=updatedAt), otherwise the disk cache keeps serving the old image - Have the image server send
Cache-Control: public, max-age=31536000, immutable, so other clients and CDNs cache it as well - macOS sandboxed apps need the Outgoing Connections (Client) entitlement
- Tune
memoryLimit,diskLimitandmaxPixelSizeinSWImageCache.initif your images are much larger or smaller than typical photos
Pitfalls
- Unstable ids rebuild views: a view model built in a computed property with
let id = UUID()gets new ids on every render, soForEachtears down and re-creates its rows (local@Stateresets,AsyncImagerestarts). The cache hides the flash, but use a stable business key (user id, name, record id) as the id anyway - Do not load images through
URLSession.shared: its sharedURLCacheis small and shared with every API GET in the app. Images get their own session and cache directory here - Decode off the main thread:
UIImage(data:)decodes lazily on the main thread during the first draw, which drops frames for large photos. Here ImageIO decodes insideTask.detached nonisolatedis not "background": with default MainActor isolation and approachable concurrency (NonisolatedNonsendingByDefault), anonisolated asynchelper runs on the caller's executor, which is the main thread. That is why the decode step is a detached task- First load still shows the placeholder until the bytes arrive from the network or are read back from disk (tens of milliseconds, longer when a cold launch reads many at once). Warm the cache with
SWImageCache.shared.image(for:)when the data arrives if the first appearance matters - Memory budget: a 1600 × 1600 photo is about 10 MB decoded. Images on screen stay alive through the view's state even after eviction; lower
maxPixelSizefor thumbnail-only screens
More in Components
- Add SheetFreeBottom sheet with text input field, Cancel and Continue buttons, presented as a medium detent sheet
- Alert Toast NotificationFreeGlobal toast-style alert overlay with preset styles (info, success, warning, error) and auto-dismiss
- Bullet Point TextFreeText label with a colored capsule bullet point indicator that accepts any View content via @ViewBuilder
- Floating LabelsFreeAnimated floating capsule labels that fade in and out at specified positions around an image
- Gradient DividerFreeHorizontal divider with a center-fade gradient effect (clear to color to clear)
- Image ThumbnailFreeReusable rounded image thumbnail with same-named ColorSet fallback — perfect for product cards, avatars, and cover images