diff --git a/README.md b/README.md index b9802d0..ce3d183 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,14 @@ The project also includes a watchOS companion app target for watch-specific demo ## Project Structure ``` +SharedPackage/ +├── Package.swift +└── Sources/ + └── SharedKit/ + ├── Constants/ + │ └── StorageKeyNames.swift + └── Models/ + └── UserProfile.swift SecureStorgageSample/ ├── ContentView.swift # Tabbed navigation ├── StorageKeys.swift # 12 example key definitions @@ -77,6 +85,7 @@ The app demonstrates various storage configurations: ## Dependencies - [LocalData](../localPackages/LocalData) - Local package for typed secure storage +- SharedKit - Local package for shared iOS/watch models and constants ## License diff --git a/SecureStorageSample Watch App/ContentView.swift b/SecureStorageSample Watch App/ContentView.swift index d280ae9..b4ec7e6 100644 --- a/SecureStorageSample Watch App/ContentView.swift +++ b/SecureStorageSample Watch App/ContentView.swift @@ -6,13 +6,14 @@ // import SwiftUI +import SharedKit struct ContentView: View { - @State private var connectivity = WatchConnectivityService.shared + @State private var store = WatchProfileStore.shared var body: some View { VStack { - if let profile = connectivity.profile { + if let profile = store.profile { Text(profile.name) .bold() Text(profile.email) @@ -30,8 +31,8 @@ struct ContentView: View { .foregroundStyle(.secondary) } - if !connectivity.statusMessage.isEmpty { - Text(connectivity.statusMessage) + if !store.statusMessage.isEmpty { + Text(store.statusMessage) .font(.caption2) .foregroundStyle(.secondary) } diff --git a/SecureStorageSample Watch App/Models/UserProfile.swift b/SecureStorageSample Watch App/Models/UserProfile.swift deleted file mode 100644 index ca2c1c5..0000000 --- a/SecureStorageSample Watch App/Models/UserProfile.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation - -nonisolated(unsafe) -struct UserProfile: Codable, Sendable { - static let storageKeyName = "user_profile.json" - - let name: String - let email: String - let age: Int? - let createdAt: Date - - var ageDescription: String { - age.map(String.init) ?? "n/a" - } -} diff --git a/SecureStorageSample Watch App/Protocols/WatchDataHandling.swift b/SecureStorageSample Watch App/Protocols/WatchDataHandling.swift new file mode 100644 index 0000000..cc23812 --- /dev/null +++ b/SecureStorageSample Watch App/Protocols/WatchDataHandling.swift @@ -0,0 +1,6 @@ +import Foundation + +protocol WatchDataHandling { + var key: String { get } + func handle(data: Data) +} diff --git a/SecureStorageSample Watch App/Services/Handlers/UserProfileWatchHandler.swift b/SecureStorageSample Watch App/Services/Handlers/UserProfileWatchHandler.swift new file mode 100644 index 0000000..b473e5f --- /dev/null +++ b/SecureStorageSample Watch App/Services/Handlers/UserProfileWatchHandler.swift @@ -0,0 +1,23 @@ +import Foundation +import SharedKit + +@MainActor +final class UserProfileWatchHandler: WatchDataHandling { + let key = UserProfile.storageKeyName + + private let store: WatchProfileStore + private let decoder = JSONDecoder() + + init(store: WatchProfileStore = .shared) { + self.store = store + } + + func handle(data: Data) { + do { + let profile = try decoder.decode(UserProfile.self, from: data) + store.setProfile(profile) + } catch { + store.setStatus("Failed to decode profile") + } + } +} diff --git a/SecureStorageSample Watch App/Services/WatchConnectivityService.swift b/SecureStorageSample Watch App/Services/WatchConnectivityService.swift index 6a3eff1..d86b0e1 100644 --- a/SecureStorageSample Watch App/Services/WatchConnectivityService.swift +++ b/SecureStorageSample Watch App/Services/WatchConnectivityService.swift @@ -1,24 +1,33 @@ import Foundation -import Observation import WatchConnectivity @MainActor -@Observable final class WatchConnectivityService: NSObject, WCSessionDelegate { static let shared = WatchConnectivityService() - private(set) var profile: UserProfile? - private(set) var statusMessage: String = "" + private let store: WatchProfileStore + private var handlers: [String: WatchDataHandling] = [:] private override init() { + self.store = .shared super.init() + registerDefaultHandlers() activateIfSupported() loadCurrentContext() } + private func registerDefaultHandlers() { + let profileHandler = UserProfileWatchHandler(store: store) + registerHandler(profileHandler) + } + + func registerHandler(_ handler: WatchDataHandling) { + handlers[handler.key] = handler + } + private func activateIfSupported() { guard WCSession.isSupported() else { - statusMessage = "WatchConnectivity not supported" + store.setStatus("WatchConnectivity not supported") return } let session = WCSession.default @@ -28,20 +37,13 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate { private func loadCurrentContext() { guard WCSession.isSupported() else { return } - updateProfile(from: WCSession.default.applicationContext) + handleContext(WCSession.default.applicationContext) } - private func updateProfile(from context: [String: Any]) { - guard let data = context[UserProfile.storageKeyName] as? Data else { - statusMessage = "No profile received" - return - } - - do { - profile = try JSONDecoder().decode(UserProfile.self, from: data) - statusMessage = "Profile synced" - } catch { - statusMessage = "Failed to decode profile" + private func handleContext(_ context: [String: Any]) { + for (key, handler) in handlers { + guard let data = context[key] as? Data else { continue } + handler.handle(data: data) } } @@ -54,6 +56,6 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate { } func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) { - updateProfile(from: applicationContext) + handleContext(applicationContext) } } diff --git a/SecureStorageSample Watch App/State/WatchProfileStore.swift b/SecureStorageSample Watch App/State/WatchProfileStore.swift new file mode 100644 index 0000000..16ebefb --- /dev/null +++ b/SecureStorageSample Watch App/State/WatchProfileStore.swift @@ -0,0 +1,23 @@ +import Foundation +import Observation +import SharedKit + +@MainActor +@Observable +final class WatchProfileStore { + static let shared = WatchProfileStore() + + private(set) var profile: UserProfile? + private(set) var statusMessage: String = "" + + private init() {} + + func setProfile(_ profile: UserProfile) { + self.profile = profile + statusMessage = "Profile synced" + } + + func setStatus(_ message: String) { + statusMessage = message + } +} diff --git a/SecureStorgageSample.xcodeproj/project.pbxproj b/SecureStorgageSample.xcodeproj/project.pbxproj index 9eb6695..cccddb4 100644 --- a/SecureStorgageSample.xcodeproj/project.pbxproj +++ b/SecureStorgageSample.xcodeproj/project.pbxproj @@ -209,6 +209,7 @@ name = SecureStorgageSample; packageProductDependencies = ( EA179D552F17379800B1D54A /* LocalData */, + EA65D7312F17DDEB00C48466 /* SharedKit */, ); productName = SecureStorgageSample; productReference = EA179D012F1722BB00B1D54A /* SecureStorgageSample.app */; @@ -232,6 +233,7 @@ ); name = SecureStorgageSampleTests; packageProductDependencies = ( + EA65D7312F17DDEB00C48466 /* SharedKit */, ); productName = SecureStorgageSampleTests; productReference = EA179D0E2F1722BC00B1D54A /* SecureStorgageSampleTests.xctest */; @@ -373,6 +375,7 @@ minimizedProjectReferenceProxies = 1; packageReferences = ( EA179D542F17379800B1D54A /* XCLocalSwiftPackageReference "../localPackages/LocalData" */, + EA65D7302F17DDEB00C48466 /* XCLocalSwiftPackageReference "SharedPackage" */, ); preferredProjectObjectVersion = 77; productRefGroup = EA179D022F1722BB00B1D54A /* Products */; @@ -1002,6 +1005,10 @@ isa = XCLocalSwiftPackageReference; relativePath = ../localPackages/LocalData; }; + EA65D7302F17DDEB00C48466 /* XCLocalSwiftPackageReference "SharedPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = SharedPackage; + }; /* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1009,6 +1016,10 @@ isa = XCSwiftPackageProductDependency; productName = LocalData; }; + EA65D7312F17DDEB00C48466 /* SharedKit */ = { + isa = XCSwiftPackageProductDependency; + productName = SharedKit; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = EA179CF92F1722BB00B1D54A /* Project object */; diff --git a/SecureStorgageSample.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist b/SecureStorgageSample.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist index 7564a6e..6f8d15b 100644 --- a/SecureStorgageSample.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/SecureStorgageSample.xcodeproj/xcuserdata/mattbruce.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ SecureStorageSample Watch App.xcscheme_^#shared#^_ orderHint - 2 + 3 SecureStorageWatch Watch App.xcscheme_^#shared#^_ @@ -17,7 +17,7 @@ SecureStorgageSample.xcscheme_^#shared#^_ orderHint - 1 + 2 SecureStorgageSampleWatch.xcscheme_^#shared#^_ diff --git a/SecureStorgageSample/Models/UserProfile.swift b/SecureStorgageSample/Models/UserProfile.swift deleted file mode 100644 index ca2c1c5..0000000 --- a/SecureStorgageSample/Models/UserProfile.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation - -nonisolated(unsafe) -struct UserProfile: Codable, Sendable { - static let storageKeyName = "user_profile.json" - - let name: String - let email: String - let age: Int? - let createdAt: Date - - var ageDescription: String { - age.map(String.init) ?? "n/a" - } -} diff --git a/SecureStorgageSample/StorageKeys.swift b/SecureStorgageSample/StorageKeys.swift index 59c3888..705b1d9 100644 --- a/SecureStorgageSample/StorageKeys.swift +++ b/SecureStorgageSample/StorageKeys.swift @@ -8,18 +8,19 @@ import Foundation import LocalData +import SharedKit // MARK: - Sample Data Models /// Simple credential model for keychain storage demo. -nonisolated(unsafe) +nonisolated struct Credential: Codable, Sendable { let username: String let password: String } /// Location data model. -nonisolated(unsafe) +nonisolated struct SampleLocationData: Codable, Sendable { let lat: Double let lon: Double @@ -268,4 +269,3 @@ extension StorageKeys { let syncPolicy: SyncPolicy = .never } } - diff --git a/SecureStorgageSample/Views/FileSystemDemo.swift b/SecureStorgageSample/Views/FileSystemDemo.swift index 9cb6682..7b8d61e 100644 --- a/SecureStorgageSample/Views/FileSystemDemo.swift +++ b/SecureStorgageSample/Views/FileSystemDemo.swift @@ -7,6 +7,7 @@ import SwiftUI import LocalData +import SharedKit struct FileSystemDemo: View { @State private var profileName = "" @@ -168,28 +169,6 @@ struct FileSystemDemo: View { } } -extension FileDirectory: Hashable { - public func hash(into hasher: inout Hasher) { - switch self { - case .documents: - hasher.combine("documents") - case .caches: - hasher.combine("caches") - case .custom(let url): - hasher.combine(url) - } - } - - public static func == (lhs: FileDirectory, rhs: FileDirectory) -> Bool { - switch (lhs, rhs) { - case (.documents, .documents): return true - case (.caches, .caches): return true - case (.custom(let a), .custom(let b)): return a == b - default: return false - } - } -} - #Preview { NavigationStack { FileSystemDemo() diff --git a/SharedPackage/Package.swift b/SharedPackage/Package.swift new file mode 100644 index 0000000..c66ceff --- /dev/null +++ b/SharedPackage/Package.swift @@ -0,0 +1,17 @@ +// swift-tools-version: 5.9 + +import PackageDescription + +let package = Package( + name: "SharedKit", + platforms: [ + .iOS(.v17), + .watchOS(.v10) + ], + products: [ + .library(name: "SharedKit", targets: ["SharedKit"]) + ], + targets: [ + .target(name: "SharedKit") + ] +) diff --git a/SharedPackage/Sources/SharedKit/Constants/StorageKeyNames.swift b/SharedPackage/Sources/SharedKit/Constants/StorageKeyNames.swift new file mode 100644 index 0000000..b23750b --- /dev/null +++ b/SharedPackage/Sources/SharedKit/Constants/StorageKeyNames.swift @@ -0,0 +1,5 @@ +import Foundation + +public enum StorageKeyNames { + public static let userProfile = "user_profile.json" +} diff --git a/SharedPackage/Sources/SharedKit/Models/UserProfile.swift b/SharedPackage/Sources/SharedKit/Models/UserProfile.swift new file mode 100644 index 0000000..dc93e55 --- /dev/null +++ b/SharedPackage/Sources/SharedKit/Models/UserProfile.swift @@ -0,0 +1,14 @@ +import Foundation + +public nonisolated struct UserProfile: Codable, Sendable { + public static let storageKeyName = StorageKeyNames.userProfile + + public let name: String + public let email: String + public let age: Int? + public let createdAt: Date + + public var ageDescription: String { + age.map(String.init) ?? "n/a" + } +}