Sample CodeiOS, iPadOS, Mac CatalystReviewed 2026-07-21View on Apple Developer

Asynchronously loading images into table and collection views

At a glance

Item Summary
Purpose Loads many images without blocking list scrolling, caches results, coalesces duplicate requests, and reloads items by stable identity.
App architecture Table and collection controllers share a process-wide ImageCache; Item identities anchor diffable snapshots, while a custom URL protocol simulates cancellable latency.
Main patterns Shared cache, request coalescing, diffable identity, asynchronous main-thread handoff, and URLProtocol test double.
Project style One UIKit target with parallel table/collection consumers and a small loading subsystem.

Project structure

Async Image Loading/
├── TableViewController.swift
├── CollectionViewController.swift
├── Item.swift
├── ImageCache.swift
├── ImageURLProtocol.swift
└── AppDelegate.swift

Structure observations

  • Table and collection screens deliberately duplicate only presentation setup; both use the same cache contract.
  • Cache policy and simulated transport/cancellation are separate types.
  • Mutable image content sits on stable-identity Item reference objects used by snapshots.

Overall architecture

Reference code

Async Image Loading/ImageCache.swift:20 — the cache returns hits immediately and merges concurrent misses for the same URL into one completion list.

final func load(url: NSURL, item: Item,
                completion: @escaping (Item, UIImage?) -> Void) {
    if let cachedImage = image(url: url) {
        DispatchQueue.main.async { completion(item, cachedImage) }
        return
    }
    if loadingResponses[url] != nil {
        loadingResponses[url]?.append(completion)
        return
    }
    loadingResponses[url] = [completion]
    ImageURLProtocol.urlSession().dataTask(with: url as URL) { data, _, error in
        // decode, cache, and dispatch completions to the main queue
    }.resume()
}

Interpretation

Consumers request by URL but update by Item identity. The cache owns transport and deduplication; list controllers remain responsible for verifying the item is still in their current snapshot before mutating/reloading it.

Ownership and state

Ownership evidence

Async Image Loading/ImageCache.swift:9 — one shared cache owns private decoded and in-flight state.

public class ImageCache {
    public static let publicCache = ImageCache()
    var placeholderImage = UIImage(systemName: "rectangle")!
    private let cachedImages = NSCache<NSURL, UIImage>()
    private var loadingResponses = [NSURL: [(Item, UIImage?) -> Void]]()
}
Owner Object or state Relationship Mutation authority
ImageCache.publicCache Decoded cache and in-flight callback registry Process-wide owner Cache methods only
Each list controller Item array, diffable source/snapshot Creates and retains Cell-provider completion on main queue
Item Stable UUID, URL, current image Reference model List completion updates image
ImageURLProtocol instance Cancellation flag and work item Per request startLoading / stopLoading on serial queue

Class and protocol design

Type Responsibility Depends on
ImageCache Cache lookup, request coalescing, fetch, decode, main-queue callback NSCache, URLSession
ImageURLProtocol Simulate delayed file transport and cancellation Serial dispatch queue, URLProtocolClient
Item / Hashable Stable diffable identity plus mutable image payload UUID, URL, UIImage
Table/collection controllers Build items, configure cells, validate/reload snapshots Diffable data sources, shared cache

No app-defined protocol abstraction is present; URLProtocol subclassing supplies the transport substitution point.

Access control

Symbol Access Verified effect Likely rationale
ImageCache and publicCache public Can be referenced outside the app module if packaged; singleton is immutable. Demonstrates a reusable cache entry point, though the sample target does not require public scope.
Decoded cache and callback registry private Consumers cannot bypass cache/coalescing invariants. Preserve one loading path.
load implicit internal final Target consumers can request images; subclasses cannot override behavior. Internal app operation with fixed semantics.
Controller imageObjects private Other files cannot mutate each screen’s displayed collection. Snapshot/item consistency stays local.
URL protocol queue private static All request state transitions use one hidden queue. Serialize cancellation/completion state.

Reference code

Async Image Loading/TableViewController.swift:9 — the screen exposes its diffable source to its own code but keeps the backing item collection private.

class TableViewController: UITableViewController {
    var dataSource: UITableViewDiffableDataSource<Section, Item>! = nil
    private var imageObjects = [Item]()
}

Logic ownership and placement

Logic Owning type or file Placement rationale
Caching, deduplication, decode ImageCache Shared across all list consumers.
Transport delay and cancellation ImageURLProtocol URL-loading test boundary.
Stable identity Item Snapshot correctness follows item UUID, not row position.
Cell/snapshot update Each list controller Must validate against its current data source.

Design patterns

Pattern Source evidence Purpose or tradeoff
Shared cache Async Image Loading/ImageCache.swift:11 Reuses decoded images across both screens; global lifetime increases memory scope.
Request coalescing Async Image Loading/ImageCache.swift:29 Avoids duplicate work for simultaneous URL requests.
Diffable identity Async Image Loading/Item.swift:13 Lets async results reload the correct item after list changes.
Main-thread handoff Async Image Loading/ImageCache.swift:24 Delivers UI-facing completions on the main queue.
Protocol test double Async Image Loading/ImageURLProtocol.swift:9 Exercises asynchronous/cancellation behavior with bundled files.

Naming conventions

  • ImageCache names policy/ownership; ImageURLProtocol names the URL-loading adapter.
  • publicCache explicitly signals shared scope; cachedImages and loadingResponses distinguish completed versus in-flight state.
  • Item.identifier names stable identity, while image is the mutable presentation payload.

Architecture takeaways

  • Centralize caching and coalesce requests by resource identity.
  • Return async results with stable item identity, then confirm current snapshot membership before reloading.
  • Keep backing item arrays private to the screen that owns its snapshot.
  • Test latency and cancellation through a URL-loading substitution rather than UI sleeps.

Source map

Source file Relevant symbols
Async Image Loading/ImageCache.swift Cache and in-flight request coalescing
Async Image Loading/ImageURLProtocol.swift Delayed cancellable transport
Async Image Loading/Item.swift Diffable identity and mutable image
Async Image Loading/TableViewController.swift Table consumer/snapshot reload
Async Image Loading/CollectionViewController.swift Collection consumer/layout