| Sources/LocalData | ||
| Tests/LocalDataTests | ||
| .gitignore | ||
| FutureEnhancements.md | ||
| Package.resolved | ||
| Package.swift | ||
| Proposal.md | ||
| README.md | ||
LocalData
LocalData provides a typed, discoverable namespace for persisted app data across UserDefaults, Keychain, and file storage with optional encryption.
Architecture
The package uses a clean, modular architecture with isolated actors for thread safety:
StorageRouter (main entry point)
├── UserDefaultsHelper
├── KeychainHelper
├── FileStorageHelper
├── EncryptionHelper
└── SyncHelper
What Ships in the Package
Protocols
- StorageKey - Define storage configuration for each data type
- StorageProviding - Abstraction for storage operations
- KeyMaterialProviding - Supplies external key material for encryption
Services (Actors)
- StorageRouter - Main entry point for all storage operations
Internal Helpers (Not Public API)
These helpers are internal implementation details used by StorageRouter. They are not part of the public API and should not be used directly.
- KeychainHelper - Reads/writes secure items with Keychain APIs.
- EncryptionHelper - Handles encryption/decryption and key derivation.
- FileStorageHelper - Reads/writes files with appropriate protection.
- UserDefaultsHelper - Wraps UserDefaults and suites safely.
- SyncHelper - Manages WatchConnectivity sync.
Models
- StorageDomain - userDefaults, keychain, fileSystem, encryptedFileSystem
- SecurityPolicy - none, keychain, encrypted (AES-256 or ChaCha20-Poly1305)
- Serializer - JSON, plist, Data, or custom
- KeyMaterialSource - Identifier for external key material providers
- PlatformAvailability - all, phoneOnly, watchOnly, phoneWithWatchSync
- SyncPolicy - never, manual, automaticSmall
- KeychainAccessibility - All 7 iOS accessibility options
- KeychainAccessControl - All 6 access control options (biometry, passcode, etc.)
- FileDirectory - documents, caches, custom URL
- StorageError - Comprehensive error types
- StorageKeyDescriptor - Audit snapshot of a key’s storage metadata
- AnyStorageKey - Type-erased storage key for catalogs
- AnyCodable - Type-erased Codable for mixed-type payloads
Usage
1. Define Keys
Extend StorageKeys with your own key types:
import LocalData
extension StorageKeys {
struct UserTokenKey: StorageKey {
typealias Value = String
let name = "user_token"
let domain: StorageDomain = .keychain(service: "com.myapp")
let security: SecurityPolicy = .keychain(
accessibility: .afterFirstUnlock,
accessControl: .biometryAny
)
let serializer: Serializer<String> = .json
let owner = "AuthService"
let description = "Stores the current user auth token."
let availability: PlatformAvailability = .phoneOnly
let syncPolicy: SyncPolicy = .never
}
}
If you omit security, it defaults to SecurityPolicy.recommended.
2. Use StorageRouter
// Save
let key = StorageKeys.UserTokenKey()
try await StorageRouter.shared.set("token123", for: key)
// Retrieve
let token = try await StorageRouter.shared.get(key)
// Remove
try await StorageRouter.shared.remove(key)
Storage Domains
| Domain | Use Case |
|---|---|
userDefaults |
Preferences, small settings |
keychain |
Credentials, tokens, sensitive data |
fileSystem |
Documents, cached data, large files |
encryptedFileSystem |
Sensitive files with encryption policies |
Security Options
Keychain Accessibility
whenUnlocked- Only when device unlockedafterFirstUnlock- After first unlock until restartwhenUnlockedThisDeviceOnly- No migration to new deviceafterFirstUnlockThisDeviceOnly- No migrationalways- Always accessible (least secure)alwaysThisDeviceOnly- Always, no migrationwhenPasscodeSetThisDeviceOnly- Requires passcode
Access Control
userPresence- Any authenticationbiometryAny- Face ID or Touch IDbiometryCurrentSet- Current enrolled biometric onlydevicePasscode- Passcode onlybiometryAnyOrDevicePasscode- Biometric preferred, passcode fallbackbiometryCurrentSetOrDevicePasscode- Current biometric or passcode
Encryption
- AES-256-GCM or ChaCha20-Poly1305
- PBKDF2-SHA256 or HKDF-SHA256 key derivation
- Configurable PBKDF2 iteration count
- Master key stored securely in keychain
- Default security policy:
SecurityPolicy.recommended(ChaCha20-Poly1305 + HKDF) - External key material providers can be registered via
StorageRouter
struct RemoteKeyProvider: KeyMaterialProviding {
func keyMaterial(for keyName: String) async throws -> Data {
// Example only: fetch from service or keychain
Data(repeating: 1, count: 32)
}
}
let source = KeyMaterialSource(id: "remote.key")
await StorageRouter.shared.registerKeyMaterialProvider(RemoteKeyProvider(), for: source)
let policy: SecurityPolicy.EncryptionPolicy = .external(
source: source,
keyDerivation: .hkdf()
)
Sync Behavior
StorageRouter can sync data to Apple Watch via WCSession when:
availabilityis.allor.phoneWithWatchSyncsyncPolicyis.manualor.automaticSmall(≤100KB)- WCSession is activated and watch is paired
The app owns WCSession activation and handling incoming updates.
Platforms
- iOS 17+
- watchOS 10+
Testing
- Unit tests use Swift Testing (
Testingpackage)
Storage Audit
LocalData can generate a catalog of all configured storage keys, even if no data has been written yet. This is useful for security reviews and compliance.
Why AnyStorageKey?
StorageKey has an associated type (Value), which means you cannot store different keys in a single array using [StorageKey] or some StorageKey. Swift requires type erasure for heterogeneous protocol values, so the catalog uses [AnyStorageKey] and builds descriptors behind the scenes.
- Define a catalog in your app that lists all keys:
struct AppStorageCatalog: StorageKeyCatalog {
static var allKeys: [AnyStorageKey] {
[
.key(StorageKeys.AppVersionKey()),
.key(StorageKeys.UserPreferencesKey())
]
}
}
- Generate a report:
let report = StorageAuditReport.renderText(for: AppStorageCatalog.self)
print(report)
- Register the catalog to enforce usage and catch duplicates:
do {
try StorageRouter.shared.registerCatalog(AppStorageCatalog.self)
} catch {
assertionFailure("Storage catalog registration failed: \(error)")
}
Each StorageKey must provide a human-readable description used in audit reports.
Dynamic key names are intentionally not supported in the core API to keep storage auditing strict and predictable.
If you need this later, see FutureEnhancements.md for a proposed design.
Sample App
See SecureStorgageSample for working examples of all storage domains and security options.