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"
+ }
+}