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.

풀 플레이어 blur 배경

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:

  1. When URL changes, @State uiImage still holds the previous track’s image
  2. Stale image shows first, then new image loads
  3. When NSCache evicts 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.

풀 플레이어 blur 배경

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:

BugCauseSymptom
Cache lookup order@State checked first → stale image + placeholder flashFlicker on track change
@State resetView re-creation + NSCache eviction at the same timeBlur background goes blank
Unstable URLRandom salt → URL changes every render → cache missImage 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:).