Add Credential, UserPreferencesKey, Value, StorageKeys (+29 more)

This commit is contained in:
Matt Bruce 2026-01-13 22:00:54 -06:00
parent 0dd3ddcbae
commit e111b9a1d3
10 changed files with 1246 additions and 104 deletions

75
README.md Normal file
View File

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

View File

@ -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;

View File

@ -7,7 +7,7 @@
<key>SecureStorgageSample.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
<integer>1</integer>
</dict>
</dict>
</dict>

View File

@ -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()
}
.tabItem {
Label("Defaults", systemImage: "gearshape.fill")
}
Button("Retrieve Version") {
Task {
do {
let key = StorageKeys.AppVersionKey()
retrievedVersion = try await StorageRouter.shared.get(key)
} catch {
print("Error retrieving: \(error)")
}
}
NavigationStack {
KeychainDemo()
}
.tabItem {
Label("Keychain", systemImage: "lock.fill")
}
Text("Retrieved: \(retrievedVersion)")
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")
}
}
.padding()
}
}

View File

@ -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<String> = .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<SampleLocationData> = .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<Bool> = .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<Credential> = .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<SampleLocationData> = .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<String> = .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> = .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<String> = .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<Bool> = .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<String> = .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<String> = .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
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}