Prefetching collection view data
At a glance
| Item | Summary |
|---|---|
| Purpose | Starts expensive cell data work before rows become visible and cancels work that scrolling makes unnecessary. |
| App architecture | One CustomDataSource is both the collection view data source and prefetch data source; it delegates asynchronous, cached, identifier-based work to AsyncFetcher. |
| Main patterns | Prefetch adapter, cache-aside fetch, request deduplication/coalescing, cancellable command, and cell-reuse identity guard. |
| Project style | Small storyboard-based UIKit app with screen composition, collection adaptation, asynchronous infrastructure, operation, cell, and display-data files. |
Project structure
CollectionViewPrefetching/
├── AppDelegate.swift
├── ViewController.swift
├── CustomDataSource.swift
├── AsyncFetcher.swift
├── AsyncFetcherOperation.swift
├── Cell.swift
├── DisplayData.swift
└── Base.lproj/
├── Main.storyboard
└── LaunchScreen.storyboard
Structure observations
ViewController.swiftperforms screen wiring only; data-source and loading policy live outside the controller.CustomDataSource.swiftis the boundary between index paths/cells and stable model identifiers.AsyncFetcher.swiftowns shared fetch coordination, whileAsyncFetcherOperation.swiftrepresents one cancellable unit of work.Cell.swiftandDisplayData.swiftkeep presentation state separate from request scheduling.
Overall architecture
flowchart LR
VC["ViewController"] -->|assigns twice| CV["UICollectionView"]
CV -->|data-source callbacks| DS["CustomDataSource"]
CV -->|prefetch and cancel callbacks| DS
DS --> Models["Model identifiers"]
DS --> Fetcher["AsyncFetcher"]
Fetcher --> Cache["NSCache"]
Fetcher --> Queue["AsyncFetcherOperation queue"]
Fetcher --> Handlers["completion handlers by UUID"]
Queue --> Data["DisplayData"]
Data -->|main queue| Guard["cell identifier guard"]
Guard --> Cell["Cell.configure"]
Reference code
CollectionViewPrefetching/CustomDataSource.swift:40 — the adapter converts an index path to a stable identifier, checks the cache, and rejects a completion if cell reuse changed that identity.
let model = models[indexPath.row]
let identifier = model.identifier
cell.representedIdentifier = identifier
// Check if the `asyncFetcher` has already fetched data for the specified identifier.
if let fetchedData = asyncFetcher.fetchedData(for: identifier) {
// The system has fetched and cached the data; use it to configure the cell.
cell.configure(with: fetchedData)
} else {
// There is no data available; clear the cell until the fetched data arrives.
cell.configure(with: nil)
// Ask the `asyncFetcher` to fetch data for the specified identifier.
asyncFetcher.fetchAsync(identifier) { fetchedData in
DispatchQueue.main.async {
/*
The `asyncFetcher` has fetched data for the identifier. Before
updating the cell, check whether the collection view has recycled it to represent other data.
*/
guard cell.representedIdentifier == identifier else { return }
// Configure the cell with the fetched image.
cell.configure(with: fetchedData)
}
}
}Interpretation
Prefetching is an advisory input into the same identifier-based fetch path used by visible cells. A prefetched result can become a cache hit later; a visible-cell request can join an in-flight fetch; cancellation removes work and callbacks for identifiers no longer expected onscreen.
Ownership and state
classDiagram
ViewController *-- CustomDataSource : private dataSource
UICollectionView o-- CustomDataSource : dataSource and prefetchDataSource
CustomDataSource *-- Model : 1000 identifier values
CustomDataSource *-- AsyncFetcher : private fetch coordinator
AsyncFetcher *-- OperationQueue : serial access and fetch queues
AsyncFetcher *-- NSCache : fetched data
AsyncFetcher *-- AsyncFetcherOperation : in-flight work
AsyncFetcher *-- CompletionHandlers : callbacks keyed by UUID
AsyncFetcherOperation *-- UUID : request identity
AsyncFetcherOperation *-- DisplayData : produced result
Cell o-- UUID : represented identity
Ownership evidence
CollectionViewPrefetching/AsyncFetcher.swift:14 — the fetcher privately retains the concurrency, callback, and cache state needed to coordinate all requests.
/// A serial `OperationQueue` to lock access to the `fetchQueue` and `completionHandlers` properties.
private let serialAccessQueue = OperationQueue()
/// An `OperationQueue` that contains `AsyncFetcherOperation`s for requested data.
private let fetchQueue = OperationQueue()
/// A dictionary of arrays of closures to call when an object has been fetched for an id.
private var completionHandlers = [UUID: [(DisplayData?) -> Void]]()
/// An `NSCache` used to store fetched objects.
private var cache = NSCache<NSUUID, DisplayData>()
// MARK: Initialization
init() {
serialAccessQueue.maxConcurrentOperationCount = 1
}| Owner | State | Relationship | Mutation authority |
|---|---|---|---|
ViewController |
One CustomDataSource |
Creates and privately retains it | Controller setup only |
CustomDataSource |
Model array and fetcher | Creates and exclusively exposes their behavior through callbacks | Data-source/prefetch methods |
AsyncFetcher |
Serial queue, fetch queue, handler table, cache | Long-lived coordinator for this data source | Its fetch/cancel/private helper methods |
AsyncFetcherOperation |
Identifier and fetched result | One operation per in-flight identifier | main() writes the result |
Cell |
representedIdentifier and displayed data |
Reused view state, not model ownership | Data source assigns/configures it |
The collection view stores callback references to the same adapter. The sample’s lifecycle ownership remains ViewController → CustomDataSource → AsyncFetcher.
Class and protocol design
| Type | Responsibility | Design note |
|---|---|---|
ViewController |
Connect the storyboard collection view to one adapter | final; no fetch policy in the screen controller |
CustomDataSource |
Supply cells and translate prefetch/cancel index paths into identifiers | Conforms to both UICollectionViewDataSource and UICollectionViewDataSourcePrefetching |
CustomDataSource.Model |
Give each logical item a stable UUID |
Minimal nested value because only the adapter uses it |
AsyncFetcher |
Deduplicate work, coalesce callbacks, cache results, and cancel by identifier | Facade over queues, operations, and cache |
AsyncFetcherOperation |
Simulate one slow, cancellable fetch | Operation command with a readable result |
Cell |
Display DisplayData and expose current represented identity |
Identity supports safe asynchronous reuse |
DisplayData |
Carry the fetched presentation value | Small result object cached by NSCache |
There is no app-defined protocol. The architectural contracts are UIKit’s two collection-view protocols and Foundation’s Operation lifecycle; protocol-oriented design here means one concrete adapter fulfills two related framework roles.
Access control
| Symbol | Access | Verified effect | Rationale |
|---|---|---|---|
ViewController.dataSource |
private |
Only the controller can retain or replace its adapter. | Prevents outside code from bypassing screen wiring. |
CustomDataSource.models / asyncFetcher |
private |
Index-path mapping and fetch implementation stay behind callback methods. | Keeps collection protocol behavior as the public shape inside the target. |
| Fetch queues, handlers, cache, and helper methods | private |
Callers can request/cache-read/cancel but cannot mutate coordination state directly. | Protects deduplication and callback invariants. |
AsyncFetcherOperation.fetchedData |
private(set) |
Other target code may read a completed result; only the operation writes it. | Expresses producer ownership. |
| Feature types and most methods | implicit internal |
Visible within the sample target, not exported as a library API. | The sample demonstrates an app implementation, not a reusable public package. |
Reference code
CollectionViewPrefetching/AsyncFetcherOperation.swift:10 — the operation exposes identity and read access to its output while retaining write authority.
class AsyncFetcherOperation: Operation {
// ...
}No declaration is public or open; private(set) is the only deliberately asymmetric setter boundary.
Logic ownership and placement
| Logic | Owner | Why it belongs there |
|---|---|---|
| Storyboard/data-source wiring | ViewController |
It knows the concrete collection view and screen lifecycle. |
| Index path → identifier mapping and cell reuse checks | CustomDataSource |
It owns models and receives cell/prefetch callbacks. |
| Prefetch start and cancellation | CustomDataSource |
UIKit supplies index paths through that protocol boundary. |
| Request deduplication, handler coalescing, cache, and serialized bookkeeping | AsyncFetcher |
These concerns span visible and speculative requests. |
| Slow work and cancellation checkpoint | AsyncFetcherOperation |
One operation encapsulates one command’s execution lifecycle. |
| Presentation reset/configuration | Cell |
The reusable view owns its visual state, not the fetch lifecycle. |
Design patterns
| Pattern | Source evidence | Purpose or tradeoff |
|---|---|---|
| Dual-role adapter | CollectionViewPrefetching/ViewController.swift:20 |
One object presents items and receives anticipatory loading hints, avoiding parallel index mapping. |
| Cache-aside | CollectionViewPrefetching/CustomDataSource.swift:44 |
Visible-cell lookup uses cached data immediately and fetches only on a miss. |
| Request deduplication | CollectionViewPrefetching/AsyncFetcher.swift:90 |
An existing operation for a UUID prevents duplicate work. |
| Callback coalescing | CollectionViewPrefetching/AsyncFetcher.swift:41 |
Multiple consumers join one identifier’s handler array. |
| Cancellable command | CollectionViewPrefetching/AsyncFetcher.swift:70 |
Prefetch cancellation locates and cancels an Operation, then clears callbacks. |
| Reuse identity guard | CollectionViewPrefetching/CustomDataSource.swift:53 |
Late results cannot paint a cell now representing another model. |
Naming conventions
- Role suffixes communicate framework boundaries:
ViewController,CustomDataSource,AsyncFetcherOperation, andCell. - Asynchronous intent is explicit in
fetchAsync; cache inspection and cancellation usefetchedData(for:)andcancelFetch(_:). representedIdentifiernames view identity rather than implying the cell owns a model.- Queue names distinguish purpose:
serialAccessQueueprotects bookkeeping, whilefetchQueueexecutes slow work. Model.identifier, operationidentifier, and cellrepresentedIdentifierkeep the same identity concept traceable across layers.
Architecture takeaways
- Feed visible-cell requests and speculative prefetches through one stable-identifier pipeline.
- Treat prefetching as an optimization: correctness still comes from cache checks and normal cell configuration.
- Coalesce duplicate requests and keep cancellation bookkeeping serialized.
- Carry model identity into reusable views and verify it before applying asynchronous results.
- Use
private(set)when consumers need results but only the producer should mutate them.
Source map
| Source file | Relevant symbols |
|---|---|
CollectionViewPrefetching/ViewController.swift |
Composition and dual data-source assignment |
CollectionViewPrefetching/CustomDataSource.swift |
Models, cell configuration, prefetch, cancellation, reuse guard |
CollectionViewPrefetching/AsyncFetcher.swift |
Cache, queues, deduplication, handler coalescing |
CollectionViewPrefetching/AsyncFetcherOperation.swift |
Identifier-scoped cancellable work |
CollectionViewPrefetching/Cell.swift |
Reuse identifier and display configuration |
CollectionViewPrefetching/DisplayData.swift |
Cached presentation result |
CollectionViewPrefetching/AppDelegate.swift |
Application entry point |