diff --git a/myApp/HaruDanim/Haru_DanimWatch Watch App/ContentView.swift b/myApp/HaruDanim/Haru_DanimWatch Watch App/ContentView.swift index 12deb62..54f9b35 100644 --- a/myApp/HaruDanim/Haru_DanimWatch Watch App/ContentView.swift +++ b/myApp/HaruDanim/Haru_DanimWatch Watch App/ContentView.swift @@ -91,6 +91,25 @@ struct ContentView: View { } } Section { + // 앱에서 즐겨찾기한 행동은 태그를 거치지 않고 첫 화면에서 바로 연다 + let favorites = snapshot.actions.filter { $0.isFavorite ?? false } + if !favorites.isEmpty { + NavigationLink { + WatchActionListView(title: String(localized: "즐겨찾기"), actions: favorites) + } label: { + HStack(spacing: 8) { + Image(systemName: "star.fill") + .font(.system(size: 11)) + .foregroundStyle(.yellow) + Text("즐겨찾기") + .lineLimit(1) + Spacer() + Text("\(favorites.count)") + .font(.caption2) + .foregroundStyle(.secondary) + } + } + } ForEach(snapshot.tags) { tag in let actions = snapshot.actions.filter { $0.tagIDs.contains(tag.id) } if !actions.isEmpty { diff --git a/myApp/HaruDanim/IOS/Core/WatchSyncManager.swift b/myApp/HaruDanim/IOS/Core/WatchSyncManager.swift index 88f1b04..88d0aa2 100644 --- a/myApp/HaruDanim/IOS/Core/WatchSyncManager.swift +++ b/myApp/HaruDanim/IOS/Core/WatchSyncManager.swift @@ -62,7 +62,8 @@ final class WatchSyncManager: NSObject { todayValue: todayValue, isRunning: action.isRunning, tickingBase: action.isRunning && !isCount ? now.addingTimeInterval(-todayValue) : nil, - tagIDs: action.sortedTags.map(\.uuid) + tagIDs: action.sortedTags.map(\.uuid), + isFavorite: action.isFavorite ) } @@ -126,8 +127,23 @@ final class WatchSyncManager: NSObject { private var lastComplicationStateKey = "" private var lastComplicationPushAt = Date.distantPast - /// 최신 스냅숏을 워치로 전송 (데이터 변경 시마다 호출) + /// 디바운스 중인 전송 작업 (연타 대비) + private var pushTask: Task? + + /// 최신 스냅숏을 워치로 전송 (데이터 변경 시마다 호출). + /// 스냅숏 생성은 모든 목표×다짐×3기간 진행률을 집계하는 비싼 작업이라, + /// 짧은 디바운스로 연타(횟수 버튼 연속 탭 등) 중의 중간 계산을 건너뛰고 + /// 마지막 상태 한 번만 만들어 보낸다. func pushSnapshot() { + pushTask?.cancel() + pushTask = Task { @MainActor in + try? await Task.sleep(for: .milliseconds(350)) + guard !Task.isCancelled else { return } + self.pushSnapshotNow() + } + } + + private func pushSnapshotNow() { guard WCSession.isSupported() else { return } let session = WCSession.default guard session.activationState == .activated, session.isPaired, session.isWatchAppInstalled else { return } diff --git a/myApp/HaruDanim/WatchShared/WatchPayload.swift b/myApp/HaruDanim/WatchShared/WatchPayload.swift index 1883f49..e1c9ab8 100644 --- a/myApp/HaruDanim/WatchShared/WatchPayload.swift +++ b/myApp/HaruDanim/WatchShared/WatchPayload.swift @@ -69,6 +69,9 @@ nonisolated struct WatchActionInfo: Codable, Identifiable, Hashable { var tickingBase: Date? /// 소속 꼬리표 id들 (비어 있으면 '꼬리표 없음' 그룹) var tagIDs: [UUID] + /// 앱에서 즐겨찾기로 등록한 행동 — 워치 첫 화면의 즐겨찾기 목록에 모인다. + /// optional인 이유: 구버전 워치에 캐시된 스냅숏(키 없음)도 디코딩되게 하기 위함 + var isFavorite: Bool? = nil } nonisolated struct WatchGoalInfo: Codable, Identifiable, Hashable {