4.9 KiB
4.9 KiB
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
- KeychainHelper - Secure keychain storage
- EncryptionHelper - AES-256-GCM or ChaCha20-Poly1305 with PBKDF2/HKDF
- FileStorageHelper - File system operations
- UserDefaultsHelper - UserDefaults with suite support
- SyncHelper - WatchConnectivity sync
Models
- StorageDomain - userDefaults, keychain, fileSystem, encryptedFileSystem
- SecurityPolicy - none, keychain, encrypted (AES-256 or ChaCha20-Poly1305)
- Serializer - JSON, plist, Data, or custom
- 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
- 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 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
EncryptionHelper
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 EncryptionHelper.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)
Sample App
See SecureStorgageSample for working examples of all storage domains and security options.