Add Credential, SampleLocationData, StorageKeys, PrivateNotesKey (+13 more); Remove Credential, SampleLocationData, StorageKeys, AppVersionKey (+13 more)

This commit is contained in:
Matt Bruce 2026-01-14 09:35:32 -06:00
parent c32ac4b601
commit 6db8ab5bc4
18 changed files with 314 additions and 272 deletions

View File

@ -41,8 +41,19 @@ SharedPackage/
└── UserProfile.swift └── UserProfile.swift
SecureStorgageSample/ SecureStorgageSample/
├── ContentView.swift # Tabbed navigation ├── ContentView.swift # Tabbed navigation
├── Models/
│ ├── Credential.swift
│ └── SampleLocationData.swift
├── StorageKeys.swift # 12 example key definitions ├── StorageKeys.swift # 12 example key definitions
├── StorageKeys/
│ ├── UserDefaults/
│ ├── Keychain/
│ ├── FileSystem/
│ ├── EncryptedFileSystem/
│ └── Platform/
├── WatchOptimized.swift # Watch data models ├── WatchOptimized.swift # Watch data models
├── Services/
│ └── WatchConnectivityService.swift
└── Views/ └── Views/
├── UserDefaultsDemo.swift ├── UserDefaultsDemo.swift
├── KeychainDemo.swift ├── KeychainDemo.swift
@ -52,8 +63,16 @@ SecureStorgageSample/
SecureStorageSample Watch App/ SecureStorageSample Watch App/
├── SecureStorageSampleApp.swift ├── SecureStorageSampleApp.swift
├── ContentView.swift ├── ContentView.swift
├── Models/
│ └── UserProfile.swift
├── Protocols/
│ └── WatchDataHandling.swift
├── State/
│ └── WatchProfileStore.swift
└── Services/ └── Services/
└── WatchConnectivityService.swift ├── WatchConnectivityService.swift
└── Handlers/
└── UserProfileWatchHandler.swift
``` ```
## Storage Key Examples ## Storage Key Examples
@ -87,6 +106,12 @@ The app demonstrates various storage configurations:
- [LocalData](../localPackages/LocalData) - Local package for typed secure storage - [LocalData](../localPackages/LocalData) - Local package for typed secure storage
- SharedKit - Local package for shared iOS/watch models and constants - SharedKit - Local package for shared iOS/watch models and constants
## Notes
- Storage keys are now split into one file per key and grouped by domain; platform-focused keys live in `StorageKeys/Platform` with comments calling out availability/sync focus.
- The shared model/constants live in `SharedPackage` (`SharedKit`) to keep the watch/iOS data contract centralized.
- The watch app uses a handler-based WatchConnectivity layer so new payload types can be added in `Services/Handlers` without bloating the main service.
## License ## License
This sample is provided for demonstration purposes. This sample is provided for demonstration purposes.

View File

@ -9,6 +9,8 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
EA179D562F17379800B1D54A /* LocalData in Frameworks */ = {isa = PBXBuildFile; productRef = EA179D552F17379800B1D54A /* LocalData */; }; EA179D562F17379800B1D54A /* LocalData in Frameworks */ = {isa = PBXBuildFile; productRef = EA179D552F17379800B1D54A /* LocalData */; };
EA65D70D2F17DDEB00C48466 /* SecureStorageSample Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = EA65D6E52F17DD6700C48466 /* SecureStorageSample Watch App.app */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; EA65D70D2F17DDEB00C48466 /* SecureStorageSample Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = EA65D6E52F17DD6700C48466 /* SecureStorageSample Watch App.app */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
EA65D9442F17EAD800C48466 /* SharedKit in Frameworks */ = {isa = PBXBuildFile; productRef = EA65D7312F17DDEB00C48466 /* SharedKit */; };
EA65D9452F17EAD800C48466 /* SharedKit in Frameworks */ = {isa = PBXBuildFile; productRef = EA65D7312F17DDEB00C48466 /* SharedKit */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -110,6 +112,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
EA65D9442F17EAD800C48466 /* SharedKit in Frameworks */,
EA179D562F17379800B1D54A /* LocalData in Frameworks */, EA179D562F17379800B1D54A /* LocalData in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -118,6 +121,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
EA65D9452F17EAD800C48466 /* SharedKit in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@ -0,0 +1,8 @@
import Foundation
/// Simple credential model for keychain storage demo.
nonisolated
struct Credential: Codable, Sendable {
let username: String
let password: String
}

View File

@ -0,0 +1,8 @@
import Foundation
/// Location data model.
nonisolated
struct SampleLocationData: Codable, Sendable {
let lat: Double
let lon: Double
}

View File

@ -1,271 +0,0 @@
//
// StorageKeys.swift
// SecureStorgageSample
//
// Example StorageKey implementations demonstrating all variations
// supported by the LocalData package.
//
import Foundation
import LocalData
import SharedKit
// MARK: - Sample Data Models
/// Simple credential model for keychain storage demo.
nonisolated
struct Credential: Codable, Sendable {
let username: String
let password: String
}
/// Location data model.
nonisolated
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 = "last_app_version"
let domain: StorageDomain = .userDefaults(suite: nil)
let security: SecurityPolicy = .none
let serializer: Serializer<String> = .json
let owner = "SampleApp"
let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .automaticSmall
}
/// Stores user preferences in a custom suite.
/// - Domain: UserDefaults (custom suite)
/// - Security: None
/// - Sync: Never
struct UserPreferencesKey: StorageKey {
typealias Value = [String: AnyCodable]
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 = "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 = UserProfile
let name = UserProfile.storageKeyName
let domain: StorageDomain
let security: SecurityPolicy = .none
let serializer: Serializer<UserProfile> = .json
let owner = "SampleApp"
let availability: PlatformAvailability = .phoneWithWatchSync
let syncPolicy: SyncPolicy = .automaticSmall
init(directory: FileDirectory = .documents) {
self.domain = .fileSystem(directory: directory)
}
}
/// 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,19 @@
import Foundation
import LocalData
extension StorageKeys {
/// 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
}
}

View File

@ -0,0 +1,22 @@
import Foundation
import LocalData
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)))
}
}
}

View File

@ -0,0 +1,17 @@
import Foundation
import LocalData
extension StorageKeys {
/// 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
}
}

View File

@ -0,0 +1,28 @@
import Foundation
import LocalData
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
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,17 @@
import Foundation
import LocalData
extension StorageKeys {
/// 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
}
}

View File

@ -0,0 +1,22 @@
import Foundation
import LocalData
import SharedKit
extension StorageKeys {
/// Stores user profile as JSON file in documents.
struct UserProfileFileKey: StorageKey {
typealias Value = UserProfile
let name = UserProfile.storageKeyName
let domain: StorageDomain
let security: SecurityPolicy = .none
let serializer: Serializer<UserProfile> = .json
let owner = "SampleApp"
let availability: PlatformAvailability = .phoneWithWatchSync
let syncPolicy: SyncPolicy = .automaticSmall
init(directory: FileDirectory = .documents) {
self.domain = .fileSystem(directory: directory)
}
}
}

View File

@ -0,0 +1,20 @@
import Foundation
import LocalData
extension StorageKeys {
/// 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
}
}

View File

@ -0,0 +1,22 @@
import Foundation
import LocalData
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)
}
}
}

View File

@ -0,0 +1,20 @@
import Foundation
import LocalData
extension StorageKeys {
/// 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
}
}

View File

@ -0,0 +1,23 @@
import Foundation
import LocalData
extension StorageKeys {
/// Syncable setting with configurable platform and sync policy.
/// Grouped under Platform to highlight availability/sync behavior.
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
}
}
}

View File

@ -0,0 +1,18 @@
import Foundation
import LocalData
extension StorageKeys {
/// Watch-only setting for vibration.
/// Grouped under Platform to highlight watch-only availability.
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
}
}

View File

@ -0,0 +1,20 @@
import Foundation
import LocalData
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 = "last_app_version"
let domain: StorageDomain = .userDefaults(suite: nil)
let security: SecurityPolicy = .none
let serializer: Serializer<String> = .json
let owner = "SampleApp"
let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .automaticSmall
}
}

View File

@ -0,0 +1,20 @@
import Foundation
import LocalData
extension StorageKeys {
/// Stores user preferences in a custom suite.
/// - Domain: UserDefaults (custom suite)
/// - Security: None
/// - Sync: Never
struct UserPreferencesKey: StorageKey {
typealias Value = [String: AnyCodable]
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 = "SampleApp"
let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never
}
}