Add WatchDataHandling, handle, UserProfileWatchHandler, registerDefaultHandlers (+6 more); Remove UserProfile, updateProfile, FileDirectory, hash
This commit is contained in:
parent
79b0531b8b
commit
c32ac4b601
@ -31,6 +31,14 @@ The project also includes a watchOS companion app target for watch-specific demo
|
|||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
|
SharedPackage/
|
||||||
|
├── Package.swift
|
||||||
|
└── Sources/
|
||||||
|
└── SharedKit/
|
||||||
|
├── Constants/
|
||||||
|
│ └── StorageKeyNames.swift
|
||||||
|
└── Models/
|
||||||
|
└── UserProfile.swift
|
||||||
SecureStorgageSample/
|
SecureStorgageSample/
|
||||||
├── ContentView.swift # Tabbed navigation
|
├── ContentView.swift # Tabbed navigation
|
||||||
├── StorageKeys.swift # 12 example key definitions
|
├── StorageKeys.swift # 12 example key definitions
|
||||||
@ -77,6 +85,7 @@ The app demonstrates various storage configurations:
|
|||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
- [LocalData](../localPackages/LocalData) - Local package for typed secure storage
|
- [LocalData](../localPackages/LocalData) - Local package for typed secure storage
|
||||||
|
- SharedKit - Local package for shared iOS/watch models and constants
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@ -6,13 +6,14 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import SharedKit
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@State private var connectivity = WatchConnectivityService.shared
|
@State private var store = WatchProfileStore.shared
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
if let profile = connectivity.profile {
|
if let profile = store.profile {
|
||||||
Text(profile.name)
|
Text(profile.name)
|
||||||
.bold()
|
.bold()
|
||||||
Text(profile.email)
|
Text(profile.email)
|
||||||
@ -30,8 +31,8 @@ struct ContentView: View {
|
|||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !connectivity.statusMessage.isEmpty {
|
if !store.statusMessage.isEmpty {
|
||||||
Text(connectivity.statusMessage)
|
Text(store.statusMessage)
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
protocol WatchDataHandling {
|
||||||
|
var key: String { get }
|
||||||
|
func handle(data: Data)
|
||||||
|
}
|
||||||
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,24 +1,33 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Observation
|
|
||||||
import WatchConnectivity
|
import WatchConnectivity
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@Observable
|
|
||||||
final class WatchConnectivityService: NSObject, WCSessionDelegate {
|
final class WatchConnectivityService: NSObject, WCSessionDelegate {
|
||||||
static let shared = WatchConnectivityService()
|
static let shared = WatchConnectivityService()
|
||||||
|
|
||||||
private(set) var profile: UserProfile?
|
private let store: WatchProfileStore
|
||||||
private(set) var statusMessage: String = ""
|
private var handlers: [String: WatchDataHandling] = [:]
|
||||||
|
|
||||||
private override init() {
|
private override init() {
|
||||||
|
self.store = .shared
|
||||||
super.init()
|
super.init()
|
||||||
|
registerDefaultHandlers()
|
||||||
activateIfSupported()
|
activateIfSupported()
|
||||||
loadCurrentContext()
|
loadCurrentContext()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func registerDefaultHandlers() {
|
||||||
|
let profileHandler = UserProfileWatchHandler(store: store)
|
||||||
|
registerHandler(profileHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerHandler(_ handler: WatchDataHandling) {
|
||||||
|
handlers[handler.key] = handler
|
||||||
|
}
|
||||||
|
|
||||||
private func activateIfSupported() {
|
private func activateIfSupported() {
|
||||||
guard WCSession.isSupported() else {
|
guard WCSession.isSupported() else {
|
||||||
statusMessage = "WatchConnectivity not supported"
|
store.setStatus("WatchConnectivity not supported")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let session = WCSession.default
|
let session = WCSession.default
|
||||||
@ -28,20 +37,13 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate {
|
|||||||
|
|
||||||
private func loadCurrentContext() {
|
private func loadCurrentContext() {
|
||||||
guard WCSession.isSupported() else { return }
|
guard WCSession.isSupported() else { return }
|
||||||
updateProfile(from: WCSession.default.applicationContext)
|
handleContext(WCSession.default.applicationContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateProfile(from context: [String: Any]) {
|
private func handleContext(_ context: [String: Any]) {
|
||||||
guard let data = context[UserProfile.storageKeyName] as? Data else {
|
for (key, handler) in handlers {
|
||||||
statusMessage = "No profile received"
|
guard let data = context[key] as? Data else { continue }
|
||||||
return
|
handler.handle(data: data)
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
profile = try JSONDecoder().decode(UserProfile.self, from: data)
|
|
||||||
statusMessage = "Profile synced"
|
|
||||||
} catch {
|
|
||||||
statusMessage = "Failed to decode profile"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,6 +56,6 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) {
|
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) {
|
||||||
updateProfile(from: applicationContext)
|
handleContext(applicationContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
23
SecureStorageSample Watch App/State/WatchProfileStore.swift
Normal file
23
SecureStorageSample Watch App/State/WatchProfileStore.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -209,6 +209,7 @@
|
|||||||
name = SecureStorgageSample;
|
name = SecureStorgageSample;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
EA179D552F17379800B1D54A /* LocalData */,
|
EA179D552F17379800B1D54A /* LocalData */,
|
||||||
|
EA65D7312F17DDEB00C48466 /* SharedKit */,
|
||||||
);
|
);
|
||||||
productName = SecureStorgageSample;
|
productName = SecureStorgageSample;
|
||||||
productReference = EA179D012F1722BB00B1D54A /* SecureStorgageSample.app */;
|
productReference = EA179D012F1722BB00B1D54A /* SecureStorgageSample.app */;
|
||||||
@ -232,6 +233,7 @@
|
|||||||
);
|
);
|
||||||
name = SecureStorgageSampleTests;
|
name = SecureStorgageSampleTests;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
|
EA65D7312F17DDEB00C48466 /* SharedKit */,
|
||||||
);
|
);
|
||||||
productName = SecureStorgageSampleTests;
|
productName = SecureStorgageSampleTests;
|
||||||
productReference = EA179D0E2F1722BC00B1D54A /* SecureStorgageSampleTests.xctest */;
|
productReference = EA179D0E2F1722BC00B1D54A /* SecureStorgageSampleTests.xctest */;
|
||||||
@ -373,6 +375,7 @@
|
|||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
EA179D542F17379800B1D54A /* XCLocalSwiftPackageReference "../localPackages/LocalData" */,
|
EA179D542F17379800B1D54A /* XCLocalSwiftPackageReference "../localPackages/LocalData" */,
|
||||||
|
EA65D7302F17DDEB00C48466 /* XCLocalSwiftPackageReference "SharedPackage" */,
|
||||||
);
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = EA179D022F1722BB00B1D54A /* Products */;
|
productRefGroup = EA179D022F1722BB00B1D54A /* Products */;
|
||||||
@ -1002,6 +1005,10 @@
|
|||||||
isa = XCLocalSwiftPackageReference;
|
isa = XCLocalSwiftPackageReference;
|
||||||
relativePath = ../localPackages/LocalData;
|
relativePath = ../localPackages/LocalData;
|
||||||
};
|
};
|
||||||
|
EA65D7302F17DDEB00C48466 /* XCLocalSwiftPackageReference "SharedPackage" */ = {
|
||||||
|
isa = XCLocalSwiftPackageReference;
|
||||||
|
relativePath = SharedPackage;
|
||||||
|
};
|
||||||
/* End XCLocalSwiftPackageReference section */
|
/* End XCLocalSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
@ -1009,6 +1016,10 @@
|
|||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = LocalData;
|
productName = LocalData;
|
||||||
};
|
};
|
||||||
|
EA65D7312F17DDEB00C48466 /* SharedKit */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = SharedKit;
|
||||||
|
};
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = EA179CF92F1722BB00B1D54A /* Project object */;
|
rootObject = EA179CF92F1722BB00B1D54A /* Project object */;
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
<key>SecureStorageSample Watch App.xcscheme_^#shared#^_</key>
|
<key>SecureStorageSample Watch App.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>2</integer>
|
<integer>3</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>SecureStorageWatch Watch App.xcscheme_^#shared#^_</key>
|
<key>SecureStorageWatch Watch App.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
@ -17,7 +17,7 @@
|
|||||||
<key>SecureStorgageSample.xcscheme_^#shared#^_</key>
|
<key>SecureStorgageSample.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>1</integer>
|
<integer>2</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>SecureStorgageSampleWatch.xcscheme_^#shared#^_</key>
|
<key>SecureStorgageSampleWatch.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
|||||||
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -8,18 +8,19 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import LocalData
|
import LocalData
|
||||||
|
import SharedKit
|
||||||
|
|
||||||
// MARK: - Sample Data Models
|
// MARK: - Sample Data Models
|
||||||
|
|
||||||
/// Simple credential model for keychain storage demo.
|
/// Simple credential model for keychain storage demo.
|
||||||
nonisolated(unsafe)
|
nonisolated
|
||||||
struct Credential: Codable, Sendable {
|
struct Credential: Codable, Sendable {
|
||||||
let username: String
|
let username: String
|
||||||
let password: String
|
let password: String
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Location data model.
|
/// Location data model.
|
||||||
nonisolated(unsafe)
|
nonisolated
|
||||||
struct SampleLocationData: Codable, Sendable {
|
struct SampleLocationData: Codable, Sendable {
|
||||||
let lat: Double
|
let lat: Double
|
||||||
let lon: Double
|
let lon: Double
|
||||||
@ -268,4 +269,3 @@ extension StorageKeys {
|
|||||||
let syncPolicy: SyncPolicy = .never
|
let syncPolicy: SyncPolicy = .never
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import LocalData
|
import LocalData
|
||||||
|
import SharedKit
|
||||||
|
|
||||||
struct FileSystemDemo: View {
|
struct FileSystemDemo: View {
|
||||||
@State private var profileName = ""
|
@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 {
|
#Preview {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
FileSystemDemo()
|
FileSystemDemo()
|
||||||
|
|||||||
17
SharedPackage/Package.swift
Normal file
17
SharedPackage/Package.swift
Normal 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")
|
||||||
|
]
|
||||||
|
)
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
public enum StorageKeyNames {
|
||||||
|
public static let userProfile = "user_profile.json"
|
||||||
|
}
|
||||||
14
SharedPackage/Sources/SharedKit/Models/UserProfile.swift
Normal file
14
SharedPackage/Sources/SharedKit/Models/UserProfile.swift
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user