← Application platforms · Chapter 2

Structure an offline reader with SwiftUI

Design a SwiftUI reading list with stable document identity, explicit navigation, persistent offline storage, and cancellable downloads, separating view state from durable library data.

After this guide, you can

Implement a SwiftUI library with ID-based navigation and saved positions, then verify reordering, deletion, offline use, and restart recovery.

An offline reader manages a library, the document currently open, and download operations. An array index such as “the second document” can identify a different document after sorting. A body held only in view state disappears when the process exits. This lesson gives identity, navigation, and storage separate responsibilities.

Use synthetic IDs such as N-1 and N-2. The initial version supports bundled text and user-triggered downloads. It makes no promise of uninterrupted background execution and includes no account synchronization. Its reading workflow should be reproducible on a device.

Model identity and download state separately

A document record contains a stable ID, title, content version, relative storage location, and last reading position. Titles and order can change without changing identity. An index entry alone does not prove that its body is readable. Check the stored file and applicable content version before presenting it as available.

Represent downloads as unavailable, downloading, available offline, or failed. Associate progress and a useful failure reason with the document ID. Reading position is durable data, while a visible dialog or expanded list group is temporary UI state. Decide which values survive relaunch before choosing storage mechanisms.

Keep selected-document identity separate from download-operation identity. Reading one saved document can overlap with downloading another. One page-wide loading Boolean cannot describe this accurately. A completion event needs the document ID and request version so an older result cannot replace newer content.

Describe the interface from state

SwiftUI views describe the interface for the current state, while the framework manages view identity and updates. Recreating a view value does not create a new business document. Use stable IDs for list items and navigation. Resolve the current document from the repository instead of maintaining separately mutable copies in several screens.

@State can own a view's local state; it is not durable disk storage. Persist positions through the repository and reload them at startup. Observation and navigation APIs depend on the deployment target and toolchain. Choose a compatible approach and pay attention to availability diagnostics when adapting examples.

Separate LibraryView, ReaderView, and a download-status area. The list emits open and download events, the reader emits position changes, and the repository performs queries and persistence. This division helps decide whether an error belongs to one row or prevents library initialization.

Create a minimal project in Xcode

Create a SwiftUI application project and confirm its run destination, signing configuration, and minimum system. Add two teaching texts as resources, then create a document model and repository. First implement resource-backed list, detail, and return navigation without networking.

Next add persistence inside the application container. Load the index on startup, read bodies when selected, and save positions at a defined frequency. A failed write must not produce a saved confirmation. Version the index and define migration or safe fallback. Preserve an unparseable old file so recovery remains possible.

Then provide controlled downloadable text and add download and retry actions. Write temporary content, validate it, commit the valid copy, and update its index state. Failure must preserve an existing readable version. Clean up only temporary data known to be obsolete. After simulator checks, test termination and reopening on the target device.

Give asynchronous work an owner

Swift async and await describe operations that may suspend. Awaiting does not automatically move work to a background thread. UI-affecting model changes must follow main-actor isolation. Evaluate the execution context of expensive parsing separately. Actor isolation also does not make business assumptions remain true across every suspension.

Each download needs an owner and termination paths. Cancellation is cooperative, so code must check it and prevent canceled results from being committed. Whether leaving a page cancels work is a product decision. Preview work can belong to the page; a retained download needs a repository or task manager owner.

Before committing, recheck that the document exists, the requested version remains relevant, and the task has not been superseded. Distinguish network, validation, and storage errors because their recovery actions differ. A preserved older copy can remain readable, with an accurate status indicating that updating failed.

Check stable selection with an offline model

The Python example retains selection by ID through reordering, then clears it when the selected document is removed. It runs no SwiftUI view, navigation container, or iOS filesystem.

items = [{"id": "N-1", "title": "First"}, {"id": "N-2", "title": "Second"}]
selected = "N-2"
items.reverse()
assert next(x for x in items if x["id"] == selected)["title"] == "Second"
items = [x for x in items if x["id"] != "N-2"]
selected = selected if any(x["id"] == selected for x in items) else None
assert selected is None
print(selected)

The actual output is None. The first assertion finds Second after reversing the list. The second checks cleanup after deletion. Saving an index could silently change the selected document. The real application must also remove invalid IDs from navigation and explain unavailable content to the reader.

Reflect recovery in the interface

Startup loads the index and presents loading, empty, successful, or failed state. An empty library is normal and can offer import or download. Corrupt storage requires recovery. A missing body should make its document unavailable offline while retaining enough information to download it again.

After process termination, a persisted “downloading” label must not imply an operation still exists. Startup should mark interrupted work for retry or recreate it, validating any temporary data. Store reading positions with document and content versions. When an update changes paragraph structure, fall back to a valid location instead of blindly applying an old offset.

Coalesce frequent position changes when appropriate, but handle pending saves during navigation and lifecycle transitions. Keep the last confirmed value separate from the current position that is still awaiting persistence.

Exercise: verify the device workflow

Implement sorting, opening, position persistence, and local-copy removal for two documents. Add a download that deliberately fails. Existing content must remain readable and retry must remain available. Removing the current document should leave the invalid detail view or show a clear missing-content state.

Test reordering, detail switching, offline reading, relaunch, cancellation, and a subsequent download. Check that selected identity, body version, and storage state agree. Record the actual toolchain and device. Python assertions establish the identity contract; application persistence and concurrency need the engineering tests described here.

Sources & further reading

Sources checked on 2026-11-10. Original engineering explanation; examples are teaching data, not production measurements.