Summary: - Sources: Configuration, Helpers, Models, Services - Tests: AppGroupTests.swift, FileStorageHelperExpansionTests.swift, FileStorageHelperTests.swift, MigrationTests.swift, RouterConfigurationTests.swift (+5 more) - Added symbols: func resolveDirectoryURL, func handleReceivedContext, enum KeychainAccessControl, enum KeychainAccessibility, enum SecurityPolicy, enum EncryptionPolicy (+5 more) - Removed symbols: func resolveDirectoryURL, func handleReceivedContext, enum KeychainAccessControl, enum KeychainAccessibility, enum SecurityPolicy, enum EncryptionPolicy (+1 more) Stats: - 18 files changed, 306 insertions(+), 58 deletions(-)
156 lines
5.2 KiB
Swift
156 lines
5.2 KiB
Swift
import Foundation
|
|
import WatchConnectivity
|
|
|
|
/// Actor that handles WatchConnectivity sync operations.
|
|
/// Manages data synchronization between iPhone and Apple Watch.
|
|
actor SyncHelper {
|
|
|
|
public static let shared = SyncHelper()
|
|
|
|
private var configuration: SyncConfiguration
|
|
|
|
internal init(configuration: SyncConfiguration = .default) {
|
|
self.configuration = configuration
|
|
}
|
|
|
|
/// Updates the sync configuration.
|
|
public func updateConfiguration(_ configuration: SyncConfiguration) {
|
|
self.configuration = configuration
|
|
}
|
|
|
|
// MARK: - Public Interface
|
|
|
|
/// Syncs data to the paired device if appropriate.
|
|
/// - Parameters:
|
|
/// - data: The data to sync.
|
|
/// - keyName: The key name for the application context.
|
|
/// - availability: The platform availability setting.
|
|
/// - syncPolicy: The sync policy setting.
|
|
/// - Throws: `StorageError.dataTooLargeForSync` if data exceeds size limit for automatic sync.
|
|
public func syncIfNeeded(
|
|
data: Data,
|
|
keyName: String,
|
|
availability: PlatformAvailability,
|
|
syncPolicy: SyncPolicy
|
|
) throws {
|
|
// Only sync for appropriate availability settings
|
|
guard availability == .all || availability == .phoneWithWatchSync else {
|
|
return
|
|
}
|
|
|
|
switch syncPolicy {
|
|
case .never:
|
|
return
|
|
|
|
case .automaticSmall:
|
|
guard data.count <= configuration.maxAutoSyncSize else {
|
|
throw StorageError.dataTooLargeForSync
|
|
}
|
|
try performSync(data: data, keyName: keyName)
|
|
|
|
case .manual:
|
|
try performSync(data: data, keyName: keyName)
|
|
}
|
|
}
|
|
|
|
/// Manually triggers a sync for the given data.
|
|
/// - Parameters:
|
|
/// - data: The data to sync.
|
|
/// - keyName: The key name for the application context.
|
|
/// - Throws: Various errors if sync fails.
|
|
public func manualSync(data: Data, keyName: String) throws {
|
|
try performSync(data: data, keyName: keyName)
|
|
}
|
|
|
|
/// Checks if sync is available.
|
|
/// - Returns: True if WatchConnectivity is supported and active.
|
|
public func isSyncAvailable() -> Bool {
|
|
guard WCSession.isSupported() else { return false }
|
|
|
|
let session = WCSession.default
|
|
guard session.activationState == .activated else { return false }
|
|
|
|
#if os(iOS)
|
|
return session.isPaired && session.isWatchAppInstalled
|
|
#else
|
|
return true
|
|
#endif
|
|
}
|
|
|
|
/// Gets the current application context.
|
|
/// - Returns: The current application context dictionary.
|
|
public func currentContext() -> [String: Any] {
|
|
guard WCSession.isSupported() else { return [:] }
|
|
return WCSession.default.applicationContext
|
|
}
|
|
|
|
// MARK: - Private Helpers
|
|
|
|
private func performSync(data: Data, keyName: String) throws {
|
|
guard WCSession.isSupported() else { return }
|
|
|
|
let session = WCSession.default
|
|
if session.delegate == nil {
|
|
setupSession()
|
|
}
|
|
|
|
guard session.activationState == .activated else { return }
|
|
|
|
#if os(iOS)
|
|
guard session.isPaired, session.isWatchAppInstalled else { return }
|
|
#endif
|
|
|
|
try session.updateApplicationContext([keyName: data])
|
|
}
|
|
|
|
private func setupSession() {
|
|
let session = WCSession.default
|
|
session.delegate = SessionDelegateProxy.shared
|
|
session.activate()
|
|
}
|
|
|
|
/// Handles received application context from the paired device.
|
|
/// This is called by the delegate proxy.
|
|
internal func handleReceivedContext(_ context: [String: Any]) async {
|
|
Logger.info(">>> [SYNC] Received application context with \(context.count) keys")
|
|
for (key, value) in context {
|
|
guard let data = value as? Data else {
|
|
continue
|
|
}
|
|
Logger.debug(">>> [SYNC] Processing received data for key: \(key)")
|
|
|
|
do {
|
|
try await StorageRouter.shared.updateFromSync(keyName: key, data: data)
|
|
} catch {
|
|
Logger.error("Failed to update storage from sync for key: \(key)", error: error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A private proxy class to handle WCSessionDelegate callbacks and route them to the SyncHelper actor.
|
|
private final class SessionDelegateProxy: NSObject, WCSessionDelegate {
|
|
static let shared = SessionDelegateProxy()
|
|
|
|
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
|
|
if let error = error {
|
|
Logger.error("WCSession activation failed: \(error.localizedDescription)")
|
|
} else {
|
|
Logger.info("WCSession activated with state: \(activationState.rawValue)")
|
|
}
|
|
}
|
|
|
|
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) {
|
|
Task {
|
|
await SyncHelper.shared.handleReceivedContext(applicationContext)
|
|
}
|
|
}
|
|
|
|
#if os(iOS)
|
|
func sessionDidBecomeInactive(_ session: WCSession) {}
|
|
func sessionDidDeactivate(_ session: WCSession) {
|
|
session.activate()
|
|
}
|
|
#endif
|
|
}
|