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 {
|
||||
var body: some View {
|
||||
TabView {
|
||||
NavigationStack {
|
||||
UserDefaultsDemo()
|
||||
List {
|
||||
Section("Storage Demos") {
|
||||
NavigationLink(value: DemoDestination.userDefaults) {
|
||||
Label("UserDefaults", systemImage: "gearshape.fill")
|
||||
}
|
||||
.tabItem {
|
||||
Label("Defaults", systemImage: "gearshape.fill")
|
||||
}
|
||||
|
||||
NavigationStack {
|
||||
KeychainDemo()
|
||||
}
|
||||
.tabItem {
|
||||
NavigationLink(value: DemoDestination.keychain) {
|
||||
Label("Keychain", systemImage: "lock.fill")
|
||||
}
|
||||
|
||||
NavigationStack {
|
||||
NavigationLink(value: DemoDestination.files) {
|
||||
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()
|
||||
}
|
||||
.tabItem {
|
||||
Label("Files", systemImage: "doc.fill")
|
||||
}
|
||||
|
||||
NavigationStack {
|
||||
case .encrypted:
|
||||
EncryptedStorageDemo()
|
||||
}
|
||||
.tabItem {
|
||||
Label("Encrypted", systemImage: "lock.shield.fill")
|
||||
}
|
||||
|
||||
NavigationStack {
|
||||
case .sync:
|
||||
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 {
|
||||
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)
|
||||
|
||||
do {
|
||||
try await StorageRouter.shared.registerCatalog(AppStorageCatalog.self, migrateImmediately: true)
|
||||
try await StorageRouter.shared.registerCatalog(AppStorageCatalog(), migrateImmediately: true)
|
||||
} catch {
|
||||
assertionFailure("Storage catalog registration failed: \(error)")
|
||||
}
|
||||
@ -58,7 +58,7 @@ struct SecureStorageSampleApp: App {
|
||||
)
|
||||
}
|
||||
#if DEBUG
|
||||
let report = StorageAuditReport.renderText(for: AppStorageCatalog.self)
|
||||
let report = StorageAuditReport.renderText(AppStorageCatalog())
|
||||
print(report)
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -3,8 +3,8 @@ import LocalData
|
||||
import SharedKit
|
||||
|
||||
|
||||
nonisolated struct AppStorageCatalog: StorageKeyCatalog {
|
||||
static var allKeys: [AnyStorageKey] {
|
||||
struct AppStorageCatalog: StorageKeyCatalog {
|
||||
var allKeys: [AnyStorageKey] {
|
||||
[
|
||||
.key(StorageKeys.AppVersionKey()),
|
||||
.key(StorageKeys.UserPreferencesKey()),
|
||||
@ -23,7 +23,14 @@ nonisolated struct AppStorageCatalog: StorageKeyCatalog {
|
||||
.key(StorageKeys.AppGroupUserDefaultsKey()),
|
||||
.key(StorageKeys.AppGroupUserProfileKey()),
|
||||
.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 owner = "SampleApp"
|
||||
let description = "Stores a shared setting readable by app extensions."
|
||||
let availability: PlatformAvailability = .all
|
||||
let availability: PlatformAvailability = .phoneOnly
|
||||
let syncPolicy: SyncPolicy = .never
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ extension StorageKeys {
|
||||
let serializer: Serializer<UserProfile> = .json
|
||||
let owner = "SampleApp"
|
||||
let description = "Stores a profile shared between the app and extensions."
|
||||
let availability: PlatformAvailability = .all
|
||||
let availability: PlatformAvailability = .phoneOnly
|
||||
let syncPolicy: SyncPolicy = .never
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
Section("App Group Storage") {
|
||||
Text("Requires App Group entitlement and matching identifier in AppGroupConfiguration.")
|
||||
.font(.caption)
|
||||
@ -113,6 +114,7 @@ struct FileSystemDemo: View {
|
||||
.foregroundStyle(.red)
|
||||
.disabled(isLoading)
|
||||
}
|
||||
#endif
|
||||
|
||||
if let profile = storedProfile {
|
||||
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