The Problem
I was building a music streaming app, and the album artwork kept flickering. Not just during track changes — even while playing the same song, the artwork would briefly disappear and reappear. The blurred background on the full player was especially bad.

I figured it was slow image loading, so I added caching. Still flickered. Dug deeper and found three bugs stacked on top of each other.
Bug 1: NSCache Lookup Order Was Backwards
I had a custom CachedAsyncImage with an NSCache layer. The problem was the lookup order.
Before
// Check @State image first
if let uiImage = uiImage ?? cache[url] {
Image(uiImage: uiImage)
} else {
placeholder
}
Why this breaks:
- When URL changes,
@State uiImagestill holds the previous track’s image - Stale image shows first, then new image loads
- When
NSCacheevicts under memory pressure,cache[url]returns nil → placeholder flash
After
// Check cache first
if let uiImage = cache[url] ?? uiImage {
Image(uiImage: uiImage)
} else {
placeholder
}
Flipped the order. Cache lookup first, fall back to @State’s previous image only when the cache misses.
Bug 2: Blur Background @State Reset
The full player used a blurred album artwork as background, also via CachedAsyncImage.
The issue: when SwiftUI re-creates the view (resetting @State) at the same time NSCache evicts the image — the background goes blank momentarily.
The Fix
Stopped using CachedAsyncImage for the background entirely. Instead, kept the last valid image in a static var:
struct CoverArtBackground: View {
let url: URL?
// Permanently retain the last successfully loaded image
static var lastImage: UIImage?
var body: some View {
if let image = Self.lastImage {
Image(uiImage: image)
.resizable()
.scaledToFill()
.blur(radius: 40)
.overlay(Color.black.opacity(0.5))
}
}
}
Being static, the image survives view re-creation and cache eviction. The background never goes blank.

Bug 3: URL Changed on Every Render
Fixed both of the above. Still flickering.
This one was the hardest to find. The API authentication used a random salt:
func authParams() -> String {
let salt = UUID().uuidString // Different every time!
let token = md5(password + salt)
return "u=\(user)&t=\(token)&s=\(salt)"
}
func getCoverArtURL(id: String, size: Int) -> URL {
// URL is different on every call because auth params change
URL(string: "\(serverURL)/rest/getCoverArt?\(authParams())&id=\(id)&size=\(size)")!
}
I was using .task(id: url) in SwiftUI. Since the URL was different on every render, the task re-executed every time. Cache key was URL-based too, so — no cache hits, ever.
The Fix
Use a stable URL (without auth params) for cache keys and .task(id:):
// Stable URL for cache key + task ID
var stableURL: String {
"\(serverURL)/rest/getCoverArt?id=\(id)&size=\(size)"
}
// Use stable ID for .task
.task(id: stableURL) {
// Now it won't re-execute for the same track
await loadImage(from: fullURL)
}
The Full Picture
Here’s what was happening all at once:
| Bug | Cause | Symptom |
|---|---|---|
| Cache lookup order | @State checked first → stale image + placeholder flash | Flicker on track change |
| @State reset | View re-creation + NSCache eviction at the same time | Blur background goes blank |
| Unstable URL | Random salt → URL changes every render → cache miss | Image reloads even for the same track |
All three happening simultaneously just looked like “everything flickers.” Fixing only one would leave you thinking “still broken” — because it was, just for a different reason.
Takeaway
Image flickering can have multiple causes at once. Check cache lookup order, View lifecycle, and URL stability — all three.
Especially if your API authentication uses random values (salt, nonce), make sure they don’t leak into cache keys or SwiftUI’s .task(id:).