Add DemoDestination, AppStorageCatalog, StorageKeys, LegacyNotificationSettingKey (+14 more)
This commit is contained in:
parent
c50c9cb60e
commit
0db85ddc49
@ -10,47 +10,47 @@ import LocalData
|
|||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TabView {
|
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
UserDefaultsDemo()
|
List {
|
||||||
|
Section("Storage Demos") {
|
||||||
|
NavigationLink(value: DemoDestination.userDefaults) {
|
||||||
|
Label("UserDefaults", systemImage: "gearshape.fill")
|
||||||
}
|
}
|
||||||
.tabItem {
|
NavigationLink(value: DemoDestination.keychain) {
|
||||||
Label("Defaults", systemImage: "gearshape.fill")
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigationStack {
|
|
||||||
KeychainDemo()
|
|
||||||
}
|
|
||||||
.tabItem {
|
|
||||||
Label("Keychain", systemImage: "lock.fill")
|
Label("Keychain", systemImage: "lock.fill")
|
||||||
}
|
}
|
||||||
|
NavigationLink(value: DemoDestination.files) {
|
||||||
NavigationStack {
|
Label("File Storage", systemImage: "doc.fill")
|
||||||
|
}
|
||||||
|
NavigationLink(value: DemoDestination.encrypted) {
|
||||||
|
Label("Encrypted Storage", systemImage: "lock.shield.fill")
|
||||||
|
}
|
||||||
|
NavigationLink(value: DemoDestination.sync) {
|
||||||
|
Label("Watch Sync", systemImage: "arrow.triangle.2.circlepath")
|
||||||
|
}
|
||||||
|
NavigationLink(value: DemoDestination.migration) {
|
||||||
|
Label("Migrations", systemImage: "sparkles")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.scrollIndicators(.hidden)
|
||||||
|
.navigationTitle("Secure Storage")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.navigationDestination(for: DemoDestination.self) { destination in
|
||||||
|
switch destination {
|
||||||
|
case .userDefaults:
|
||||||
|
UserDefaultsDemo()
|
||||||
|
case .keychain:
|
||||||
|
KeychainDemo()
|
||||||
|
case .files:
|
||||||
FileSystemDemo()
|
FileSystemDemo()
|
||||||
}
|
case .encrypted:
|
||||||
.tabItem {
|
|
||||||
Label("Files", systemImage: "doc.fill")
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigationStack {
|
|
||||||
EncryptedStorageDemo()
|
EncryptedStorageDemo()
|
||||||
}
|
case .sync:
|
||||||
.tabItem {
|
|
||||||
Label("Encrypted", systemImage: "lock.shield.fill")
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigationStack {
|
|
||||||
PlatformSyncDemo()
|
PlatformSyncDemo()
|
||||||
|
case .migration:
|
||||||
|
MigrationHubView()
|
||||||
}
|
}
|
||||||
.tabItem {
|
|
||||||
Label("Sync", systemImage: "arrow.triangle.2.circlepath")
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigationStack {
|
|
||||||
MigrationDemo()
|
|
||||||
}
|
|
||||||
.tabItem {
|
|
||||||
Label("Migration", systemImage: "sparkles")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -59,3 +59,12 @@ struct ContentView: View {
|
|||||||
#Preview {
|
#Preview {
|
||||||
ContentView()
|
ContentView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum DemoDestination: Hashable {
|
||||||
|
case userDefaults
|
||||||
|
case keychain
|
||||||
|
case files
|
||||||
|
case encrypted
|
||||||
|
case sync
|
||||||
|
case migration
|
||||||
|
}
|
||||||
|
|||||||
6
SecureStorageSample/Models/ProfileName.swift
Normal file
6
SecureStorageSample/Models/ProfileName.swift
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
nonisolated struct ProfileName: Codable, Sendable {
|
||||||
|
let firstName: String
|
||||||
|
let lastName: String
|
||||||
|
}
|
||||||
6
SecureStorageSample/Models/UnifiedSettings.swift
Normal file
6
SecureStorageSample/Models/UnifiedSettings.swift
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
nonisolated struct UnifiedSettings: Codable, Sendable {
|
||||||
|
let notificationsEnabled: Bool
|
||||||
|
let theme: String
|
||||||
|
}
|
||||||
@ -48,7 +48,7 @@ struct SecureStorageSampleApp: App {
|
|||||||
await StorageRouter.shared.updateStorageConfiguration(storageConfig)
|
await StorageRouter.shared.updateStorageConfiguration(storageConfig)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try await StorageRouter.shared.registerCatalog(AppStorageCatalog.self, migrateImmediately: true)
|
try await StorageRouter.shared.registerCatalog(AppStorageCatalog(), migrateImmediately: true)
|
||||||
} catch {
|
} catch {
|
||||||
assertionFailure("Storage catalog registration failed: \(error)")
|
assertionFailure("Storage catalog registration failed: \(error)")
|
||||||
}
|
}
|
||||||
@ -58,7 +58,7 @@ struct SecureStorageSampleApp: App {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
let report = StorageAuditReport.renderText(for: AppStorageCatalog.self)
|
let report = StorageAuditReport.renderText(AppStorageCatalog())
|
||||||
print(report)
|
print(report)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import LocalData
|
|||||||
import SharedKit
|
import SharedKit
|
||||||
|
|
||||||
|
|
||||||
nonisolated struct AppStorageCatalog: StorageKeyCatalog {
|
struct AppStorageCatalog: StorageKeyCatalog {
|
||||||
static var allKeys: [AnyStorageKey] {
|
var allKeys: [AnyStorageKey] {
|
||||||
[
|
[
|
||||||
.key(StorageKeys.AppVersionKey()),
|
.key(StorageKeys.AppVersionKey()),
|
||||||
.key(StorageKeys.UserPreferencesKey()),
|
.key(StorageKeys.UserPreferencesKey()),
|
||||||
@ -23,7 +23,14 @@ nonisolated struct AppStorageCatalog: StorageKeyCatalog {
|
|||||||
.key(StorageKeys.AppGroupUserDefaultsKey()),
|
.key(StorageKeys.AppGroupUserDefaultsKey()),
|
||||||
.key(StorageKeys.AppGroupUserProfileKey()),
|
.key(StorageKeys.AppGroupUserProfileKey()),
|
||||||
.key(StorageKeys.LegacyMigrationSourceKey()),
|
.key(StorageKeys.LegacyMigrationSourceKey()),
|
||||||
.key(StorageKeys.ModernMigrationDestinationKey())
|
.key(StorageKeys.ModernMigrationDestinationKey()),
|
||||||
|
.key(StorageKeys.LegacyProfileNameKey()),
|
||||||
|
.key(StorageKeys.ModernProfileNameKey()),
|
||||||
|
.key(StorageKeys.LegacyNotificationSettingKey()),
|
||||||
|
.key(StorageKeys.LegacyThemeSettingKey()),
|
||||||
|
.key(StorageKeys.ModernUnifiedSettingsKey()),
|
||||||
|
.key(StorageKeys.LegacyAppModeKey()),
|
||||||
|
.key(StorageKeys.ModernAppModeKey())
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ extension StorageKeys {
|
|||||||
let serializer: Serializer<String> = .json
|
let serializer: Serializer<String> = .json
|
||||||
let owner = "SampleApp"
|
let owner = "SampleApp"
|
||||||
let description = "Stores a shared setting readable by app extensions."
|
let description = "Stores a shared setting readable by app extensions."
|
||||||
let availability: PlatformAvailability = .all
|
let availability: PlatformAvailability = .phoneOnly
|
||||||
let syncPolicy: SyncPolicy = .never
|
let syncPolicy: SyncPolicy = .never
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ extension StorageKeys {
|
|||||||
let serializer: Serializer<UserProfile> = .json
|
let serializer: Serializer<UserProfile> = .json
|
||||||
let owner = "SampleApp"
|
let owner = "SampleApp"
|
||||||
let description = "Stores a profile shared between the app and extensions."
|
let description = "Stores a profile shared between the app and extensions."
|
||||||
let availability: PlatformAvailability = .all
|
let availability: PlatformAvailability = .phoneOnly
|
||||||
let syncPolicy: SyncPolicy = .never
|
let syncPolicy: SyncPolicy = .never
|
||||||
|
|
||||||
init(directory: FileDirectory = .documents) {
|
init(directory: FileDirectory = .documents) {
|
||||||
|
|||||||
@ -0,0 +1,73 @@
|
|||||||
|
import Foundation
|
||||||
|
import LocalData
|
||||||
|
|
||||||
|
extension StorageKeys {
|
||||||
|
struct LegacyNotificationSettingKey: StorageKey {
|
||||||
|
typealias Value = Bool
|
||||||
|
|
||||||
|
let name = "legacy_notification_setting"
|
||||||
|
let domain: StorageDomain = .userDefaults(suite: nil)
|
||||||
|
let security: SecurityPolicy = .none
|
||||||
|
let serializer: Serializer<Bool> = .json
|
||||||
|
let owner = "MigrationDemo"
|
||||||
|
let description = "Legacy notification setting stored as Bool."
|
||||||
|
let availability: PlatformAvailability = .all
|
||||||
|
let syncPolicy: SyncPolicy = .never
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LegacyThemeSettingKey: StorageKey {
|
||||||
|
typealias Value = String
|
||||||
|
|
||||||
|
let name = "legacy_theme_setting"
|
||||||
|
let domain: StorageDomain = .userDefaults(suite: nil)
|
||||||
|
let security: SecurityPolicy = .none
|
||||||
|
let serializer: Serializer<String> = .json
|
||||||
|
let owner = "MigrationDemo"
|
||||||
|
let description = "Legacy theme setting stored as a string."
|
||||||
|
let availability: PlatformAvailability = .all
|
||||||
|
let syncPolicy: SyncPolicy = .never
|
||||||
|
}
|
||||||
|
|
||||||
|
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] = [
|
||||||
|
.key(StorageKeys.LegacyNotificationSettingKey()),
|
||||||
|
.key(StorageKeys.LegacyThemeSettingKey())
|
||||||
|
]
|
||||||
|
|
||||||
|
return AnyStorageMigration(
|
||||||
|
DefaultAggregatingMigration(
|
||||||
|
destinationKey: self,
|
||||||
|
sourceKeys: sources
|
||||||
|
) { sources in
|
||||||
|
var notificationsEnabled = false
|
||||||
|
var theme = "system"
|
||||||
|
|
||||||
|
for source in sources {
|
||||||
|
if let boolValue = source.value as? Bool {
|
||||||
|
notificationsEnabled = boolValue
|
||||||
|
} else if let stringValue = source.value as? String {
|
||||||
|
theme = stringValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return UnifiedSettings(
|
||||||
|
notificationsEnabled: notificationsEnabled,
|
||||||
|
theme: theme
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
import Foundation
|
||||||
|
import LocalData
|
||||||
|
|
||||||
|
extension StorageKeys {
|
||||||
|
struct LegacyAppModeKey: StorageKey {
|
||||||
|
typealias Value = String
|
||||||
|
|
||||||
|
let name = "legacy_app_mode"
|
||||||
|
let domain: StorageDomain = .userDefaults(suite: nil)
|
||||||
|
let security: SecurityPolicy = .none
|
||||||
|
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,
|
||||||
|
accessControl: .userPresence
|
||||||
|
)
|
||||||
|
let serializer: Serializer<String> = .json
|
||||||
|
let owner = "MigrationDemo"
|
||||||
|
let description = "Modern app mode with conditional migration."
|
||||||
|
let availability: PlatformAvailability = .all
|
||||||
|
let syncPolicy: SyncPolicy = .never
|
||||||
|
|
||||||
|
var migration: AnyStorageMigration? {
|
||||||
|
let destination = self
|
||||||
|
let legacy = StorageKeys.LegacyAppModeKey()
|
||||||
|
let fallback = AnyStorageMigration(
|
||||||
|
SimpleLegacyMigration(destinationKey: destination, sourceKey: .key(legacy))
|
||||||
|
)
|
||||||
|
|
||||||
|
return AnyStorageMigration(
|
||||||
|
AppVersionConditionalMigration(
|
||||||
|
destinationKey: destination,
|
||||||
|
minAppVersion: "99.0",
|
||||||
|
fallbackMigration: fallback
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
import Foundation
|
||||||
|
import LocalData
|
||||||
|
|
||||||
|
extension StorageKeys {
|
||||||
|
struct LegacyProfileNameKey: StorageKey {
|
||||||
|
typealias Value = String
|
||||||
|
|
||||||
|
let name = "legacy_profile_name"
|
||||||
|
let domain: StorageDomain = .userDefaults(suite: nil)
|
||||||
|
let security: SecurityPolicy = .none
|
||||||
|
let serializer: Serializer<String> = .json
|
||||||
|
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,
|
||||||
|
accessControl: .userPresence
|
||||||
|
)
|
||||||
|
let serializer: Serializer<ProfileName> = .json
|
||||||
|
let owner = "MigrationDemo"
|
||||||
|
let description = "Modern profile name stored as structured data."
|
||||||
|
let availability: PlatformAvailability = .all
|
||||||
|
let syncPolicy: SyncPolicy = .never
|
||||||
|
|
||||||
|
var migration: AnyStorageMigration? {
|
||||||
|
let sourceKey = StorageKeys.LegacyProfileNameKey()
|
||||||
|
return AnyStorageMigration(
|
||||||
|
DefaultTransformingMigration(
|
||||||
|
destinationKey: self,
|
||||||
|
sourceKey: sourceKey
|
||||||
|
) { value in
|
||||||
|
let parts = value.split(separator: " ", maxSplits: 1).map(String.init)
|
||||||
|
let firstName = parts.first ?? ""
|
||||||
|
let lastName = parts.count > 1 ? parts[1] : ""
|
||||||
|
return ProfileName(firstName: firstName, lastName: lastName)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
103
SecureStorageSample/Views/AggregatingMigrationDemo.swift
Normal file
103
SecureStorageSample/Views/AggregatingMigrationDemo.swift
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import LocalData
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
struct AggregatingMigrationDemo: View {
|
||||||
|
@State private var notificationsEnabled = false
|
||||||
|
@State private var theme = ""
|
||||||
|
@State private var modernValue = ""
|
||||||
|
@State private var statusMessage = ""
|
||||||
|
@State private var isLoading = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
Section("The Scenario") {
|
||||||
|
Text("Two legacy settings are aggregated into a single unified settings model during migration.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(Color.Text.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("Step 1: Setup Legacy Data") {
|
||||||
|
Toggle("Notifications Enabled", isOn: $notificationsEnabled)
|
||||||
|
|
||||||
|
TextField("Theme", text: $theme)
|
||||||
|
.textFieldStyle(.roundedBorder)
|
||||||
|
|
||||||
|
Button(action: saveToLegacy) {
|
||||||
|
Label("Save Legacy Settings", systemImage: "tray.and.arrow.down")
|
||||||
|
}
|
||||||
|
.disabled(theme.isEmpty || isLoading)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("Step 2: Trigger Migration") {
|
||||||
|
Text("Load the modern key to aggregate legacy values into unified settings.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(Color.Text.secondary)
|
||||||
|
|
||||||
|
Button(action: loadFromModern) {
|
||||||
|
Label("Load Unified Settings", systemImage: "sparkles")
|
||||||
|
}
|
||||||
|
.disabled(isLoading)
|
||||||
|
|
||||||
|
if !modernValue.isEmpty {
|
||||||
|
LabeledContent("Unified Settings", value: modernValue)
|
||||||
|
.foregroundStyle(Color.Status.success)
|
||||||
|
.bold()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !statusMessage.isEmpty {
|
||||||
|
Section {
|
||||||
|
Text(statusMessage)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(statusMessage.contains("Error") ? Color.Status.error : Color.Status.info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Aggregating Migration")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveToLegacy() {
|
||||||
|
isLoading = true
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
try await StorageRouter.shared.set(
|
||||||
|
notificationsEnabled,
|
||||||
|
for: StorageKeys.LegacyNotificationSettingKey()
|
||||||
|
)
|
||||||
|
try await StorageRouter.shared.set(
|
||||||
|
theme,
|
||||||
|
for: StorageKeys.LegacyThemeSettingKey()
|
||||||
|
)
|
||||||
|
statusMessage = "✓ Saved legacy settings"
|
||||||
|
} catch {
|
||||||
|
statusMessage = "Error: \(error.localizedDescription)"
|
||||||
|
}
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadFromModern() {
|
||||||
|
isLoading = true
|
||||||
|
statusMessage = "Loading unified settings..."
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let value = try await StorageRouter.shared.get(StorageKeys.ModernUnifiedSettingsKey())
|
||||||
|
modernValue = format(value)
|
||||||
|
statusMessage = "✓ Migration complete."
|
||||||
|
} catch StorageError.notFound {
|
||||||
|
statusMessage = "Modern key is empty (and no legacy data found)."
|
||||||
|
} catch {
|
||||||
|
statusMessage = "Error: \(error.localizedDescription)"
|
||||||
|
}
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func format(_ value: UnifiedSettings) -> String {
|
||||||
|
let notificationsText = value.notificationsEnabled ? "On" : "Off"
|
||||||
|
let themeText = value.theme.isEmpty ? "system" : value.theme
|
||||||
|
return "Notifications: \(notificationsText), Theme: \(themeText)"
|
||||||
|
}
|
||||||
|
}
|
||||||
93
SecureStorageSample/Views/ConditionalMigrationDemo.swift
Normal file
93
SecureStorageSample/Views/ConditionalMigrationDemo.swift
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import LocalData
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
struct ConditionalMigrationDemo: View {
|
||||||
|
@State private var legacyValue = ""
|
||||||
|
@State private var modernValue = ""
|
||||||
|
@State private var statusMessage = ""
|
||||||
|
@State private var isLoading = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
Section("The Scenario") {
|
||||||
|
Text("A conditional migration only runs when the app version is below a threshold.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(Color.Text.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("Step 1: Setup Legacy Data") {
|
||||||
|
Text("Save a legacy app mode string that will migrate only if the version condition is met.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(Color.Text.secondary)
|
||||||
|
|
||||||
|
TextField("Legacy App Mode", text: $legacyValue)
|
||||||
|
.textFieldStyle(.roundedBorder)
|
||||||
|
|
||||||
|
Button(action: saveToLegacy) {
|
||||||
|
Label("Save Legacy Mode", systemImage: "square.and.arrow.down")
|
||||||
|
}
|
||||||
|
.disabled(legacyValue.isEmpty || isLoading)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("Step 2: Trigger Conditional Migration") {
|
||||||
|
Text("Load the modern key to run the conditional migration.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(Color.Text.secondary)
|
||||||
|
|
||||||
|
Button(action: loadFromModern) {
|
||||||
|
Label("Load Modern Mode", systemImage: "sparkles")
|
||||||
|
}
|
||||||
|
.disabled(isLoading)
|
||||||
|
|
||||||
|
if !modernValue.isEmpty {
|
||||||
|
LabeledContent("Migrated Mode", value: modernValue)
|
||||||
|
.foregroundStyle(Color.Status.success)
|
||||||
|
.bold()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !statusMessage.isEmpty {
|
||||||
|
Section {
|
||||||
|
Text(statusMessage)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(statusMessage.contains("Error") ? Color.Status.error : Color.Status.info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Conditional Migration")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveToLegacy() {
|
||||||
|
isLoading = true
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let key = StorageKeys.LegacyAppModeKey()
|
||||||
|
try await StorageRouter.shared.set(legacyValue, for: key)
|
||||||
|
statusMessage = "✓ Saved legacy app mode"
|
||||||
|
} catch {
|
||||||
|
statusMessage = "Error: \(error.localizedDescription)"
|
||||||
|
}
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadFromModern() {
|
||||||
|
isLoading = true
|
||||||
|
statusMessage = "Loading modern mode..."
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let key = StorageKeys.ModernAppModeKey()
|
||||||
|
let value = try await StorageRouter.shared.get(key)
|
||||||
|
modernValue = value
|
||||||
|
statusMessage = "✓ Conditional migration complete."
|
||||||
|
} catch StorageError.notFound {
|
||||||
|
statusMessage = "Modern key is empty (and no legacy data found)."
|
||||||
|
} catch {
|
||||||
|
statusMessage = "Error: \(error.localizedDescription)"
|
||||||
|
}
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -83,6 +83,7 @@ struct FileSystemDemo: View {
|
|||||||
.disabled(isLoading)
|
.disabled(isLoading)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
Section("App Group Storage") {
|
Section("App Group Storage") {
|
||||||
Text("Requires App Group entitlement and matching identifier in AppGroupConfiguration.")
|
Text("Requires App Group entitlement and matching identifier in AppGroupConfiguration.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
@ -113,6 +114,7 @@ struct FileSystemDemo: View {
|
|||||||
.foregroundStyle(.red)
|
.foregroundStyle(.red)
|
||||||
.disabled(isLoading)
|
.disabled(isLoading)
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if let profile = storedProfile {
|
if let profile = storedProfile {
|
||||||
Section("Retrieved Profile") {
|
Section("Retrieved Profile") {
|
||||||
|
|||||||
39
SecureStorageSample/Views/MigrationHubView.swift
Normal file
39
SecureStorageSample/Views/MigrationHubView.swift
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct MigrationHubView: View {
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
Section("Migration Demos") {
|
||||||
|
NavigationLink("Simple Legacy Migration") {
|
||||||
|
MigrationDemo()
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationLink("Transforming Migration") {
|
||||||
|
TransformingMigrationDemo()
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationLink("Aggregating Migration") {
|
||||||
|
AggregatingMigrationDemo()
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationLink("Conditional Migration") {
|
||||||
|
ConditionalMigrationDemo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("Overview") {
|
||||||
|
Text("Explore migrations that move legacy values into modern storage with transformation, aggregation, and conditional rules.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(Color.Text.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Migrations")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
NavigationStack {
|
||||||
|
MigrationHubView()
|
||||||
|
}
|
||||||
|
}
|
||||||
102
SecureStorageSample/Views/TransformingMigrationDemo.swift
Normal file
102
SecureStorageSample/Views/TransformingMigrationDemo.swift
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import LocalData
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
struct TransformingMigrationDemo: View {
|
||||||
|
@State private var legacyValue = ""
|
||||||
|
@State private var modernValue = ""
|
||||||
|
@State private var statusMessage = ""
|
||||||
|
@State private var isLoading = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
Section("The Scenario") {
|
||||||
|
Text("A legacy profile name stored as a single string is migrated into a structured model with first and last names.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(Color.Text.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("Step 1: Setup Legacy Data") {
|
||||||
|
Text("Save a full name into the legacy UserDefaults key.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(Color.Text.secondary)
|
||||||
|
|
||||||
|
TextField("Legacy Full Name", text: $legacyValue)
|
||||||
|
.textFieldStyle(.roundedBorder)
|
||||||
|
|
||||||
|
Button(action: saveToLegacy) {
|
||||||
|
Label("Save Legacy Name", systemImage: "person.crop.circle.badge.plus")
|
||||||
|
}
|
||||||
|
.disabled(legacyValue.isEmpty || isLoading)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("Step 2: Trigger Migration") {
|
||||||
|
Text("Load the modern key to transform the legacy value into a structured profile.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(Color.Text.secondary)
|
||||||
|
|
||||||
|
Button(action: loadFromModern) {
|
||||||
|
Label("Load Modern Profile", systemImage: "sparkles")
|
||||||
|
}
|
||||||
|
.disabled(isLoading)
|
||||||
|
|
||||||
|
if !modernValue.isEmpty {
|
||||||
|
LabeledContent("Migrated Name", value: modernValue)
|
||||||
|
.foregroundStyle(Color.Status.success)
|
||||||
|
.bold()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !statusMessage.isEmpty {
|
||||||
|
Section {
|
||||||
|
Text(statusMessage)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(statusMessage.contains("Error") ? Color.Status.error : Color.Status.info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Transforming Migration")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveToLegacy() {
|
||||||
|
isLoading = true
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let key = StorageKeys.LegacyProfileNameKey()
|
||||||
|
try await StorageRouter.shared.set(legacyValue, for: key)
|
||||||
|
statusMessage = "✓ Saved legacy name \(legacyValue)"
|
||||||
|
} catch {
|
||||||
|
statusMessage = "Error: \(error.localizedDescription)"
|
||||||
|
}
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadFromModern() {
|
||||||
|
isLoading = true
|
||||||
|
statusMessage = "Loading modern profile..."
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let key = StorageKeys.ModernProfileNameKey()
|
||||||
|
let value = try await StorageRouter.shared.get(key)
|
||||||
|
modernValue = format(value)
|
||||||
|
statusMessage = "✓ Migration complete."
|
||||||
|
} catch StorageError.notFound {
|
||||||
|
statusMessage = "Modern key is empty (and no legacy data found)."
|
||||||
|
} catch {
|
||||||
|
statusMessage = "Error: \(error.localizedDescription)"
|
||||||
|
}
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func format(_ value: ProfileName) -> String {
|
||||||
|
let trimmedFirst = value.firstName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let trimmedLast = value.lastName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if trimmedLast.isEmpty {
|
||||||
|
return trimmedFirst
|
||||||
|
}
|
||||||
|
return "\(trimmedFirst) \(trimmedLast)"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user