Compare commits

..

No commits in common. "d8e49d8d8db563b44ee8a67d12d1e7f65fd2d028" and "c18f134ce2a353cf73fc78d8b4d616db8134160d" have entirely different histories.

33 changed files with 447 additions and 447 deletions

View File

@ -100,7 +100,7 @@ SecureStorageSample Watch App/
## Storage Design Philosophy ## Storage Design Philosophy
This app intentionally uses a **Type-Safe Storage Design**. Unlike standard iOS development which uses string keys (e.g., `UserDefaults.standard.string(forKey: "user_name")`), this library requires you to define `StorageKey` values. This app intentionally uses a **Type-Safe Storage Design**. Unlike standard iOS development which uses string keys (e.g., `UserDefaults.standard.string(forKey: "user_name")`), this library requires you to define a `StorageKey` type.
### Why types instead of strings? ### Why types instead of strings?
1. **Safety**: The compiler prevents typos. You can't accidentally load from `"user_name"` and save to `"username"`. 1. **Safety**: The compiler prevents typos. You can't accidentally load from `"user_name"` and save to `"username"`.
@ -144,7 +144,7 @@ The app demonstrates various storage configurations:
- Global sync configuration (max file size) in app `init` - Global sync configuration (max file size) in app `init`
### Data Migration ### Data Migration
- **Fallback**: Automatically moves data from `legacyMigrationSource` to `modernMigrationDestination` on first access using protocol-based migration. - **Fallback**: Automatically moves data from `LegacyMigrationSourceKey` to `ModernMigrationDestinationKey` on first access using protocol-based migration.
- **Transforming**: Converts a legacy full-name string into a structured `ProfileName`. - **Transforming**: Converts a legacy full-name string into a structured `ProfileName`.
- **Aggregating**: Combines legacy notification + theme settings into `UnifiedSettings`. - **Aggregating**: Combines legacy notification + theme settings into `UnifiedSettings`.
- **Conditional**: Migrates app mode only when the version rule is met. - **Conditional**: Migrates app mode only when the version rule is met.
@ -170,7 +170,6 @@ This approach centralizes infrastructure settings and avoids hardcoding environm
## Notes ## 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. - 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.
- Keys are declared as `StorageKey<Value>` static properties via constrained extensions (e.g., `extension StorageKey where Value == String`).
- The shared model/constants live in `SharedPackage` (`SharedKit`) to keep the watch/iOS data contract centralized. - The shared model/constants live in `SharedPackage` (`SharedKit`) to keep the watch/iOS data contract centralized.
- Keychain service IDs and App Group identifiers are centralized in `SharedKit/Constants/StorageServiceIdentifiers.swift` to avoid hardcoded strings in keys. - Keychain service IDs and App Group identifiers are centralized in `SharedKit/Constants/StorageServiceIdentifiers.swift` to avoid hardcoded strings in keys.
- The watch app uses a handler-based WatchConnectivity layer so new payload types can be added in `Services/Handlers` without bloating the main service. - The watch app uses a handler-based WatchConnectivity layer so new payload types can be added in `Services/Handlers` without bloating the main service.

View File

@ -6,31 +6,31 @@ import SharedKit
struct AppStorageCatalog: StorageKeyCatalog { struct AppStorageCatalog: StorageKeyCatalog {
var allKeys: [AnyStorageKey] { var allKeys: [AnyStorageKey] {
[ [
.key(.appVersion), .key(StorageKeys.AppVersionKey()),
.key(.userPreferences), .key(StorageKeys.UserPreferencesKey()),
.key(.credentials), .key(StorageKeys.CredentialsKey()),
.key(.lastLocation), .key(StorageKeys.LastLocationKey()),
.key(.apiToken), .key(StorageKeys.APITokenKey()),
.key(.userProfileFile), .key(StorageKeys.UserProfileFileKey()),
.key(.cachedData), .key(StorageKeys.CachedDataKey()),
.key(.settingsPlist), .key(StorageKeys.SettingsPlistKey()),
.key(.sessionLogs), .key(StorageKeys.SessionLogsKey()),
.key(.privateNotes), .key(StorageKeys.PrivateNotesKey()),
.key(.externalSessionLogs), .key(StorageKeys.ExternalSessionLogsKey()),
.key(.watchVibration), .key(StorageKeys.WatchVibrationKey()),
.key(.syncableSetting), .key(StorageKeys.SyncableSettingKey()),
.key(.externalKeyMaterial), .key(StorageKeys.ExternalKeyMaterialKey()),
.key(.appGroupUserDefaults), .key(StorageKeys.AppGroupUserDefaultsKey()),
.key(.appGroupUserProfile), .key(StorageKeys.AppGroupUserProfileKey()),
.key(.legacyMigrationSource), .key(StorageKeys.LegacyMigrationSourceKey()),
.key(.modernMigrationDestination), .key(StorageKeys.ModernMigrationDestinationKey()),
.key(.legacyProfileName), .key(StorageKeys.LegacyProfileNameKey()),
.key(.modernProfileName), .key(StorageKeys.ModernProfileNameKey()),
.key(.legacyNotificationSetting), .key(StorageKeys.LegacyNotificationSettingKey()),
.key(.legacyThemeSetting), .key(StorageKeys.LegacyThemeSettingKey()),
.key(.modernUnifiedSettings), .key(StorageKeys.ModernUnifiedSettingsKey()),
.key(.legacyAppMode), .key(StorageKeys.LegacyAppModeKey()),
.key(.modernAppMode) .key(StorageKeys.ModernAppModeKey())
] ]
} }
} }

View File

@ -9,8 +9,8 @@ struct ExternalKeyMaterialProvider: KeyMaterialProviding {
} }
func keyMaterial(for keyName: String) async throws -> Data { func keyMaterial(for keyName: String) async throws -> Data {
let key = StorageKey.externalKeyMaterial let key = StorageKeys.ExternalKeyMaterialKey()
if let existing = try? await StorageRouter.shared.get(key) { if let existing = try await StorageRouter.shared.get(key) as Data? {
return existing return existing
} }

View File

@ -1,20 +1,21 @@
import Foundation import Foundation
import LocalData import LocalData
import SharedKit import SharedKit
extension StorageKeys {
extension StorageKey where Value == String {
/// Stores a shared setting in App Group UserDefaults. /// Stores a shared setting in App Group UserDefaults.
/// - Domain: App Group UserDefaults /// - Domain: App Group UserDefaults
/// - Security: None /// - Security: None
/// - Sync: Never /// - Sync: Never
nonisolated static let appGroupUserDefaults = StorageKey( struct AppGroupUserDefaultsKey: StorageKey {
name: "app_group_setting", typealias Value = String
domain: .appGroupUserDefaults(identifier: StorageServiceIdentifiers.appGroupIdentifier),
security: .none, let name = "app_group_setting"
serializer: .json, let domain: StorageDomain = .appGroupUserDefaults(identifier: StorageServiceIdentifiers.appGroupIdentifier)
owner: "SampleApp", let security: SecurityPolicy = .none
description: "Stores a shared setting readable by app extensions.", let serializer: Serializer<String> = .json
availability: .phoneOnly, let owner = "SampleApp"
syncPolicy: .never let description = "Stores a shared setting readable by app extensions."
) let availability: PlatformAvailability = .phoneOnly
let syncPolicy: SyncPolicy = .never
}
} }

View File

@ -2,40 +2,29 @@ import Foundation
import LocalData import LocalData
import SharedKit import SharedKit
extension StorageKey where Value == UserProfile { extension StorageKeys {
/// Stores a shared user profile in the App Group container. /// Stores a shared user profile in the App Group container.
/// - Domain: App Group File System /// - Domain: App Group File System
/// - Security: None /// - Security: None
/// - Sync: Never /// - Sync: Never
nonisolated static let appGroupUserProfile = StorageKey( struct AppGroupUserProfileKey: StorageKey {
name: "app_group_user_profile.json", typealias Value = UserProfile
domain: .appGroupFileSystem(
identifier: StorageServiceIdentifiers.appGroupIdentifier,
directory: .documents
),
security: .none,
serializer: .json,
owner: "SampleApp",
description: "Stores a profile shared between the app and extensions.",
availability: .phoneOnly,
syncPolicy: .never
)
nonisolated static func appGroupUserProfileKey( let directory: FileDirectory
directory: FileDirectory = .documents let name = "app_group_user_profile.json"
) -> StorageKey { let security: SecurityPolicy = .none
StorageKey( let serializer: Serializer<UserProfile> = .json
name: "app_group_user_profile.json", let owner = "SampleApp"
domain: .appGroupFileSystem( let description = "Stores a profile shared between the app and extensions."
identifier: StorageServiceIdentifiers.appGroupIdentifier, let availability: PlatformAvailability = .phoneOnly
directory: directory let syncPolicy: SyncPolicy = .never
),
security: .none, init(directory: FileDirectory = .documents) {
serializer: .json, self.directory = directory
owner: "SampleApp", }
description: "Stores a profile shared between the app and extensions.",
availability: .phoneOnly, var domain: StorageDomain {
syncPolicy: .never .appGroupFileSystem(identifier: StorageServiceIdentifiers.appGroupIdentifier, directory: directory)
) }
} }
} }

View File

@ -2,19 +2,21 @@ import Foundation
import LocalData import LocalData
import SharedKit import SharedKit
extension StorageKey where Value == [String: AnyCodable] { extension StorageKeys {
/// Stores user preferences in App Group UserDefaults. /// Stores user preferences in App Group UserDefaults.
/// - Domain: App Group UserDefaults /// - Domain: App Group UserDefaults
/// - Security: None /// - Security: None
/// - Sync: Never /// - Sync: Never
nonisolated static let userPreferences = StorageKey( struct UserPreferencesKey: StorageKey {
name: "user_preferences", typealias Value = [String: AnyCodable]
domain: .appGroupUserDefaults(identifier: StorageServiceIdentifiers.appGroupIdentifier),
security: .none, let name = "user_preferences"
serializer: .json, let domain: StorageDomain = .appGroupUserDefaults(identifier: StorageServiceIdentifiers.appGroupIdentifier)
owner: "SampleApp", let security: SecurityPolicy = .none
description: "Stores shared user preferences for app configuration screens.", let serializer: Serializer<[String: AnyCodable]> = .json
availability: .all, let owner = "SampleApp"
syncPolicy: .never let description = "Stores shared user preferences for app configuration screens."
) let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never
}
} }

View File

@ -1,16 +1,20 @@
import Foundation import Foundation
import LocalData import LocalData
extension StorageKey where Value == [String] { extension StorageKeys {
/// Stores session logs with encryption using external key material. /// Stores session logs with encryption using external key material.
nonisolated static let externalSessionLogs = StorageKey( struct ExternalSessionLogsKey: StorageKey {
name: "external_session_logs.json", typealias Value = [String]
domain: .encryptedFileSystem(directory: .caches),
security: .encrypted(.external(source: SampleKeyMaterialSources.external)), let name = "external_session_logs.json"
serializer: .json, let domain: StorageDomain = .encryptedFileSystem(directory: .caches)
owner: "SampleApp", let security: SecurityPolicy = .encrypted(
description: "Stores session logs encrypted with external key material.", .external(source: SampleKeyMaterialSources.external)
availability: .phoneOnly, )
syncPolicy: .never let serializer: Serializer<[String]> = .json
) let owner = "SampleApp"
let description = "Stores session logs encrypted with external key material."
let availability: PlatformAvailability = .phoneOnly
let syncPolicy: SyncPolicy = .never
}
} }

View File

@ -1,16 +1,20 @@
import Foundation import Foundation
import LocalData import LocalData
extension StorageKey where Value == String { extension StorageKeys {
/// Stores private notes with encryption. /// Stores private notes with encryption.
nonisolated static let privateNotes = StorageKey( struct PrivateNotesKey: StorageKey {
name: "private_notes.enc", typealias Value = String
domain: .encryptedFileSystem(directory: .documents),
security: .encrypted(.aes256(keyDerivation: .pbkdf2(iterations: 50_000))), let name = "private_notes.enc"
serializer: .json, let domain: StorageDomain = .encryptedFileSystem(directory: .documents)
owner: "SampleApp", let security: SecurityPolicy = .encrypted(
description: "Stores private notes encrypted at rest.", .aes256(keyDerivation: .pbkdf2(iterations: 50_000))
availability: .phoneOnly, )
syncPolicy: .never let serializer: Serializer<String> = .json
) let owner = "SampleApp"
let description = "Stores private notes encrypted at rest."
let availability: PlatformAvailability = .phoneOnly
let syncPolicy: SyncPolicy = .never
}
} }

View File

@ -1,30 +1,24 @@
import Foundation import Foundation
import LocalData import LocalData
extension StorageKey where Value == [String] { extension StorageKeys {
/// Stores session logs with full encryption. /// Stores session logs with full encryption.
/// Configurable PBKDF2 iterations. /// Configurable PBKDF2 iterations.
nonisolated static let sessionLogs = StorageKey( struct SessionLogsKey: StorageKey {
name: "session_logs.json", typealias Value = [String]
domain: .encryptedFileSystem(directory: .caches),
security: .encrypted(.aes256(keyDerivation: .pbkdf2(iterations: 10_000))),
serializer: .json,
owner: "SampleApp",
description: "Stores session logs encrypted with PBKDF2-derived keys.",
availability: .phoneOnly,
syncPolicy: .never
)
nonisolated static func sessionLogsKey(iterations: Int) -> StorageKey { let name = "session_logs.json"
StorageKey( let domain: StorageDomain = .encryptedFileSystem(directory: .caches)
name: "session_logs.json", let security: SecurityPolicy
domain: .encryptedFileSystem(directory: .caches), let serializer: Serializer<[String]> = .json
security: .encrypted(.aes256(keyDerivation: .pbkdf2(iterations: iterations))), let owner = "SampleApp"
serializer: .json, let description = "Stores session logs encrypted with PBKDF2-derived keys."
owner: "SampleApp", let availability: PlatformAvailability = .phoneOnly
description: "Stores session logs encrypted with PBKDF2-derived keys.", let syncPolicy: SyncPolicy = .never
availability: .phoneOnly,
syncPolicy: .never init(iterations: Int = 10_000) {
) // NOTE: PBKDF2 iterations must remain stable for existing data; changing this breaks decryption.
self.security = .encrypted(.aes256(keyDerivation: .pbkdf2(iterations: iterations)))
}
} }
} }

View File

@ -1,16 +1,18 @@
import Foundation import Foundation
import LocalData import LocalData
extension StorageKey where Value == Data { extension StorageKeys {
/// Stores cached data files. /// Stores cached data files.
nonisolated static let cachedData = StorageKey( struct CachedDataKey: StorageKey {
name: "cached_data.bin", typealias Value = Data
domain: .fileSystem(directory: .caches),
security: .none, let name = "cached_data.bin"
serializer: .data, let domain: StorageDomain = .fileSystem(directory: .caches)
owner: "SampleApp", let security: SecurityPolicy = .none
description: "Stores cached binary data that can be regenerated.", let serializer: Serializer<Data> = .data
availability: .all, let owner = "SampleApp"
syncPolicy: .never let description = "Stores cached binary data that can be regenerated."
) let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never
}
} }

View File

@ -1,13 +1,15 @@
import Foundation import Foundation
import LocalData import LocalData
extension StorageKey where Value == String { extension StorageKeys {
/// Example using custom serializer for specialized encoding. /// Example using custom serializer for specialized encoding.
nonisolated static let customEncoded = StorageKey( struct CustomEncodedKey: StorageKey {
name: "custom_encoded", typealias Value = String
domain: .fileSystem(directory: .documents),
security: .none, let name = "custom_encoded"
serializer: .custom( let domain: StorageDomain = .fileSystem(directory: .documents)
let security: SecurityPolicy = .none
let serializer: Serializer<String> = .custom(
encode: { value in encode: { value in
Data(value.utf8).base64EncodedData() Data(value.utf8).base64EncodedData()
}, },
@ -18,10 +20,10 @@ extension StorageKey where Value == String {
} }
return string return string
} }
), )
owner: "SampleApp", let owner = "SampleApp"
description: "Stores custom-encoded string data (Base64 example).", let description = "Stores custom-encoded string data (Base64 example)."
availability: .all, let availability: PlatformAvailability = .all
syncPolicy: .never let syncPolicy: SyncPolicy = .never
) }
} }

View File

@ -1,16 +1,18 @@
import Foundation import Foundation
import LocalData import LocalData
extension StorageKey where Value == [String: AnyCodable] { extension StorageKeys {
/// Stores settings as property list. /// Stores settings as property list.
nonisolated static let settingsPlist = StorageKey( struct SettingsPlistKey: StorageKey {
name: "settings.plist", typealias Value = [String: AnyCodable]
domain: .fileSystem(directory: .documents),
security: .none, let name = "settings.plist"
serializer: .plist, let domain: StorageDomain = .fileSystem(directory: .documents)
owner: "SampleApp", let security: SecurityPolicy = .none
description: "Stores app settings exported as a property list.", let serializer: Serializer<[String: AnyCodable]> = .plist
availability: .all, let owner = "SampleApp"
syncPolicy: .never let description = "Stores app settings exported as a property list."
) let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never
}
} }

View File

@ -2,29 +2,22 @@ import Foundation
import LocalData import LocalData
import SharedKit import SharedKit
extension StorageKey where Value == UserProfile { extension StorageKeys {
/// Stores user profile as JSON file in documents. /// Stores user profile as JSON file in documents.
nonisolated static let userProfileFile = StorageKey( struct UserProfileFileKey: StorageKey {
name: UserProfile.storageKeyName, typealias Value = UserProfile
domain: .fileSystem(directory: .documents),
security: .none,
serializer: .json,
owner: "SampleApp",
description: "Stores a lightweight user profile for file storage and watch sync.",
availability: .phoneWithWatchSync,
syncPolicy: .automaticSmall
)
nonisolated static func userProfileFileKey(directory: FileDirectory = .documents) -> StorageKey { let name = UserProfile.storageKeyName
StorageKey( let domain: StorageDomain
name: UserProfile.storageKeyName, let security: SecurityPolicy = .none
domain: .fileSystem(directory: directory), let serializer: Serializer<UserProfile> = .json
security: .none, let owner = "SampleApp"
serializer: .json, let description = "Stores a lightweight user profile for file storage and watch sync."
owner: "SampleApp", let availability: PlatformAvailability = .phoneWithWatchSync
description: "Stores a lightweight user profile for file storage and watch sync.", let syncPolicy: SyncPolicy = .automaticSmall
availability: .phoneWithWatchSync,
syncPolicy: .automaticSmall init(directory: FileDirectory = .documents) {
) self.domain = .fileSystem(directory: directory)
}
} }
} }

View File

@ -2,19 +2,21 @@ import Foundation
import LocalData import LocalData
import SharedKit import SharedKit
extension StorageKey where Value == String { extension StorageKeys {
/// Stores API token in keychain. /// Stores API token in keychain.
nonisolated static let apiToken = StorageKey( struct APITokenKey: StorageKey {
name: "api_token", typealias Value = String
domain: .keychain(service: StorageServiceIdentifiers.keychainAPIToken),
security: .keychain( let name = "api_token"
let domain: StorageDomain = .keychain(service: StorageServiceIdentifiers.keychainAPIToken)
let security: SecurityPolicy = .keychain(
accessibility: .whenUnlockedThisDeviceOnly, accessibility: .whenUnlockedThisDeviceOnly,
accessControl: nil accessControl: nil
), )
serializer: .json, let serializer: Serializer<String> = .json
owner: "SampleApp", let owner = "SampleApp"
description: "Stores API auth token for network requests.", let description = "Stores API auth token for network requests."
availability: .phoneOnly, let availability: PlatformAvailability = .phoneOnly
syncPolicy: .never let syncPolicy: SyncPolicy = .never
) }
} }

View File

@ -2,33 +2,23 @@ import Foundation
import LocalData import LocalData
import SharedKit import SharedKit
extension StorageKey where Value == Credential { extension StorageKeys {
/// Stores user credentials securely in keychain. /// Stores user credentials securely in keychain.
/// Configurable accessibility and access control. /// Configurable accessibility and access control.
nonisolated static let credentials = StorageKey( struct CredentialsKey: StorageKey {
name: "user_credentials", typealias Value = Credential
domain: .keychain(service: StorageServiceIdentifiers.keychainCredentials),
security: .keychain(accessibility: .afterFirstUnlock, accessControl: nil),
serializer: .json,
owner: "SampleApp",
description: "Stores user credentials for sign-in flows.",
availability: .phoneOnly,
syncPolicy: .never
)
nonisolated static func credentialsKey( let name = "user_credentials"
accessibility: KeychainAccessibility = .afterFirstUnlock, let domain: StorageDomain = .keychain(service: StorageServiceIdentifiers.keychainCredentials)
accessControl: KeychainAccessControl? = nil let security: SecurityPolicy
) -> StorageKey { let serializer: Serializer<Credential> = .json
StorageKey( let owner = "SampleApp"
name: "user_credentials", let description = "Stores user credentials for sign-in flows."
domain: .keychain(service: StorageServiceIdentifiers.keychainCredentials), let availability: PlatformAvailability = .phoneOnly
security: .keychain(accessibility: accessibility, accessControl: accessControl), let syncPolicy: SyncPolicy = .never
serializer: .json,
owner: "SampleApp", init(accessibility: KeychainAccessibility = .afterFirstUnlock, accessControl: KeychainAccessControl? = nil) {
description: "Stores user credentials for sign-in flows.", self.security = .keychain(accessibility: accessibility, accessControl: accessControl)
availability: .phoneOnly, }
syncPolicy: .never
)
} }
} }

View File

@ -2,19 +2,21 @@ import Foundation
import LocalData import LocalData
import SharedKit import SharedKit
extension StorageKey where Value == Data { extension StorageKeys {
/// Stores external key material used for encryption policies. /// Stores external key material used for encryption policies.
nonisolated static let externalKeyMaterial = StorageKey( struct ExternalKeyMaterialKey: StorageKey {
name: "external_key_material", typealias Value = Data
domain: .keychain(service: StorageServiceIdentifiers.keychainExternalKeyMaterial),
security: .keychain( let name = "external_key_material"
let domain: StorageDomain = .keychain(service: StorageServiceIdentifiers.keychainExternalKeyMaterial)
let security: SecurityPolicy = .keychain(
accessibility: .afterFirstUnlock, accessibility: .afterFirstUnlock,
accessControl: nil accessControl: nil
), )
serializer: .data, let serializer: Serializer<Data> = .data
owner: "SampleApp", let owner = "SampleApp"
description: "Stores external key material used by encryption policies.", let description = "Stores external key material used by encryption policies."
availability: .phoneOnly, let availability: PlatformAvailability = .phoneOnly
syncPolicy: .never let syncPolicy: SyncPolicy = .never
) }
} }

View File

@ -2,19 +2,21 @@ import Foundation
import LocalData import LocalData
import SharedKit import SharedKit
extension StorageKey where Value == SampleLocationData { extension StorageKeys {
/// Stores sensitive location data in keychain with biometric protection. /// Stores sensitive location data in keychain with biometric protection.
nonisolated static let lastLocation = StorageKey( struct LastLocationKey: StorageKey {
name: "last_known_location", typealias Value = SampleLocationData
domain: .keychain(service: StorageServiceIdentifiers.keychainLocation),
security: .keychain( let name = "last_known_location"
let domain: StorageDomain = .keychain(service: StorageServiceIdentifiers.keychainLocation)
let security: SecurityPolicy = .keychain(
accessibility: .afterFirstUnlock, accessibility: .afterFirstUnlock,
accessControl: .userPresence accessControl: .userPresence
), )
serializer: .json, let serializer: Serializer<SampleLocationData> = .json
owner: "SampleApp", let owner = "SampleApp"
description: "Stores last known location for location-aware features.", let description = "Stores last known location for location-aware features."
availability: .phoneOnly, let availability: PlatformAvailability = .phoneOnly
syncPolicy: .never let syncPolicy: SyncPolicy = .never
) }
} }

View File

@ -1,51 +1,54 @@
import Foundation import Foundation
import LocalData import LocalData
extension StorageKey where Value == Bool { extension StorageKeys {
nonisolated static let legacyNotificationSetting = StorageKey( struct LegacyNotificationSettingKey: StorageKey {
name: "legacy_notification_setting", typealias Value = Bool
domain: .userDefaults(suite: nil),
security: .none,
serializer: .json,
owner: "MigrationDemo",
description: "Legacy notification setting stored as Bool.",
availability: .all,
syncPolicy: .never
)
}
extension StorageKey where Value == String { let name = "legacy_notification_setting"
nonisolated static let legacyThemeSetting = StorageKey( let domain: StorageDomain = .userDefaults(suite: nil)
name: "legacy_theme_setting", let security: SecurityPolicy = .none
domain: .userDefaults(suite: nil), let serializer: Serializer<Bool> = .json
security: .none, let owner = "MigrationDemo"
serializer: .json, let description = "Legacy notification setting stored as Bool."
owner: "MigrationDemo", let availability: PlatformAvailability = .all
description: "Legacy theme setting stored as a string.", let syncPolicy: SyncPolicy = .never
availability: .all, }
syncPolicy: .never
)
}
extension StorageKey where Value == UnifiedSettings { struct LegacyThemeSettingKey: StorageKey {
typealias Value = String
nonisolated static let modernUnifiedSettings = StorageKey(
name: "modern_unified_settings", let name = "legacy_theme_setting"
domain: .fileSystem(directory: .documents), let domain: StorageDomain = .userDefaults(suite: nil)
security: .none, let security: SecurityPolicy = .none
serializer: .json, let serializer: Serializer<String> = .json
owner: "MigrationDemo", let owner = "MigrationDemo"
description: "Modern unified settings aggregated from legacy keys.", let description = "Legacy theme setting stored as a string."
availability: .all, let availability: PlatformAvailability = .all
syncPolicy: .never, let syncPolicy: SyncPolicy = .never
migration: { key in }
struct ModernUnifiedSettingsKey: StorageKey {
typealias Value = UnifiedSettings
let name = "modern_unified_settings"
let domain: StorageDomain = .fileSystem(directory: .documents)
let security: SecurityPolicy = .none
let serializer: Serializer<UnifiedSettings> = .json
let owner = "MigrationDemo"
let description = "Modern unified settings aggregated from legacy keys."
let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never
var migration: AnyStorageMigration? {
let sources: [AnyStorageKey] = [ let sources: [AnyStorageKey] = [
.key(.legacyNotificationSetting), .key(StorageKeys.LegacyNotificationSettingKey()),
.key(.legacyThemeSetting) .key(StorageKeys.LegacyThemeSettingKey())
] ]
return AnyStorageMigration( return AnyStorageMigration(
DefaultAggregatingMigration( DefaultAggregatingMigration(
destinationKey: key, destinationKey: self,
sourceKeys: sources sourceKeys: sources
) { sources in ) { sources in
var notificationsEnabled = false var notificationsEnabled = false
@ -66,5 +69,5 @@ extension StorageKey where Value == UnifiedSettings {
} }
) )
} }
) }
} }

View File

@ -1,43 +1,49 @@
import Foundation import Foundation
import LocalData import LocalData
extension StorageKey where Value == String { extension StorageKeys {
nonisolated static let legacyAppMode = StorageKey( struct LegacyAppModeKey: StorageKey {
name: "legacy_app_mode", typealias Value = String
domain: .userDefaults(suite: nil),
security: .none,
serializer: .json,
owner: "MigrationDemo",
description: "Legacy app mode stored in UserDefaults.",
availability: .all,
syncPolicy: .never
)
nonisolated static let modernAppMode = StorageKey( let name = "legacy_app_mode"
name: "modern_app_mode", let domain: StorageDomain = .userDefaults(suite: nil)
domain: .keychain(service: "com.mbrucedogs.securestorage"), let security: SecurityPolicy = .none
security: .keychain( let serializer: Serializer<String> = .json
let owner = "MigrationDemo"
let description = "Legacy app mode stored in UserDefaults."
let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never
}
struct ModernAppModeKey: StorageKey {
typealias Value = String
let name = "modern_app_mode"
let domain: StorageDomain = .keychain(service: "com.mbrucedogs.securestorage")
let security: SecurityPolicy = .keychain(
accessibility: .afterFirstUnlock, accessibility: .afterFirstUnlock,
accessControl: .userPresence accessControl: .userPresence
), )
serializer: .json, let serializer: Serializer<String> = .json
owner: "MigrationDemo", let owner = "MigrationDemo"
description: "Modern app mode with conditional migration.", let description = "Modern app mode with conditional migration."
availability: .all, let availability: PlatformAvailability = .all
syncPolicy: .never, let syncPolicy: SyncPolicy = .never
migration: { key in
let legacy = StorageKey.legacyAppMode var migration: AnyStorageMigration? {
let destination = self
let legacy = StorageKeys.LegacyAppModeKey()
let fallback = AnyStorageMigration( let fallback = AnyStorageMigration(
SimpleLegacyMigration(destinationKey: key, sourceKey: .key(legacy)) SimpleLegacyMigration(destinationKey: destination, sourceKey: .key(legacy))
) )
return AnyStorageMigration( return AnyStorageMigration(
AppVersionConditionalMigration( AppVersionConditionalMigration(
destinationKey: key, destinationKey: destination,
minAppVersion: "99.0", minAppVersion: "99.0",
fallbackMigration: fallback fallbackMigration: fallback
) )
) )
} }
) }
} }

View File

@ -1,39 +1,44 @@
import Foundation import Foundation
import LocalData import LocalData
extension StorageKey where Value == String { extension StorageKeys {
/// The legacy key where data starts (in UserDefaults) /// The legacy key where data starts (in UserDefaults)
nonisolated static let legacyMigrationSource = StorageKey( struct LegacyMigrationSourceKey: StorageKey {
name: "legacy_user_id", typealias Value = String
domain: .userDefaults(suite: nil),
security: .none, let name = "legacy_user_id"
serializer: .json, let domain: StorageDomain = .userDefaults(suite: nil)
owner: "MigrationDemo", let security: SecurityPolicy = .none
description: "Legacy key in UserDefaults from an older version of the app.", let serializer: Serializer<String> = .json
availability: .all, let owner = "MigrationDemo"
syncPolicy: .never let description = "Legacy key in UserDefaults from an older version of the app."
) let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never
}
/// The modern key where data should end up (in Keychain) /// The modern key where data should end up (in Keychain)
nonisolated static let modernMigrationDestination = StorageKey( struct ModernMigrationDestinationKey: StorageKey {
name: "secure_user_id", typealias Value = String
domain: .keychain(service: "com.mbrucedogs.securestorage"),
security: .keychain( let name = "secure_user_id"
let domain: StorageDomain = .keychain(service: "com.mbrucedogs.securestorage")
let security: SecurityPolicy = .keychain(
accessibility: .afterFirstUnlock, accessibility: .afterFirstUnlock,
accessControl: .userPresence accessControl: .userPresence
), )
serializer: .json, let serializer: Serializer<String> = .json
owner: "MigrationDemo", let owner = "MigrationDemo"
description: "Modern key in Keychain with biometric security.", let description = "Modern key in Keychain with biometric security."
availability: .all, let availability: PlatformAvailability = .all
syncPolicy: .never, let syncPolicy: SyncPolicy = .never
migration: { key in
var migration: AnyStorageMigration? {
AnyStorageMigration( AnyStorageMigration(
SimpleLegacyMigration( SimpleLegacyMigration(
destinationKey: key, destinationKey: self,
sourceKey: .key(.legacyMigrationSource) sourceKey: .key(LegacyMigrationSourceKey())
) )
) )
} }
) }
} }

View File

@ -1,37 +1,40 @@
import Foundation import Foundation
import LocalData import LocalData
extension StorageKey where Value == String { extension StorageKeys {
nonisolated static let legacyProfileName = StorageKey( struct LegacyProfileNameKey: StorageKey {
name: "legacy_profile_name", typealias Value = String
domain: .userDefaults(suite: nil),
security: .none,
serializer: .json,
owner: "MigrationDemo",
description: "Legacy profile name stored as a single string.",
availability: .all,
syncPolicy: .never
)
}
extension StorageKey where Value == ProfileName { let name = "legacy_profile_name"
nonisolated static let modernProfileName = StorageKey( let domain: StorageDomain = .userDefaults(suite: nil)
name: "modern_profile_name", let security: SecurityPolicy = .none
domain: .keychain(service: "com.mbrucedogs.securestorage"), let serializer: Serializer<String> = .json
security: .keychain( let owner = "MigrationDemo"
let description = "Legacy profile name stored as a single string."
let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never
}
struct ModernProfileNameKey: StorageKey {
typealias Value = ProfileName
let name = "modern_profile_name"
let domain: StorageDomain = .keychain(service: "com.mbrucedogs.securestorage")
let security: SecurityPolicy = .keychain(
accessibility: .afterFirstUnlock, accessibility: .afterFirstUnlock,
accessControl: .userPresence accessControl: .userPresence
), )
serializer: .json, let serializer: Serializer<ProfileName> = .json
owner: "MigrationDemo", let owner = "MigrationDemo"
description: "Modern profile name stored as structured data.", let description = "Modern profile name stored as structured data."
availability: .all, let availability: PlatformAvailability = .all
syncPolicy: .never, let syncPolicy: SyncPolicy = .never
migration: { key in
let sourceKey = StorageKey<String>.legacyProfileName var migration: AnyStorageMigration? {
let sourceKey = StorageKeys.LegacyProfileNameKey()
return AnyStorageMigration( return AnyStorageMigration(
DefaultTransformingMigration( DefaultTransformingMigration(
destinationKey: key, destinationKey: self,
sourceKey: sourceKey sourceKey: sourceKey
) { value in ) { value in
let parts = value.split(separator: " ", maxSplits: 1).map(String.init) let parts = value.split(separator: " ", maxSplits: 1).map(String.init)
@ -41,5 +44,5 @@ extension StorageKey where Value == ProfileName {
} }
) )
} }
) }
} }

View File

@ -1,33 +1,24 @@
import Foundation import Foundation
import LocalData import LocalData
extension StorageKey where Value == String { extension StorageKeys {
/// Syncable setting with configurable platform and sync policy. /// Syncable setting with configurable platform and sync policy.
/// Grouped under Platform to highlight availability/sync behavior. /// Grouped under Platform to highlight availability/sync behavior.
nonisolated static let syncableSetting = StorageKey( struct SyncableSettingKey: StorageKey {
name: "syncable_setting", typealias Value = String
domain: .userDefaults(suite: nil),
security: .none,
serializer: .json,
owner: "SampleApp",
description: "Stores a setting that can be synced to watch.",
availability: .all,
syncPolicy: .never
)
nonisolated static func syncableSettingKey( let name = "syncable_setting"
availability: PlatformAvailability = .all, let domain: StorageDomain = .userDefaults(suite: nil)
syncPolicy: SyncPolicy = .never let security: SecurityPolicy = .none
) -> StorageKey { let serializer: Serializer<String> = .json
StorageKey( let owner = "SampleApp"
name: "syncable_setting", let description = "Stores a setting that can be synced to watch."
domain: .userDefaults(suite: nil), let availability: PlatformAvailability
security: .none, let syncPolicy: SyncPolicy
serializer: .json,
owner: "SampleApp", init(availability: PlatformAvailability = .all, syncPolicy: SyncPolicy = .never) {
description: "Stores a setting that can be synced to watch.", self.availability = availability
availability: availability, self.syncPolicy = syncPolicy
syncPolicy: syncPolicy }
)
} }
} }

View File

@ -1,17 +1,19 @@
import Foundation import Foundation
import LocalData import LocalData
extension StorageKey where Value == Bool { extension StorageKeys {
/// Watch-only setting for vibration. /// Watch-only setting for vibration.
/// Grouped under Platform to highlight watch-only availability. /// Grouped under Platform to highlight watch-only availability.
nonisolated static let watchVibration = StorageKey( struct WatchVibrationKey: StorageKey {
name: "watch_vibration_enabled", typealias Value = Bool
domain: .userDefaults(suite: nil),
security: .none, let name = "watch_vibration_enabled"
serializer: .json, let domain: StorageDomain = .userDefaults(suite: nil)
owner: "SampleApp", let security: SecurityPolicy = .none
description: "Controls haptic feedback on watch-only experiences.", let serializer: Serializer<Bool> = .json
availability: .watchOnly, let owner = "SampleApp"
syncPolicy: .never let description = "Controls haptic feedback on watch-only experiences."
) let availability: PlatformAvailability = .watchOnly
let syncPolicy: SyncPolicy = .never
}
} }

View File

@ -1,19 +1,21 @@
import Foundation import Foundation
import LocalData import LocalData
extension StorageKey where Value == String { extension StorageKeys {
/// Stores the app version in standard UserDefaults. /// Stores the app version in standard UserDefaults.
/// - Domain: UserDefaults (standard) /// - Domain: UserDefaults (standard)
/// - Security: None /// - Security: None
/// - Sync: Automatic for small data /// - Sync: Automatic for small data
nonisolated static let appVersion = StorageKey( struct AppVersionKey: StorageKey {
name: "last_app_version", typealias Value = String
domain: .userDefaults(suite: nil),
security: .none, let name = "last_app_version"
serializer: .json, let domain: StorageDomain = .userDefaults(suite: nil)
owner: "SampleApp", let security: SecurityPolicy = .none
description: "Tracks the last app version for migration and UI messaging.", let serializer: Serializer<String> = .json
availability: .all, let owner = "SampleApp"
syncPolicy: .automaticSmall let description = "Tracks the last app version for migration and UI messaging."
) let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .automaticSmall
}
} }

View File

@ -64,11 +64,11 @@ struct AggregatingMigrationDemo: View {
do { do {
try await StorageRouter.shared.set( try await StorageRouter.shared.set(
notificationsEnabled, notificationsEnabled,
for: StorageKey.legacyNotificationSetting for: StorageKeys.LegacyNotificationSettingKey()
) )
try await StorageRouter.shared.set( try await StorageRouter.shared.set(
theme, theme,
for: StorageKey.legacyThemeSetting for: StorageKeys.LegacyThemeSettingKey()
) )
statusMessage = "✓ Saved legacy settings" statusMessage = "✓ Saved legacy settings"
} catch { } catch {
@ -83,7 +83,7 @@ struct AggregatingMigrationDemo: View {
statusMessage = "Loading unified settings..." statusMessage = "Loading unified settings..."
Task { Task {
do { do {
let value = try await StorageRouter.shared.get(StorageKey.modernUnifiedSettings) let value = try await StorageRouter.shared.get(StorageKeys.ModernUnifiedSettingsKey())
modernValue = format(value) modernValue = format(value)
statusMessage = "✓ Migration complete." statusMessage = "✓ Migration complete."
} catch StorageError.notFound { } catch StorageError.notFound {

View File

@ -63,7 +63,7 @@ struct ConditionalMigrationDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKey.legacyAppMode let key = StorageKeys.LegacyAppModeKey()
try await StorageRouter.shared.set(legacyValue, for: key) try await StorageRouter.shared.set(legacyValue, for: key)
statusMessage = "✓ Saved legacy app mode" statusMessage = "✓ Saved legacy app mode"
} catch { } catch {
@ -78,7 +78,7 @@ struct ConditionalMigrationDemo: View {
statusMessage = "Loading modern mode..." statusMessage = "Loading modern mode..."
Task { Task {
do { do {
let key = StorageKey.modernAppMode let key = StorageKeys.ModernAppModeKey()
let value = try await StorageRouter.shared.get(key) let value = try await StorageRouter.shared.get(key)
modernValue = value modernValue = value
statusMessage = "✓ Conditional migration complete." statusMessage = "✓ Conditional migration complete."

View File

@ -132,12 +132,12 @@ struct EncryptedStorageDemo: View {
Task { Task {
do { do {
if useExternalKeyProvider { if useExternalKeyProvider {
let key = StorageKey.externalSessionLogs let key = StorageKeys.ExternalSessionLogsKey()
let logs = try await updatedLogs(for: key) let logs = try await updatedLogs(for: key)
try await StorageRouter.shared.set(logs, for: key) try await StorageRouter.shared.set(logs, for: key)
storedLogs = logs storedLogs = logs
} else { } else {
let key = StorageKey.sessionLogsKey(iterations: iterations) let key = StorageKeys.SessionLogsKey(iterations: iterations)
let logs = try await updatedLogs(for: key) let logs = try await updatedLogs(for: key)
try await StorageRouter.shared.set(logs, for: key) try await StorageRouter.shared.set(logs, for: key)
storedLogs = logs storedLogs = logs
@ -157,10 +157,10 @@ struct EncryptedStorageDemo: View {
Task { Task {
do { do {
if useExternalKeyProvider { if useExternalKeyProvider {
let key = StorageKey.externalSessionLogs let key = StorageKeys.ExternalSessionLogsKey()
storedLogs = try await StorageRouter.shared.get(key) storedLogs = try await StorageRouter.shared.get(key)
} else { } else {
let key = StorageKey.sessionLogsKey(iterations: iterations) let key = StorageKeys.SessionLogsKey(iterations: iterations)
storedLogs = try await StorageRouter.shared.get(key) storedLogs = try await StorageRouter.shared.get(key)
} }
statusMessage = "✓ Decrypted \(storedLogs.count) log entries" statusMessage = "✓ Decrypted \(storedLogs.count) log entries"
@ -179,10 +179,10 @@ struct EncryptedStorageDemo: View {
Task { Task {
do { do {
if useExternalKeyProvider { if useExternalKeyProvider {
let key = StorageKey.externalSessionLogs let key = StorageKeys.ExternalSessionLogsKey()
try await StorageRouter.shared.remove(key) try await StorageRouter.shared.remove(key)
} else { } else {
let key = StorageKey.sessionLogsKey(iterations: iterations) let key = StorageKeys.SessionLogsKey(iterations: iterations)
try await StorageRouter.shared.remove(key) try await StorageRouter.shared.remove(key)
} }
storedLogs = [] storedLogs = []
@ -194,7 +194,7 @@ struct EncryptedStorageDemo: View {
} }
} }
private func updatedLogs(for key: StorageKey<[String]>) async throws -> [String] { private func updatedLogs<Key: StorageKey>(for key: Key) async throws -> [String] where Key.Value == [String] {
var logs: [String] var logs: [String]
do { do {
logs = try await StorageRouter.shared.get(key) logs = try await StorageRouter.shared.get(key)

View File

@ -172,7 +172,7 @@ struct FileSystemDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKey.userProfileFileKey(directory: selectedDirectory) let key = StorageKeys.UserProfileFileKey(directory: selectedDirectory)
let profile = UserProfile( let profile = UserProfile(
name: profileName, name: profileName,
email: profileEmail, email: profileEmail,
@ -192,7 +192,7 @@ struct FileSystemDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKey.userProfileFileKey(directory: selectedDirectory) let key = StorageKeys.UserProfileFileKey(directory: selectedDirectory)
let profile = try await StorageRouter.shared.get(key) let profile = try await StorageRouter.shared.get(key)
storedProfile = profile storedProfile = profile
@ -216,7 +216,7 @@ struct FileSystemDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKey.userProfileFileKey(directory: selectedDirectory) let key = StorageKeys.UserProfileFileKey(directory: selectedDirectory)
try await StorageRouter.shared.remove(key) try await StorageRouter.shared.remove(key)
storedProfile = nil storedProfile = nil
statusMessage = "✓ File deleted" statusMessage = "✓ File deleted"
@ -231,7 +231,7 @@ struct FileSystemDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKey.appGroupUserProfileKey(directory: selectedDirectory) let key = StorageKeys.AppGroupUserProfileKey(directory: selectedDirectory)
let profile = UserProfile( let profile = UserProfile(
name: profileName, name: profileName,
email: profileEmail, email: profileEmail,
@ -251,7 +251,7 @@ struct FileSystemDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKey.appGroupUserProfileKey(directory: selectedDirectory) let key = StorageKeys.AppGroupUserProfileKey(directory: selectedDirectory)
let profile = try await StorageRouter.shared.get(key) let profile = try await StorageRouter.shared.get(key)
appGroupProfile = profile appGroupProfile = profile
@ -275,7 +275,7 @@ struct FileSystemDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKey.appGroupUserProfileKey(directory: selectedDirectory) let key = StorageKeys.AppGroupUserProfileKey(directory: selectedDirectory)
try await StorageRouter.shared.remove(key) try await StorageRouter.shared.remove(key)
appGroupProfile = nil appGroupProfile = nil
appGroupStatusMessage = "✓ App Group file deleted" appGroupStatusMessage = "✓ App Group file deleted"

View File

@ -117,7 +117,7 @@ struct KeychainDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKey.credentialsKey( let key = StorageKeys.CredentialsKey(
accessibility: selectedAccessibility, accessibility: selectedAccessibility,
accessControl: selectedAccessControl accessControl: selectedAccessControl
) )
@ -135,7 +135,7 @@ struct KeychainDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKey.credentialsKey( let key = StorageKeys.CredentialsKey(
accessibility: selectedAccessibility, accessibility: selectedAccessibility,
accessControl: selectedAccessControl accessControl: selectedAccessControl
) )
@ -161,7 +161,7 @@ struct KeychainDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKey.credentialsKey( let key = StorageKeys.CredentialsKey(
accessibility: selectedAccessibility, accessibility: selectedAccessibility,
accessControl: selectedAccessControl accessControl: selectedAccessControl
) )

View File

@ -87,7 +87,7 @@ struct MigrationDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKey.legacyMigrationSource let key = StorageKeys.LegacyMigrationSourceKey()
try await StorageRouter.shared.set(legacyValue, for: key) try await StorageRouter.shared.set(legacyValue, for: key)
statusMessage = "✓ Saved '\(legacyValue)' to legacy UserDefaults" statusMessage = "✓ Saved '\(legacyValue)' to legacy UserDefaults"
} catch { } catch {
@ -102,7 +102,7 @@ struct MigrationDemo: View {
statusMessage = "Retrieving from Modern..." statusMessage = "Retrieving from Modern..."
Task { Task {
do { do {
let key = StorageKey.modernMigrationDestination let key = StorageKeys.ModernMigrationDestinationKey()
let value = try await StorageRouter.shared.get(key) let value = try await StorageRouter.shared.get(key)
modernValue = value modernValue = value
statusMessage = "✓ Success! Data migrated from UserDefaults to Keychain." statusMessage = "✓ Success! Data migrated from UserDefaults to Keychain."
@ -120,7 +120,7 @@ struct MigrationDemo: View {
statusMessage = "Running manual migration..." statusMessage = "Running manual migration..."
Task { Task {
do { do {
let key = StorageKey.modernMigrationDestination let key = StorageKeys.ModernMigrationDestinationKey()
_ = try await StorageRouter.shared.forceMigration(for: key) _ = try await StorageRouter.shared.forceMigration(for: key)
// Refresh modern value display // Refresh modern value display
modernValue = try await StorageRouter.shared.get(key) modernValue = try await StorageRouter.shared.get(key)
@ -136,7 +136,7 @@ struct MigrationDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKey.legacyMigrationSource let key = StorageKeys.LegacyMigrationSourceKey()
let exists = try await StorageRouter.shared.exists(key) let exists = try await StorageRouter.shared.exists(key)
statusMessage = exists ? "⚠️ Legacy data STILL EXISTS in UserDefaults!" : "✅ Legacy data was successfully DELETED." statusMessage = exists ? "⚠️ Legacy data STILL EXISTS in UserDefaults!" : "✅ Legacy data was successfully DELETED."
} catch { } catch {

View File

@ -114,7 +114,7 @@ struct PlatformSyncDemo: View {
LabeledContent("Platform", value: selectedPlatform.displayName) LabeledContent("Platform", value: selectedPlatform.displayName)
LabeledContent("Sync", value: selectedSync.displayName) LabeledContent("Sync", value: selectedSync.displayName)
LabeledContent("Max Auto-Sync Size", value: "50 KB") LabeledContent("Max Auto-Sync Size", value: "50 KB")
LabeledContent("Key Name", value: StorageKey.syncableSetting.name) LabeledContent("Key Name", value: StorageKeys.SyncableSettingKey().name)
} }
Section("Watch Notes") { Section("Watch Notes") {
@ -141,7 +141,7 @@ struct PlatformSyncDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKey.syncableSettingKey( let key = StorageKeys.SyncableSettingKey(
availability: selectedPlatform, availability: selectedPlatform,
syncPolicy: selectedSync syncPolicy: selectedSync
) )
@ -161,7 +161,7 @@ struct PlatformSyncDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKey.syncableSettingKey( let key = StorageKeys.SyncableSettingKey(
availability: selectedPlatform, availability: selectedPlatform,
syncPolicy: selectedSync syncPolicy: selectedSync
) )
@ -184,7 +184,7 @@ struct PlatformSyncDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKey.syncableSettingKey( let key = StorageKeys.SyncableSettingKey(
availability: selectedPlatform, availability: selectedPlatform,
syncPolicy: selectedSync syncPolicy: selectedSync
) )

View File

@ -63,7 +63,7 @@ struct TransformingMigrationDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKey.legacyProfileName let key = StorageKeys.LegacyProfileNameKey()
try await StorageRouter.shared.set(legacyValue, for: key) try await StorageRouter.shared.set(legacyValue, for: key)
statusMessage = "✓ Saved legacy name \(legacyValue)" statusMessage = "✓ Saved legacy name \(legacyValue)"
} catch { } catch {
@ -78,7 +78,7 @@ struct TransformingMigrationDemo: View {
statusMessage = "Loading modern profile..." statusMessage = "Loading modern profile..."
Task { Task {
do { do {
let key = StorageKey.modernProfileName let key = StorageKeys.ModernProfileNameKey()
let value = try await StorageRouter.shared.get(key) let value = try await StorageRouter.shared.get(key)
modernValue = format(value) modernValue = format(value)
statusMessage = "✓ Migration complete." statusMessage = "✓ Migration complete."

View File

@ -140,7 +140,7 @@ struct UserDefaultsDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKey.appVersion let key = StorageKeys.AppVersionKey()
try await StorageRouter.shared.set(inputText, for: key) try await StorageRouter.shared.set(inputText, for: key)
statusMessage = "✓ Saved successfully" statusMessage = "✓ Saved successfully"
} catch { } catch {
@ -154,7 +154,7 @@ struct UserDefaultsDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKey.appVersion let key = StorageKeys.AppVersionKey()
let value = try await StorageRouter.shared.get(key) let value = try await StorageRouter.shared.get(key)
storedValue = value storedValue = value
inputText = value // Sync to field inputText = value // Sync to field
@ -173,7 +173,7 @@ struct UserDefaultsDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKey.appVersion let key = StorageKeys.AppVersionKey()
try await StorageRouter.shared.remove(key) try await StorageRouter.shared.remove(key)
storedValue = "" storedValue = ""
statusMessage = "✓ Removed successfully" statusMessage = "✓ Removed successfully"
@ -188,7 +188,7 @@ struct UserDefaultsDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKey.appGroupUserDefaults let key = StorageKeys.AppGroupUserDefaultsKey()
try await StorageRouter.shared.set(inputText, for: key) try await StorageRouter.shared.set(inputText, for: key)
statusMessage = "✓ App Group saved successfully" statusMessage = "✓ App Group saved successfully"
} catch { } catch {
@ -202,7 +202,7 @@ struct UserDefaultsDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKey.appGroupUserDefaults let key = StorageKeys.AppGroupUserDefaultsKey()
let value = try await StorageRouter.shared.get(key) let value = try await StorageRouter.shared.get(key)
appGroupStoredValue = value appGroupStoredValue = value
inputText = value // Sync to field inputText = value // Sync to field
@ -221,7 +221,7 @@ struct UserDefaultsDemo: View {
isLoading = true isLoading = true
Task { Task {
do { do {
let key = StorageKey.appGroupUserDefaults let key = StorageKeys.AppGroupUserDefaultsKey()
try await StorageRouter.shared.remove(key) try await StorageRouter.shared.remove(key)
appGroupStoredValue = "" appGroupStoredValue = ""
statusMessage = "✓ App Group value removed" statusMessage = "✓ App Group value removed"