From 0cdb566ad5cf722f94c5f604cb3281b4843a772d Mon Sep 17 00:00:00 2001 From: songyc macbook Date: Fri, 10 Jul 2026 14:44:48 +0900 Subject: [PATCH] =?UTF-8?q?fix(watch):=20=EC=BB=B4=ED=94=8C=EB=A6=AC?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=85=98=EC=9D=B4=20=EC=9B=8C=EC=B9=98=20?= =?UTF-8?q?=EC=95=B1=EC=9D=84=20=EC=97=B4=EC=A7=80=20=EC=95=8A=EC=95=84?= =?UTF-8?q?=EB=8F=84=20=EA=B0=B1=EC=8B=A0=EB=90=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 워치 앱 시작(App.init) 시점에 WCSession 활성화: 백그라운드로 깨어나 스냅숏을 받을 수 있도록 델리게이트를 미리 설정 - iPhone이 transferCurrentComplicationUserInfo 전용 채널로도 스냅숏 전송 (전송 예산 고려: 측정 상태 변화 또는 30분 경과 시에만) - 워치 didReceiveUserInfo 수신 → 캐시 저장 + 컴플리케이션 타임라인 갱신 - ComplicationStore.defaults 단일 인스턴스화 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7 --- .../Haru_DanimWatchApp.swift | 6 +++++ .../WatchStore.swift | 10 +++++++++ .../ComplicationSupport.swift | 4 +--- .../HaruDanim/IOS/Core/WatchSyncManager.swift | 22 ++++++++++++++++++- 4 files changed, 38 insertions(+), 4 deletions(-) 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: 명령 처리