From 0dd3ddcbae4e67367cb65243f6760637ff54140f Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 13 Jan 2026 21:20:49 -0600 Subject: [PATCH] Add SampleLocationData, StorageKeys, AppVersionKey, Value (+10 more) --- .gitignore | 6 ++ .../project.pbxproj | 46 +++++++++++++ SecureStorgageSample/ContentView.swift | 34 ++++++++- SecureStorgageSample/StorageKeys.swift | 69 +++++++++++++++++++ SecureStorgageSample/WatchOptimized.swift | 61 ++++++++++++++++ 5 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 SecureStorgageSample/StorageKeys.swift create mode 100644 SecureStorgageSample/WatchOptimized.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a6510b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +.build/ +.swiftpm/ +DerivedData/ +*.xcodeproj/project.xcworkspace/xcuserdata +*.xcodeproj/xcuserdata diff --git a/SecureStorgageSample.xcodeproj/project.pbxproj b/SecureStorgageSample.xcodeproj/project.pbxproj index 78eede9..67711ff 100644 --- a/SecureStorgageSample.xcodeproj/project.pbxproj +++ b/SecureStorgageSample.xcodeproj/project.pbxproj @@ -6,6 +6,13 @@ objectVersion = 77; objects = { +/* Begin PBXBuildFile section */ + EA179D392F17290D00B1D54A /* StorageKeys in Frameworks */ = {isa = PBXBuildFile; productRef = EA179D382F17290D00B1D54A /* StorageKeys */; }; + EA179D402F17326400B1D54A /* StorageKeys in Frameworks */ = {isa = PBXBuildFile; productRef = EA179D3F2F17326400B1D54A /* StorageKeys */; }; + EA179D532F17367700B1D54A /* StorageKeys in Frameworks */ = {isa = PBXBuildFile; productRef = EA179D522F17367700B1D54A /* StorageKeys */; }; + EA179D562F17379800B1D54A /* LocalData in Frameworks */ = {isa = PBXBuildFile; productRef = EA179D552F17379800B1D54A /* LocalData */; }; +/* End PBXBuildFile section */ + /* Begin PBXContainerItemProxy section */ EA179D0F2F1722BC00B1D54A /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; @@ -52,6 +59,10 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + EA179D532F17367700B1D54A /* StorageKeys in Frameworks */, + EA179D402F17326400B1D54A /* StorageKeys in Frameworks */, + EA179D562F17379800B1D54A /* LocalData in Frameworks */, + EA179D392F17290D00B1D54A /* StorageKeys in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -112,6 +123,10 @@ ); name = SecureStorgageSample; packageProductDependencies = ( + EA179D382F17290D00B1D54A /* StorageKeys */, + EA179D3F2F17326400B1D54A /* StorageKeys */, + EA179D522F17367700B1D54A /* StorageKeys */, + EA179D552F17379800B1D54A /* LocalData */, ); productName = SecureStorgageSample; productReference = EA179D012F1722BB00B1D54A /* SecureStorgageSample.app */; @@ -195,6 +210,9 @@ ); mainGroup = EA179CF82F1722BB00B1D54A; minimizedProjectReferenceProxies = 1; + packageReferences = ( + EA179D542F17379800B1D54A /* XCLocalSwiftPackageReference "../localPackages/LocalData" */, + ); preferredProjectObjectVersion = 77; productRefGroup = EA179D022F1722BB00B1D54A /* Products */; projectDirPath = ""; @@ -402,6 +420,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -433,6 +452,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -570,6 +590,32 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + EA179D542F17379800B1D54A /* XCLocalSwiftPackageReference "../localPackages/LocalData" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../localPackages/LocalData; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + EA179D382F17290D00B1D54A /* StorageKeys */ = { + isa = XCSwiftPackageProductDependency; + productName = StorageKeys; + }; + EA179D3F2F17326400B1D54A /* StorageKeys */ = { + isa = XCSwiftPackageProductDependency; + productName = StorageKeys; + }; + EA179D522F17367700B1D54A /* StorageKeys */ = { + isa = XCSwiftPackageProductDependency; + productName = StorageKeys; + }; + EA179D552F17379800B1D54A /* LocalData */ = { + isa = XCSwiftPackageProductDependency; + productName = LocalData; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = EA179CF92F1722BB00B1D54A /* Project object */; } diff --git a/SecureStorgageSample/ContentView.swift b/SecureStorgageSample/ContentView.swift index d631310..28acd8c 100644 --- a/SecureStorgageSample/ContentView.swift +++ b/SecureStorgageSample/ContentView.swift @@ -6,14 +6,46 @@ // import SwiftUI +import LocalData struct ContentView: View { + @State private var appVersion: String = "" + @State private var retrievedVersion: String = "" + var body: some View { VStack { Image(systemName: "globe") .imageScale(.large) .foregroundStyle(.tint) - Text("Hello, world!") + Text("Secure Storage Sample") + + TextField("Enter app version", text: $appVersion) + .textFieldStyle(.roundedBorder) + .padding() + + Button("Save Version") { + Task { + do { + let key = StorageKeys.AppVersionKey() + try await StorageRouter.shared.set(appVersion, for: key) + } catch { + print("Error saving: \(error)") + } + } + } + + Button("Retrieve Version") { + Task { + do { + let key = StorageKeys.AppVersionKey() + retrievedVersion = try await StorageRouter.shared.get(key) + } catch { + print("Error retrieving: \(error)") + } + } + } + + Text("Retrieved: \(retrievedVersion)") } .padding() } diff --git a/SecureStorgageSample/StorageKeys.swift b/SecureStorgageSample/StorageKeys.swift new file mode 100644 index 0000000..7f5b746 --- /dev/null +++ b/SecureStorgageSample/StorageKeys.swift @@ -0,0 +1,69 @@ +import Foundation +import LocalData + +struct SampleLocationData: Codable, Sendable { + let lat: Double + let lon: Double +} + +extension StorageKeys { + struct AppVersionKey: StorageKey { + typealias Value = String + + let name: String = "last_app_version" + let domain: StorageDomain = .userDefaults(suite: nil) + let security: SecurityPolicy = .none + let serializer: Serializer = .json + let owner: String = "SampleApp" + let availability: PlatformAvailability = .all + let syncPolicy: SyncPolicy = .automaticSmall + } + + struct LastLocationKey: StorageKey { + typealias Value = SampleLocationData + + let name: String = "last_known_location" + let domain: StorageDomain = .keychain(service: "com.example.app.security") + let security: SecurityPolicy = .keychain(accessibility: .afterFirstUnlock, accessControl: .userPresence) + let serializer: Serializer = .json + let owner: String = "SampleApp" + let availability: PlatformAvailability = .phoneOnly + let syncPolicy: SyncPolicy = .manual + } + + struct UserProfileFileKey: StorageKey { + typealias Value = [String: AnyCodable] + + let name: String = "user_profile.json" + let domain: StorageDomain = .fileSystem(directory: .documents) + let security: SecurityPolicy = .none + let serializer: Serializer<[String: AnyCodable]> = .json + let owner: String = "SampleApp" + let availability: PlatformAvailability = .phoneOnly + let syncPolicy: SyncPolicy = .never + } + + struct SessionLogsKey: StorageKey { + typealias Value = [String] + + let name: String = "session_logs.json" + let domain: StorageDomain = .encryptedFileSystem(directory: .caches) + let security: SecurityPolicy = .encrypted(.aes256(keyDerivation: .pbkdf2(iterations: 10_000))) + let serializer: Serializer<[String]> = .json + let owner: String = "SampleApp" + let availability: PlatformAvailability = .phoneOnly + let syncPolicy: SyncPolicy = .never + } + + struct WatchVibrationKey: StorageKey { + typealias Value = Bool + + let name: String = "watch_vibration_enabled" + let domain: StorageDomain = .userDefaults(suite: nil) + let security: SecurityPolicy = .none + let serializer: Serializer = .json + let owner: String = "SampleApp" + let availability: PlatformAvailability = .watchOnly + let syncPolicy: SyncPolicy = .never + } +} diff --git a/SecureStorgageSample/WatchOptimized.swift b/SecureStorgageSample/WatchOptimized.swift new file mode 100644 index 0000000..8b5726d --- /dev/null +++ b/SecureStorgageSample/WatchOptimized.swift @@ -0,0 +1,61 @@ +import Foundation + +// Example shared models for Watch-optimized data + +struct Workout: Codable { + let date: Date + // Add other properties as needed +} + +struct Preferences: Codable { + let isPremium: Bool + let appearance: Appearance +} + +enum Appearance: Codable { + case light, dark +} + +// Shared model (iOS + watchOS) +struct FullUserProfile: Codable { + let id: UUID + let fullName: String + let email: String + let avatarURL: URL? + let allWorkouts: [Workout] // ← potentially huge + let preferences: Preferences + + // Lightweight Watch version — computed + var watchVersion: WatchUserProfile { + WatchUserProfile( + id: id, + displayName: String(fullName.split(separator: " ").first ?? Substring(fullName)), + hasAvatar: avatarURL != nil, + recentWorkoutCount: allWorkouts.count, + lastWorkoutDate: allWorkouts.last?.date, + showPremiumBadge: preferences.isPremium, + darkModeEnabled: preferences.appearance == .dark + ) + } +} + +struct WatchUserProfile: Codable, Sendable { + let id: UUID + let displayName: String + let hasAvatar: Bool + let recentWorkoutCount: Int + let lastWorkoutDate: Date? + let showPremiumBadge: Bool + let darkModeEnabled: Bool +} + +// Example usage on phone (commented out) +// let profile = FullUserProfile(...) // full heavy object +// let watchData = try JSONEncoder().encode(profile.watchVersion) +// WCSession.default.updateApplicationContext(["profile": watchData]) + +// Alternative: WatchRepresentable protocol +public protocol WatchRepresentable { + associatedtype WatchVersion: Codable, Sendable + var watchVersion: WatchVersion { get } +}