Swift 6 Strict Concurrency · Zero @Model Boilerplate · v1.1.2

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.git
iOS 17.0+macOS 14.0+tvOS 17.0+watchOS 10.0+visionOS 1.0+
Swift 6 Strict Concurrency
import SwiftLocalStorage

// 1. Pure Codable DTO - Zero @Model Boilerplate
struct Product: Codable, Identifiable, Sendable {
    let id: Int
    let title: String
    let price: Double
}

// 2. Open Store (Actor-Isolated Engine)
let storage = try LocalStorage()

// 3. Save with TTL Expiration & Auto-Indexing
try await storage.save(product, expiration: .hours(1))

// 4. Fetch Typed DTO from Cache
let cached = try await storage.fetch(Product.self, id: 42) // Product?

// 5. Reactive SwiftUI Live Query
for try await products in storage.updates(of: Product.self) {
    self.products = products
}
0 Data Races TTL Cache Expiry Envelope V3
Interactive Execution Engine

How 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

Step 1 of 6
Engine Event LogLive Trace

Save Pipeline (Codable -> Envelope -> SwiftData)

How a pure Swift struct is encoded outside the actor and committed atomically into the StoredRecord V3 schema.

Zero Data Races
Stage 1 of 6Caller Context (MainActor / Task)

1. Caller Task

Invokes `storage.save(user, expiration: .hours(2))`

Non-blocking Swift async call. No @Model references needed.

Architecture Isolation SeamSwiftData stays inside the actor
1. Caller TaskEncodes pure Codable structs
2. ModelActor BoundaryTransfers raw Byte Snapshots
3. StoredRecord V3Committed in SwiftData SQLite
Interactive Code Playground

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.swift
import SwiftLocalStorage

// 1. Declare your domain DTO
struct Article: Codable, Identifiable, Sendable {
    let id: UUID
    let title: String
    let author: String
    let viewCount: Int
}

// 2. Open store
let storage = try LocalStorage()

// 3. Single record insert or update (upsert)
let article = Article(id: UUID(), title: "Swift 6 Concurrency", author: "Husnain", viewCount: 1200)
try await storage.save(article)

// 4. Batch save (Executed in a SINGLE atomic database transaction)
let batch: [Article] = fetchNewArticles()
try await storage.save(batch)

// 5. Fetch by ID or Fetch All
let fetched = try await storage.fetch(Article.self, id: article.id) // Article?
let allArticles = try await storage.fetch(Article.self)             // [Article]

// 6. Check existence and count
let exists = try await storage.exists(Article.self, id: article.id)
let total  = try await storage.count(Article.self)

// 7. Bulk or individual deletion
try await storage.delete(Article.self, id: article.id)
try await storage.deleteAll(Article.self)
Core CRUD

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.

Key Architectural Highlights:
Zero @Model classes or schema configuration
Single-transaction batch saving for hundreds of items
Missing records return nil (safe optional semantics)
Zero SwiftData Error Leaks
API Ref →
Concurrency Architecture

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

Live Actor Queue Simulator10 / 10 Tasks Done

10 Concurrent Background Tasks Writing Simultaneously

Thread & Execution Model Analysis
Strict Mode Verified
Execution Mechanism

JSON/Custom encoding runs in parallel on caller worker threads. Byte snapshots are serialized onto the background @ModelActor mailbox queue.

Direct SwiftData / Raw CoreData Failure Mode:

Direct SwiftData ModelContext access crashes immediately with 'EXC_BAD_ACCESS' or data corruption due to non-Sendable context sharing.

SwiftLocalStorage Engine Guarantee:

100% thread safe. Serialized transaction execution in ModelActor queue guarantees atomic commits and consistent ordering.

Isolation@ModelActor
Data Races0 Races
BoundaryByte Snapshots
Swift StandardSwift 6.0+
Storage Engine Visualizer

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.

Select Active Record:
SwiftData StoredRecord Row (Schema V3)Key: "User:usr_01"
envelope_key (PK)User:usr_01
type_name & schema_versionUser (v2)
insertion_sequence#1001 (Monotonic)
lifecycle_timestampscreated: 10 mins ago | updated: 10 mins ago
B-Tree Index Slots (Indexed Queries):
stringIndex0 (role)"admin"
stringIndex1 (emailDomain)"github.com"
numberIndex0 (age)28
numberIndex1 (lastSeen)1727280000
payload (Decoded on read outside actor)142 bytes
{
  "id": "usr_01",
  "name": "Husnain Ali",
  "role": "admin",
  "age": 28,
  "lastSeen": "2026-09-25T20:00:00Z"
}
Architectural Matrix

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 SwiftDataUserDefaultsCoreData
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
Evolution & Version History

From Genesis to Production Standard.

Track the rapid iteration of SwiftLocalStorage across 8 milestones, up to the current v1.1.2 release.

v1.1.0Current Release · Query Track

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.

Shipped Capabilities & Deliverables:
StorageFilter.hasPrefix(_:_:) and .oneOf(_:_:) for string indexes
updates(of:matching:orderedBy:options:) live indexed queries
where: filters and migrateAll walk 500 records at a time
Contradictory string conditions match nothing instead of trapping
Fix: parallel store creation no longer crashes on macOS 15
Engine Capabilities

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.

Envelope V3 Architecture

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.

0 ModelContainer Boilerplate
Swift 6 Concurrency

@ModelActor Isolation

All database operations are confined behind an internal `@ModelActor` queue. Only immutable Sendable byte snapshots cross boundaries.

0 Data Races
Smart Caching

Granular TTL Expirations

Attach `.minutes()`, `.hours()`, or `.date()` policies to any record. Expired data reads as nil and purges automatically on access.

Lazy Purge on Read
Indexed Engine

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.

28.5x Faster Queries
Reactive UI

Live SwiftUI Observation

Subscribe to coalesced live queries (`updates(of:)`) and typed event streams (`changes(of:)`) that auto-cancel when views disappear.

60 FPS Coalesced Updates
Ecosystem Pairing

SwiftNetworkKit Companion

Engineered to pair with SwiftNetworkKit for complete Offline-First architectures, instantaneous cache reads, and seamless background sync.

End-to-End Offline First
Data Integrity

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.

Zero-Downtime Upgrades
Testing & Previews

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.

0.15ms Test Execution
Companion Architecture

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 Layer
Network DSL

High-performance declarative HTTP networking with actor-isolated token refreshes, OAuth PKCE flows, SPKI public-key SSL pinning, and automatic retry policies.

Type-safe Endpoint protocol DSL
Single-flight 401 TokenManager actor
SPKI SHA-256 certificate pinning
Exponential backoff & jitter retries

SwiftLocalStorage

Local Persistence Layer
Storage DSL

Protocol-oriented storage engine persisting pure Codable DTOs directly into SwiftData SQLite with @ModelActor thread-safety and smart TTL cache invalidation.

Zero @Model class boilerplate
Granular TTL cache expirations (.hours, .days)
Store-level B-Tree index pushdown
Reactive SwiftUI updates(of:) live streams
OfflineFirstUserRepository.swift (SwiftNetworkKit + SwiftLocalStorage)
import SwiftUI
import SwiftLocalStorage
import SwiftNetworkKit

// 1. Unified Domain Model
struct UserProfile: Codable, Identifiable, Sendable {
    let id: String
    let name: String
    let email: String
    let avatarURL: URL?
}

// 2. SwiftNetworkKit Endpoint Definition
struct GetUserProfileEndpoint: Endpoint {
    typealias Response = UserProfile
    let userId: String
    
    var path: String { "/v1/users/\(userId)" }
    var method: HTTPMethod { .get }
    var authRequirement: AuthRequirement { .bearer }
}

// 3. Offline-First Repository Combining Both Libraries
final class UserRepository: Sendable {
    private let client: NetworkClient
    private let storage: LocalRepository<UserProfile>
    
    init(client: NetworkClient, storage: LocalStorage) {
        self.client = client
        self.storage = storage.repository(UserProfile.self)
    }
    
    /// Stale-While-Revalidate: Returns cached data immediately, then updates from network
    func getUser(id: String) async throws -> UserProfile {
        // Step A: Instant Local Cache Read
        if let cached = try await storage.fetch(id: id) {
            // Asynchronously revalidate in background if approaching expiration
            Task {
                if let fresh = try? await client.request(GetUserProfileEndpoint(userId: id)) {
                    try? await storage.save(fresh, expiration: .hours(6))
                }
            }
            return cached
        }
        
        // Step B: Network Fetch via SwiftNetworkKit
        let fresh = try await client.request(GetUserProfileEndpoint(userId: id))
        
        // Step C: Save to SwiftLocalStorage with 6-hour TTL
        try await storage.save(fresh, expiration: .hours(6))
        return fresh
    }
}
Swift 6 Concurrency Safe TTL Automatic Revalidation
View SwiftNetworkKit on GitHub
Vision & Trajectory

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.

Shipped (v0.1 - v0.6)

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
Status: Delivered in v0.1 – v0.6
Shipped (v1.0 – v1.1)

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)
Status: Shipped in v1.0.0 and v1.1.0
Beyond 1.0 Candidates

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
Community DrivenDiscuss →
Performance & Microbenchmarks

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.

Device: Apple M3 Max (16-core CPU, 128 GB RAM)
Toolchain: Swift 6.0 / Xcode 16.0
OS: macOS 15.0 / iOS 18.0 Simulator
Release: v1.1.2
CRUDLower is faster

Single Record Insert (save)

Encoding Codable DTO, building envelope metadata, saving row via @ModelActor

SwiftLocalStorage0.12 ms
Direct SwiftData (@Model)0.38 ms
CoreData0.44 ms
BatchLower is faster

Batch Save (100 records)

Single transaction envelope commit for 100 items (38 µs/record)

SwiftLocalStorage3.8 ms
Direct SwiftData (@Model)28.4 ms
CoreData19.2 ms
BatchLower is faster

Batch Save (1,000 records)

Bulk transactional batch write with index calculation

SwiftLocalStorage29.2 ms
Direct SwiftData (@Model)294 ms
CoreData168 ms
CRUDLower is faster

Fetch by ID (Primary Key)

Direct index hit + JSON payload decode into strongly-typed DTO

SwiftLocalStorage0.04 ms
Direct SwiftData (@Model)0.11 ms
CoreData0.14 ms
QueriesLower is faster

Indexed Filter & Sort Page (50 of 10,000)

SwiftData-level B-tree predicate filter + ordering; only 50 rows decoded

SwiftLocalStorage0.85 ms
Direct SwiftData (@Model)1.2 ms
CoreData1.45 ms
QueriesLower is faster

Unindexed In-Memory Closure Filter (10,000 items)

Full scan and decode of 10,000 DTOs evaluated with custom Swift closure

SwiftLocalStorage24.2 ms
Direct SwiftData (@Model)31 ms
CoreData42 ms
QueriesLower is faster

Indexed Count Query (10,000 items)

SwiftData index count without decoding payloads (103x faster than full scan)

SwiftLocalStorage0.18 ms
Direct SwiftData (@Model)0.22 ms
CoreData0.28 ms
CacheLower is faster

Lazy Cache Expiration Purge (on fetch)

Expired record detected via metadata timestamp, deleted, and nil returned

SwiftLocalStorage0.08 ms
MigrationLower is faster

DTO Schema Migration (100 records)

V1 -> V2 schema evolution applied with transformation pipeline

SwiftLocalStorage1.8 ms
Direct SwiftData (@Model)8.4 ms
CoreData12 ms
TestingLower is faster

In-Memory Engine (Test Double, 100 ops)

Zero disk I/O, isolated memory store for blazing fast unit tests

SwiftLocalStorage0.15 ms
Direct SwiftData (@Model)4.2 ms
CoreData6.8 ms

Payload Size Scaling & Throughput

Throughput performance across encoded DTO payload sizes from 1 KB to 10 MB.

Zero In-Memory Fragmentation
Payload Size1 KB
12,800 ops/s
~0.078 ms latency
Payload Size10 KB
8,900 ops/s
~0.112 ms latency
Payload Size100 KB
2,400 ops/s
~0.416 ms latency
Payload Size1 MB
340 ops/s
~2.94 ms latency
Payload Size10 MB
39 ops/s
~25.6 ms latency
Type Dictionary & API Reference

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).

Showing 19 of 19 documented public symbolsView DocC Catalog in Repo
Actor@ModelActor / Custom Isolated Actor
core

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
Parameters
configuration : LocalStorageConfigurationDatabase file URL, schema setup, in-memory mode flag, and custom logger.
logger : StorageLogger?Optional diagnostics logger for tracking queries, purges, and timings.
Swift Example
// 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")
StructSendable Domain Handle
repository

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>: Sendable
Parameters
storage : LocalStorageThe underlying isolated LocalStorage actor instance.
type : T.TypeThe model metatype conforming to Identifiable, Codable, and Sendable.
Swift Example
final 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)
    }
}
ProtocolCompile-Time Index Extraction
indexed

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, Sendable
Swift Example
struct 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)
        ]
    }
}
StructSendable Property Descriptor
indexed

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
Parameters
name : StringIndexed column identifier used in queries and sort expressions.
keyPath : KeyPath<T, V>Swift keypath to the string, numeric, boolean, or date property.
Swift Example
// Index on a nested or root property
StorageIndex("category", \Product.category)
StorageIndex("price", \Product.price)
StorageIndex("isFeatured", \Product.isFeatured)
EnumSendable SQL Predicate Value
indexed

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
Swift Example
// 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)
EnumSendable Order Descriptor
indexed

StorageIndexSort

SQL-level sorting clause applied directly within the SQLite engine to indexed columns for zero-memory ordering.

public enum StorageIndexSort: Sendable
Swift Example
// 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)
)
StructSendable Value Type
core

FetchOptions

Configures fetch limit, offset pagination, record sorting order, and whether expired records should be automatically purged during retrieval.

public struct FetchOptions: Sendable
Parameters
limit : Int?Maximum number of records to return from the store.
offset : Int?Number of matching records to skip before collecting results.
sort : StorageSortRecord sorting strategy (.newestFirst, .oldestFirst, .updatedRecent).
purgeExpired : BoolWhen true, automatically removes expired records encountered during query.
Swift Example
let options = FetchOptions(
    limit: 20,
    offset: 40,
    sort: .newestFirst,
    purgeExpired: true
)
let recentPosts = try await storage.fetch(Post.self, options: options)
StructSendable Page Container
indexed

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>: Sendable
Swift Example
let 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...
}
EnumSendable Value Type
cache

CacheExpiration

Granular Time-To-Live (TTL) cache expiration policies. Supports relative duration helpers, absolute target dates, and indefinite persistence.

public enum CacheExpiration: Sendable, Equatable
Swift Example
// 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))
StructSendable Diagnostic Value
cache

StorageMetadata

Metadata envelope detailing payload creation timestamp, last modification timestamp, computed expiration timestamp, payload size in bytes, and schema version.

public struct StorageMetadata: Sendable, Equatable
Swift Example
if 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)")
}
EnumSendable Event Value
observation

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>: Sendable
Swift Example
for 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")
    }
}
Struct / AsyncSequenceAsyncThrowingStream Pipeline
observation

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
Swift Example
// 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)")
        }
    }
}
ProtocolCompile-Time Schema Versioning
migrations

LocalStorageVersioned

Protocol declaring schema version number and sequential migration transformation steps for automated lazy on-read DTO migrations.

public protocol LocalStorageVersioned: Identifiable, Codable, Sendable
Swift Example
struct 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)
            }
        ]
    }
}
StructSendable Migration Step
migrations

StorageMigration

Discrete schema migration definition holding source version number, destination version number, and a throwing raw Data transformation closure.

public struct StorageMigration: Sendable
Parameters
fromVersion : IntStarting schema version integer.
toVersion : IntTarget schema version integer.
transform : @Sendable (Data) throws -> DataClosure upgrading serialized raw JSON data to next schema version.
Swift Example
let 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)
}
ProtocolSendable Observability Interface
logging

StorageLogger

Pluggable logging interface for observing storage events, SQL query timings, cache hits, lazy TTL purges, and database errors.

public protocol StorageLogger: Sendable
Swift Example
public protocol StorageLogger: Sendable {
    func log(level: StorageLogLevel, message: String, metadata: [String: String]?)
    func record(duration: TimeInterval, operation: String)
}
ClassApple Unified OSLog Integration
logging

OSLogStorageLogger

Production-ready logger utilizing Apple os.Logger subsystem with categorized diagnostic logging and signposts for Instruments profiling.

public final class OSLogStorageLogger: StorageLogger, @unchecked Sendable
Swift Example
let osLogger = OSLogStorageLogger(
    subsystem: "com.acme.app",
    category: "LocalStorage"
)

let storage = try LocalStorage(
    configuration: .default,
    logger: osLogger
)
ActorZero-Disk In-Memory Isolation
testing

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: Sendable
Swift Example
final 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)
    }
}
Enum / ErrorSendable Error Type
errors

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, Equatable
Swift Example
do {
    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)")
}
StructSendable Configuration Value
core

LocalStorageConfiguration

Central configuration struct controlling database file URL, in-memory mode, schema migrations, automatic expiration purge intervals, and custom logger.

public struct LocalStorageConfiguration: Sendable
Swift Example
var config = LocalStorageConfiguration()
config.isInMemory = false
config.autoPurgeExpiredOnRead = true
config.databaseDirectory = .applicationSupportDirectory
config.logger = OSLogStorageLogger()

let storage = try LocalStorage(configuration: config)
Integration & Installation

Add to your project in seconds.

SwiftLocalStorage has zero external dependencies and supports all Apple platforms out of the box with Swift Package Manager.

Package.swift Dependency
// swift-tools-version:6.0
import PackageDescription

let package = Package(
    name: "MyProject",
    platforms: [
        .iOS(.v17),
        .macOS(.v14),
        .tvOS(.v17),
        .watchOS(.v10),
        .visionOS(.v1)
    ],
    dependencies: [
        .package(url: "https://github.com/ihusnainalii/SwiftLocalStorage.git", from: "1.1.2"),
        .package(url: "https://github.com/ihusnainalii/SwiftNetworkKit.git", from: "1.1.2")
    ],
    targets: [
        .target(
            name: "MyProject",
            dependencies: [
                .product(name: "SwiftLocalStorage", package: "SwiftLocalStorage"),
                .product(name: "SwiftNetworkKit", package: "SwiftNetworkKit")
            ]
        )
    ]
)
Swift Package Manager Repository URL
https://github.com/ihusnainalii/SwiftLocalStorage.git
Xcode GUI Installation:
  1. In Xcode, navigate to File > Add Package Dependencies…
  2. Paste the repository URL above into the search bar.
  3. Select Up to Next Major Version from 1.1.2.
  4. Add SwiftLocalStorage to your main app target.
Run SwiftUI Clean MVVM Demo
# 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=thread
Get in touch

Questions, 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.

Email

husnainali593@gmail.com

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

@ihusnainalii

Browse the source, releases, and changelog here, or star the repo. My other projects live on this profile too.

LinkedIn

Husnain Ali

If you want to see my full work history and background, or just connect, find me here.

Portfolio

ihusnainalii.github.io

See the apps I've shipped, case studies, and my writing on iOS and Swift.

Bug reports & features

GitHub Issues

Hit 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.