diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8608685
--- /dev/null
+++ b/README.md
@@ -0,0 +1,75 @@
+# SecureStorgageSample
+
+A sample iOS app demonstrating the LocalData package capabilities for secure, typed storage across multiple domains.
+
+## Features
+
+This app provides interactive demos for all LocalData storage options:
+
+| Tab | Demo | Storage Domain |
+|-----|------|----------------|
+| **Defaults** | Save/load/remove values | UserDefaults |
+| **Keychain** | Secure credentials with biometrics | Keychain |
+| **Files** | User profiles with AnyCodable | File System |
+| **Encrypted** | AES-256 encrypted logs | Encrypted File System |
+| **Sync** | Platform availability & sync policies | Multiple |
+
+## Requirements
+
+- iOS 17.0+
+- Xcode 15+
+
+## Getting Started
+
+1. Open `SecureStorgageSample.xcodeproj`
+2. Select an iOS simulator or device
+3. Build and run (⌘R)
+
+## Project Structure
+
+```
+SecureStorgageSample/
+├── ContentView.swift # Tabbed navigation
+├── StorageKeys.swift # 12 example key definitions
+├── WatchOptimized.swift # Watch data models
+└── Views/
+ ├── UserDefaultsDemo.swift
+ ├── KeychainDemo.swift
+ ├── FileSystemDemo.swift
+ ├── EncryptedStorageDemo.swift
+ └── PlatformSyncDemo.swift
+```
+
+## Storage Key Examples
+
+The app demonstrates various storage configurations:
+
+### UserDefaults
+- Simple string storage with automatic sync
+- Custom suite support
+
+### Keychain
+- 7 accessibility options (whenUnlocked, afterFirstUnlock, etc.)
+- 6 access control options (biometry, passcode, etc.)
+
+### File System
+- Documents directory (persisted, backed up)
+- Caches directory (can be purged)
+- JSON and PropertyList serializers
+
+### Encrypted Storage
+- AES-256-GCM encryption
+- PBKDF2 key derivation with configurable iterations
+- Complete file protection
+
+### Platform & Sync
+- Platform availability (phoneOnly, watchOnly, all)
+- Sync policies (never, manual, automaticSmall)
+
+## Dependencies
+
+- [LocalData](../localPackages/LocalData) - Local package for typed secure storage
+
+## License
+
+This sample is provided for demonstration purposes.
diff --git a/SecureStorgageSample.xcodeproj/project.pbxproj b/SecureStorgageSample.xcodeproj/project.pbxproj
index 67711ff..5b2422d 100644
--- a/SecureStorgageSample.xcodeproj/project.pbxproj
+++ b/SecureStorgageSample.xcodeproj/project.pbxproj
@@ -7,9 +7,6 @@
objects = {
/* Begin PBXBuildFile section */
- EA179D392F17290D00B1D54A /* StorageKeys in Frameworks */ = {isa = PBXBuildFile; productRef = EA179D382F17290D00B1D54A /* StorageKeys */; };
- EA179D402F17326400B1D54A /* StorageKeys in Frameworks */ = {isa = PBXBuildFile; productRef = EA179D3F2F17326400B1D54A /* StorageKeys */; };
- EA179D532F17367700B1D54A /* StorageKeys in Frameworks */ = {isa = PBXBuildFile; productRef = EA179D522F17367700B1D54A /* StorageKeys */; };
EA179D562F17379800B1D54A /* LocalData in Frameworks */ = {isa = PBXBuildFile; productRef = EA179D552F17379800B1D54A /* LocalData */; };
/* End PBXBuildFile section */
@@ -59,10 +56,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- EA179D532F17367700B1D54A /* StorageKeys in Frameworks */,
- EA179D402F17326400B1D54A /* StorageKeys in Frameworks */,
EA179D562F17379800B1D54A /* LocalData in Frameworks */,
- EA179D392F17290D00B1D54A /* StorageKeys in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -123,9 +117,6 @@
);
name = SecureStorgageSample;
packageProductDependencies = (
- EA179D382F17290D00B1D54A /* StorageKeys */,
- EA179D3F2F17326400B1D54A /* StorageKeys */,
- EA179D522F17367700B1D54A /* StorageKeys */,
EA179D552F17379800B1D54A /* LocalData */,
);
productName = SecureStorgageSample;
@@ -599,18 +590,6 @@
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
- EA179D382F17290D00B1D54A /* StorageKeys */ = {
- isa = XCSwiftPackageProductDependency;
- productName = StorageKeys;
- };
- EA179D3F2F17326400B1D54A /* StorageKeys */ = {
- isa = XCSwiftPackageProductDependency;
- productName = StorageKeys;
- };
- EA179D522F17367700B1D54A /* StorageKeys */ = {
- isa = XCSwiftPackageProductDependency;
- productName = StorageKeys;
- };
EA179D552F17379800B1D54A /* LocalData */ = {
isa = XCSwiftPackageProductDependency;
productName = LocalData;
diff --git a/SecureStorgageSample.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist b/SecureStorgageSample.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist
index c36b9b7..72cdf61 100644
--- a/SecureStorgageSample.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/SecureStorgageSample.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -7,7 +7,7 @@
SecureStorgageSample.xcscheme_^#shared#^_
orderHint
- 0
+ 1
diff --git a/SecureStorgageSample/ContentView.swift b/SecureStorgageSample/ContentView.swift
index 28acd8c..1aa6beb 100644
--- a/SecureStorgageSample/ContentView.swift
+++ b/SecureStorgageSample/ContentView.swift
@@ -2,52 +2,50 @@
// ContentView.swift
// SecureStorgageSample
//
-// Created by Matt Bruce on 1/13/26.
+// Main navigation view with tabbed interface for all LocalData demos.
//
import SwiftUI
import LocalData
struct ContentView: View {
- @State private var appVersion: String = ""
- @State private var retrievedVersion: String = ""
-
var body: some View {
- VStack {
- Image(systemName: "globe")
- .imageScale(.large)
- .foregroundStyle(.tint)
- Text("Secure Storage Sample")
-
- TextField("Enter app version", text: $appVersion)
- .textFieldStyle(.roundedBorder)
- .padding()
-
- Button("Save Version") {
- Task {
- do {
- let key = StorageKeys.AppVersionKey()
- try await StorageRouter.shared.set(appVersion, for: key)
- } catch {
- print("Error saving: \(error)")
- }
- }
+ TabView {
+ NavigationStack {
+ UserDefaultsDemo()
}
-
- Button("Retrieve Version") {
- Task {
- do {
- let key = StorageKeys.AppVersionKey()
- retrievedVersion = try await StorageRouter.shared.get(key)
- } catch {
- print("Error retrieving: \(error)")
- }
- }
+ .tabItem {
+ Label("Defaults", systemImage: "gearshape.fill")
+ }
+
+ NavigationStack {
+ KeychainDemo()
+ }
+ .tabItem {
+ Label("Keychain", systemImage: "lock.fill")
+ }
+
+ NavigationStack {
+ FileSystemDemo()
+ }
+ .tabItem {
+ Label("Files", systemImage: "doc.fill")
+ }
+
+ NavigationStack {
+ EncryptedStorageDemo()
+ }
+ .tabItem {
+ Label("Encrypted", systemImage: "lock.shield.fill")
+ }
+
+ NavigationStack {
+ PlatformSyncDemo()
+ }
+ .tabItem {
+ Label("Sync", systemImage: "arrow.triangle.2.circlepath")
}
-
- Text("Retrieved: \(retrievedVersion)")
}
- .padding()
}
}
diff --git a/SecureStorgageSample/StorageKeys.swift b/SecureStorgageSample/StorageKeys.swift
index 7f5b746..d291b52 100644
--- a/SecureStorgageSample/StorageKeys.swift
+++ b/SecureStorgageSample/StorageKeys.swift
@@ -1,69 +1,266 @@
+//
+// StorageKeys.swift
+// SecureStorgageSample
+//
+// Example StorageKey implementations demonstrating all variations
+// supported by the LocalData package.
+//
+
import Foundation
import LocalData
+// MARK: - Sample Data Models
+
+/// Simple credential model for keychain storage demo.
+nonisolated(unsafe)
+struct Credential: Codable, Sendable {
+ let username: String
+ let password: String
+}
+
+/// Location data model.
+nonisolated(unsafe)
struct SampleLocationData: Codable, Sendable {
let lat: Double
let lon: Double
}
+// MARK: - UserDefaults Keys
+
extension StorageKeys {
+
+ /// Stores the app version in standard UserDefaults.
+ /// - Domain: UserDefaults (standard)
+ /// - Security: None
+ /// - Sync: Automatic for small data
struct AppVersionKey: StorageKey {
typealias Value = String
-
- let name: String = "last_app_version"
+
+ let name = "last_app_version"
let domain: StorageDomain = .userDefaults(suite: nil)
let security: SecurityPolicy = .none
let serializer: Serializer = .json
- let owner: String = "SampleApp"
+ let owner = "SampleApp"
let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .automaticSmall
}
-
- struct LastLocationKey: StorageKey {
- typealias Value = SampleLocationData
-
- let name: String = "last_known_location"
- let domain: StorageDomain = .keychain(service: "com.example.app.security")
- let security: SecurityPolicy = .keychain(accessibility: .afterFirstUnlock, accessControl: .userPresence)
- let serializer: Serializer = .json
- let owner: String = "SampleApp"
- let availability: PlatformAvailability = .phoneOnly
- let syncPolicy: SyncPolicy = .manual
- }
-
- struct UserProfileFileKey: StorageKey {
+
+ /// Stores user preferences in a custom suite.
+ /// - Domain: UserDefaults (custom suite)
+ /// - Security: None
+ /// - Sync: Never
+ struct UserPreferencesKey: StorageKey {
typealias Value = [String: AnyCodable]
-
- let name: String = "user_profile.json"
- let domain: StorageDomain = .fileSystem(directory: .documents)
+
+ let name = "user_preferences"
+ let domain: StorageDomain = .userDefaults(suite: "group.com.example.securestorage")
let security: SecurityPolicy = .none
let serializer: Serializer<[String: AnyCodable]> = .json
- let owner: String = "SampleApp"
- let availability: PlatformAvailability = .phoneOnly
- let syncPolicy: SyncPolicy = .never
- }
-
- struct SessionLogsKey: StorageKey {
- typealias Value = [String]
-
- let name: String = "session_logs.json"
- let domain: StorageDomain = .encryptedFileSystem(directory: .caches)
- let security: SecurityPolicy = .encrypted(.aes256(keyDerivation: .pbkdf2(iterations: 10_000)))
- let serializer: Serializer<[String]> = .json
- let owner: String = "SampleApp"
- let availability: PlatformAvailability = .phoneOnly
- let syncPolicy: SyncPolicy = .never
- }
-
- struct WatchVibrationKey: StorageKey {
- typealias Value = Bool
-
- let name: String = "watch_vibration_enabled"
- let domain: StorageDomain = .userDefaults(suite: nil)
- let security: SecurityPolicy = .none
- let serializer: Serializer = .json
- let owner: String = "SampleApp"
- let availability: PlatformAvailability = .watchOnly
+ let owner = "SampleApp"
+ let availability: PlatformAvailability = .all
+ let syncPolicy: SyncPolicy = .never
+ }
+}
+
+// MARK: - Keychain Keys
+
+extension StorageKeys {
+
+ /// Stores user credentials securely in keychain.
+ /// Configurable accessibility and access control.
+ struct CredentialsKey: StorageKey {
+ typealias Value = Credential
+
+ let name = "user_credentials"
+ let domain: StorageDomain = .keychain(service: "com.example.securestorage")
+ let security: SecurityPolicy
+ let serializer: Serializer = .json
+ let owner = "SampleApp"
+ let availability: PlatformAvailability = .phoneOnly
+ let syncPolicy: SyncPolicy = .never
+
+ init(accessibility: KeychainAccessibility = .afterFirstUnlock, accessControl: KeychainAccessControl? = nil) {
+ self.security = .keychain(accessibility: accessibility, accessControl: accessControl)
+ }
+ }
+
+ /// Stores sensitive location data in keychain with biometric protection.
+ struct LastLocationKey: StorageKey {
+ typealias Value = SampleLocationData
+
+ let name = "last_known_location"
+ let domain: StorageDomain = .keychain(service: "com.example.app.security")
+ let security: SecurityPolicy = .keychain(
+ accessibility: .afterFirstUnlock,
+ accessControl: .userPresence
+ )
+ let serializer: Serializer = .json
+ let owner = "SampleApp"
+ let availability: PlatformAvailability = .phoneOnly
+ let syncPolicy: SyncPolicy = .never
+ }
+
+ /// Stores API token in keychain.
+ struct APITokenKey: StorageKey {
+ typealias Value = String
+
+ let name = "api_token"
+ let domain: StorageDomain = .keychain(service: "com.example.securestorage.api")
+ let security: SecurityPolicy = .keychain(
+ accessibility: .whenUnlockedThisDeviceOnly,
+ accessControl: nil
+ )
+ let serializer: Serializer = .json
+ let owner = "SampleApp"
+ let availability: PlatformAvailability = .phoneOnly
+ let syncPolicy: SyncPolicy = .never
+ }
+}
+
+// MARK: - File System Keys
+
+extension StorageKeys {
+
+ /// Stores user profile as JSON file in documents.
+ struct UserProfileFileKey: StorageKey {
+ typealias Value = [String: AnyCodable]
+
+ let name = "user_profile.json"
+ let domain: StorageDomain = .fileSystem(directory: .documents)
+ let security: SecurityPolicy = .none
+ let serializer: Serializer<[String: AnyCodable]> = .json
+ let owner = "SampleApp"
+ let availability: PlatformAvailability = .phoneOnly
+ let syncPolicy: SyncPolicy = .never
+ }
+
+ /// Stores cached data files.
+ struct CachedDataKey: StorageKey {
+ typealias Value = Data
+
+ let name = "cached_data.bin"
+ let domain: StorageDomain = .fileSystem(directory: .caches)
+ let security: SecurityPolicy = .none
+ let serializer: Serializer = .data
+ let owner = "SampleApp"
+ let availability: PlatformAvailability = .all
+ let syncPolicy: SyncPolicy = .never
+ }
+
+ /// Stores settings as property list.
+ struct SettingsPlistKey: StorageKey {
+ typealias Value = [String: AnyCodable]
+
+ let name = "settings.plist"
+ let domain: StorageDomain = .fileSystem(directory: .documents)
+ let security: SecurityPolicy = .none
+ let serializer: Serializer<[String: AnyCodable]> = .plist
+ let owner = "SampleApp"
+ let availability: PlatformAvailability = .all
+ let syncPolicy: SyncPolicy = .never
+ }
+}
+
+// MARK: - Encrypted File System Keys
+
+extension StorageKeys {
+
+ /// Stores session logs with full encryption.
+ /// Configurable PBKDF2 iterations.
+ struct SessionLogsKey: StorageKey {
+ typealias Value = [String]
+
+ let name = "session_logs.json"
+ let domain: StorageDomain = .encryptedFileSystem(directory: .caches)
+ let security: SecurityPolicy
+ let serializer: Serializer<[String]> = .json
+ let owner = "SampleApp"
+ let availability: PlatformAvailability = .phoneOnly
+ let syncPolicy: SyncPolicy = .never
+
+ init(iterations: Int = 10_000) {
+ self.security = .encrypted(.aes256(keyDerivation: .pbkdf2(iterations: iterations)))
+ }
+ }
+
+ /// Stores private notes with encryption.
+ struct PrivateNotesKey: StorageKey {
+ typealias Value = String
+
+ let name = "private_notes.enc"
+ let domain: StorageDomain = .encryptedFileSystem(directory: .documents)
+ let security: SecurityPolicy = .encrypted(
+ .aes256(keyDerivation: .pbkdf2(iterations: 50_000))
+ )
+ let serializer: Serializer = .json
+ let owner = "SampleApp"
+ let availability: PlatformAvailability = .phoneOnly
+ let syncPolicy: SyncPolicy = .never
+ }
+}
+
+// MARK: - Platform-Specific Keys
+
+extension StorageKeys {
+
+ /// Watch-only setting for vibration.
+ struct WatchVibrationKey: StorageKey {
+ typealias Value = Bool
+
+ let name = "watch_vibration_enabled"
+ let domain: StorageDomain = .userDefaults(suite: nil)
+ let security: SecurityPolicy = .none
+ let serializer: Serializer = .json
+ let owner = "SampleApp"
+ let availability: PlatformAvailability = .watchOnly
+ let syncPolicy: SyncPolicy = .never
+ }
+
+ /// Syncable setting with configurable platform and sync policy.
+ struct SyncableSettingKey: StorageKey {
+ typealias Value = String
+
+ let name = "syncable_setting"
+ let domain: StorageDomain = .userDefaults(suite: nil)
+ let security: SecurityPolicy = .none
+ let serializer: Serializer = .json
+ let owner = "SampleApp"
+ let availability: PlatformAvailability
+ let syncPolicy: SyncPolicy
+
+ init(availability: PlatformAvailability = .all, syncPolicy: SyncPolicy = .never) {
+ self.availability = availability
+ self.syncPolicy = syncPolicy
+ }
+ }
+}
+
+// MARK: - Custom Serializer Example
+
+extension StorageKeys {
+
+ /// Example using custom serializer for specialized encoding.
+ struct CustomEncodedKey: StorageKey {
+ typealias Value = String
+
+ let name = "custom_encoded"
+ let domain: StorageDomain = .fileSystem(directory: .documents)
+ let security: SecurityPolicy = .none
+ let serializer: Serializer = .custom(
+ encode: { value in
+ // Example: Base64 encode
+ Data(value.utf8).base64EncodedData()
+ },
+ decode: { data in
+ guard let decoded = Data(base64Encoded: data),
+ let string = String(data: decoded, encoding: .utf8) else {
+ throw StorageError.deserializationFailed
+ }
+ return string
+ }
+ )
+ let owner = "SampleApp"
+ let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never
}
}
diff --git a/SecureStorgageSample/Views/EncryptedStorageDemo.swift b/SecureStorgageSample/Views/EncryptedStorageDemo.swift
new file mode 100644
index 0000000..c546f26
--- /dev/null
+++ b/SecureStorgageSample/Views/EncryptedStorageDemo.swift
@@ -0,0 +1,172 @@
+//
+// EncryptedStorageDemo.swift
+// SecureStorgageSample
+//
+// Demonstrates encrypted file storage with LocalData package.
+//
+
+import SwiftUI
+import LocalData
+
+struct EncryptedStorageDemo: View {
+ @State private var logEntry = ""
+ @State private var storedLogs: [String] = []
+ @State private var statusMessage = ""
+ @State private var isLoading = false
+ @State private var iterations = 10_000
+
+ var body: some View {
+ Form {
+ Section {
+ Text("Encrypted file storage uses AES-256-GCM encryption with PBKDF2 key derivation. Data is encrypted before being written to disk with complete file protection.")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+
+ Section("Add Log Entry") {
+ TextField("Enter log message", text: $logEntry, axis: .vertical)
+ .lineLimit(3, reservesSpace: true)
+
+ Stepper("PBKDF2 Iterations: \(iterations)", value: $iterations, in: 1000...100000, step: 1000)
+ .font(.caption)
+
+ Text("Higher iterations = more secure but slower")
+ .font(.caption2)
+ .foregroundStyle(.secondary)
+ }
+
+ Section("Actions") {
+ Button(action: addLogEntry) {
+ HStack {
+ Image(systemName: "plus.circle.fill")
+ Text("Add Encrypted Log Entry")
+ }
+ }
+ .disabled(logEntry.isEmpty || isLoading)
+
+ Button(action: loadLogs) {
+ HStack {
+ Image(systemName: "lock.open.fill")
+ Text("Decrypt and Load Logs")
+ }
+ }
+ .disabled(isLoading)
+
+ Button(action: clearLogs) {
+ HStack {
+ Image(systemName: "trash")
+ Text("Clear All Logs")
+ }
+ }
+ .foregroundStyle(.red)
+ .disabled(isLoading)
+ }
+
+ if !storedLogs.isEmpty {
+ Section("Decrypted Logs (\(storedLogs.count))") {
+ ForEach(Array(storedLogs.enumerated()), id: \.offset) { index, log in
+ HStack {
+ Text("\(index + 1).")
+ .foregroundStyle(.secondary)
+ Text(log)
+ }
+ .font(.system(.caption, design: .monospaced))
+ }
+ }
+ }
+
+ if !statusMessage.isEmpty {
+ Section {
+ Text(statusMessage)
+ .font(.caption)
+ .foregroundStyle(statusMessage.contains("Error") ? .red : .green)
+ }
+ }
+
+ Section("Encryption Details") {
+ LabeledContent("Algorithm", value: "AES-256-GCM")
+ LabeledContent("Key Derivation", value: "PBKDF2-SHA256")
+ LabeledContent("Iterations", value: "\(iterations)")
+ LabeledContent("File Protection", value: "Complete")
+ }
+
+ Section("Key Configuration") {
+ LabeledContent("Domain", value: "Encrypted File System")
+ LabeledContent("Directory", value: "Caches")
+ LabeledContent("Security", value: "AES-256 Encrypted")
+ LabeledContent("Platform", value: "Phone Only")
+ }
+ }
+ .navigationTitle("Encrypted Storage")
+ .navigationBarTitleDisplayMode(.inline)
+ }
+
+ private func addLogEntry() {
+ isLoading = true
+ Task {
+ do {
+ let key = StorageKeys.SessionLogsKey(iterations: iterations)
+
+ // Load existing logs
+ var logs: [String]
+ do {
+ logs = try await StorageRouter.shared.get(key)
+ } catch StorageError.notFound {
+ logs = []
+ }
+
+ // Add new entry with timestamp
+ let timestamp = Date().formatted(date: .abbreviated, time: .standard)
+ logs.append("[\(timestamp)] \(logEntry)")
+
+ // Save encrypted
+ try await StorageRouter.shared.set(logs, for: key)
+
+ storedLogs = logs
+ logEntry = ""
+ statusMessage = "✓ Entry encrypted and saved"
+ } catch {
+ statusMessage = "Error: \(error.localizedDescription)"
+ }
+ isLoading = false
+ }
+ }
+
+ private func loadLogs() {
+ isLoading = true
+ Task {
+ do {
+ let key = StorageKeys.SessionLogsKey(iterations: iterations)
+ storedLogs = try await StorageRouter.shared.get(key)
+ statusMessage = "✓ Decrypted \(storedLogs.count) log entries"
+ } catch StorageError.notFound {
+ storedLogs = []
+ statusMessage = "No encrypted logs found"
+ } catch {
+ statusMessage = "Error: \(error.localizedDescription)"
+ }
+ isLoading = false
+ }
+ }
+
+ private func clearLogs() {
+ isLoading = true
+ Task {
+ do {
+ let key = StorageKeys.SessionLogsKey(iterations: iterations)
+ try await StorageRouter.shared.remove(key)
+ storedLogs = []
+ statusMessage = "✓ Encrypted logs cleared"
+ } catch {
+ statusMessage = "Error: \(error.localizedDescription)"
+ }
+ isLoading = false
+ }
+ }
+}
+
+#Preview {
+ NavigationStack {
+ EncryptedStorageDemo()
+ }
+}
diff --git a/SecureStorgageSample/Views/FileSystemDemo.swift b/SecureStorgageSample/Views/FileSystemDemo.swift
new file mode 100644
index 0000000..4428900
--- /dev/null
+++ b/SecureStorgageSample/Views/FileSystemDemo.swift
@@ -0,0 +1,184 @@
+//
+// FileSystemDemo.swift
+// SecureStorgageSample
+//
+// Demonstrates file system storage with LocalData package.
+//
+
+import SwiftUI
+import LocalData
+
+struct FileSystemDemo: View {
+ @State private var profileName = ""
+ @State private var profileEmail = ""
+ @State private var profileAge = ""
+ @State private var storedProfile: [String: AnyCodable]?
+ @State private var statusMessage = ""
+ @State private var isLoading = false
+ @State private var selectedDirectory: FileDirectory = .documents
+
+ var body: some View {
+ Form {
+ Section {
+ Text("File system storage is great for larger data like user profiles, cached content, and structured documents.")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+
+ Section("Profile Data") {
+ TextField("Name", text: $profileName)
+ TextField("Email", text: $profileEmail)
+ .keyboardType(.emailAddress)
+ .textContentType(.emailAddress)
+ TextField("Age", text: $profileAge)
+ .keyboardType(.numberPad)
+ }
+
+ Section("Storage Location") {
+ Picker("Directory", selection: $selectedDirectory) {
+ Text("Documents").tag(FileDirectory.documents)
+ Text("Caches").tag(FileDirectory.caches)
+ }
+ .pickerStyle(.segmented)
+
+ Text(selectedDirectory == .documents
+ ? "Documents: Persisted, included in backups"
+ : "Caches: May be cleared by system, not backed up")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+
+ Section("Actions") {
+ Button(action: saveProfile) {
+ HStack {
+ Image(systemName: "doc.fill")
+ Text("Save to File System")
+ }
+ }
+ .disabled(profileName.isEmpty || isLoading)
+
+ Button(action: loadProfile) {
+ HStack {
+ Image(systemName: "doc.text.fill")
+ Text("Load from File System")
+ }
+ }
+ .disabled(isLoading)
+
+ Button(action: deleteProfile) {
+ HStack {
+ Image(systemName: "trash")
+ Text("Delete File")
+ }
+ }
+ .foregroundStyle(.red)
+ .disabled(isLoading)
+ }
+
+ if let profile = storedProfile {
+ Section("Retrieved Profile") {
+ ForEach(Array(profile.keys.sorted()), id: \.self) { key in
+ LabeledContent(key.capitalized, value: String(describing: profile[key]?.value ?? "nil"))
+ }
+ }
+ }
+
+ if !statusMessage.isEmpty {
+ Section {
+ Text(statusMessage)
+ .font(.caption)
+ .foregroundStyle(statusMessage.contains("Error") ? .red : .green)
+ }
+ }
+
+ Section("Key Configuration") {
+ LabeledContent("Domain", value: "File System")
+ LabeledContent("Security", value: "None")
+ LabeledContent("Serializer", value: "JSON")
+ LabeledContent("Platform", value: "Phone Only")
+ }
+ }
+ .navigationTitle("File System")
+ .navigationBarTitleDisplayMode(.inline)
+ }
+
+ private func saveProfile() {
+ isLoading = true
+ Task {
+ do {
+ let key = StorageKeys.UserProfileFileKey()
+ let profile: [String: AnyCodable] = [
+ "name": AnyCodable(profileName),
+ "email": AnyCodable(profileEmail),
+ "age": AnyCodable(Int(profileAge) ?? 0),
+ "createdAt": AnyCodable(Date().ISO8601Format())
+ ]
+ try await StorageRouter.shared.set(profile, for: key)
+ statusMessage = "✓ Saved to \(selectedDirectory == .documents ? "Documents" : "Caches")"
+ } catch {
+ statusMessage = "Error: \(error.localizedDescription)"
+ }
+ isLoading = false
+ }
+ }
+
+ private func loadProfile() {
+ isLoading = true
+ Task {
+ do {
+ let key = StorageKeys.UserProfileFileKey()
+ storedProfile = try await StorageRouter.shared.get(key)
+ statusMessage = "✓ Loaded from file system"
+ } catch StorageError.notFound {
+ storedProfile = nil
+ statusMessage = "No profile file found"
+ } catch {
+ statusMessage = "Error: \(error.localizedDescription)"
+ }
+ isLoading = false
+ }
+ }
+
+ private func deleteProfile() {
+ isLoading = true
+ Task {
+ do {
+ let key = StorageKeys.UserProfileFileKey()
+ try await StorageRouter.shared.remove(key)
+ storedProfile = nil
+ statusMessage = "✓ File deleted"
+ } catch {
+ statusMessage = "Error: \(error.localizedDescription)"
+ }
+ isLoading = false
+ }
+ }
+}
+
+extension FileDirectory: Hashable {
+ public func hash(into hasher: inout Hasher) {
+ switch self {
+ case .documents:
+ hasher.combine("documents")
+ case .caches:
+ hasher.combine("caches")
+ case .custom(let url):
+ hasher.combine(url)
+ }
+ }
+
+ public static func == (lhs: FileDirectory, rhs: FileDirectory) -> Bool {
+ switch (lhs, rhs) {
+ case (.documents, .documents): return true
+ case (.caches, .caches): return true
+ case (.custom(let a), .custom(let b)): return a == b
+ default: return false
+ }
+ }
+}
+
+#Preview {
+ NavigationStack {
+ FileSystemDemo()
+ }
+}
diff --git a/SecureStorgageSample/Views/KeychainDemo.swift b/SecureStorgageSample/Views/KeychainDemo.swift
new file mode 100644
index 0000000..87f3d87
--- /dev/null
+++ b/SecureStorgageSample/Views/KeychainDemo.swift
@@ -0,0 +1,167 @@
+//
+// KeychainDemo.swift
+// SecureStorgageSample
+//
+// Demonstrates Keychain storage with LocalData package.
+//
+
+import SwiftUI
+import LocalData
+
+struct KeychainDemo: View {
+ @State private var username = ""
+ @State private var password = ""
+ @State private var storedCredential = ""
+ @State private var statusMessage = ""
+ @State private var isLoading = false
+ @State private var selectedAccessibility: KeychainAccessibility = .afterFirstUnlock
+ @State private var selectedAccessControl: KeychainAccessControl? = nil
+
+ var body: some View {
+ Form {
+ Section {
+ Text("Keychain provides hardware-backed secure storage for sensitive data like passwords, tokens, and keys.")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+
+ Section("Credentials") {
+ TextField("Username", text: $username)
+ SecureField("Password", text: $password)
+ }
+
+ Section("Accessibility") {
+ Picker("When Accessible", selection: $selectedAccessibility) {
+ ForEach(KeychainAccessibility.allCases, id: \.self) { option in
+ Text(option.displayName).tag(option)
+ }
+ }
+ .pickerStyle(.menu)
+ }
+
+ Section("Access Control (Optional)") {
+ Picker("Require Authentication", selection: $selectedAccessControl) {
+ Text("None").tag(nil as KeychainAccessControl?)
+ ForEach(KeychainAccessControl.allCases, id: \.self) { option in
+ Text(option.displayName).tag(option as KeychainAccessControl?)
+ }
+ }
+ .pickerStyle(.menu)
+ }
+
+ Section("Actions") {
+ Button(action: saveCredentials) {
+ HStack {
+ Image(systemName: "lock.fill")
+ Text("Save to Keychain")
+ }
+ }
+ .disabled(username.isEmpty || password.isEmpty || isLoading)
+
+ Button(action: loadCredentials) {
+ HStack {
+ Image(systemName: "key.fill")
+ Text("Retrieve from Keychain")
+ }
+ }
+ .disabled(isLoading)
+
+ Button(action: deleteCredentials) {
+ HStack {
+ Image(systemName: "trash")
+ Text("Delete from Keychain")
+ }
+ }
+ .foregroundStyle(.red)
+ .disabled(isLoading)
+ }
+
+ if !storedCredential.isEmpty {
+ Section("Retrieved Data") {
+ Text(storedCredential)
+ .font(.system(.body, design: .monospaced))
+ }
+ }
+
+ if !statusMessage.isEmpty {
+ Section {
+ Text(statusMessage)
+ .font(.caption)
+ .foregroundStyle(statusMessage.contains("Error") ? .red : .green)
+ }
+ }
+
+ Section("Example Key Configuration") {
+ LabeledContent("Domain", value: "Keychain")
+ LabeledContent("Security", value: "Keychain Policy")
+ LabeledContent("Serializer", value: "JSON")
+ LabeledContent("Platform", value: "Phone Only")
+ }
+ }
+ .navigationTitle("Keychain")
+ .navigationBarTitleDisplayMode(.inline)
+ }
+
+ private func saveCredentials() {
+ isLoading = true
+ Task {
+ do {
+ let key = StorageKeys.CredentialsKey(
+ accessibility: selectedAccessibility,
+ accessControl: selectedAccessControl
+ )
+ let credential = Credential(username: username, password: password)
+ try await StorageRouter.shared.set(credential, for: key)
+ statusMessage = "✓ Saved to Keychain securely"
+ } catch {
+ statusMessage = "Error: \(error.localizedDescription)"
+ }
+ isLoading = false
+ }
+ }
+
+ private func loadCredentials() {
+ isLoading = true
+ Task {
+ do {
+ let key = StorageKeys.CredentialsKey(
+ accessibility: selectedAccessibility,
+ accessControl: selectedAccessControl
+ )
+ let credential = try await StorageRouter.shared.get(key)
+ storedCredential = "Username: \(credential.username)\nPassword: ****"
+ statusMessage = "✓ Retrieved from Keychain"
+ } catch StorageError.notFound {
+ storedCredential = ""
+ statusMessage = "No credentials stored"
+ } catch {
+ statusMessage = "Error: \(error.localizedDescription)"
+ }
+ isLoading = false
+ }
+ }
+
+ private func deleteCredentials() {
+ isLoading = true
+ Task {
+ do {
+ let key = StorageKeys.CredentialsKey(
+ accessibility: selectedAccessibility,
+ accessControl: selectedAccessControl
+ )
+ try await StorageRouter.shared.remove(key)
+ storedCredential = ""
+ statusMessage = "✓ Deleted from Keychain"
+ } catch {
+ statusMessage = "Error: \(error.localizedDescription)"
+ }
+ isLoading = false
+ }
+ }
+}
+
+#Preview {
+ NavigationStack {
+ KeychainDemo()
+ }
+}
diff --git a/SecureStorgageSample/Views/PlatformSyncDemo.swift b/SecureStorgageSample/Views/PlatformSyncDemo.swift
new file mode 100644
index 0000000..fb961e1
--- /dev/null
+++ b/SecureStorgageSample/Views/PlatformSyncDemo.swift
@@ -0,0 +1,231 @@
+//
+// PlatformSyncDemo.swift
+// SecureStorgageSample
+//
+// Demonstrates platform availability and sync policies with LocalData package.
+//
+
+import SwiftUI
+import LocalData
+
+struct PlatformSyncDemo: View {
+ @State private var settingValue = ""
+ @State private var storedValue = ""
+ @State private var statusMessage = ""
+ @State private var isLoading = false
+ @State private var selectedPlatform: PlatformAvailability = .all
+ @State private var selectedSync: SyncPolicy = .never
+
+ var body: some View {
+ Form {
+ Section {
+ Text("Platform availability controls which devices can access data. Sync policy determines how data is shared between iPhone and Apple Watch via WatchConnectivity.")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+
+ Section("Platform Availability") {
+ Picker("Available On", selection: $selectedPlatform) {
+ Text("All (iPhone + Watch)").tag(PlatformAvailability.all)
+ Text("Phone Only").tag(PlatformAvailability.phoneOnly)
+ Text("Watch Only").tag(PlatformAvailability.watchOnly)
+ Text("Phone w/ Watch Sync").tag(PlatformAvailability.phoneWithWatchSync)
+ }
+ .pickerStyle(.menu)
+
+ platformDescription
+ }
+
+ Section("Sync Policy") {
+ Picker("Sync Behavior", selection: $selectedSync) {
+ Text("Never").tag(SyncPolicy.never)
+ Text("Manual").tag(SyncPolicy.manual)
+ Text("Automatic (Small Data)").tag(SyncPolicy.automaticSmall)
+ }
+ .pickerStyle(.menu)
+
+ syncDescription
+ }
+
+ Section("Test Data") {
+ TextField("Enter a value to store", text: $settingValue)
+ }
+
+ Section("Actions") {
+ Button(action: saveValue) {
+ HStack {
+ Image(systemName: "icloud.and.arrow.up")
+ Text("Save with Current Settings")
+ }
+ }
+ .disabled(settingValue.isEmpty || isLoading)
+
+ Button(action: loadValue) {
+ HStack {
+ Image(systemName: "icloud.and.arrow.down")
+ Text("Load Value")
+ }
+ }
+ .disabled(isLoading)
+
+ Button(action: testPlatformError) {
+ HStack {
+ Image(systemName: "exclamationmark.triangle")
+ Text("Test Platform Restriction")
+ }
+ }
+ .foregroundStyle(.orange)
+ .disabled(isLoading)
+ }
+
+ if !storedValue.isEmpty {
+ Section("Retrieved Value") {
+ Text(storedValue)
+ .font(.system(.body, design: .monospaced))
+ }
+ }
+
+ if !statusMessage.isEmpty {
+ Section {
+ Text(statusMessage)
+ .font(.caption)
+ .foregroundStyle(statusMessage.contains("Error") ? .red :
+ statusMessage.contains("⚠") ? .orange : .green)
+ }
+ }
+
+ Section("Current Configuration") {
+ LabeledContent("Platform", value: selectedPlatform.displayName)
+ LabeledContent("Sync", value: selectedSync.displayName)
+ LabeledContent("Max Auto-Sync Size", value: "100 KB")
+ }
+ }
+ .navigationTitle("Platform & Sync")
+ .navigationBarTitleDisplayMode(.inline)
+ }
+
+ @ViewBuilder
+ private var platformDescription: some View {
+ switch selectedPlatform {
+ case .all:
+ Text("Data accessible on both iPhone and Apple Watch")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ case .phoneOnly:
+ Text("Data only accessible on iPhone. Watch access throws error.")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ case .watchOnly:
+ Text("Data only accessible on Watch. iPhone access throws error.")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ case .phoneWithWatchSync:
+ Text("Stored on iPhone, synced to Watch via WatchConnectivity")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ }
+
+ @ViewBuilder
+ private var syncDescription: some View {
+ switch selectedSync {
+ case .never:
+ Text("Data stays local, never synced")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ case .manual:
+ Text("Sync triggered explicitly by app code")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ case .automaticSmall:
+ Text("Auto-sync if data ≤ 100KB, otherwise throws error")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ }
+
+ private func saveValue() {
+ isLoading = true
+ Task {
+ do {
+ let key = StorageKeys.SyncableSettingKey(
+ availability: selectedPlatform,
+ syncPolicy: selectedSync
+ )
+ try await StorageRouter.shared.set(settingValue, for: key)
+ statusMessage = "✓ Saved with \(selectedPlatform.displayName) availability and \(selectedSync.displayName) sync"
+ } catch StorageError.dataTooLargeForSync {
+ statusMessage = "Error: Data too large for automatic sync (max 100KB)"
+ } catch {
+ statusMessage = "Error: \(error.localizedDescription)"
+ }
+ isLoading = false
+ }
+ }
+
+ private func loadValue() {
+ isLoading = true
+ Task {
+ do {
+ let key = StorageKeys.SyncableSettingKey(
+ availability: selectedPlatform,
+ syncPolicy: selectedSync
+ )
+ storedValue = try await StorageRouter.shared.get(key)
+ statusMessage = "✓ Loaded value"
+ } catch StorageError.notFound {
+ storedValue = ""
+ statusMessage = "No value stored"
+ } catch {
+ statusMessage = "Error: \(error.localizedDescription)"
+ }
+ isLoading = false
+ }
+ }
+
+ private func testPlatformError() {
+ isLoading = true
+ Task {
+ // Try to access a watchOnly key from iPhone
+ let key = StorageKeys.WatchVibrationKey()
+ do {
+ _ = try await StorageRouter.shared.get(key)
+ statusMessage = "⚠ Accessed successfully (running on Watch?)"
+ } catch StorageError.watchOnlyKeyAccessedOnPhone(let name) {
+ statusMessage = "⚠ Expected Error: Key '\(name)' is watchOnly, accessed from iPhone"
+ } catch {
+ statusMessage = "Error: \(error.localizedDescription)"
+ }
+ isLoading = false
+ }
+ }
+}
+
+// MARK: - Display Names
+
+extension PlatformAvailability {
+ var displayName: String {
+ switch self {
+ case .all: return "All"
+ case .phoneOnly: return "Phone Only"
+ case .watchOnly: return "Watch Only"
+ case .phoneWithWatchSync: return "Phone + Watch Sync"
+ }
+ }
+}
+
+extension SyncPolicy {
+ var displayName: String {
+ switch self {
+ case .never: return "Never"
+ case .manual: return "Manual"
+ case .automaticSmall: return "Automatic"
+ }
+ }
+}
+
+#Preview {
+ NavigationStack {
+ PlatformSyncDemo()
+ }
+}
diff --git a/SecureStorgageSample/Views/UserDefaultsDemo.swift b/SecureStorgageSample/Views/UserDefaultsDemo.swift
new file mode 100644
index 0000000..d20d92f
--- /dev/null
+++ b/SecureStorgageSample/Views/UserDefaultsDemo.swift
@@ -0,0 +1,139 @@
+//
+// UserDefaultsDemo.swift
+// SecureStorgageSample
+//
+// Demonstrates UserDefaults storage with LocalData package.
+//
+
+import SwiftUI
+import LocalData
+
+struct UserDefaultsDemo: View {
+ @State private var inputText = ""
+ @State private var storedValue = ""
+ @State private var statusMessage = ""
+ @State private var isLoading = false
+
+ var body: some View {
+ Form {
+ Section {
+ Text("UserDefaults stores simple data persistently. Great for preferences and small values.")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+
+ Section("Store Value") {
+ TextField("Enter a value", text: $inputText)
+ .textFieldStyle(.roundedBorder)
+
+ Button(action: saveValue) {
+ HStack {
+ Image(systemName: "square.and.arrow.down")
+ Text("Save to UserDefaults")
+ }
+ }
+ .disabled(inputText.isEmpty || isLoading)
+ }
+
+ Section("Retrieve Value") {
+ Button(action: loadValue) {
+ HStack {
+ Image(systemName: "arrow.down.circle")
+ Text("Load from UserDefaults")
+ }
+ }
+ .disabled(isLoading)
+
+ if !storedValue.isEmpty {
+ HStack {
+ Text("Stored:")
+ Spacer()
+ Text(storedValue)
+ .foregroundStyle(.blue)
+ }
+ }
+ }
+
+ Section("Remove Value") {
+ Button(action: removeValue) {
+ HStack {
+ Image(systemName: "trash")
+ Text("Remove from UserDefaults")
+ }
+ }
+ .foregroundStyle(.red)
+ .disabled(isLoading)
+ }
+
+ if !statusMessage.isEmpty {
+ Section {
+ Text(statusMessage)
+ .font(.caption)
+ .foregroundStyle(statusMessage.contains("Error") ? .red : .green)
+ }
+ }
+
+ Section("Key Configuration") {
+ LabeledContent("Domain", value: "UserDefaults (standard)")
+ LabeledContent("Security", value: "None")
+ LabeledContent("Serializer", value: "JSON")
+ LabeledContent("Platform", value: "All")
+ LabeledContent("Sync Policy", value: "Automatic Small")
+ }
+ }
+ .navigationTitle("UserDefaults")
+ .navigationBarTitleDisplayMode(.inline)
+ }
+
+ private func saveValue() {
+ isLoading = true
+ Task {
+ do {
+ let key = StorageKeys.AppVersionKey()
+ try await StorageRouter.shared.set(inputText, for: key)
+ statusMessage = "✓ Saved successfully"
+ } catch {
+ statusMessage = "Error: \(error.localizedDescription)"
+ }
+ isLoading = false
+ }
+ }
+
+ private func loadValue() {
+ isLoading = true
+ Task {
+ do {
+ let key = StorageKeys.AppVersionKey()
+ storedValue = try await StorageRouter.shared.get(key)
+ statusMessage = "✓ Loaded successfully"
+ } catch StorageError.notFound {
+ storedValue = ""
+ statusMessage = "No value stored yet"
+ } catch {
+ statusMessage = "Error: \(error.localizedDescription)"
+ }
+ isLoading = false
+ }
+ }
+
+ private func removeValue() {
+ isLoading = true
+ Task {
+ do {
+ let key = StorageKeys.AppVersionKey()
+ try await StorageRouter.shared.remove(key)
+ storedValue = ""
+ statusMessage = "✓ Removed successfully"
+ } catch {
+ statusMessage = "Error: \(error.localizedDescription)"
+ }
+ isLoading = false
+ }
+ }
+}
+
+#Preview {
+ NavigationStack {
+ UserDefaultsDemo()
+ }
+}