Skip to content

Show a failed CachedAsyncImage download as a failure - #17

Merged
Jeehut merged 2 commits into
mainfrom
wip/cachedasyncimage-failure
Oct 15, 2026
Merged

Jeehut merged 2 commits into
mainfrom
wip/cachedasyncimage-failure

Conversation

@Jeehut

@Jeehut Jeehut commented Oct 14, 2026

Copy link
Copy Markdown
Member

Overview

CachedAsyncImage returned early on a failed download (no data, or data that is not an image), so the progress view spun forever. It also kept showing the old image when the URL changed, and never cancelled a download.

Loading now lives in an internal Loader with the phases idle, loading, loaded and failed. A failure shows a neutral photo placeholder and is retried only when the view appears again or the URL changes. The view loads with .task(id: url), which also cancels the download when the view disappears and replaces the onChange(of:perform:) that warned on visionOS.

Split out of #7. The placeholder #warning stays; a custom placeholder will be its own PR.

How this was verified

CachedAsyncImageTests run the real Loader with stubbed downloads: a thrown error and non-image data both end in .failed, and a successful load ends in .loaded and is served from the cache on the next load (one download for two loads).

main has no Loader, so the tests cannot compile against it. For the red run, the one line that sets the failed phase was removed, which is main's behavior (stay loading):

✘ dataThatIsNotAnImageEndsInFailedPhase(): loader.phase == .failed
✘ failedDownloadEndsInFailedPhase(): loader.phase == .failed
✘ Test run with 3 tests in 1 suite failed with 2 issues.

Green, this branch:

✔ Test run with 3 tests in 1 suite passed.

When a download returned no data or data that is not an image, the
completion handler returned early and isLoading stayed true, so the
progress view spun forever. The state after a download is now decided
by a small pure function that ends loading in both cases.
After a failed download the view was back in its initial state, which
looks exactly like a load that never started. The loading now lives
in a small loader object with explicit idle, loading, loaded and
failed phases: a failure shows a neutral placeholder and is not
retried automatically, only when the view appears again or the URL
changes (.task(id:), which also cancels the download when the view
disappears). The tests run the loader itself with stubbed downloads.
@Jeehut
Jeehut merged commit 2b97d03 into main Oct 15, 2026
5 checks passed
@Jeehut
Jeehut deleted the wip/cachedasyncimage-failure branch October 15, 2026 09:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant