CodeKits.devNew developer offer

Offline Reading in a SwiftUI App

A practical walkthrough of offline reading in a swiftui app, with code excerpts, failure checks and integration guidance.

By Jonathan · 5 min read

Published

Offline Reading in a SwiftUI App tutorial cover
In this article

This walkthrough covers Offline Reading in a SwiftUI App. A SwiftUI catalogue app with a working API client. This first version caches read-only records; offline mutations and synchronization are a separate design.

The video is an illustrated code walkthrough with AI narration. Code blocks are integration excerpts, not a complete tested application. Helper names such as stores, middleware and clients describe responsibilities you must implement in your existing project. No live server, payment provider or device execution is claimed.

Before you begin

Use a separate development environment, fictional data and test credentials. Record dependency versions and follow the official references below for your installed versions. Keep a working baseline and add one workflow at a time.

1. Choose an honest offline scope

Start with read-only offline access. A cached catalogue can remain useful when the network disappears without pretending that every server feature still works. Define which records are safe to keep on the device, how long they remain valid, and what must be removed on logout. Show the last successful refresh time so users can distinguish current data from an older local copy.

Choose an honest offline scope
Online: fetch -> validate -> display -> cache
Offline: load last valid cache -> mark stale
No cache: explain unavailable content
Writes: disabled until a separate sync design exists

2. Version the stored envelope

Store metadata alongside the cached records. A schema version lets future app releases reject or migrate incompatible files. An account identifier prevents one person's data from being loaded for another account. Save the successful refresh time rather than the last attempted request. Validate the envelope when reading it, and have a clear fallback for corruption instead of crashing during application startup.

Version the stored envelope
struct CacheEnvelope: Codable {
 let schemaVersion: Int
 let savedAt: Date
 let accountID: String
 let records: [Kit]
}

3. Write atomically

Write a complete new cache atomically so interruption doesn't leave a partially written file as the only copy. Keep the path inside an application-controlled storage location and choose a directory appropriate to whether the data is disposable or essential. Apply suitable platform data protection for sensitive records. This file is a cache, not a replacement for the authoritative server database.

Write atomically
let data = try JSONEncoder().encode(envelope)
try data.write(to: cacheURL, options: [.atomic])
// cacheURL is inside an app-controlled directory.
// Apply suitable file protection for the data.

4. Load local data before refreshing

Load a valid local snapshot promptly, then attempt a network refresh. This avoids an empty screen while waiting for connectivity. Don't overwrite useful cached content with a loading placeholder unless the product requires it. If no valid cache exists, show a distinct first-load state. Account for cancellation and account changes so a response from an earlier session can't populate the current user's interface.

Load local data before refreshing
func start() async {
 if let cached = try? cache.load(accountID: accountID) {
  records = cached.records
  lastUpdated = cached.savedAt
 }
 await refresh()
}

5. Replace only after successful decoding

Treat a successful, validated server response as the point at which new data becomes authoritative. Decide how to handle a disk-write failure: the interface may still display fresh data while warning that offline storage wasn't updated. Don't turn a server error page into an empty catalogue. Preserve the previous snapshot when the request or decoding fails, and make that stale state visible.

Replace only after successful decoding
let fresh = try await client.fetchKits()
try cache.save(fresh, accountID: accountID)
records = fresh
lastUpdated = Date()
errorMessage = nil

6. Treat reachability as a hint

A network-path signal can suggest when to retry, but it doesn't prove that the API is reachable or authorized. Let the actual request determine the result. Debounce reconnect-triggered refreshes and avoid overlapping loads. Respect cancellation and use bounded retry behaviour. A quiet stale-content banner is often clearer than repeatedly presenting the same failure alert while the device remains offline.

Treat reachability as a hint
Network path changed -> consider refresh
Actual request result -> decide success or failure
Avoid endless retries and repeated alerts
Use one coordinated load operation

7. Protect account boundaries

Offline storage extends the lifetime of data on the device. Apply the same account boundaries as the online application. Clear or segregate cached records when users switch accounts, and consider whether some sensitive data should never be cached at all. If you later add offline edits, introduce identifiers, an outbox, retry rules, and conflict resolution deliberately. A read cache alone doesn't solve synchronization.

Protect account boundaries
onLogout:
 cancel pending loads
 clear account-specific cache
 clear in-memory records
 reset refresh metadata

Integration checks

Test airplane mode with and without cache, corrupt files, schema changes, disk failure, stale responses after logout, reconnection and cancellation. Verify on simulator and physical device.

These are verification instructions, not reported test results. The excerpts have not been executed as a complete integrated application. Review authentication, database schema, dependency versions and operational settings before deployment.

Official references

Explore CodeKits development services.