A SwiftUI sample app that shows how to load remote images the way real apps usually need to.
Visible-row loading • async/await • cancellation • caching • memory-aware eviction
This project demonstrates a production-shaped async image-loading pipeline for a large SwiftUI list.
Instead of downloading images for every row up front, the app only starts work for rows that appear on screen, cancels work for rows that disappear, reuses cached images when possible, and trims non-visible images when memory grows too large.
The screenshot shows the app’s SwiftUI list UI, and the GIF demonstrates the key runtime behavior: visible-row loading, cancellation of off-screen work, and cache reuse while scrolling.
Many image-loading examples stop at a simple URLSession call and a dictionary cache. This repo goes further and demonstrates the practical concerns that usually show up in real product code:
- loading only for visible rows
- cancelling work when rows leave screen
- avoiding duplicate downloads for the same URL
- reusing decoded images from memory cache
- protecting visible images while evicting older off-screen ones first
- explaining the architecture in a way that is easy to study and demo
- 1,000 deterministic SwiftUI list rows
- async image fetching with
async/await - row lifecycle-driven loading via
onAppearandonDisappear - actor-backed image pipeline and cache
- in-flight request coalescing
- visibility-aware memory trimming
- a live cache summary shown in the UI
- A row becomes visible in the SwiftUI
List PhotoRowViewtriggersonAppearPhotoRowViewModelrequests the image fromImagePipelineImagePipelinefirst checksMemoryImageCache- If no cached image exists, a remote fetch begins with
URLSession - If another row asks for the same URL, it joins the same in-flight task
- When the row disappears, the task is cancelled and that URL becomes non-visible in cache state
- When memory grows, non-visible images are evicted before visible ones
The project is intentionally split into three layers:
- SwiftUI views render rows and react to visibility changes
- row view models own per-row task lifecycle
- actor-backed services coordinate fetching, caching, and eviction
Best files to read first:
AsyncImageTableDemoApp/Views/PhotoListView.swiftAsyncImageTableDemoApp/Views/PhotoRowView.swiftAsyncImageTableDemoApp/ViewModels/PhotoRowViewModel.swiftAsyncImageTableDemoApp/Services/ImagePipeline.swiftAsyncImageTableDemoApp/Services/MemoryImageCache.swift
More detail:
The app-level cache is configured with a budget of roughly 28 MB.
In practical terms:
- visible images are protected while their rows are on screen
- non-visible images are the first eviction candidates
- trimming runs after inserts, when rows become non-visible, and on memory warning
- the cache reports visible count, cached count, and estimated memory usage in the UI
For the full explanation, see docs/CACHE_AND_MEMORY.md.
- Open
AsyncImageTableDemoApp.xcodeprojin Xcode - Run the
AsyncImageTableDemoAppscheme on an iPhone simulator - Scroll slowly to watch visible-row loading
- Scroll quickly to observe cancellation
- Scroll back upward to see cached image reuse
- Watch the live cache summary in the navigation area
If you are showing this project to someone else:
- Start at the top of the list and explain the problem
- Scroll slowly and point out on-demand image loading
- Scroll quickly and explain why off-screen work is cancelled
- Scroll back upward and show cached reuse
- Open the pipeline and cache code to connect the behavior to the architecture
A longer walkthrough is in docs/DEMO_GUIDE.md.
- iOS engineers learning better async image-loading patterns
- SwiftUI developers who want a realistic cache and cancellation example
- interview candidates who want a strong demo project
- content creators writing or recording Swift concurrency tutorials
The test target focuses on the concurrency core rather than UI visuals.
It verifies:
- cache eviction behavior
- in-flight request coalescing
- disk cache support
- thumbnail downsampling
- request prioritization for near-visible rows
- debug overlay for cache hits, misses, cancellations, and evictions
- benchmark mode for scroll and memory behavior
If you want to improve the image pipeline, strengthen memory behavior, add debug tooling, or polish the demo experience, contributions are welcome.

