Add Design, Spacing, Color, Status (+1 more)

This commit is contained in:
Matt Bruce 2026-01-16 13:47:24 -06:00
parent 67dbfa9e09
commit c50c9cb60e
4 changed files with 49 additions and 15 deletions

View File

@ -122,8 +122,11 @@ 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 `LegacyMigrationSourceKey` to `ModernMigrationDestinationKey` on first access. - **Fallback**: Automatically moves data from `LegacyMigrationSourceKey` to `ModernMigrationDestinationKey` on first access using protocol-based migration.
- **Manual Sweep**: Explicitly triggers a "drain" of legacy keys to the Keychain using `StorageRouter.shared.migrate(for:)`. - **Transforming**: Converts a legacy full-name string into a structured `ProfileName`.
- **Aggregating**: Combines legacy notification + theme settings into `UnifiedSettings`.
- **Conditional**: Migrates app mode only when the version rule is met.
- **Manual Sweep**: Explicitly triggers a "drain" of legacy keys to the Keychain using `StorageRouter.shared.forceMigration(for:)`.
- **Startup Sweep**: Automatically cleanses all registered legacy keys at app launch via `registerCatalog(..., migrateImmediately: true)`. - **Startup Sweep**: Automatically cleanses all registered legacy keys at app launch via `registerCatalog(..., migrateImmediately: true)`.
## Global Configuration ## Global Configuration

View File

@ -0,0 +1,23 @@
import SwiftUI
enum Design {
enum Spacing {
static let xSmall: CGFloat = 4
static let small: CGFloat = 8
static let medium: CGFloat = 16
static let large: CGFloat = 24
}
}
extension Color {
enum Status {
static let success = Color.green
static let info = Color.blue
static let warning = Color.orange
static let error = Color.red
}
enum Text {
static let secondary = Color.secondary
}
}

View File

@ -32,9 +32,13 @@ extension StorageKeys {
let availability: PlatformAvailability = .all let availability: PlatformAvailability = .all
let syncPolicy: SyncPolicy = .never let syncPolicy: SyncPolicy = .never
// Define the migration path var migration: AnyStorageMigration? {
var migrationSources: [AnyStorageKey] { AnyStorageMigration(
[AnyStorageKey(LegacyMigrationSourceKey())] SimpleLegacyMigration(
destinationKey: self,
sourceKey: .key(LegacyMigrationSourceKey())
)
)
} }
} }
} }

View File

@ -13,12 +13,13 @@ struct MigrationDemo: View {
Section("The Scenario") { Section("The Scenario") {
Text("Imagine you have an old version of the app that stored a User ID in plain UserDefaults. Now, you want to move it to the secure Keychain automatically without the user noticing.") Text("Imagine you have an old version of the app that stored a User ID in plain UserDefaults. Now, you want to move it to the secure Keychain automatically without the user noticing.")
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(Color.Text.secondary)
} }
Section("Step 1: Setup Legacy Data") { Section("Step 1: Setup Legacy Data") {
Text("First, save a value to the 'legacy' key in UserDefaults.") Text("First, save a value to the 'legacy' key in UserDefaults.")
.font(.caption) .font(.caption)
.foregroundStyle(Color.Text.secondary)
TextField("Legacy Value", text: $legacyValue) TextField("Legacy Value", text: $legacyValue)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
@ -30,8 +31,9 @@ struct MigrationDemo: View {
} }
Section("Step 2: Trigger Migration") { Section("Step 2: Trigger Migration") {
Text("Now, attempt to load from the 'modern' Keychain key. It will automatically check the legacy key, move the data, and delete the old record.") Text("Now, attempt to load from the 'modern' Keychain key. It will automatically check the legacy key, move the data, and delete the old record using the new migration protocol.")
.font(.caption) .font(.caption)
.foregroundStyle(Color.Text.secondary)
Button(action: loadFromModern) { Button(action: loadFromModern) {
Label("Load from Modern (Keychain)", systemImage: "sparkles") Label("Load from Modern (Keychain)", systemImage: "sparkles")
@ -40,17 +42,18 @@ struct MigrationDemo: View {
if !modernValue.isEmpty { if !modernValue.isEmpty {
LabeledContent("Migrated Value", value: modernValue) LabeledContent("Migrated Value", value: modernValue)
.foregroundStyle(.green) .foregroundStyle(Color.Status.success)
.bold() .bold()
} }
} }
Section("Step 3: Proactive Sweep (Drain)") { Section("Step 3: Proactive Sweep (Drain)") {
Text("Even if the modern key already has data, you can force a 'Sweep' of legacy sources. Try saving a NEW value to Legacy, then click Drain.") Text("Even if the modern key already has data, you can force a sweep of legacy sources. Try saving a new value to Legacy, then click Force Migration.")
.font(.caption) .font(.caption)
.foregroundStyle(Color.Text.secondary)
Button(action: runManualMigration) { Button(action: runManualMigration) {
Label("Drain Migration Sources", systemImage: "arrow.up.circle.badge.clock") Label("Force Migration", systemImage: "arrow.up.circle.badge.clock")
} }
.disabled(isLoading) .disabled(isLoading)
} }
@ -58,6 +61,7 @@ struct MigrationDemo: View {
Section("Step 4: Verify Cleanup") { Section("Step 4: Verify Cleanup") {
Text("Check if the data was actually removed from UserDefaults after migration.") Text("Check if the data was actually removed from UserDefaults after migration.")
.font(.caption) .font(.caption)
.foregroundStyle(Color.Text.secondary)
Button(action: checkLegacyExists) { Button(action: checkLegacyExists) {
Label("Check Legacy Exists?", systemImage: "magnifyingglass") Label("Check Legacy Exists?", systemImage: "magnifyingglass")
@ -69,7 +73,7 @@ struct MigrationDemo: View {
Section { Section {
Text(statusMessage) Text(statusMessage)
.font(.caption) .font(.caption)
.foregroundStyle(statusMessage.contains("Error") ? .red : .blue) .foregroundStyle(statusMessage.contains("Error") ? Color.Status.error : Color.Status.info)
} }
} }
} }
@ -113,14 +117,14 @@ struct MigrationDemo: View {
private func runManualMigration() { private func runManualMigration() {
isLoading = true isLoading = true
statusMessage = "Draining migration sources..." statusMessage = "Running manual migration..."
Task { Task {
do { do {
let key = StorageKeys.ModernMigrationDestinationKey() let key = StorageKeys.ModernMigrationDestinationKey()
try await StorageRouter.shared.migrate(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)
statusMessage = "Proactive migration complete. Legacy data drained into Keychain." statusMessage = "Manual migration complete. Legacy data drained into Keychain."
} catch { } catch {
statusMessage = "Error: \(error.localizedDescription)" statusMessage = "Error: \(error.localizedDescription)"
} }