Compare commits
No commits in common. "3019e19d5b24f4c71666a7e5a0da76d16bca8688" and "d8e49d8d8db563b44ee8a67d12d1e7f65fd2d028" have entirely different histories.
3019e19d5b
...
d8e49d8d8d
@ -8,7 +8,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import LocalData
|
import LocalData
|
||||||
|
|
||||||
/// Defines all demo destinations and provides display metadata for the list.
|
|
||||||
enum DemoDestination: Hashable, CaseIterable {
|
enum DemoDestination: Hashable, CaseIterable {
|
||||||
case userDefaults
|
case userDefaults
|
||||||
case keychain
|
case keychain
|
||||||
@ -17,8 +16,6 @@ enum DemoDestination: Hashable, CaseIterable {
|
|||||||
case sync
|
case sync
|
||||||
case migration
|
case migration
|
||||||
|
|
||||||
/// Type-erased destination view for NavigationStack routing.
|
|
||||||
/// The enum keeps navigation data-driven and centralized.
|
|
||||||
var view: some View {
|
var view: some View {
|
||||||
switch self {
|
switch self {
|
||||||
case .userDefaults:
|
case .userDefaults:
|
||||||
@ -71,7 +68,6 @@ enum DemoDestination: Hashable, CaseIterable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Root navigation list that links to each LocalData demo screen.
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
/// Design tokens for spacing and common UI colors used in the sample.
|
|
||||||
enum Design {
|
enum Design {
|
||||||
enum Spacing {
|
enum Spacing {
|
||||||
static let xSmall: CGFloat = 4
|
static let xSmall: CGFloat = 4
|
||||||
@ -11,7 +10,6 @@ enum Design {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension Color {
|
extension Color {
|
||||||
/// Semantic colors used for status messaging in demo screens.
|
|
||||||
enum Status {
|
enum Status {
|
||||||
static let success = Color.green
|
static let success = Color.green
|
||||||
static let info = Color.blue
|
static let info = Color.blue
|
||||||
@ -19,7 +17,6 @@ extension Color {
|
|||||||
static let error = Color.red
|
static let error = Color.red
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Semantic text colors for secondary labels.
|
|
||||||
enum Text {
|
enum Text {
|
||||||
static let secondary = Color.secondary
|
static let secondary = Color.secondary
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Simple credential model for the Keychain demo.
|
/// Simple credential model for keychain storage demo.
|
||||||
/// Using `Codable` keeps the storage path identical to more complex real-world credentials.
|
|
||||||
nonisolated
|
nonisolated
|
||||||
struct Credential: Codable, Sendable {
|
struct Credential: Codable, Sendable {
|
||||||
let username: String
|
let username: String
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Structured name model used in migration demos.
|
|
||||||
/// This replaces a legacy single-string name with distinct components.
|
|
||||||
nonisolated struct ProfileName: Codable, Sendable {
|
nonisolated struct ProfileName: Codable, Sendable {
|
||||||
let firstName: String
|
let firstName: String
|
||||||
let lastName: String
|
let lastName: String
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import LocalData
|
import LocalData
|
||||||
|
|
||||||
/// Identifiers for custom key material sources used by the sample.
|
|
||||||
/// LocalData uses these identifiers to look up registered providers.
|
|
||||||
nonisolated enum SampleKeyMaterialSources {
|
nonisolated enum SampleKeyMaterialSources {
|
||||||
nonisolated static let external = KeyMaterialSource(id: "sample.external.key")
|
nonisolated static let external = KeyMaterialSource(id: "sample.external.key")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Lightweight location model for the Keychain demo.
|
/// Location data model.
|
||||||
/// It shows that LocalData can store structured data even in secure domains.
|
|
||||||
nonisolated
|
nonisolated
|
||||||
struct SampleLocationData: Codable, Sendable {
|
struct SampleLocationData: Codable, Sendable {
|
||||||
let lat: Double
|
let lat: Double
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Aggregated settings model produced by migration demos.
|
|
||||||
/// It combines multiple legacy settings into a single modern payload.
|
|
||||||
nonisolated struct UnifiedSettings: Codable, Sendable {
|
nonisolated struct UnifiedSettings: Codable, Sendable {
|
||||||
let notificationsEnabled: Bool
|
let notificationsEnabled: Bool
|
||||||
let theme: String
|
let theme: String
|
||||||
|
|||||||
@ -10,13 +10,9 @@ import LocalData
|
|||||||
import SharedKit
|
import SharedKit
|
||||||
|
|
||||||
@main
|
@main
|
||||||
/// App entry point that centralizes LocalData configuration and storage key registration.
|
|
||||||
/// Keeping this bootstrap in one place makes the sample's storage behavior easy to audit.
|
|
||||||
struct SecureStorageSampleApp: App {
|
struct SecureStorageSampleApp: App {
|
||||||
init() {
|
init() {
|
||||||
// Log derived identifiers up front so it's obvious which services the sample uses.
|
|
||||||
StorageServiceIdentifiers.logConfiguration()
|
StorageServiceIdentifiers.logConfiguration()
|
||||||
// Spin up WCSession early so background sync opportunities aren't missed.
|
|
||||||
_ = WatchConnectivityService.shared
|
_ = WatchConnectivityService.shared
|
||||||
Task {
|
Task {
|
||||||
// 1. Global Encryption Configuration
|
// 1. Global Encryption Configuration
|
||||||
@ -51,18 +47,15 @@ struct SecureStorageSampleApp: App {
|
|||||||
)
|
)
|
||||||
await StorageRouter.shared.updateStorageConfiguration(storageConfig)
|
await StorageRouter.shared.updateStorageConfiguration(storageConfig)
|
||||||
|
|
||||||
// Register keys once so LocalData can validate and migrate them consistently.
|
|
||||||
do {
|
do {
|
||||||
try await StorageRouter.shared.registerCatalog(AppStorageCatalog(), migrateImmediately: true)
|
try await StorageRouter.shared.registerCatalog(AppStorageCatalog(), migrateImmediately: true)
|
||||||
} catch {
|
} catch {
|
||||||
assertionFailure("Storage catalog registration failed: \(error)")
|
assertionFailure("Storage catalog registration failed: \(error)")
|
||||||
}
|
}
|
||||||
// Provide external key material for the encrypted storage demo.
|
|
||||||
await StorageRouter.shared.registerKeyMaterialProvider(
|
await StorageRouter.shared.registerKeyMaterialProvider(
|
||||||
ExternalKeyMaterialProvider(),
|
ExternalKeyMaterialProvider(),
|
||||||
for: SampleKeyMaterialSources.external
|
for: SampleKeyMaterialSources.external
|
||||||
)
|
)
|
||||||
// If a watch is paired, send the current syncable payloads on launch.
|
|
||||||
await StorageRouter.shared.syncRegisteredKeysIfNeeded()
|
await StorageRouter.shared.syncRegisteredKeysIfNeeded()
|
||||||
}
|
}
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
|||||||
@ -3,11 +3,8 @@ import LocalData
|
|||||||
import SharedKit
|
import SharedKit
|
||||||
|
|
||||||
|
|
||||||
/// Catalog of all storage keys used by the sample app.
|
|
||||||
/// Registering a catalog allows LocalData to audit keys and run migrations up front.
|
|
||||||
struct AppStorageCatalog: StorageKeyCatalog {
|
struct AppStorageCatalog: StorageKeyCatalog {
|
||||||
var allKeys: [AnyStorageKey] {
|
var allKeys: [AnyStorageKey] {
|
||||||
// Order here is purely for readability in audit reports.
|
|
||||||
[
|
[
|
||||||
.key(.appVersion),
|
.key(.appVersion),
|
||||||
.key(.userPreferences),
|
.key(.userPreferences),
|
||||||
|
|||||||
@ -2,22 +2,18 @@ import CryptoKit
|
|||||||
import Foundation
|
import Foundation
|
||||||
import LocalData
|
import LocalData
|
||||||
|
|
||||||
/// Supplies external key material for the encrypted storage demo.
|
|
||||||
/// The provider persists the generated key material so encryption remains stable across launches.
|
|
||||||
nonisolated
|
nonisolated
|
||||||
struct ExternalKeyMaterialProvider: KeyMaterialProviding {
|
struct ExternalKeyMaterialProvider: KeyMaterialProviding {
|
||||||
private enum Constants {
|
private enum Constants {
|
||||||
static let keyLength = 32
|
static let keyLength = 32
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a stable 256-bit key, generating and persisting it on first use.
|
|
||||||
func keyMaterial(for keyName: String) async throws -> Data {
|
func keyMaterial(for keyName: String) async throws -> Data {
|
||||||
let key = StorageKey.externalKeyMaterial
|
let key = StorageKey.externalKeyMaterial
|
||||||
if let existing = try? await StorageRouter.shared.get(key) {
|
if let existing = try? await StorageRouter.shared.get(key) {
|
||||||
return existing
|
return existing
|
||||||
}
|
}
|
||||||
|
|
||||||
// CryptoKit ensures the material is cryptographically random.
|
|
||||||
let symmetricKey = SymmetricKey(size: .bits256)
|
let symmetricKey = SymmetricKey(size: .bits256)
|
||||||
let material = symmetricKey.withUnsafeBytes { Data($0) }
|
let material = symmetricKey.withUnsafeBytes { Data($0) }
|
||||||
guard material.count == Constants.keyLength else {
|
guard material.count == Constants.keyLength else {
|
||||||
|
|||||||
@ -4,8 +4,6 @@ import SharedKit
|
|||||||
import WatchConnectivity
|
import WatchConnectivity
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
/// iOS-side WatchConnectivity bridge that keeps the watch app in sync with LocalData.
|
|
||||||
/// This class focuses on coordination and delegates data payload creation to LocalData.
|
|
||||||
final class WatchConnectivityService: NSObject, WCSessionDelegate {
|
final class WatchConnectivityService: NSObject, WCSessionDelegate {
|
||||||
static let shared = WatchConnectivityService()
|
static let shared = WatchConnectivityService()
|
||||||
|
|
||||||
@ -14,7 +12,6 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate {
|
|||||||
activateIfSupported()
|
activateIfSupported()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Activates WCSession only on supported devices (no-ops on unsupported hardware).
|
|
||||||
private func activateIfSupported() {
|
private func activateIfSupported() {
|
||||||
guard WCSession.isSupported() else { return }
|
guard WCSession.isSupported() else { return }
|
||||||
let session = WCSession.default
|
let session = WCSession.default
|
||||||
@ -32,7 +29,6 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate {
|
|||||||
} else {
|
} else {
|
||||||
Logger.debug("iOS WCSession activated with state: \(activationState.rawValue)")
|
Logger.debug("iOS WCSession activated with state: \(activationState.rawValue)")
|
||||||
}
|
}
|
||||||
// Try to sync any previously registered keys as soon as WCSession is ready.
|
|
||||||
Task {
|
Task {
|
||||||
await StorageRouter.shared.syncRegisteredKeysIfNeeded()
|
await StorageRouter.shared.syncRegisteredKeysIfNeeded()
|
||||||
}
|
}
|
||||||
@ -48,7 +44,6 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate {
|
|||||||
|
|
||||||
func sessionWatchStateDidChange(_ session: WCSession) {
|
func sessionWatchStateDidChange(_ session: WCSession) {
|
||||||
Logger.debug("iOS WCSession watch state changed: paired=\(session.isPaired) installed=\(session.isWatchAppInstalled)")
|
Logger.debug("iOS WCSession watch state changed: paired=\(session.isPaired) installed=\(session.isWatchAppInstalled)")
|
||||||
// A watch install or pairing change is a good time to re-send syncable keys.
|
|
||||||
Task {
|
Task {
|
||||||
await StorageRouter.shared.syncRegisteredKeysIfNeeded()
|
await StorageRouter.shared.syncRegisteredKeysIfNeeded()
|
||||||
}
|
}
|
||||||
@ -56,14 +51,12 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate {
|
|||||||
|
|
||||||
func sessionReachabilityDidChange(_ session: WCSession) {
|
func sessionReachabilityDidChange(_ session: WCSession) {
|
||||||
Logger.debug("iOS WCSession reachability changed: reachable=\(session.isReachable)")
|
Logger.debug("iOS WCSession reachability changed: reachable=\(session.isReachable)")
|
||||||
// When reachability flips, attempt to push any pending sync payloads.
|
|
||||||
Task {
|
Task {
|
||||||
await StorageRouter.shared.syncRegisteredKeysIfNeeded()
|
await StorageRouter.shared.syncRegisteredKeysIfNeeded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
|
func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
|
||||||
// Watch may request a sync without needing a reply (fire-and-forget message).
|
|
||||||
if let request = message[WatchSyncMessageKeys.requestSync] as? Bool, request {
|
if let request = message[WatchSyncMessageKeys.requestSync] as? Bool, request {
|
||||||
Logger.debug("iOS received watch sync request")
|
Logger.debug("iOS received watch sync request")
|
||||||
Task {
|
Task {
|
||||||
@ -81,7 +74,6 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate {
|
|||||||
didReceiveMessage message: [String: Any],
|
didReceiveMessage message: [String: Any],
|
||||||
replyHandler: @escaping ([String: Any]) -> Void
|
replyHandler: @escaping ([String: Any]) -> Void
|
||||||
) {
|
) {
|
||||||
// Reply-based handshake: watch expects a payload immediately if reachable.
|
|
||||||
if let request = message[WatchSyncMessageKeys.requestSync] as? Bool, request {
|
if let request = message[WatchSyncMessageKeys.requestSync] as? Bool, request {
|
||||||
Logger.debug("iOS received watch sync request (reply)")
|
Logger.debug("iOS received watch sync request (reply)")
|
||||||
Task {
|
Task {
|
||||||
@ -92,7 +84,6 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to build a snapshot payload, retrying briefly in case keys are still initializing.
|
|
||||||
private func buildSyncReplyPayload() async -> [String: Any] {
|
private func buildSyncReplyPayload() async -> [String: Any] {
|
||||||
let maxAttempts = 3
|
let maxAttempts = 3
|
||||||
for attempt in 1...maxAttempts {
|
for attempt in 1...maxAttempts {
|
||||||
@ -113,7 +104,6 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any]) {
|
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any]) {
|
||||||
// transferUserInfo can deliver requests when the watch was previously unreachable.
|
|
||||||
if let request = userInfo[WatchSyncMessageKeys.requestSync] as? Bool, request {
|
if let request = userInfo[WatchSyncMessageKeys.requestSync] as? Bool, request {
|
||||||
Logger.debug("iOS received queued watch sync request")
|
Logger.debug("iOS received queued watch sync request")
|
||||||
Task {
|
Task {
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import LocalData
|
import LocalData
|
||||||
import SharedKit
|
import SharedKit
|
||||||
|
|
||||||
/// App Group UserDefaults key used to demonstrate shared preferences between targets.
|
|
||||||
extension StorageKey where Value == String {
|
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
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import LocalData
|
import LocalData
|
||||||
import SharedKit
|
import SharedKit
|
||||||
|
|
||||||
/// App Group file key for sharing a user profile across app targets.
|
|
||||||
extension StorageKey where Value == UserProfile {
|
extension StorageKey where Value == UserProfile {
|
||||||
/// 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
|
||||||
@ -22,9 +21,6 @@ extension StorageKey where Value == UserProfile {
|
|||||||
syncPolicy: .never
|
syncPolicy: .never
|
||||||
)
|
)
|
||||||
|
|
||||||
/// Creates a version of the key for a different App Group directory.
|
|
||||||
/// Sample-only: production apps should avoid dynamic App Group configuration and
|
|
||||||
/// migrate when storage settings change.
|
|
||||||
nonisolated static func appGroupUserProfileKey(
|
nonisolated static func appGroupUserProfileKey(
|
||||||
directory: FileDirectory = .documents
|
directory: FileDirectory = .documents
|
||||||
) -> StorageKey {
|
) -> StorageKey {
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import LocalData
|
import LocalData
|
||||||
import SharedKit
|
import SharedKit
|
||||||
|
|
||||||
/// App Group UserDefaults key for a generic preferences dictionary.
|
|
||||||
extension StorageKey where Value == [String: AnyCodable] {
|
extension StorageKey where Value == [String: AnyCodable] {
|
||||||
/// Stores user preferences in App Group UserDefaults.
|
/// Stores user preferences in App Group UserDefaults.
|
||||||
/// - Domain: App Group UserDefaults
|
/// - Domain: App Group UserDefaults
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import LocalData
|
import LocalData
|
||||||
|
|
||||||
/// Encrypted file system key that pulls key material from a registered provider.
|
|
||||||
extension StorageKey where Value == [String] {
|
extension StorageKey where Value == [String] {
|
||||||
/// Stores session logs with encryption using external key material.
|
/// Stores session logs with encryption using external key material.
|
||||||
nonisolated static let externalSessionLogs = StorageKey(
|
nonisolated static let externalSessionLogs = StorageKey(
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import LocalData
|
import LocalData
|
||||||
|
|
||||||
/// Encrypted file system key for long-lived private notes in Documents.
|
|
||||||
extension StorageKey where Value == String {
|
extension StorageKey where Value == String {
|
||||||
/// Stores private notes with encryption.
|
/// Stores private notes with encryption.
|
||||||
nonisolated static let privateNotes = StorageKey(
|
nonisolated static let privateNotes = StorageKey(
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import LocalData
|
import LocalData
|
||||||
|
|
||||||
/// Encrypted file system key for log data with configurable key derivation.
|
|
||||||
extension StorageKey where Value == [String] {
|
extension StorageKey where Value == [String] {
|
||||||
/// Stores session logs with full encryption.
|
/// Stores session logs with full encryption.
|
||||||
/// Configurable PBKDF2 iterations.
|
/// Configurable PBKDF2 iterations.
|
||||||
@ -16,9 +15,6 @@ extension StorageKey where Value == [String] {
|
|||||||
syncPolicy: .never
|
syncPolicy: .never
|
||||||
)
|
)
|
||||||
|
|
||||||
/// Builds a variant with a custom PBKDF2 iteration count for demo purposes.
|
|
||||||
/// Sample-only: production apps should treat encryption parameters as fixed and
|
|
||||||
/// perform a migration if they must change.
|
|
||||||
nonisolated static func sessionLogsKey(iterations: Int) -> StorageKey {
|
nonisolated static func sessionLogsKey(iterations: Int) -> StorageKey {
|
||||||
StorageKey(
|
StorageKey(
|
||||||
name: "session_logs.json",
|
name: "session_logs.json",
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import LocalData
|
import LocalData
|
||||||
|
|
||||||
/// File system key for non-critical cache data stored in Caches.
|
|
||||||
extension StorageKey where Value == Data {
|
extension StorageKey where Value == Data {
|
||||||
/// Stores cached data files.
|
/// Stores cached data files.
|
||||||
nonisolated static let cachedData = StorageKey(
|
nonisolated static let cachedData = StorageKey(
|
||||||
|
|||||||
@ -0,0 +1,27 @@
|
|||||||
|
import Foundation
|
||||||
|
import LocalData
|
||||||
|
|
||||||
|
extension StorageKey where Value == String {
|
||||||
|
/// Example using custom serializer for specialized encoding.
|
||||||
|
nonisolated static let customEncoded = StorageKey(
|
||||||
|
name: "custom_encoded",
|
||||||
|
domain: .fileSystem(directory: .documents),
|
||||||
|
security: .none,
|
||||||
|
serializer: .custom(
|
||||||
|
encode: { value in
|
||||||
|
Data(value.utf8).base64EncodedData()
|
||||||
|
},
|
||||||
|
decode: { data in
|
||||||
|
guard let decoded = Data(base64Encoded: data),
|
||||||
|
let string = String(data: decoded, encoding: .utf8) else {
|
||||||
|
throw StorageError.deserializationFailed
|
||||||
|
}
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
),
|
||||||
|
owner: "SampleApp",
|
||||||
|
description: "Stores custom-encoded string data (Base64 example).",
|
||||||
|
availability: .all,
|
||||||
|
syncPolicy: .never
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import LocalData
|
import LocalData
|
||||||
|
|
||||||
/// File system key that persists a property list export.
|
|
||||||
extension StorageKey where Value == [String: AnyCodable] {
|
extension StorageKey where Value == [String: AnyCodable] {
|
||||||
/// Stores settings as property list.
|
/// Stores settings as property list.
|
||||||
nonisolated static let settingsPlist = StorageKey(
|
nonisolated static let settingsPlist = StorageKey(
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import LocalData
|
import LocalData
|
||||||
import SharedKit
|
import SharedKit
|
||||||
|
|
||||||
/// File system key for the shared UserProfile payload.
|
|
||||||
extension StorageKey where Value == UserProfile {
|
extension StorageKey where Value == UserProfile {
|
||||||
/// Stores user profile as JSON file in documents.
|
/// Stores user profile as JSON file in documents.
|
||||||
nonisolated static let userProfileFile = StorageKey(
|
nonisolated static let userProfileFile = StorageKey(
|
||||||
@ -16,9 +15,6 @@ extension StorageKey where Value == UserProfile {
|
|||||||
syncPolicy: .automaticSmall
|
syncPolicy: .automaticSmall
|
||||||
)
|
)
|
||||||
|
|
||||||
/// Builds a profile key for an alternate file directory.
|
|
||||||
/// Sample-only: production apps should keep directory decisions static; if the
|
|
||||||
/// storage domain changes, migrate instead of toggling the key at runtime.
|
|
||||||
nonisolated static func userProfileFileKey(directory: FileDirectory = .documents) -> StorageKey {
|
nonisolated static func userProfileFileKey(directory: FileDirectory = .documents) -> StorageKey {
|
||||||
StorageKey(
|
StorageKey(
|
||||||
name: UserProfile.storageKeyName,
|
name: UserProfile.storageKeyName,
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import LocalData
|
import LocalData
|
||||||
import SharedKit
|
import SharedKit
|
||||||
|
|
||||||
/// Keychain key for short-lived API tokens that should not sync off-device.
|
|
||||||
extension StorageKey where Value == String {
|
extension StorageKey where Value == String {
|
||||||
/// Stores API token in keychain.
|
/// Stores API token in keychain.
|
||||||
nonisolated static let apiToken = StorageKey(
|
nonisolated static let apiToken = StorageKey(
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import LocalData
|
import LocalData
|
||||||
import SharedKit
|
import SharedKit
|
||||||
|
|
||||||
/// Keychain key used for credentials with optional access control variations.
|
|
||||||
extension StorageKey where Value == Credential {
|
extension StorageKey where Value == Credential {
|
||||||
/// Stores user credentials securely in keychain.
|
/// Stores user credentials securely in keychain.
|
||||||
/// Configurable accessibility and access control.
|
/// Configurable accessibility and access control.
|
||||||
@ -17,9 +16,6 @@ extension StorageKey where Value == Credential {
|
|||||||
syncPolicy: .never
|
syncPolicy: .never
|
||||||
)
|
)
|
||||||
|
|
||||||
/// Builds a key with custom Keychain accessibility or access control options.
|
|
||||||
/// Sample-only: production apps should not allow dynamic key configuration; treat
|
|
||||||
/// StorageKey settings as fixed and migrate if security policies change.
|
|
||||||
nonisolated static func credentialsKey(
|
nonisolated static func credentialsKey(
|
||||||
accessibility: KeychainAccessibility = .afterFirstUnlock,
|
accessibility: KeychainAccessibility = .afterFirstUnlock,
|
||||||
accessControl: KeychainAccessControl? = nil
|
accessControl: KeychainAccessControl? = nil
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import LocalData
|
import LocalData
|
||||||
import SharedKit
|
import SharedKit
|
||||||
|
|
||||||
/// Keychain key for externally provided key material used in encryption demos.
|
|
||||||
extension StorageKey where Value == Data {
|
extension StorageKey where Value == Data {
|
||||||
/// Stores external key material used for encryption policies.
|
/// Stores external key material used for encryption policies.
|
||||||
nonisolated static let externalKeyMaterial = StorageKey(
|
nonisolated static let externalKeyMaterial = StorageKey(
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import Foundation
|
|||||||
import LocalData
|
import LocalData
|
||||||
import SharedKit
|
import SharedKit
|
||||||
|
|
||||||
/// Keychain key for sensitive location data with user presence required.
|
|
||||||
extension StorageKey where Value == SampleLocationData {
|
extension StorageKey where Value == SampleLocationData {
|
||||||
/// Stores sensitive location data in keychain with biometric protection.
|
/// Stores sensitive location data in keychain with biometric protection.
|
||||||
nonisolated static let lastLocation = StorageKey(
|
nonisolated static let lastLocation = StorageKey(
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import LocalData
|
import LocalData
|
||||||
|
|
||||||
/// Legacy notification toggle stored in UserDefaults.
|
|
||||||
extension StorageKey where Value == Bool {
|
extension StorageKey where Value == Bool {
|
||||||
nonisolated static let legacyNotificationSetting = StorageKey(
|
nonisolated static let legacyNotificationSetting = StorageKey(
|
||||||
name: "legacy_notification_setting",
|
name: "legacy_notification_setting",
|
||||||
@ -15,7 +14,6 @@ extension StorageKey where Value == Bool {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Legacy theme preference stored separately from notifications.
|
|
||||||
extension StorageKey where Value == String {
|
extension StorageKey where Value == String {
|
||||||
nonisolated static let legacyThemeSetting = StorageKey(
|
nonisolated static let legacyThemeSetting = StorageKey(
|
||||||
name: "legacy_theme_setting",
|
name: "legacy_theme_setting",
|
||||||
@ -29,7 +27,6 @@ extension StorageKey where Value == String {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Modern unified settings created by aggregating multiple legacy keys.
|
|
||||||
extension StorageKey where Value == UnifiedSettings {
|
extension StorageKey where Value == UnifiedSettings {
|
||||||
|
|
||||||
nonisolated static let modernUnifiedSettings = StorageKey(
|
nonisolated static let modernUnifiedSettings = StorageKey(
|
||||||
@ -51,7 +48,6 @@ extension StorageKey where Value == UnifiedSettings {
|
|||||||
destinationKey: key,
|
destinationKey: key,
|
||||||
sourceKeys: sources
|
sourceKeys: sources
|
||||||
) { sources in
|
) { sources in
|
||||||
// Merge legacy values into a single strongly-typed settings model.
|
|
||||||
var notificationsEnabled = false
|
var notificationsEnabled = false
|
||||||
var theme = "system"
|
var theme = "system"
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import LocalData
|
import LocalData
|
||||||
import SharedKit
|
|
||||||
|
|
||||||
/// Keys that demonstrate conditional migrations based on app version.
|
|
||||||
extension StorageKey where Value == String {
|
extension StorageKey where Value == String {
|
||||||
nonisolated static let legacyAppMode = StorageKey(
|
nonisolated static let legacyAppMode = StorageKey(
|
||||||
name: "legacy_app_mode",
|
name: "legacy_app_mode",
|
||||||
@ -17,7 +15,7 @@ extension StorageKey where Value == String {
|
|||||||
|
|
||||||
nonisolated static let modernAppMode = StorageKey(
|
nonisolated static let modernAppMode = StorageKey(
|
||||||
name: "modern_app_mode",
|
name: "modern_app_mode",
|
||||||
domain: .keychain(service: StorageServiceIdentifiers.keychainLocation),
|
domain: .keychain(service: "com.mbrucedogs.securestorage"),
|
||||||
security: .keychain(
|
security: .keychain(
|
||||||
accessibility: .afterFirstUnlock,
|
accessibility: .afterFirstUnlock,
|
||||||
accessControl: .userPresence
|
accessControl: .userPresence
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import LocalData
|
import LocalData
|
||||||
import SharedKit
|
|
||||||
|
|
||||||
/// Keys that demonstrate a simple legacy-to-modern migration path.
|
|
||||||
extension StorageKey where Value == String {
|
extension StorageKey where Value == String {
|
||||||
/// The legacy key where data starts (in UserDefaults).
|
/// The legacy key where data starts (in UserDefaults)
|
||||||
nonisolated static let legacyMigrationSource = StorageKey(
|
nonisolated static let legacyMigrationSource = StorageKey(
|
||||||
name: "legacy_user_id",
|
name: "legacy_user_id",
|
||||||
domain: .userDefaults(suite: nil),
|
domain: .userDefaults(suite: nil),
|
||||||
@ -16,10 +14,10 @@ extension StorageKey where Value == String {
|
|||||||
syncPolicy: .never
|
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(
|
nonisolated static let modernMigrationDestination = StorageKey(
|
||||||
name: "secure_user_id",
|
name: "secure_user_id",
|
||||||
domain: .keychain(service: StorageServiceIdentifiers.keychainLocation),
|
domain: .keychain(service: "com.mbrucedogs.securestorage"),
|
||||||
security: .keychain(
|
security: .keychain(
|
||||||
accessibility: .afterFirstUnlock,
|
accessibility: .afterFirstUnlock,
|
||||||
accessControl: .userPresence
|
accessControl: .userPresence
|
||||||
@ -30,7 +28,6 @@ extension StorageKey where Value == String {
|
|||||||
availability: .all,
|
availability: .all,
|
||||||
syncPolicy: .never,
|
syncPolicy: .never,
|
||||||
migration: { key in
|
migration: { key in
|
||||||
// Simple "move on first read" migration for legacy identifiers.
|
|
||||||
AnyStorageMigration(
|
AnyStorageMigration(
|
||||||
SimpleLegacyMigration(
|
SimpleLegacyMigration(
|
||||||
destinationKey: key,
|
destinationKey: key,
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import LocalData
|
import LocalData
|
||||||
import SharedKit
|
|
||||||
|
|
||||||
/// Legacy string payload that will be transformed into a structured model.
|
|
||||||
extension StorageKey where Value == String {
|
extension StorageKey where Value == String {
|
||||||
nonisolated static let legacyProfileName = StorageKey(
|
nonisolated static let legacyProfileName = StorageKey(
|
||||||
name: "legacy_profile_name",
|
name: "legacy_profile_name",
|
||||||
@ -16,11 +14,10 @@ extension StorageKey where Value == String {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Modern structured payload produced by transforming migration.
|
|
||||||
extension StorageKey where Value == ProfileName {
|
extension StorageKey where Value == ProfileName {
|
||||||
nonisolated static let modernProfileName = StorageKey(
|
nonisolated static let modernProfileName = StorageKey(
|
||||||
name: "modern_profile_name",
|
name: "modern_profile_name",
|
||||||
domain: .keychain(service: StorageServiceIdentifiers.keychainLocation),
|
domain: .keychain(service: "com.mbrucedogs.securestorage"),
|
||||||
security: .keychain(
|
security: .keychain(
|
||||||
accessibility: .afterFirstUnlock,
|
accessibility: .afterFirstUnlock,
|
||||||
accessControl: .userPresence
|
accessControl: .userPresence
|
||||||
@ -37,7 +34,6 @@ extension StorageKey where Value == ProfileName {
|
|||||||
destinationKey: key,
|
destinationKey: key,
|
||||||
sourceKey: sourceKey
|
sourceKey: sourceKey
|
||||||
) { value in
|
) { value in
|
||||||
// Split "First Last" into a structured name for safer usage.
|
|
||||||
let parts = value.split(separator: " ", maxSplits: 1).map(String.init)
|
let parts = value.split(separator: " ", maxSplits: 1).map(String.init)
|
||||||
let firstName = parts.first ?? ""
|
let firstName = parts.first ?? ""
|
||||||
let lastName = parts.count > 1 ? parts[1] : ""
|
let lastName = parts.count > 1 ? parts[1] : ""
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import LocalData
|
import LocalData
|
||||||
|
|
||||||
/// UserDefaults key that highlights platform availability and sync policy options.
|
|
||||||
extension StorageKey where Value == String {
|
extension StorageKey where Value == String {
|
||||||
/// 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.
|
||||||
@ -16,9 +15,6 @@ extension StorageKey where Value == String {
|
|||||||
syncPolicy: .never
|
syncPolicy: .never
|
||||||
)
|
)
|
||||||
|
|
||||||
/// Builds a variant to demonstrate different availability and sync policies.
|
|
||||||
/// Sample-only: production apps should keep availability and sync policies static;
|
|
||||||
/// if these change, migrate the data rather than altering the key at runtime.
|
|
||||||
nonisolated static func syncableSettingKey(
|
nonisolated static func syncableSettingKey(
|
||||||
availability: PlatformAvailability = .all,
|
availability: PlatformAvailability = .all,
|
||||||
syncPolicy: SyncPolicy = .never
|
syncPolicy: SyncPolicy = .never
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import LocalData
|
import LocalData
|
||||||
|
|
||||||
/// UserDefaults key for watch-only preferences, used to demonstrate availability constraints.
|
|
||||||
extension StorageKey where Value == Bool {
|
extension StorageKey where Value == Bool {
|
||||||
/// 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.
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import LocalData
|
import LocalData
|
||||||
|
|
||||||
/// UserDefaults key used in the demo and as a lightweight migration flag.
|
|
||||||
extension StorageKey where Value == String {
|
extension StorageKey where Value == String {
|
||||||
/// Stores the app version in standard UserDefaults.
|
/// Stores the app version in standard UserDefaults.
|
||||||
/// - Domain: UserDefaults (standard)
|
/// - Domain: UserDefaults (standard)
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import SwiftUI
|
|||||||
import LocalData
|
import LocalData
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
/// Demonstrates aggregating multiple legacy keys into a UnifiedSettings model.
|
|
||||||
struct AggregatingMigrationDemo: View {
|
struct AggregatingMigrationDemo: View {
|
||||||
@State private var notificationsEnabled = false
|
@State private var notificationsEnabled = false
|
||||||
@State private var theme = ""
|
@State private var theme = ""
|
||||||
@ -59,7 +58,6 @@ struct AggregatingMigrationDemo: View {
|
|||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Seeds the legacy keys to simulate pre-migration settings.
|
|
||||||
private func saveToLegacy() {
|
private func saveToLegacy() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
@ -80,7 +78,6 @@ struct AggregatingMigrationDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads the modern unified key, triggering the aggregation migration.
|
|
||||||
private func loadFromModern() {
|
private func loadFromModern() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
statusMessage = "Loading unified settings..."
|
statusMessage = "Loading unified settings..."
|
||||||
@ -98,7 +95,6 @@ struct AggregatingMigrationDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Formats the aggregated settings for display.
|
|
||||||
private func format(_ value: UnifiedSettings) -> String {
|
private func format(_ value: UnifiedSettings) -> String {
|
||||||
let notificationsText = value.notificationsEnabled ? "On" : "Off"
|
let notificationsText = value.notificationsEnabled ? "On" : "Off"
|
||||||
let themeText = value.theme.isEmpty ? "system" : value.theme
|
let themeText = value.theme.isEmpty ? "system" : value.theme
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import SwiftUI
|
|||||||
import LocalData
|
import LocalData
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
/// Demonstrates a migration that only runs when app version criteria are met.
|
|
||||||
struct ConditionalMigrationDemo: View {
|
struct ConditionalMigrationDemo: View {
|
||||||
@State private var legacyValue = ""
|
@State private var legacyValue = ""
|
||||||
@State private var modernValue = ""
|
@State private var modernValue = ""
|
||||||
@ -60,7 +59,6 @@ struct ConditionalMigrationDemo: View {
|
|||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Seeds legacy data that will migrate when the version condition passes.
|
|
||||||
private func saveToLegacy() {
|
private func saveToLegacy() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
@ -75,7 +73,6 @@ struct ConditionalMigrationDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads the modern key to trigger the conditional migration.
|
|
||||||
private func loadFromModern() {
|
private func loadFromModern() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
statusMessage = "Loading modern mode..."
|
statusMessage = "Loading modern mode..."
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import SwiftUI
|
|||||||
import LocalData
|
import LocalData
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
/// Demonstrates encrypted file storage with PBKDF2 and external key material options.
|
|
||||||
struct EncryptedStorageDemo: View {
|
struct EncryptedStorageDemo: View {
|
||||||
@State private var logEntry = ""
|
@State private var logEntry = ""
|
||||||
@State private var storedLogs: [String] = []
|
@State private var storedLogs: [String] = []
|
||||||
@ -128,7 +127,6 @@ struct EncryptedStorageDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Appends an encrypted log entry using the selected key derivation mode.
|
|
||||||
private func addLogEntry() {
|
private func addLogEntry() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
@ -154,7 +152,6 @@ struct EncryptedStorageDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decrypts and loads log entries into memory for display.
|
|
||||||
private func loadLogs() {
|
private func loadLogs() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
@ -177,7 +174,6 @@ struct EncryptedStorageDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clears the encrypted log file for a clean slate.
|
|
||||||
private func clearLogs() {
|
private func clearLogs() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
@ -198,7 +194,6 @@ struct EncryptedStorageDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads existing logs, appends the new entry, and returns the updated payload.
|
|
||||||
private func updatedLogs(for key: StorageKey<[String]>) async throws -> [String] {
|
private func updatedLogs(for key: StorageKey<[String]>) async throws -> [String] {
|
||||||
var logs: [String]
|
var logs: [String]
|
||||||
do {
|
do {
|
||||||
@ -219,7 +214,6 @@ struct EncryptedStorageDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Visual reminder that PBKDF2 iterations must remain stable to decrypt existing data.
|
|
||||||
private struct IterationWarningView: View {
|
private struct IterationWarningView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Text("PBKDF2 iterations must match the value used during encryption. Changing this after saving will prevent decryption.")
|
Text("PBKDF2 iterations must match the value used during encryption. Changing this after saving will prevent decryption.")
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import LocalData
|
|||||||
import SharedKit
|
import SharedKit
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
/// Demonstrates file storage in Documents/Caches plus App Group file sharing.
|
|
||||||
struct FileSystemDemo: View {
|
struct FileSystemDemo: View {
|
||||||
@State private var profileName = ""
|
@State private var profileName = ""
|
||||||
@State private var profileEmail = ""
|
@State private var profileEmail = ""
|
||||||
@ -169,7 +168,6 @@ struct FileSystemDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Saves a UserProfile to the selected file system directory.
|
|
||||||
private func saveProfile() {
|
private func saveProfile() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
@ -190,7 +188,6 @@ struct FileSystemDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads a UserProfile from the selected directory and updates the form.
|
|
||||||
private func loadProfile() {
|
private func loadProfile() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
@ -215,7 +212,6 @@ struct FileSystemDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes the profile file from the selected directory.
|
|
||||||
private func deleteProfile() {
|
private func deleteProfile() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
@ -231,7 +227,6 @@ struct FileSystemDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Persists a UserProfile into the App Group container.
|
|
||||||
private func saveAppGroupProfile() {
|
private func saveAppGroupProfile() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
@ -252,7 +247,6 @@ struct FileSystemDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads a UserProfile from the App Group container.
|
|
||||||
private func loadAppGroupProfile() {
|
private func loadAppGroupProfile() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
@ -277,7 +271,6 @@ struct FileSystemDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes the App Group profile file to demonstrate shared cleanup.
|
|
||||||
private func deleteAppGroupProfile() {
|
private func deleteAppGroupProfile() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import SwiftUI
|
|||||||
import LocalData
|
import LocalData
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
/// Demonstrates Keychain storage with configurable accessibility and access control.
|
|
||||||
struct KeychainDemo: View {
|
struct KeychainDemo: View {
|
||||||
@State private var username = ""
|
@State private var username = ""
|
||||||
@State private var password = ""
|
@State private var password = ""
|
||||||
@ -114,7 +113,6 @@ struct KeychainDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stores credentials using the currently selected Keychain policies.
|
|
||||||
private func saveCredentials() {
|
private func saveCredentials() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
@ -133,7 +131,6 @@ struct KeychainDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads credentials using the currently selected Keychain policies.
|
|
||||||
private func loadCredentials() {
|
private func loadCredentials() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
@ -160,7 +157,6 @@ struct KeychainDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes the stored credentials from the Keychain.
|
|
||||||
private func deleteCredentials() {
|
private func deleteCredentials() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import SwiftUI
|
|||||||
import LocalData
|
import LocalData
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
/// Demonstrates a simple legacy key migration from UserDefaults to Keychain.
|
|
||||||
struct MigrationDemo: View {
|
struct MigrationDemo: View {
|
||||||
@State private var legacyValue = ""
|
@State private var legacyValue = ""
|
||||||
@State private var modernValue = ""
|
@State private var modernValue = ""
|
||||||
@ -84,7 +83,6 @@ struct MigrationDemo: View {
|
|||||||
|
|
||||||
// MARK: - Actions
|
// MARK: - Actions
|
||||||
|
|
||||||
/// Seeds the legacy key to simulate pre-migration data.
|
|
||||||
private func saveToLegacy() {
|
private func saveToLegacy() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
@ -99,7 +97,6 @@ struct MigrationDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads the modern key, triggering the configured migration if needed.
|
|
||||||
private func loadFromModern() {
|
private func loadFromModern() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
statusMessage = "Retrieving from Modern..."
|
statusMessage = "Retrieving from Modern..."
|
||||||
@ -118,7 +115,6 @@ struct MigrationDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Forces a migration sweep to drain legacy data even if the modern key exists.
|
|
||||||
private func runManualMigration() {
|
private func runManualMigration() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
statusMessage = "Running manual migration..."
|
statusMessage = "Running manual migration..."
|
||||||
@ -136,7 +132,6 @@ struct MigrationDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks whether the legacy key still exists after migration.
|
|
||||||
private func checkLegacyExists() {
|
private func checkLegacyExists() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
/// Entry point for the migration demos, grouped by strategy.
|
|
||||||
struct MigrationHubView: View {
|
struct MigrationHubView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import LocalData
|
|||||||
import WatchConnectivity
|
import WatchConnectivity
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
/// Demonstrates platform availability and Watch sync policies for a single key.
|
|
||||||
struct PlatformSyncDemo: View {
|
struct PlatformSyncDemo: View {
|
||||||
@State private var settingValue = ""
|
@State private var settingValue = ""
|
||||||
@State private var storedValue = ""
|
@State private var storedValue = ""
|
||||||
@ -138,7 +137,6 @@ struct PlatformSyncDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Saves the value using the current availability and sync policy selection.
|
|
||||||
private func saveValue() {
|
private func saveValue() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
@ -159,7 +157,6 @@ struct PlatformSyncDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads the value using the current availability and sync policy selection.
|
|
||||||
private func loadValue() {
|
private func loadValue() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
@ -183,7 +180,6 @@ struct PlatformSyncDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts a read to show the platform access error when applicable.
|
|
||||||
private func testPlatformError() {
|
private func testPlatformError() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
@ -206,7 +202,6 @@ struct PlatformSyncDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Refreshes the WCSession status shown in the UI.
|
|
||||||
private func refreshWatchStatus() {
|
private func refreshWatchStatus() {
|
||||||
watchStatus = WatchStatus.current()
|
watchStatus = WatchStatus.current()
|
||||||
}
|
}
|
||||||
@ -214,7 +209,6 @@ struct PlatformSyncDemo: View {
|
|||||||
|
|
||||||
// MARK: - Display Names
|
// MARK: - Display Names
|
||||||
|
|
||||||
/// UI-friendly names for platform availability options.
|
|
||||||
extension PlatformAvailability {
|
extension PlatformAvailability {
|
||||||
var displayName: String {
|
var displayName: String {
|
||||||
switch self {
|
switch self {
|
||||||
@ -226,7 +220,6 @@ extension PlatformAvailability {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// UI-friendly names for sync policies.
|
|
||||||
extension SyncPolicy {
|
extension SyncPolicy {
|
||||||
var displayName: String {
|
var displayName: String {
|
||||||
switch self {
|
switch self {
|
||||||
@ -243,7 +236,6 @@ extension SyncPolicy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Snapshot of current WatchConnectivity state for display purposes.
|
|
||||||
private struct WatchStatus: Equatable {
|
private struct WatchStatus: Equatable {
|
||||||
let isSupported: Bool
|
let isSupported: Bool
|
||||||
let isPaired: Bool
|
let isPaired: Bool
|
||||||
@ -281,7 +273,6 @@ private struct WatchStatus: Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read-only UI for displaying WatchConnectivity state.
|
|
||||||
private struct WatchStatusView: View {
|
private struct WatchStatusView: View {
|
||||||
let status: WatchStatus
|
let status: WatchStatus
|
||||||
|
|
||||||
@ -314,7 +305,6 @@ private struct WatchStatusView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Explanatory text for each platform availability option.
|
|
||||||
private struct PlatformAvailabilityDescriptionView: View {
|
private struct PlatformAvailabilityDescriptionView: View {
|
||||||
let availability: PlatformAvailability
|
let availability: PlatformAvailability
|
||||||
|
|
||||||
@ -338,7 +328,6 @@ private struct PlatformAvailabilityDescriptionView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Explanatory text for each sync policy option.
|
|
||||||
private struct SyncPolicyDescriptionView: View {
|
private struct SyncPolicyDescriptionView: View {
|
||||||
let syncPolicy: SyncPolicy
|
let syncPolicy: SyncPolicy
|
||||||
|
|
||||||
@ -360,7 +349,6 @@ private struct SyncPolicyDescriptionView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Predicts the expected behavior given the selected availability and sync policy.
|
|
||||||
private struct ExpectedOutcomeView: View {
|
private struct ExpectedOutcomeView: View {
|
||||||
let availability: PlatformAvailability
|
let availability: PlatformAvailability
|
||||||
let syncPolicy: SyncPolicy
|
let syncPolicy: SyncPolicy
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import SwiftUI
|
|||||||
import LocalData
|
import LocalData
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
/// Demonstrates transforming a legacy string into a structured ProfileName.
|
|
||||||
struct TransformingMigrationDemo: View {
|
struct TransformingMigrationDemo: View {
|
||||||
@State private var legacyValue = ""
|
@State private var legacyValue = ""
|
||||||
@State private var modernValue = ""
|
@State private var modernValue = ""
|
||||||
@ -60,7 +59,6 @@ struct TransformingMigrationDemo: View {
|
|||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Seeds the legacy string value to simulate pre-migration data.
|
|
||||||
private func saveToLegacy() {
|
private func saveToLegacy() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
@ -75,7 +73,6 @@ struct TransformingMigrationDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads the modern key, triggering the transforming migration.
|
|
||||||
private func loadFromModern() {
|
private func loadFromModern() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
statusMessage = "Loading modern profile..."
|
statusMessage = "Loading modern profile..."
|
||||||
@ -94,7 +91,6 @@ struct TransformingMigrationDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Formats the migrated name for display in the demo UI.
|
|
||||||
private func format(_ value: ProfileName) -> String {
|
private func format(_ value: ProfileName) -> String {
|
||||||
let trimmedFirst = value.firstName.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmedFirst = value.firstName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let trimmedLast = value.lastName.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmedLast = value.lastName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import SwiftUI
|
|||||||
import LocalData
|
import LocalData
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
/// Demonstrates typed UserDefaults keys, including App Group sharing.
|
|
||||||
struct UserDefaultsDemo: View {
|
struct UserDefaultsDemo: View {
|
||||||
@State private var inputText = ""
|
@State private var inputText = ""
|
||||||
@State private var storedValue = ""
|
@State private var storedValue = ""
|
||||||
@ -137,7 +136,6 @@ struct UserDefaultsDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Persists the current input value with the standard UserDefaults key.
|
|
||||||
private func saveValue() {
|
private func saveValue() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
@ -152,7 +150,6 @@ struct UserDefaultsDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads the standard UserDefaults value and reflects it in the UI.
|
|
||||||
private func loadValue() {
|
private func loadValue() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
@ -172,7 +169,6 @@ struct UserDefaultsDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes the standard UserDefaults value to reset the demo.
|
|
||||||
private func removeValue() {
|
private func removeValue() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
@ -188,7 +184,6 @@ struct UserDefaultsDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Saves the input value into the App Group UserDefaults container.
|
|
||||||
private func saveAppGroupValue() {
|
private func saveAppGroupValue() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
@ -203,7 +198,6 @@ struct UserDefaultsDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads the App Group value to demonstrate cross-target sharing.
|
|
||||||
private func loadAppGroupValue() {
|
private func loadAppGroupValue() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
@ -223,7 +217,6 @@ struct UserDefaultsDemo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clears the App Group value to demonstrate cleanup behavior.
|
|
||||||
private func removeAppGroupValue() {
|
private func removeAppGroupValue() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
|
|||||||
@ -1,24 +1,22 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Example shared models that demonstrate creating Watch-optimized payloads.
|
// Example shared models for Watch-optimized data
|
||||||
/// The goal is to minimize what needs to traverse WatchConnectivity.
|
|
||||||
struct Workout: Codable {
|
struct Workout: Codable {
|
||||||
let date: Date
|
let date: Date
|
||||||
// Add other properties as needed
|
// Add other properties as needed
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Preferences included in the full profile; used to derive a slimmer watch payload.
|
|
||||||
struct Preferences: Codable {
|
struct Preferences: Codable {
|
||||||
let isPremium: Bool
|
let isPremium: Bool
|
||||||
let appearance: Appearance
|
let appearance: Appearance
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Display preferences for the demo data.
|
|
||||||
enum Appearance: Codable {
|
enum Appearance: Codable {
|
||||||
case light, dark
|
case light, dark
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shared full-fidelity profile used on iOS, with a computed watch-friendly version.
|
// Shared model (iOS + watchOS)
|
||||||
struct FullUserProfile: Codable {
|
struct FullUserProfile: Codable {
|
||||||
let id: UUID
|
let id: UUID
|
||||||
let fullName: String
|
let fullName: String
|
||||||
@ -27,7 +25,7 @@ struct FullUserProfile: Codable {
|
|||||||
let allWorkouts: [Workout] // ← potentially huge
|
let allWorkouts: [Workout] // ← potentially huge
|
||||||
let preferences: Preferences
|
let preferences: Preferences
|
||||||
|
|
||||||
/// Lightweight Watch version computed from the full profile.
|
// Lightweight Watch version — computed
|
||||||
var watchVersion: WatchUserProfile {
|
var watchVersion: WatchUserProfile {
|
||||||
WatchUserProfile(
|
WatchUserProfile(
|
||||||
id: id,
|
id: id,
|
||||||
@ -41,7 +39,6 @@ struct FullUserProfile: Codable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Slimmed-down payload intended for watch display only.
|
|
||||||
struct WatchUserProfile: Codable, Sendable {
|
struct WatchUserProfile: Codable, Sendable {
|
||||||
let id: UUID
|
let id: UUID
|
||||||
let displayName: String
|
let displayName: String
|
||||||
@ -57,7 +54,7 @@ struct WatchUserProfile: Codable, Sendable {
|
|||||||
// let watchData = try JSONEncoder().encode(profile.watchVersion)
|
// let watchData = try JSONEncoder().encode(profile.watchVersion)
|
||||||
// WCSession.default.updateApplicationContext(["profile": watchData])
|
// WCSession.default.updateApplicationContext(["profile": watchData])
|
||||||
|
|
||||||
/// Optional protocol to standardize access to a watch-friendly payload.
|
// Alternative: WatchRepresentable protocol
|
||||||
public protocol WatchRepresentable {
|
public protocol WatchRepresentable {
|
||||||
associatedtype WatchVersion: Codable, Sendable
|
associatedtype WatchVersion: Codable, Sendable
|
||||||
var watchVersion: WatchVersion { get }
|
var watchVersion: WatchVersion { get }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user