feat(watch): favorites list on the first screen, debounced snapshot push

- 워치 첫 화면 최상단에 '즐겨찾기' 항목 추가 — 앱에서 즐겨찾기한 행동을
  태그를 거치지 않고 한 탭 만에 연다. WatchActionInfo.isFavorite는
  optional로 실어 구버전 캐시 스냅숏도 그대로 디코딩된다.
- 워치 스냅숏 전송 디바운스(350ms): 모든 커밋마다 목표×다짐×3기간
  진행률을 전부 재집계하던 것을, 연타 중에는 마지막 상태 한 번만
  계산해 보내도록 변경 (명령 응답 경로의 즉시 스냅숏은 그대로).

iOS·watchOS 스킴 빌드 확인.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
This commit is contained in:
songyc macbook 2026-07-13 23:56:32 +09:00
parent 42b95c5d7f
commit b980d7df7a
3 changed files with 40 additions and 2 deletions

View File

@ -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 {

View File

@ -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<Void, Never>?
/// ( ).
/// ××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 }

View File

@ -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 {