From 9cd1652727235f98175d4aa5e47bd6adcf42190c Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 16 Jan 2026 15:53:15 -0600 Subject: [PATCH] Signed-off-by: Matt Bruce --- .../ContentView.swift | 93 ++++++- .../Design/WatchDesignConstants.swift | 22 ++ .../SyncableSettingWatchHandler.swift | 29 ++ .../Handlers/UserProfileWatchHandler.swift | 2 + .../Services/WatchConnectivityService.swift | 50 +++- .../State/WatchProfileStore.swift | 13 + .../SecureStorageSampleApp.swift | 6 +- .../Services/WatchConnectivityService.swift | 81 +++++- .../Views/PlatformSyncDemo.swift | 256 +++++++++++++----- .../SharedKit/Constants/StorageKeyNames.swift | 1 + .../Constants/WatchSyncMessageKeys.swift | 5 + 11 files changed, 481 insertions(+), 77 deletions(-) create mode 100644 SecureStorageSample Watch App/Design/WatchDesignConstants.swift create mode 100644 SecureStorageSample Watch App/Services/Handlers/SyncableSettingWatchHandler.swift create mode 100644 localPackages/SharedPackage/Sources/SharedKit/Constants/WatchSyncMessageKeys.swift diff --git a/SecureStorageSample Watch App/ContentView.swift b/SecureStorageSample Watch App/ContentView.swift index b4ec7e6..2a33bbe 100644 --- a/SecureStorageSample Watch App/ContentView.swift +++ b/SecureStorageSample Watch App/ContentView.swift @@ -12,8 +12,53 @@ struct ContentView: View { @State private var store = WatchProfileStore.shared var body: some View { - VStack { - if let profile = store.profile { + VStack(alignment: .leading) { + PhoneConnectivityBadge(isReachable: store.isPhoneReachable) + ProfileSectionView(profile: store.profile) + Divider() + SyncableSettingSectionView( + value: store.syncValue, + updatedAt: store.syncUpdatedAt + ) + + if !store.statusMessage.isEmpty { + StatusMessageView(message: store.statusMessage) + } + } + .padding() + } +} + +#Preview { + ContentView() +} + +private struct PhoneConnectivityBadge: View { + let isReachable: Bool + + var body: some View { + if !isReachable { + Text("Open iPhone App to Sync") + .font(.caption2) + .foregroundStyle(Color.Status.warning) + .padding(.horizontal, WatchDesign.Spacing.small) + .padding(.vertical, WatchDesign.Spacing.xSmall) + .background(Color.Status.warning.opacity(WatchDesign.Opacity.subtle)) + .clipShape(.rect(cornerRadius: WatchDesign.CornerRadius.pill)) + } + } +} + +private struct ProfileSectionView: View { + let profile: UserProfile? + + var body: some View { + VStack(alignment: .leading) { + Text("User Profile") + .font(.caption) + .foregroundStyle(.secondary) + + if let profile { Text(profile.name) .bold() Text(profile.email) @@ -30,17 +75,43 @@ struct ContentView: View { .font(.caption) .foregroundStyle(.secondary) } - - if !store.statusMessage.isEmpty { - Text(store.statusMessage) - .font(.caption2) - .foregroundStyle(.secondary) - } } - .padding() } } -#Preview { - ContentView() +private struct SyncableSettingSectionView: View { + let value: String? + let updatedAt: Date? + + var body: some View { + VStack(alignment: .leading) { + Text("Syncable Setting") + .font(.caption) + .foregroundStyle(.secondary) + + if let value { + Text(value) + .font(.system(.caption, design: .monospaced)) + if let updatedAt { + Text(updatedAt, format: .dateTime) + .font(.caption2) + .foregroundStyle(.secondary) + } + } else { + Text("No sync value received") + .font(.caption) + .foregroundStyle(.secondary) + } + } + } +} + +private struct StatusMessageView: View { + let message: String + + var body: some View { + Text(message) + .font(.caption2) + .foregroundStyle(.secondary) + } } diff --git a/SecureStorageSample Watch App/Design/WatchDesignConstants.swift b/SecureStorageSample Watch App/Design/WatchDesignConstants.swift new file mode 100644 index 0000000..76c49cb --- /dev/null +++ b/SecureStorageSample Watch App/Design/WatchDesignConstants.swift @@ -0,0 +1,22 @@ +import SwiftUI + +enum WatchDesign { + enum Spacing { + static let xSmall: CGFloat = 4 + static let small: CGFloat = 8 + } + + enum CornerRadius { + static let pill: CGFloat = 12 + } + + enum Opacity { + static let subtle: Double = 0.2 + } +} + +extension Color { + enum Status { + static let warning = Color.orange + } +} diff --git a/SecureStorageSample Watch App/Services/Handlers/SyncableSettingWatchHandler.swift b/SecureStorageSample Watch App/Services/Handlers/SyncableSettingWatchHandler.swift new file mode 100644 index 0000000..36fa1e8 --- /dev/null +++ b/SecureStorageSample Watch App/Services/Handlers/SyncableSettingWatchHandler.swift @@ -0,0 +1,29 @@ +import Foundation +import SharedKit + +@MainActor +final class SyncableSettingWatchHandler: WatchDataHandling { + let key = StorageKeyNames.syncableSetting + + private let store: WatchProfileStore + private let decoder = JSONDecoder() + + init(store: WatchProfileStore) { + self.store = store + } + + convenience init() { + self.init(store: .shared) + } + + func handle(data: Data) { + do { + let value = try decoder.decode(String.self, from: data) + store.setSyncValue(value) + Logger.debug("Watch synced syncable setting") + } catch { + store.setStatus("Failed to decode sync value") + Logger.error("Watch failed to decode syncable setting", error: error) + } + } +} diff --git a/SecureStorageSample Watch App/Services/Handlers/UserProfileWatchHandler.swift b/SecureStorageSample Watch App/Services/Handlers/UserProfileWatchHandler.swift index 5632b70..8389fcb 100644 --- a/SecureStorageSample Watch App/Services/Handlers/UserProfileWatchHandler.swift +++ b/SecureStorageSample Watch App/Services/Handlers/UserProfileWatchHandler.swift @@ -20,8 +20,10 @@ final class UserProfileWatchHandler: WatchDataHandling { do { let profile = try decoder.decode(UserProfile.self, from: data) store.setProfile(profile) + Logger.debug("Watch synced user profile") } catch { store.setStatus("Failed to decode profile") + Logger.error("Watch failed to decode user profile", error: error) } } } diff --git a/SecureStorageSample Watch App/Services/WatchConnectivityService.swift b/SecureStorageSample Watch App/Services/WatchConnectivityService.swift index d86b0e1..d6fd1db 100644 --- a/SecureStorageSample Watch App/Services/WatchConnectivityService.swift +++ b/SecureStorageSample Watch App/Services/WatchConnectivityService.swift @@ -1,4 +1,5 @@ import Foundation +import SharedKit import WatchConnectivity @MainActor @@ -19,6 +20,9 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate { private func registerDefaultHandlers() { let profileHandler = UserProfileWatchHandler(store: store) registerHandler(profileHandler) + + let syncableSettingHandler = SyncableSettingWatchHandler(store: store) + registerHandler(syncableSettingHandler) } func registerHandler(_ handler: WatchDataHandling) { @@ -37,10 +41,15 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate { private func loadCurrentContext() { guard WCSession.isSupported() else { return } - handleContext(WCSession.default.applicationContext) + let context = WCSession.default.applicationContext + let keys = context.keys.sorted().joined(separator: ", ") + Logger.debug("Watch loaded current context keys: [\(keys)]") + handleContext(context) } private func handleContext(_ context: [String: Any]) { + let keys = context.keys.sorted().joined(separator: ", ") + Logger.debug("Watch handling context keys: [\(keys)]") for (key, handler) in handlers { guard let data = context[key] as? Data else { continue } handler.handle(data: data) @@ -52,10 +61,49 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate { activationDidCompleteWith activationState: WCSessionActivationState, error: Error? ) { + if let error { + Logger.error("Watch WCSession activation failed", error: error) + } else { + Logger.debug("Watch WCSession activated with state: \(activationState.rawValue)") + } + updateReachability(using: session) loadCurrentContext() + requestSyncIfNeeded() } func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) { + Logger.debug("Watch received application context with \(applicationContext.count) keys") handleContext(applicationContext) } + + func sessionReachabilityDidChange(_ session: WCSession) { + Logger.debug("Watch reachability changed: reachable=\(session.isReachable)") + updateReachability(using: session) + requestSyncIfNeeded() + } + + private func updateReachability(using session: WCSession) { + store.setPhoneReachable(session.isReachable) + } + + private func requestSyncIfNeeded() { + guard WCSession.isSupported() else { return } + let session = WCSession.default + if session.isReachable { + store.setPhoneReachable(true) + store.setStatus("Requesting sync from iPhone...") + session.sendMessage([WatchSyncMessageKeys.requestSync: true]) { reply in + Logger.debug("Watch received sync reply with \(reply.count) keys") + self.handleContext(reply) + } errorHandler: { error in + Logger.error("Watch sync request failed", error: error) + } + Logger.debug("Watch requested sync from iPhone (reachable)") + } else { + store.setPhoneReachable(false) + store.setStatus("Open the iPhone app to sync.") + session.transferUserInfo([WatchSyncMessageKeys.requestSync: true]) + Logger.debug("Watch queued sync request for iPhone launch") + } + } } diff --git a/SecureStorageSample Watch App/State/WatchProfileStore.swift b/SecureStorageSample Watch App/State/WatchProfileStore.swift index 16ebefb..3b32640 100644 --- a/SecureStorageSample Watch App/State/WatchProfileStore.swift +++ b/SecureStorageSample Watch App/State/WatchProfileStore.swift @@ -8,6 +8,9 @@ final class WatchProfileStore { static let shared = WatchProfileStore() private(set) var profile: UserProfile? + private(set) var syncValue: String? + private(set) var syncUpdatedAt: Date? + private(set) var isPhoneReachable = false private(set) var statusMessage: String = "" private init() {} @@ -17,6 +20,16 @@ final class WatchProfileStore { statusMessage = "Profile synced" } + func setSyncValue(_ value: String) { + syncValue = value + syncUpdatedAt = Date() + statusMessage = "Syncable setting updated" + } + + func setPhoneReachable(_ isReachable: Bool) { + isPhoneReachable = isReachable + } + func setStatus(_ message: String) { statusMessage = message } diff --git a/SecureStorageSample/SecureStorageSampleApp.swift b/SecureStorageSample/SecureStorageSampleApp.swift index ba98db9..58d9162 100644 --- a/SecureStorageSample/SecureStorageSampleApp.swift +++ b/SecureStorageSample/SecureStorageSampleApp.swift @@ -56,10 +56,12 @@ struct SecureStorageSampleApp: App { ExternalKeyMaterialProvider(), for: SampleKeyMaterialSources.external ) + await StorageRouter.shared.syncRegisteredKeysIfNeeded() } #if DEBUG - let report = StorageAuditReport.renderText(AppStorageCatalog()) - print(report) + // Disabled to keep console focused on sync logs. + // let report = StorageAuditReport.renderText(AppStorageCatalog()) + // print(report) #endif } diff --git a/SecureStorageSample/Services/WatchConnectivityService.swift b/SecureStorageSample/Services/WatchConnectivityService.swift index 9daddf1..d8723b6 100644 --- a/SecureStorageSample/Services/WatchConnectivityService.swift +++ b/SecureStorageSample/Services/WatchConnectivityService.swift @@ -1,4 +1,6 @@ import Foundation +import LocalData +import SharedKit import WatchConnectivity @MainActor @@ -22,7 +24,14 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate { activationDidCompleteWith activationState: WCSessionActivationState, error: Error? ) { - // Intentionally empty: activation state is handled by WCSession. + if let error { + Logger.error("iOS WCSession activation failed", error: error) + } else { + Logger.debug("iOS WCSession activated with state: \(activationState.rawValue)") + } + Task { + await StorageRouter.shared.syncRegisteredKeysIfNeeded() + } } func sessionDidBecomeInactive(_ session: WCSession) { @@ -32,4 +41,74 @@ final class WatchConnectivityService: NSObject, WCSessionDelegate { func sessionDidDeactivate(_ session: WCSession) { session.activate() } + + func sessionWatchStateDidChange(_ session: WCSession) { + Logger.debug("iOS WCSession watch state changed: paired=\(session.isPaired) installed=\(session.isWatchAppInstalled)") + Task { + await StorageRouter.shared.syncRegisteredKeysIfNeeded() + } + } + + func sessionReachabilityDidChange(_ session: WCSession) { + Logger.debug("iOS WCSession reachability changed: reachable=\(session.isReachable)") + Task { + await StorageRouter.shared.syncRegisteredKeysIfNeeded() + } + } + + func session(_ session: WCSession, didReceiveMessage message: [String: Any]) { + if let request = message[WatchSyncMessageKeys.requestSync] as? Bool, request { + Logger.debug("iOS received watch sync request") + Task { + let snapshot = await StorageRouter.shared.syncSnapshot() + if snapshot.isEmpty { + Logger.debug("iOS sync snapshot empty; falling back to application context") + } + await StorageRouter.shared.syncRegisteredKeysIfNeeded() + } + } + } + + func session( + _ session: WCSession, + didReceiveMessage message: [String: Any], + replyHandler: @escaping ([String: Any]) -> Void + ) { + if let request = message[WatchSyncMessageKeys.requestSync] as? Bool, request { + Logger.debug("iOS received watch sync request (reply)") + Task { + let payload = await buildSyncReplyPayload() + replyHandler(payload) + await StorageRouter.shared.syncRegisteredKeysIfNeeded() + } + } + } + + private func buildSyncReplyPayload() async -> [String: Any] { + let maxAttempts = 3 + for attempt in 1...maxAttempts { + let snapshot = await StorageRouter.shared.syncSnapshot() + if !snapshot.isEmpty { + Logger.debug("iOS sync reply snapshot ready on attempt \(attempt)") + return snapshot + } + + if attempt < maxAttempts { + Logger.debug("iOS sync reply snapshot empty; retrying (\(attempt))") + try? await Task.sleep(for: .milliseconds(300)) + } + } + + Logger.debug("iOS sync reply snapshot empty after retries; replying with ack only") + return ["ack": true] + } + + func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any]) { + if let request = userInfo[WatchSyncMessageKeys.requestSync] as? Bool, request { + Logger.debug("iOS received queued watch sync request") + Task { + await StorageRouter.shared.syncRegisteredKeysIfNeeded() + } + } + } } diff --git a/SecureStorageSample/Views/PlatformSyncDemo.swift b/SecureStorageSample/Views/PlatformSyncDemo.swift index 7360c3c..6164633 100644 --- a/SecureStorageSample/Views/PlatformSyncDemo.swift +++ b/SecureStorageSample/Views/PlatformSyncDemo.swift @@ -7,6 +7,7 @@ import SwiftUI import LocalData +import WatchConnectivity @MainActor struct PlatformSyncDemo: View { @@ -16,8 +17,9 @@ struct PlatformSyncDemo: View { @State private var isLoading = false @State private var selectedPlatform: PlatformAvailability = .all @State private var selectedSync: SyncPolicy = .never + @State private var watchStatus = WatchStatus.current() @FocusState private var isFieldFocused: Bool - + var body: some View { Form { Section { @@ -25,7 +27,11 @@ struct PlatformSyncDemo: View { .font(.caption) .foregroundStyle(.secondary) } - + + Section("Watch Status") { + WatchStatusView(status: watchStatus) + } + Section("Platform Availability") { Picker("Available On", selection: $selectedPlatform) { Text("All (iPhone + Watch)").tag(PlatformAvailability.all) @@ -34,10 +40,10 @@ struct PlatformSyncDemo: View { Text("Phone w/ Watch Sync").tag(PlatformAvailability.phoneWithWatchSync) } .pickerStyle(.menu) - - platformDescription + + PlatformAvailabilityDescriptionView(availability: selectedPlatform) } - + Section("Sync Policy") { Picker("Sync Behavior", selection: $selectedSync) { Text("Never").tag(SyncPolicy.never) @@ -45,15 +51,15 @@ struct PlatformSyncDemo: View { Text("Automatic (Small Data)").tag(SyncPolicy.automaticSmall) } .pickerStyle(.menu) - - syncDescription + + SyncPolicyDescriptionView(syncPolicy: selectedSync) } - + Section("Test Data") { TextField("Enter a value to store", text: $settingValue) .focused($isFieldFocused) } - + Section("Actions") { Button(action: saveValue) { HStack { @@ -62,7 +68,7 @@ struct PlatformSyncDemo: View { } } .disabled(settingValue.isEmpty || isLoading) - + Button(action: loadValue) { HStack { Image(systemName: "icloud.and.arrow.down") @@ -70,7 +76,7 @@ struct PlatformSyncDemo: View { } } .disabled(isLoading) - + Button(action: testPlatformError) { HStack { Image(systemName: "exclamationmark.triangle") @@ -80,30 +86,46 @@ struct PlatformSyncDemo: View { .foregroundStyle(.orange) .disabled(isLoading) } - + if !storedValue.isEmpty { Section("Retrieved Value") { Text(storedValue) .font(.system(.body, design: .monospaced)) } } - + if !statusMessage.isEmpty { Section { Text(statusMessage) .font(.caption) - .foregroundStyle(statusMessage.contains("Error") ? .red : + .foregroundStyle(statusMessage.contains("Error") ? .red : statusMessage.contains("⚠") ? .orange : .green) } } - + + Section("Expected Result") { + ExpectedOutcomeView( + availability: selectedPlatform, + syncPolicy: selectedSync + ) + } + Section("Current Configuration") { LabeledContent("Platform", value: selectedPlatform.displayName) LabeledContent("Sync", value: selectedSync.displayName) - LabeledContent("Max Auto-Sync Size", value: "100 KB") + LabeledContent("Max Auto-Sync Size", value: "50 KB") + } + + Section("Watch Notes") { + Text("The watch app only shows data it is configured to handle. For this demo, the watch displays the syncable setting and user profile.") + .font(.caption) + .foregroundStyle(.secondary) } } .navigationBarTitleDisplayMode(.inline) + .task { + refreshWatchStatus() + } .toolbar { ToolbarItemGroup(placement: .keyboard) { Spacer() @@ -113,47 +135,7 @@ struct PlatformSyncDemo: View { } } } - - @ViewBuilder - private var platformDescription: some View { - switch selectedPlatform { - case .all: - Text("Data accessible on both iPhone and Apple Watch") - .font(.caption) - .foregroundStyle(.secondary) - case .phoneOnly: - Text("Data only accessible on iPhone. Watch access throws error.") - .font(.caption) - .foregroundStyle(.secondary) - case .watchOnly: - Text("Data only accessible on Watch. iPhone access throws error.") - .font(.caption) - .foregroundStyle(.secondary) - case .phoneWithWatchSync: - Text("Stored on iPhone, synced to Watch via WatchConnectivity") - .font(.caption) - .foregroundStyle(.secondary) - } - } - - @ViewBuilder - private var syncDescription: some View { - switch selectedSync { - case .never: - Text("Data stays local, never synced") - .font(.caption) - .foregroundStyle(.secondary) - case .manual: - Text("Sync triggered explicitly by app code") - .font(.caption) - .foregroundStyle(.secondary) - case .automaticSmall: - Text("Auto-sync if data ≤ 100KB, otherwise throws error") - .font(.caption) - .foregroundStyle(.secondary) - } - } - + private func saveValue() { isLoading = true Task { @@ -165,14 +147,15 @@ struct PlatformSyncDemo: View { try await StorageRouter.shared.set(settingValue, for: key) statusMessage = "✓ Saved with \(selectedPlatform.displayName) availability and \(selectedSync.displayName) sync" } catch StorageError.dataTooLargeForSync { - statusMessage = "Error: Data too large for automatic sync (max 100KB)" + statusMessage = "Error: Data too large for automatic sync (max 50KB)" } catch { statusMessage = "Error: \(error.localizedDescription)" } isLoading = false + refreshWatchStatus() } } - + private func loadValue() { isLoading = true Task { @@ -183,7 +166,7 @@ struct PlatformSyncDemo: View { ) let value = try await StorageRouter.shared.get(key) storedValue = value - settingValue = value // Sync to field + settingValue = value statusMessage = "✓ Loaded value" } catch StorageError.notFound { storedValue = "" @@ -192,13 +175,13 @@ struct PlatformSyncDemo: View { statusMessage = "Error: \(error.localizedDescription)" } isLoading = false + refreshWatchStatus() } } - + private func testPlatformError() { isLoading = true Task { - // Try to access a watchOnly key from iPhone let key = StorageKeys.WatchVibrationKey() do { _ = try await StorageRouter.shared.get(key) @@ -209,8 +192,13 @@ struct PlatformSyncDemo: View { statusMessage = "Error: \(error.localizedDescription)" } isLoading = false + refreshWatchStatus() } } + + private func refreshWatchStatus() { + watchStatus = WatchStatus.current() + } } // MARK: - Display Names @@ -241,3 +229,147 @@ extension SyncPolicy { PlatformSyncDemo() } } + +private struct WatchStatus: Equatable { + let isSupported: Bool + let isPaired: Bool + let isWatchAppInstalled: Bool + let isReachable: Bool + let activationState: WCSessionActivationState? + + init( + isSupported: Bool = false, + isPaired: Bool = false, + isWatchAppInstalled: Bool = false, + isReachable: Bool = false, + activationState: WCSessionActivationState? = nil + ) { + self.isSupported = isSupported + self.isPaired = isPaired + self.isWatchAppInstalled = isWatchAppInstalled + self.isReachable = isReachable + self.activationState = activationState + } + + static func current() -> WatchStatus { + guard WCSession.isSupported() else { + return WatchStatus(isSupported: false) + } + + let session = WCSession.default + return WatchStatus( + isSupported: true, + isPaired: session.isPaired, + isWatchAppInstalled: session.isWatchAppInstalled, + isReachable: session.isReachable, + activationState: session.activationState + ) + } +} + +private struct WatchStatusView: View { + let status: WatchStatus + + var body: some View { + if status.isSupported { + LabeledContent("Paired", value: status.isPaired ? "Yes" : "No") + LabeledContent("Watch App Installed", value: status.isWatchAppInstalled ? "Yes" : "No") + LabeledContent("Reachable", value: status.isReachable ? "Yes" : "No") + LabeledContent("WCSession", value: activationLabel(for: status.activationState)) + } else { + Text("WatchConnectivity not supported on this device.") + .font(.caption) + .foregroundStyle(.secondary) + } + } + + private func activationLabel(for state: WCSessionActivationState?) -> String { + switch state { + case .activated: + return "Activated" + case .inactive: + return "Inactive" + case .notActivated: + return "Not Activated" + case .none: + return "Unavailable" + @unknown default: + return "Unknown" + } + } +} + +private struct PlatformAvailabilityDescriptionView: View { + let availability: PlatformAvailability + + var body: some View { + Text(descriptionText) + .font(.caption) + .foregroundStyle(.secondary) + } + + private var descriptionText: String { + switch availability { + case .all: + return "Data accessible on both iPhone and Apple Watch" + case .phoneOnly: + return "Data only accessible on iPhone. Watch access throws error." + case .watchOnly: + return "Data only accessible on Watch. iPhone access throws error." + case .phoneWithWatchSync: + return "Stored on iPhone, synced to Watch via WatchConnectivity" + } + } +} + +private struct SyncPolicyDescriptionView: View { + let syncPolicy: SyncPolicy + + var body: some View { + Text(descriptionText) + .font(.caption) + .foregroundStyle(.secondary) + } + + private var descriptionText: String { + switch syncPolicy { + case .never: + return "Data stays local, never synced" + case .manual: + return "Sync triggered by an explicit save call" + case .automaticSmall: + return "Auto-sync if data ≤ 50KB, otherwise throws error" + } + } +} + +private struct ExpectedOutcomeView: View { + let availability: PlatformAvailability + let syncPolicy: SyncPolicy + + var body: some View { + Text(expectedText) + .font(.caption) + .foregroundStyle(Color.Status.info) + } + + private var expectedText: String { + if availability == .phoneOnly { + return "Expected: Stored only on iPhone. Watch cannot read this key." + } + + if availability == .watchOnly { + return "Expected: iPhone access fails. Watch can read this key." + } + + if availability == .phoneWithWatchSync { + return syncPolicy == .never + ? "Expected: Stored on iPhone only. Sync disabled." + : "Expected: Stored on iPhone and synced to Watch." + } + + return syncPolicy == .never + ? "Expected: Stored on both devices when written locally." + : "Expected: Stored and synced between iPhone and Watch." + } +} diff --git a/localPackages/SharedPackage/Sources/SharedKit/Constants/StorageKeyNames.swift b/localPackages/SharedPackage/Sources/SharedKit/Constants/StorageKeyNames.swift index b23750b..2fbde89 100644 --- a/localPackages/SharedPackage/Sources/SharedKit/Constants/StorageKeyNames.swift +++ b/localPackages/SharedPackage/Sources/SharedKit/Constants/StorageKeyNames.swift @@ -2,4 +2,5 @@ import Foundation public enum StorageKeyNames { public static let userProfile = "user_profile.json" + public static let syncableSetting = "syncable_setting" } diff --git a/localPackages/SharedPackage/Sources/SharedKit/Constants/WatchSyncMessageKeys.swift b/localPackages/SharedPackage/Sources/SharedKit/Constants/WatchSyncMessageKeys.swift new file mode 100644 index 0000000..b783a0e --- /dev/null +++ b/localPackages/SharedPackage/Sources/SharedKit/Constants/WatchSyncMessageKeys.swift @@ -0,0 +1,5 @@ +import Foundation + +public enum WatchSyncMessageKeys { + public static let requestSync = "request_sync" +}