The protocol-oriented persistence engine for Swift.
Persist pure Codable & Identifiable DTOs with SwiftData backing, @ModelActor isolation, smart TTL cache expiration, in-store indexed queries, live SwiftUI change feeds, and safe forward schema migrations. Engineered as the offline-first persistence companion to SwiftNetworkKit.
https://github.com/ihusnainalii/SwiftLocalStorage.gitHow SwiftLocalStorage executes under the hood.
Step through real-world storage operations to observe how pure Codable DTOs are encoded outside the actor, indexed, validated against TTL rules, and committed into SwiftData.
Select Storage Pipeline
Save Pipeline (Codable -> Envelope -> SwiftData)
How a pure Swift struct is encoded outside the actor and committed atomically into the StoredRecord V3 schema.
1. Caller Task
Invokes `storage.save(user, expiration: .hours(2))`Non-blocking Swift async call. No @Model references needed.
Designed for Swift 6 developers.
Explore production-ready recipes for entity storage, indexed queries, reactive SwiftUI live streams, TTL cache expiration, and forward schema migrations.
Entity Storage & Batch Saves
Unlike raw SwiftData or CoreData, SwiftLocalStorage persists regular Swift structs directly into an optimized envelope schema. Batch saves execute in a single atomic database transaction.
Swift 6 strict concurrency. Zero data races.
SwiftData's `ModelContext` is inherently non-Sendable. SwiftLocalStorage hides it completely behind an internal `@ModelActor` actor, guaranteeing compiler-enforced memory safety across all Apple platforms.
Concurrency Scenarios
10 Concurrent Background Tasks Writing Simultaneously
Thread & Execution Model AnalysisJSON/Custom encoding runs in parallel on caller worker threads. Byte snapshots are serialized onto the background @ModelActor mailbox queue.
Direct SwiftData ModelContext access crashes immediately with 'EXC_BAD_ACCESS' or data corruption due to non-Sendable context sharing.
100% thread safe. Serialized transaction execution in ModelActor queue guarantees atomic commits and consistent ordering.
Inside the Envelope Architecture.
Inspect how pure Codable DTOs translate into single-table StoredRecord V3 rows, how index slots accelerate queries, and how TTL lifecycles purge stale cache entries in real-time.
{
"id": "usr_01",
"name": "Husnain Ali",
"role": "admin",
"age": 28,
"lastSeen": "2026-09-25T20:00:00Z"
}How does SwiftLocalStorage compare?
A feature-by-feature architectural comparison across modern Apple persistence engines, raw databases, and local caching alternatives.
| Capability | SwiftLocalStorageSwift 6 | Direct SwiftData | UserDefaults | CoreData |
|---|---|---|---|---|
Pure Codable DTO Persistence Architecture & Data Model | Zero @Model boilerplate | Requires @Model classes | Small blobs / Plist only | Requires .xcdatamodeld & NSManagedObject |
Zero-Dependency Footprint Architecture & Data Model | 100% Apple Native (0 external deps) | Apple Native | Apple Native | Apple Native |
Swift 6 Strict Concurrency Concurrency & Safety | Actor-isolated via @ModelActor (0 races) | Non-Sendable ModelContext leaks | Process-wide lock contention | Thread-confined NSManagedObject |
Smart TTL Cache Expiration Caching & Lifecycle | Granular TTL (.hours, .days, lazy purge) | Manual implementation required | No expiration support | No expiration support |
Optional Cache Miss Semantics Caching & Lifecycle | Returns nil (clean control flow) | Throws errors or empty arrays | Returns nil | Throws on missing / faulted |
In-Store Indexed Queries Queries & Performance | B-Tree Slot Pushdown (28.5x faster) | SwiftData #Predicate | Full scan in RAM | NSPredicate |
SwiftUI Observation & Change Streams UI Integration | updates(of:) with write coalescing | @Query macro (@Model only) | @AppStorage (primitives only) | @FetchRequest / NSFetchedResultsController |
Forward DTO Schema Migrations Evolution & Testing | Typed StorageMigration on read | SchemaMigrationPlan | Manual dictionary edits | Mapping models / lightweight migration |
In-Memory Test Doubles Evolution & Testing | .inMemory configuration (0 disk I/O) | InMemory ModelContainer | Suite name dictionary | NSInMemoryStoreType |
From Genesis to Production Standard.
Track the rapid iteration of SwiftLocalStorage across 8 milestones, up to the current v1.1.2 release.
Indexed Live Queries & String Filters
Live queries over indexed filters, prefix and any-of string filters evaluated in the store, and batched walks for closure filters and bulk migration. Source-compatible with 1.0.
Precision engineered for modern Apple apps.
A cohesive persistence framework built from the ground up to solve real-world caching, concurrency, and indexing challenges on Apple platforms.
Zero-Boilerplate Codable DTO Persistence
Persist pure Swift structs directly into SwiftData without ever touching a ModelContainer, ModelContext, or writing @Model classes. SwiftData remains a pure internal implementation detail.
@ModelActor Isolation
All database operations are confined behind an internal `@ModelActor` queue. Only immutable Sendable byte snapshots cross boundaries.
Granular TTL Expirations
Attach `.minutes()`, `.hours()`, or `.date()` policies to any record. Expired data reads as nil and purges automatically on access.
Store-Level B-Tree Queries
Declare up to 3 string and 3 number indexes. Filters (.equals, .atLeast, .between) and sort orders run natively inside SQLite.
Live SwiftUI Observation
Subscribe to coalesced live queries (`updates(of:)`) and typed event streams (`changes(of:)`) that auto-cancel when views disappear.
SwiftNetworkKit Companion
Engineered to pair with SwiftNetworkKit for complete Offline-First architectures, instantaneous cache reads, and seamless background sync.
Forward DTO Schema Migrations
Evolve your data models across app releases with step-by-step `StorageMigration` pipelines, lazy upgrade write-backs, and eager bulk migration tools.
Hermetic In-Memory Test Doubles
Instantiate `.inMemory` storage instances for parallel unit tests and SwiftUI previews with zero file-system writes and sub-millisecond execution.
The Modern Swift Stack: SwiftLocalStorage + SwiftNetworkKit
Engineered to operate seamlessly together. Combine declarative Swift 6 networking with actor-isolated local persistence to build resilient, offline-first Apple applications.
SwiftNetworkKit
Remote Network LayerHigh-performance declarative HTTP networking with actor-isolated token refreshes, OAuth PKCE flows, SPKI public-key SSL pinning, and automatic retry policies.
SwiftLocalStorage
Local Persistence LayerProtocol-oriented storage engine persisting pure Codable DTOs directly into SwiftData SQLite with @ModelActor thread-safety and smart TTL cache invalidation.
Stable at 1.x, and beyond.
SwiftLocalStorage follows Semantic Versioning with a disciplined roadmap focused on zero-dependency reliability, compiler-enforced safety, and high-throughput caching.
Production Core & Indexing
Complete feature set covering CRUD, batch saves, TTL caching, reactive live queries, schema migrations, and in-store B-tree index slots.
- Zero-boilerplate SwiftData envelope (Schema V3)
- Swift 6 strict concurrency with @ModelActor
- LocalStorageIndexed B-tree query pushdown
- Live SwiftUI AsyncStreams with write coalescing
- 96% test coverage floor with in-memory double
1.0 API Freeze & 1.1 Queries
Public API frozen for all of 1.x, with DocC documentation, benchmarks, and 1.1 live indexed queries plus prefix / any-of string filters.
- Interactive Apple DocC documentation catalog
- Full benchmarks target (1, 100, 1000 records; 10MB payloads)
- Strict multi-platform CI verification (iOS, macOS, tvOS, watchOS, visionOS)
- Live indexed queries & prefix / any-of filters (1.1)
Ecosystem & Extensions
Exploratory directions for deep network caching integration and pluggable low-level storage engines.
- CachedRepository: SwiftNetworkKit + SwiftLocalStorage integration (cacheFirst, SWR)
- Alternative lightweight SQLite & File storage backends
- Optional Hardware-backed Secure Enclave field encryption
Engineered for microsecond latency.
Verified on Apple Silicon hardware running Swift 6.0 release binaries. Observe how single-transaction batch commits and store-level index pushdowns outpace raw database abstractions.
Single Record Insert (save)
Encoding Codable DTO, building envelope metadata, saving row via @ModelActor
Batch Save (100 records)
Single transaction envelope commit for 100 items (38 µs/record)
Batch Save (1,000 records)
Bulk transactional batch write with index calculation
Fetch by ID (Primary Key)
Direct index hit + JSON payload decode into strongly-typed DTO
Indexed Filter & Sort Page (50 of 10,000)
SwiftData-level B-tree predicate filter + ordering; only 50 rows decoded
Unindexed In-Memory Closure Filter (10,000 items)
Full scan and decode of 10,000 DTOs evaluated with custom Swift closure
Indexed Count Query (10,000 items)
SwiftData index count without decoding payloads (103x faster than full scan)
Lazy Cache Expiration Purge (on fetch)
Expired record detected via metadata timestamp, deleted, and nil returned
DTO Schema Migration (100 records)
V1 -> V2 schema evolution applied with transformation pipeline
In-Memory Engine (Test Double, 100 ops)
Zero disk I/O, isolated memory store for blazing fast unit tests
Payload Size Scaling & Throughput
Throughput performance across encoded DTO payload sizes from 1 KB to 10 MB.
Comprehensive public API Reference
A curated tour of the most-used types with copy-pasteable examples. The full generated list of every public type is in the DocC catalog (19 types).
LocalStorage
The primary database orchestrator. Encapsulates SwiftData StoredRecord schema, atomic transactions, TTL expiration, batching, and reactive observation streams with strict concurrency safety.
public actor LocalStorage: Sendable// Initialize on-disk SwiftData store
let storage = try LocalStorage()
// Save a Codable model with a 2-hour TTL
try await storage.save(product, expiration: .hours(2))
// Fetch live record by primary key
let item: Product? = try await storage.fetch(Product.self, id: "sku_108")LocalRepository<T>
Lightweight, strongly-typed repository wrapper providing scoped CRUD operations, streams, and queries for a specific model type. Ideal for Clean Architecture and MVVM injection.
public struct LocalRepository<T: Identifiable & Codable & Sendable>: Sendablefinal class ProductService {
private let repository: LocalRepository<Product>
init(storage: LocalStorage) {
self.repository = storage.repository(Product.self)
}
func cache(_ product: Product) async throws {
try await repository.save(product, expiration: .days(1))
}
func get(id: String) async throws -> Product? {
try await repository.fetch(id: id)
}
}LocalStorageIndexed
Protocol enabling DTO types to declare indexed columns. SwiftLocalStorage extracts these values during encoding and maps them into SwiftData queryable index columns for ultra-fast SQLite filtering.
public protocol LocalStorageIndexed: Identifiable, Codable, Sendablestruct Product: LocalStorageIndexed {
let id: String
var title: String
var category: String
var price: Double
var rating: Double
static var storageIndexes: [StorageIndex<Product>] {
[
StorageIndex("category", \.category),
StorageIndex("price", \.price),
StorageIndex("rating", \.rating)
]
}
}StorageIndex<T>
Type-safe property index descriptor. Binds a column name to a Swift KeyPath for automated extraction during JSON envelope serialization.
public struct StorageIndex<T: Sendable>: Sendable// Index on a nested or root property
StorageIndex("category", \Product.category)
StorageIndex("price", \Product.price)
StorageIndex("isFeatured", \Product.isFeatured)StorageIndexFilter
SQL-level filter predicates evaluated directly in SwiftData. Supports equality, range bounds, numerical comparisons, string pattern matching, and set inclusion without decoding payload JSON.
public enum StorageIndexFilter: Sendable// Combine multiple indexed predicates
let filters: [StorageIndexFilter] = [
.equals("category", "electronics"),
.atLeast("rating", 4.5),
.between("price", 100.0...500.0),
.startsWith("sku", "TECH_")
]
let results = try await storage.fetch(Product.self, matching: filters)StorageIndexSort
SQL-level sorting clause applied directly within the SQLite engine to indexed columns for zero-memory ordering.
public enum StorageIndexSort: Sendable// Sort ascending or descending by indexed column
let sortOrder: StorageIndexSort = .descending("rating")
let topRated = try await storage.fetch(
Product.self,
matching: [.equals("category", "books")],
orderedBy: sortOrder,
options: FetchOptions(limit: 10)
)FetchOptions
Configures fetch limit, offset pagination, record sorting order, and whether expired records should be automatically purged during retrieval.
public struct FetchOptions: Sendablelet options = FetchOptions(
limit: 20,
offset: 40,
sort: .newestFirst,
purgeExpired: true
)
let recentPosts = try await storage.fetch(Post.self, options: options)StoragePage<T>
Standardized 1-based pagination result container. Returns the current slice of items along with total item count, total pages, current page index, and boolean nextPage indicators.
public struct StoragePage<T: Sendable>: Sendablelet page: StoragePage<Product> = try await storage.page(
Product.self,
matching: [.atLeast("price", 50.0)],
orderedBy: .ascending("price"),
page: 1,
pageSize: 25
)
print("Page \(page.page) of \(page.totalPages), total items: \(page.totalCount)")
if page.hasNextPage {
// Fetch page 2...
}CacheExpiration
Granular Time-To-Live (TTL) cache expiration policies. Supports relative duration helpers, absolute target dates, and indefinite persistence.
public enum CacheExpiration: Sendable, Equatable// Standard expiration options
let policy1: CacheExpiration = .never
let policy2: CacheExpiration = .seconds(30)
let policy3: CacheExpiration = .minutes(15)
let policy4: CacheExpiration = .hours(6)
let policy5: CacheExpiration = .days(7)
let policy6: CacheExpiration = .date(Date().addingTimeInterval(3600))StorageMetadata
Metadata envelope detailing payload creation timestamp, last modification timestamp, computed expiration timestamp, payload size in bytes, and schema version.
public struct StorageMetadata: Sendable, Equatableif let meta = try await storage.metadata(User.self, id: "usr_42") {
print("Created at: \(meta.createdAt)")
print("Updated at: \(meta.updatedAt)")
print("Expires at: \(meta.expiresAt?.description ?? "Never")")
print("Payload size: \(meta.sizeInBytes) bytes")
print("Is expired: \(meta.isExpired)")
}StorageChange<T>
Discrete change event notification emitted whenever a database write commits. Dispatches inserted, updated, deleted, cleared, or expired events.
public enum StorageChange<T: Identifiable & Codable & Sendable>: Sendablefor await change in storage.changes(of: TaskItem.self) {
switch change {
case .inserted(let task):
print("Created task: \(task.title)")
case .updated(let task):
print("Updated task: \(task.title)")
case .deleted(let id):
print("Deleted task ID: \(id)")
case .expired(let id):
print("Task expired ID: \(id)")
case .cleared:
print("All tasks cleared from cache")
}
}StorageSequence<T>
Reactive AsyncSequence yielding the initial database snapshot immediately, followed by fresh updated result arrays on every coalesced mutation.
public struct StorageSequence<T: Identifiable & Codable & Sendable>: AsyncSequence// SwiftUI ViewModel observation
@Observable @MainActor
final class FeedViewModel {
var posts: [Post] = []
func observeFeed(storage: LocalStorage) async {
do {
for try await updatedPosts in storage.updates(of: Post.self) {
self.posts = updatedPosts
}
} catch {
print("Stream terminated with error: \(error)")
}
}
}LocalStorageVersioned
Protocol declaring schema version number and sequential migration transformation steps for automated lazy on-read DTO migrations.
public protocol LocalStorageVersioned: Identifiable, Codable, Sendablestruct UserProfile: LocalStorageVersioned {
let id: String
var fullName: String
var email: String
static var currentVersion: Int { 2 }
static var migrations: [StorageMigration] {
[
StorageMigration(from: 1, to: 2) { oldData in
// Transform V1 legacy JSON payload into V2 schema
var json = try JSONSerialization.jsonObject(with: oldData) as! [String: Any]
let first = json.removeValue(forKey: "firstName") as? String ?? ""
let last = json.removeValue(forKey: "lastName") as? String ?? ""
json["fullName"] = "\(first) \(last)".trimmingCharacters(in: .whitespaces)
return try JSONSerialization.data(withJSONObject: json)
}
]
}
}StorageMigration
Discrete schema migration definition holding source version number, destination version number, and a throwing raw Data transformation closure.
public struct StorageMigration: Sendablelet v1ToV2 = StorageMigration(from: 1, to: 2) { data in
var json = try JSONDecoder().decode(LegacyPayload.self, from: data)
let upgraded = ModernPayload(legacy: json)
return try JSONEncoder().encode(upgraded)
}StorageLogger
Pluggable logging interface for observing storage events, SQL query timings, cache hits, lazy TTL purges, and database errors.
public protocol StorageLogger: Sendablepublic protocol StorageLogger: Sendable {
func log(level: StorageLogLevel, message: String, metadata: [String: String]?)
func record(duration: TimeInterval, operation: String)
}OSLogStorageLogger
Production-ready logger utilizing Apple os.Logger subsystem with categorized diagnostic logging and signposts for Instruments profiling.
public final class OSLogStorageLogger: StorageLogger, @unchecked Sendablelet osLogger = OSLogStorageLogger(
subsystem: "com.acme.app",
category: "LocalStorage"
)
let storage = try LocalStorage(
configuration: .default,
logger: osLogger
)InMemoryStorageEngine
Lightweight, pure in-memory test double matching LocalStorage behaviors without creating SQLite database files on disk. Perfect for deterministic unit and UI tests.
public actor InMemoryStorageEngine: Sendablefinal class UserViewModelTests: XCTestCase {
func testUserSave() async throws {
let storage = try LocalStorage(configuration: .inMemory)
let repository = storage.repository(User.self)
try await repository.save(User.mock)
let fetched = try await repository.fetch(id: User.mock.id)
XCTAssertEqual(fetched?.name, User.mock.name)
}
}LocalStorageError
Comprehensive, strongly typed error enum covering record lookup misses, JSON encoding failures, DTO decoding mismatches, migration chain gaps, and SQLite storage faults.
public enum LocalStorageError: Error, Sendable, Equatabledo {
let user = try await storage.fetch(User.self, id: "usr_99")
} catch LocalStorageError.notFound(let id) {
print("Record \(id) not found or expired")
} catch LocalStorageError.decodingFailed(let reason) {
print("Schema mismatch: \(reason)")
} catch LocalStorageError.migrationFailed(let from, let to, let err) {
print("Migration \(from)->\(to) failed: \(err)")
} catch {
print("Storage engine error: \(error)")
}LocalStorageConfiguration
Central configuration struct controlling database file URL, in-memory mode, schema migrations, automatic expiration purge intervals, and custom logger.
public struct LocalStorageConfiguration: Sendablevar config = LocalStorageConfiguration()
config.isInMemory = false
config.autoPurgeExpiredOnRead = true
config.databaseDirectory = .applicationSupportDirectory
config.logger = OSLogStorageLogger()
let storage = try LocalStorage(configuration: config)Add to your project in seconds.
SwiftLocalStorage has zero external dependencies and supports all Apple platforms out of the box with Swift Package Manager.
https://github.com/ihusnainalii/SwiftLocalStorage.git- In Xcode, navigate to File > Add Package Dependencies…
- Paste the repository URL above into the search bar.
- Select Up to Next Major Version from
1.1.2. - Add SwiftLocalStorage to your main app target.
# Run the Clean Architecture SwiftUI Demo app
open Examples/SwiftLocalStorageDemo/SwiftLocalStorageDemo.xcodeproj
# Run the test suite with thread sanitizer & strict concurrency
swift test --parallel --sanitize=threadQuestions, feedback, or collaboration
I build and maintain SwiftLocalStorage and SwiftNetworkKit. Open a GitHub issue for bugs and feature requests, or reach me directly through any of the channels below.
Mail me directly for consulting, partnerships, or anything that doesn't belong in a public issue. I usually reply within a day or two.
GitHub
@ihusnainaliiBrowse the source, releases, and changelog here, or star the repo. My other projects live on this profile too.
If you want to see my full work history and background, or just connect, find me here.
Portfolio
ihusnainalii.github.ioSee the apps I've shipped, case studies, and my writing on iOS and Swift.
Bug reports & features
GitHub IssuesHit a bug or want a feature? Open an issue with a repro and I'll take a look. This is the fastest way to reach me.
Based in
Riyadh, Saudi Arabia
I'm on GMT+3 (Arabia Standard Time). I take calls roughly 9am to 7pm local, and I'm happy to work async if you're in another timezone.