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:
parent
42b95c5d7f
commit
b980d7df7a
@ -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 {
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user