Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
6742e63105
commit
d36ad5a785
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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 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)
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
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 */;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
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