Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-01-14 09:22:55 -06:00
parent 6742e63105
commit d36ad5a785
15 changed files with 139 additions and 79 deletions

View File

@ -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

View File

@ -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)
}

View File

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

View File

@ -0,0 +1,6 @@
import Foundation
protocol WatchDataHandling {
var key: String { get }
func handle(data: Data)
}

View File

@ -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")
}
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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 */;

View File

@ -7,7 +7,7 @@
<key>SecureStorageSample Watch App.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
<integer>3</integer>
</dict>
<key>SecureStorageWatch Watch App.xcscheme_^#shared#^_</key>
<dict>
@ -17,7 +17,7 @@
<key>SecureStorgageSample.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
<integer>2</integer>
</dict>
<key>SecureStorgageSampleWatch.xcscheme_^#shared#^_</key>
<dict>

View File

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

View File

@ -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
}
}

View File

@ -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()

View File

@ -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")
]
)

View File

@ -0,0 +1,5 @@
import Foundation
public enum StorageKeyNames {
public static let userProfile = "user_profile.json"
}

View File

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