diff --git a/myApp/HaruDanim/Haru_DanimWatch Watch App/Haru_DanimWatchApp.swift b/myApp/HaruDanim/Haru_DanimWatch Watch App/Haru_DanimWatchApp.swift index bd97eea..05c9384 100644 --- a/myApp/HaruDanim/Haru_DanimWatch Watch App/Haru_DanimWatchApp.swift +++ b/myApp/HaruDanim/Haru_DanimWatch Watch App/Haru_DanimWatchApp.swift @@ -9,6 +9,12 @@ import SwiftUI @main struct Haru_DanimWatch_Watch_AppApp: App { + init() { + // 컴플리케이션용 스냅숏은 워치 앱이 백그라운드로 깨어나 받는다. + // 그 시점에 델리게이트가 있어야 하므로 화면 표시 전에 세션을 활성화한다 + WatchStore.shared.activate() + } + var body: some Scene { WindowGroup { ContentView() diff --git a/myApp/HaruDanim/Haru_DanimWatch Watch App/WatchStore.swift b/myApp/HaruDanim/Haru_DanimWatch Watch App/WatchStore.swift index fb5bd03..fbc2846 100644 --- a/myApp/HaruDanim/Haru_DanimWatch Watch App/WatchStore.swift +++ b/myApp/HaruDanim/Haru_DanimWatch Watch App/WatchStore.swift @@ -113,6 +113,16 @@ extension WatchStore: WCSessionDelegate { WatchStore.shared.apply(decoded) } } + + /// iPhone이 컴플리케이션 갱신용 전용 채널로 미는 스냅숏. + /// 워치 앱이 꺼져 있어도 백그라운드로 깨어나 수신 → 캐시 + 타임라인 갱신(apply) + nonisolated func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) { + guard let data = userInfo[WatchSync.snapshotKey] as? Data, + let decoded = WatchSnapshot.decode(data) else { return } + Task { @MainActor in + WatchStore.shared.apply(decoded) + } + } } // MARK: - 색 변환 (워치 타깃 전용) diff --git a/myApp/HaruDanim/Haru_DanimWatchWidgets/ComplicationSupport.swift b/myApp/HaruDanim/Haru_DanimWatchWidgets/ComplicationSupport.swift index 90e03ae..9ba5053 100644 --- a/myApp/HaruDanim/Haru_DanimWatchWidgets/ComplicationSupport.swift +++ b/myApp/HaruDanim/Haru_DanimWatchWidgets/ComplicationSupport.swift @@ -15,9 +15,7 @@ import WidgetKit // MARK: - 스냅숏 로드 nonisolated enum ComplicationStore { - static var defaults: UserDefaults { - UserDefaults(suiteName: "group.com.yechan.HaruDanim") ?? .standard - } + static let defaults: UserDefaults = UserDefaults(suiteName: "group.com.yechan.HaruDanim") ?? .standard static func snapshot() -> WatchSnapshot? { guard let data = defaults.data(forKey: WatchSync.cachedSnapshotKey) else { return nil } diff --git a/myApp/HaruDanim/IOS/Core/WatchSyncManager.swift b/myApp/HaruDanim/IOS/Core/WatchSyncManager.swift index a62ddf1..5d1582b 100644 --- a/myApp/HaruDanim/IOS/Core/WatchSyncManager.swift +++ b/myApp/HaruDanim/IOS/Core/WatchSyncManager.swift @@ -113,13 +113,33 @@ final class WatchSyncManager: NSObject { return snapshot } + /// 컴플리케이션 전용 전송의 최근 상태 (하루 전송 예산이 있어 절약해서 보냄) + private var lastComplicationStateKey = "" + private var lastComplicationPushAt = Date.distantPast + /// 최신 스냅숏을 워치로 전송 (데이터 변경 시마다 호출) func pushSnapshot() { guard WCSession.isSupported() else { return } let session = WCSession.default guard session.activationState == .activated, session.isPaired, session.isWatchAppInstalled else { return } - guard let data = makeSnapshot().encoded() else { return } + let snapshot = makeSnapshot() + guard let data = snapshot.encoded() else { return } + // applicationContext: 워치 앱이 다음에 열릴 때 확실히 전달되는 기본 채널 try? session.updateApplicationContext([WatchSync.snapshotKey: data]) + pushForComplicationIfNeeded(session: session, snapshot: snapshot, data: data) + } + + /// 워치 앱이 꺼져 있어도 컴플리케이션이 갱신되도록 전용 채널로 전송. + /// 시스템이 하루 전송 횟수를 제한하므로 측정 상태가 바뀌었거나 30분이 지났을 때만 보낸다 + private func pushForComplicationIfNeeded(session: WCSession, snapshot: WatchSnapshot, data: Data) { + guard session.isComplicationEnabled else { return } + let stateKey = "\(snapshot.runningName ?? "")|\(snapshot.extraRunningCount)|\(snapshot.isPremium)" + let now = Date.now + guard stateKey != lastComplicationStateKey + || now.timeIntervalSince(lastComplicationPushAt) > 30 * 60 else { return } + lastComplicationStateKey = stateKey + lastComplicationPushAt = now + session.transferCurrentComplicationUserInfo([WatchSync.snapshotKey: data]) } // MARK: 명령 처리