feat(watch): per-goal opt-in filter for complication list
- 목표 편집에 '애플워치에서 보기' 토글 추가 (iPhone 전용 — 워치가 iPhone과 페어링되므로 iPad에서는 숨김·무의미, 기본 꺼짐) — 목표·다짐이 늘면 컴플리케이션 목록이 폭증하는 문제의 옵트인 해법 (사용자 요청) - 저장은 기기 로컬 LocalPrefs `local.watchGoals` (§15-3 노출 설정 원칙, pinnedGoals 선례) — CloudKit 스키마 무변경·페이로드 무변경으로 리스크 최소화 - 필터는 WatchSyncManager.makeSnapshot 한 곳: 스냅숏의 목표 = 진행 중 ∩ 체크됨 → 목록(recommendations)·표시 모두 자동 반영, 완료 목표는 기존 진행 중 조건으로 자동 제외. 행동·측정 중 상태(워치 앱 화면·현재 현황·iPad 기록의 동기화 경로)는 필터와 무관 — 지연 추가 0 - 검증(페어링 시뮬레이터 E2E): -watchGoals 1 → 워치 컴플리케이션에 토익 목표+영어 공부 다짐만 표시, -watchGoals 0 → '목표 없음'/'다짐 없음' 빈 상태로 우아하게 처리(크래시 없음), 현재 현황(독서 타이머)은 필터와 무관하게 정상 수신. 아이폰 편집기 토글 렌더 확인. Debug·Store·워치 3스킴 빌드 성공 - 도움말 컴플리케이션 항목 갱신 + 신규 문구 en/ja 완비(카탈로그 766키 클린), §14에 -watchGoals 인자, §5.2/§6.4/§11 문서화 - 주의: 기본 꺼짐이라 업데이트 직후 기존에 페이스에 올린 목표·다짐 컴플리케이션은 빈 상태가 됨 — 목표 편집에서 원하는 목표를 한 번 켜 주면 복구 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
This commit is contained in:
parent
2a176fa8a2
commit
6880983b54
@ -145,7 +145,7 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f
|
||||
|
||||
### 5.2 동기화되는 데이터 vs 기기 로컬 설정 (`Shared/LocalPrefs.swift`)
|
||||
- **데이터**(행동·목표·기록·일기)는 SwiftData(+CloudKit), **"이 기기에서 어떻게 보여줄지"는 App Group UserDefaults**:
|
||||
- `local.actionOrder`(모음 탭 배치 — 위젯·워치·인텐트 나열 순서도 이걸 따름), `local.pinnedGoals`(모음 탭 노출 목표), `local.collapsedGoals`, `local.notePromptActions`(기록 시 메모 창)
|
||||
- `local.actionOrder`(모음 탭 배치 — 위젯·워치·인텐트 나열 순서도 이걸 따름), `local.pinnedGoals`(모음 탭 노출 목표), `local.collapsedGoals`, `local.notePromptActions`(기록 시 메모 창), `local.watchGoals`(워치 컴플리케이션에 노출할 목표 — 기본 비어 있음(옵트인), 워치는 이 아이폰과 페어링되므로 기기 로컬이 자연스러움)
|
||||
- 대응하는 @Model 레거시 필드는 스키마 안정성 위해 남겨두고 1회 이관(`adoptLegacyModelValuesIfNeeded`)
|
||||
- 설정 키는 `Shared/Settings.swift` — 집계 기준·테마·대표시간 등은 App Group defaults(위젯 공유), navStyle·visibleTabs·radialTabOrder·diary.section* 은 standard(기기 로컬)
|
||||
|
||||
@ -186,7 +186,7 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f
|
||||
### 6.4 목표 탭 (`GoalViews`, `QuestEditorView`)
|
||||
- 진행 중 목표만 기본 목록(섹션당 목표 행 + 다짐 행들, 다짐 접기/펼치기는 기기 로컬). 완료(달성/미달성) 목표는 "완료된 목표" 별도 화면
|
||||
- 목표 행: 상태 뱃지(진행 중/달성/미달성/확인 필요) + 기간 진행률 바(종료일 있을 때). "확인 필요"는 탭해서 달성/미달성 직접 선택
|
||||
- 목표 편집: 내용·아이콘·색·시작일(과거 가능)·종료일(선택)·**달성 판정 기준 슬라이더(10~100%, 5% 단위, 기본 100%)**
|
||||
- 목표 편집: 내용·아이콘·색·시작일(과거 가능)·종료일(선택)·**달성 판정 기준 슬라이더(10~100%, 5% 단위, 기본 100%)**·**'애플워치에서 보기' 토글(iPhone 전용 — 기본 꺼짐, 켠 목표만 워치 컴플리케이션 목록에 노출. 기기 로컬 `local.watchGoals` §5.2)**
|
||||
- 목표 상세: 다짐 목록(탭=수정, 스와이프 삭제, 순서 모드), 다짐 추가(무료 한도 3개), 종료일 없으면 수동 종료(기준<100%면 안내 문구에 기준 표기), 삭제
|
||||
- 다짐 편집 3단계: ①대상(행동 또는 꼬리표+측정 기준) ②주기(하루[매일/요일/날짜/n번째 요일]·주·월·특정 기간 — ordinalWeekday 구현은 weekdayOrdinal 즉 "그 달의 n번째 ◯요일"이며 UI 문구도 그렇게 표기. 하루 단위는 '마감 시각까지만 집계' 토글+시간 선택 — §4.2 마감 규칙) ③목표량(시간 휠 또는 횟수 스테퍼)+방향(이상 달성/이하 유지)
|
||||
- **연속 달성**(`QuestProgress.streak` → `QuestStreakInfo`): 하루 단위=적용일만 연속 계산("연속 N일" — 매주 수요일 다짐을 4주 연속이면 연속 4일), 주간/월간="연속 N주/N달", 특정 기간=nil. 진행 중인 오늘/이번 주기는 atLeast면 미달이어도 끊기지 않고(건너뜀) 채웠으면 포함, atMost는 넘는 순간 끊김. **하한 = 목표 시작일** — 행동이 아니라 "목표 안 다짐"의 연속이므로 목표 시작 전 날짜/주기는 세지 않음(주·월은 시작일이 걸친 부분 주기까지 포함). 특히 atMost는 기록 없는 날이 전부 달성이라 이 하한이 없으면 과거 전체가 연속으로 잡힘. 소급 상한 400일(성능). 기록을 하루 키로 1회 버킷팅해 O(기록+일수). **미래 시각 기록(수동 입력)은 진행률·게이지에는 즉시 반영되지만 연속 계산은 now까지만 집계** — 그 시각이 지나야 연속에 반영된다(선기록으로 연속이 미리 쌓이지 않게 하는 의도된 동작, 고치지 말 것). 표기: 목표 탭 다짐 행 + 위젯 ②다짐 줄·③링 셀·④현황 그리드 (불꽃+노랑, 0이면 숨김). '스트릭'이라는 단어는 한국어 UI에 쓰지 않는다
|
||||
@ -288,7 +288,8 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f
|
||||
- 워치→iPhone: `sendMessage`(응답=최신 스냅숏), 실패/미활성 시 `transferUserInfo` 큐 폴백 (5분 지난 큐 명령은 무시)
|
||||
- 스냅숏은 워치 App Group defaults에 캐시(오래된 스냅숏 역행 방지: generatedAt 비교) → 컴플리케이션이 읽음
|
||||
- **컴플리케이션** 3종 (15분 .after 타임라인): 현재 현황(대표 행동+타이머+N) / 목표 달성률(기간 선택, 게이지) / 다짐 달성률(rectangular는 3기간 모두, **atMost는 게이지 대신 한도 안/초과 상태 표현**)
|
||||
- **추천 목록(recommendations) = 선택지 전부**: 워치는 컴플리케이션 파라미터 편집 UI가 없어 페이스 편집기의 목록이 recommendations()가 돌려준 조합으로 한정된다 — 진행 중 **목표 전부 × 3기간, 다짐 전부 × 3기간**을 상한 없이 나열할 것(예전 prefix(2) 캡이 "3번째 목표부터 페이스에 추가 불가" 버그였음). 워치 앱이 스냅숏을 적용할 때 `invalidateConfigurationRecommendations()`를 호출해 목표·다짐 변경이 목록에 자동 반영된다
|
||||
- **추천 목록(recommendations) = 선택지 전부**: 워치는 컴플리케이션 파라미터 편집 UI가 없어 페이스 편집기의 목록이 recommendations()가 돌려준 조합으로 한정된다 — 스냅숏에 담긴 **목표 전부 × 3기간, 다짐 전부 × 3기간**을 상한 없이 나열할 것(예전 prefix(2) 캡이 "3번째 목표부터 페이스에 추가 불가" 버그였음). 워치 앱이 스냅숏을 적용할 때 `invalidateConfigurationRecommendations()`를 호출해 목표·다짐 변경이 목록에 자동 반영된다
|
||||
- **스냅숏의 목표 = 진행 중 ∩ '애플워치에서 보기' 켠 것**(`makeSnapshot`의 단일 필터 지점, `LocalPrefs.showsOnWatch`) — 목표·다짐이 많아지면 목록이 폭증하므로 옵트인(기본 전부 꺼짐 → 컴플리케이션은 '목표 없음' 빈 상태로 시작). 완료 목표는 진행 중 조건으로 자동 제외. 행동·측정 중 상태(워치 앱 화면, 현재 현황)는 필터와 무관. 페이스에 이미 올린 미체크·완료 목표 컴플리케이션은 스냅숏 첫 목표로 폴백(없으면 빈 상태)
|
||||
- **유령 타이머 안전장치**(현재 현황): '측정 중' 스냅숏이 `generatedAt` 기준 2시간을 넘기면 무한 타이머 대신 "새로고침 필요" 표시로 강등(`StatusProvider.runningTrustInterval`) — 종료 푸시가 유실돼도 유령 타이머가 몇 시간씩 남지 않음. 워치 앱을 열면 즉시 복구, 진짜 장시간 측정도 다음 스냅숏에 타이머 복귀(데이터 무영향)
|
||||
- ⚠️ `recommendations()`의 description에 포맷 텍스트 금지 — `Text(verbatim:)`만 (WidgetKit assertion으로 익스텐션 즉사)
|
||||
|
||||
@ -325,7 +326,7 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f
|
||||
|---|---|
|
||||
| 모음 | `-startEditing` `-expandGoalCard` `-pinGoals <N>` `-openStatsFor "이름"` `-openHistoryFor "이름"` `-autoStart "이름"` |
|
||||
| 측정 알림 | `-settings.longSessionAlertHours <N>`(설정 주입) `-longSessionAlertTestSeconds <N>`(시간 대신 N초 발화, provisional 권한 자동) `-alertDump`(예약 상태 → Documents/session-alerts.txt) |
|
||||
| 목표 | `-goalShowEditor` `-goalShowFinished` `-goalReorder` `-goalScrollBottom` `-questShowEditor`(다짐 편집기 표시 — 마감 시각 다짐 우선, 없으면 첫 다짐) `-endGoalYesterday "제목"`(종료일을 어제로 — 실행 시점 자동 판정 검증) `-streakDump`(모든 다짐의 연속 계산 결과 → Documents/streak-dump.txt) `-progressSelfTest`(spanProgress 수치 37건 자가 검증[환산 캡·수행일 집계·마감·atMost·custom] → Documents/progress-self-test.txt — 인메모리 컨테이너·설정 고정이라 실데이터/기기 설정 무영향) |
|
||||
| 목표 | `-goalShowEditor` `-goalShowFinished` `-goalReorder` `-goalScrollBottom` `-questShowEditor`(다짐 편집기 표시 — 마감 시각 다짐 우선, 없으면 첫 다짐) `-endGoalYesterday "제목"`(종료일을 어제로 — 실행 시점 자동 판정 검증) `-watchGoals <N>`(생성순 목표 N개를 '애플워치에서 보기'로 설정, 0=전체 해제 — 설정 후 스냅숏 즉시 푸시, 컴플리케이션 필터 검증용) `-streakDump`(모든 다짐의 연속 계산 결과 → Documents/streak-dump.txt) `-progressSelfTest`(spanProgress 수치 37건 자가 검증[환산 캡·수행일 집계·마감·atMost·custom] → Documents/progress-self-test.txt — 인메모리 컨테이너·설정 고정이라 실데이터/기기 설정 무영향) |
|
||||
| 기록/통계 | `-historyMode timetable` `-historyWeekly` `-excludeActions "이름,이름"` `-historyShowFilter` `-statShowFilter` `-statSpan <day|week|month>` `-statScrollBottom` `-showExport` `-exportDump`(Documents/export-dump.png) |
|
||||
| 설정/기타 | `-settingsScrollGoal` `-settingsScrollPremium` `-premiumPreview` `-helpPreview` `-dataExportPreview`(+`-dataExportRun`이면 CSV 자동 생성 후 Documents 복사) `-widgetPreview YES|lock` `-widgetPreviewScroll <앵커>` `-widgetRenderMode accented|vibrant`(틴트/투명 홈 렌더링 분기 검증) `cloudSync`(standard bool로 강제) `-intentSmokeTest`(시리 인텐트 10개 시나리오 실행 → Documents/intent-smoke-test.txt, -seedDemo·-premium과 함께) |
|
||||
| 버블 | `-radialExpanded` `-radialDoubleTapTest`(2초 뒤 FAB 더블탭 재현 — `-settings.radialDoubleTapTab <탭>`과 함께) |
|
||||
|
||||
@ -47,6 +47,7 @@ struct ContentView: View {
|
||||
DebugSeed.seedBulkIfRequested(context: context)
|
||||
DebugSeed.autoStartIfRequested(context: context)
|
||||
DebugSeed.pinGoalsIfRequested(context: context)
|
||||
DebugSeed.watchGoalsIfRequested(context: context)
|
||||
DebugSeed.endGoalYesterdayIfRequested(context: context)
|
||||
DebugSeed.dumpStreaksIfRequested(context: context)
|
||||
DebugSeed.runProgressSelfTestIfRequested()
|
||||
|
||||
@ -438,6 +438,20 @@ enum DebugSeed {
|
||||
try? lines.joined(separator: "\n").write(to: url, atomically: true, encoding: .utf8)
|
||||
}
|
||||
|
||||
/// `-watchGoals <N>` 런치 인자: 생성순 목표 N개를 '애플워치에서 보기'로 설정
|
||||
/// (0이면 전체 해제 — 워치 컴플리케이션 필터 검증용. 설정 후 스냅숏을 즉시 푸시)
|
||||
static func watchGoalsIfRequested(context: ModelContext) {
|
||||
guard UserDefaults.standard.object(forKey: "watchGoals") != nil else { return }
|
||||
let n = UserDefaults.standard.integer(forKey: "watchGoals")
|
||||
let descriptor = FetchDescriptor<Goal>(sortBy: [SortDescriptor(\.createdAt)])
|
||||
guard let goals = try? context.fetch(descriptor) else { return }
|
||||
LocalPrefs.defaults.set(
|
||||
LocalPrefs.rawValue(goals.prefix(n).map(\.uuid)),
|
||||
forKey: LocalPrefsKeys.watchGoals
|
||||
)
|
||||
WatchSyncManager.shared.pushSnapshot()
|
||||
}
|
||||
|
||||
/// `-pinGoals <N>` 런치 인자: 생성순으로 목표 N개를 모음 탭에 표시 (다중 목표 카드 검증용)
|
||||
static func pinGoalsIfRequested(context: ModelContext) {
|
||||
let n = UserDefaults.standard.integer(forKey: "pinGoals")
|
||||
|
||||
@ -67,10 +67,14 @@ final class WatchSyncManager: NSObject {
|
||||
)
|
||||
}
|
||||
|
||||
let goals = (try? context.fetch(FetchDescriptor<Goal>(
|
||||
// 컴플리케이션(목표·다짐 달성률)에는 진행 중이면서 '애플워치에서 보기'를 켠 목표만 보낸다.
|
||||
// 목록(recommendations)·표시 모두 이 스냅숏 기반이라 여기 한 곳이 유일한 필터 지점이고,
|
||||
// 행동·측정 중 상태(워치 앱 화면, 현재 현황 컴플리케이션)에는 영향이 없다.
|
||||
let goals = ((try? context.fetch(FetchDescriptor<Goal>(
|
||||
predicate: #Predicate { $0.statusRaw == "inProgress" },
|
||||
sortBy: [SortDescriptor(\.sortOrder), SortDescriptor(\.createdAt)]
|
||||
))) ?? []
|
||||
))) ?? [])
|
||||
.filter { LocalPrefs.showsOnWatch($0.uuid) }
|
||||
snapshot.goals = goals.map { goal in
|
||||
WatchGoalInfo(
|
||||
id: goal.uuid,
|
||||
|
||||
@ -792,12 +792,6 @@
|
||||
},
|
||||
"'%@ · %@' 오늘은 수행일이 아니에요.": {
|
||||
"localizations": {
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": "'%1$@ · %2$@' 오늘은 수행일이 아니에요."
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
@ -809,6 +803,12 @@
|
||||
"state": "translated",
|
||||
"value": "「%1$@ · %2$@」は今日は実行日ではありません。"
|
||||
}
|
||||
},
|
||||
"ko": {
|
||||
"stringUnit": {
|
||||
"state": "new",
|
||||
"value": "'%1$@ · %2$@' 오늘은 수행일이 아니에요."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -6757,6 +6757,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"애플워치": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Apple Watch"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Apple Watch"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"애플워치 (프리미엄)": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
@ -6789,6 +6805,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"애플워치에서 보기": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Show on Apple Watch"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Apple Watchに表示"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"앱 설정과 일치": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
@ -7461,18 +7493,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"워치 페이스를 길게 눌러 '편집' → 컴플리케이션 자리에서 하루 다님을 골라요. 현재 측정 중인 행동, 특정 목표의 달성률, 특정 다짐의 진행률을 페이스에서 바로 볼 수 있어요. 어떤 목표·다짐을 보여줄지는 컴플리케이션을 탭해 설정해요.": {
|
||||
"워치 페이스를 길게 눌러 '편집' → 컴플리케이션 자리에서 하루 다님을 골라요. 현재 측정 중인 행동, 목표의 달성률, 다짐의 진행률을 페이스에서 바로 볼 수 있어요. 목표·다짐 달성률 목록에는 목표를 만들거나 수정할 때 '애플워치에서 보기'를 켠 목표와 그 다짐들만 하루·주간·월간으로 나타나요 — 기본은 꺼져 있고, 완료된 목표는 목록에서 자동으로 사라져요.": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Long-press your watch face, choose 'Edit', and pick Haru Danim in a complication slot. You can see the currently tracked action, a goal's completion rate, or a quest's progress right on the face. Tap the complication to choose which goal or quest to show."
|
||||
"value": "Long-press your watch face, choose 'Edit', and pick Haru Danim in a complication slot. You can see the currently tracked action, a goal's completion rate, or a quest's progress right on the face. The goal and quest lists only include goals with 'Show on Apple Watch' turned on (when creating or editing a goal), each offered as daily, weekly and monthly — it's off by default, and finished goals disappear from the list automatically."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ウォッチフェイスを長押しして「編集」→コンプリケーション枠でHaru Danimを選びます。計測中のアクション、特定の目標の達成率、特定のクエストの進捗をフェイスで直接確認できます。どの目標・クエストを表示するかはコンプリケーションをタップして設定します。"
|
||||
"value": "ウォッチフェイスを長押しして「編集」→コンプリケーション枠でHaru Danimを選びます。計測中のアクション、目標の達成率、クエストの進捗をフェイスで直接確認できます。目標・クエスト達成率の一覧には、目標の作成・編集時に「Apple Watchに表示」をオンにした目標とそのクエストだけが1日・週間・月間で表示されます — 初期状態はオフで、完了した目標は一覧から自動的に消えます。"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -9901,6 +9933,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"켜면 이 목표와 소속 다짐들의 진행률이 애플워치 컴플리케이션 목록에 나타나요. 완료된 목표는 목록에서 자동으로 사라져요. 이 설정은 워치와 연결된 이 아이폰에만 저장돼요.": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "When on, this goal and its quests appear in the Apple Watch complication list. Finished goals disappear from the list automatically. This setting is saved only on this iPhone, the one paired with your watch."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "オンにすると、この目標とそのクエストの達成率がApple Watchのコンプリケーション一覧に表示されます。完了した目標は一覧から自動的に消えます。この設定はウォッチとペアリングされたこのiPhoneにのみ保存されます。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"켜면 횟수를 추가할 때마다 메모 창이 떠요. 건너뛸 수 있어요.": {
|
||||
"localizations": {
|
||||
"en": {
|
||||
|
||||
@ -614,6 +614,8 @@ struct GoalEditorView: View {
|
||||
@State private var hasEndDate = false
|
||||
@State private var endDate: Date = .now
|
||||
@State private var achieveThreshold = 100
|
||||
/// 워치 컴플리케이션 목록 노출 여부 — 기기 로컬 설정(LocalPrefs), 기본 꺼짐
|
||||
@State private var showsOnWatch = false
|
||||
@State private var showingSymbolPicker = false
|
||||
|
||||
var body: some View {
|
||||
@ -677,6 +679,23 @@ struct GoalEditorView: View {
|
||||
} footer: {
|
||||
Text("목표가 끝날 때 다짐들의 평균 달성률이 이 값 이상이면 '달성 완료'로 판정해요. 100%면 모든 다짐을 채워야 달성이에요. 다짐이 없는 목표는 지금처럼 직접 선택해요.")
|
||||
}
|
||||
// 워치는 iPhone과 페어링되고 이 설정은 기기 로컬이라 iPad에서는 의미가 없다
|
||||
if !DeviceLayout.isPad {
|
||||
Section {
|
||||
Toggle(isOn: $showsOnWatch) {
|
||||
Label {
|
||||
Text("애플워치에서 보기")
|
||||
} icon: {
|
||||
Image(systemName: "applewatch")
|
||||
.foregroundStyle(AppTheme.green)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("애플워치")
|
||||
} footer: {
|
||||
Text("켜면 이 목표와 소속 다짐들의 진행률이 애플워치 컴플리케이션 목록에 나타나요. 완료된 목표는 목록에서 자동으로 사라져요. 이 설정은 워치와 연결된 이 아이폰에만 저장돼요.")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(goal == nil ? String(localized: "목표 추가") : String(localized: "목표 수정"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
@ -703,6 +722,7 @@ struct GoalEditorView: View {
|
||||
color = goal.color
|
||||
startDate = goal.startDate
|
||||
achieveThreshold = goal.achieveThresholdPercent
|
||||
showsOnWatch = LocalPrefs.showsOnWatch(goal.uuid)
|
||||
if let end = goal.endDate {
|
||||
hasEndDate = true
|
||||
endDate = end
|
||||
@ -719,6 +739,8 @@ struct GoalEditorView: View {
|
||||
goal.startDate = startDate
|
||||
goal.endDate = hasEndDate ? endDate : nil
|
||||
goal.achieveThresholdPercent = achieveThreshold
|
||||
// 워치 노출은 기기 로컬 설정 — CloudKit 모델에 쓰지 않는다 (LocalPrefs 참고)
|
||||
LocalPrefs.setShowsOnWatch(goal.uuid, enabled: showsOnWatch)
|
||||
if goal.status == .inProgress {
|
||||
// 종료일을 이미 지난 날짜로 고쳤다면 목록에 돌아가기 전에 즉시 판정
|
||||
// (판정 기준 시점은 종료일의 하루 끝 — evaluateIfEnded 참고)
|
||||
@ -742,6 +764,7 @@ struct GoalEditorView: View {
|
||||
let existing = (try? context.fetch(FetchDescriptor<Goal>())) ?? []
|
||||
newGoal.sortOrder = (existing.map(\.sortOrder).max() ?? -1) + 1
|
||||
context.insert(newGoal)
|
||||
LocalPrefs.setShowsOnWatch(newGoal.uuid, enabled: showsOnWatch)
|
||||
}
|
||||
DataChange.commit(context: context)
|
||||
dismiss()
|
||||
|
||||
@ -280,7 +280,7 @@ struct HelpView: View {
|
||||
HelpTopic(
|
||||
symbol: "watchface.applewatch.case",
|
||||
title: "컴플리케이션 추가",
|
||||
body: "워치 페이스를 길게 눌러 '편집' → 컴플리케이션 자리에서 하루 다님을 골라요. 현재 측정 중인 행동, 특정 목표의 달성률, 특정 다짐의 진행률을 페이스에서 바로 볼 수 있어요. 어떤 목표·다짐을 보여줄지는 컴플리케이션을 탭해 설정해요."
|
||||
body: "워치 페이스를 길게 눌러 '편집' → 컴플리케이션 자리에서 하루 다님을 골라요. 현재 측정 중인 행동, 목표의 달성률, 다짐의 진행률을 페이스에서 바로 볼 수 있어요. 목표·다짐 달성률 목록에는 목표를 만들거나 수정할 때 '애플워치에서 보기'를 켠 목표와 그 다짐들만 하루·주간·월간으로 나타나요 — 기본은 꺼져 있고, 완료된 목표는 목록에서 자동으로 사라져요."
|
||||
),
|
||||
HelpTopic(
|
||||
symbol: "arrow.triangle.2.circlepath",
|
||||
|
||||
@ -27,6 +27,10 @@ nonisolated enum LocalPrefsKeys {
|
||||
static let collapsedGoals = "local.collapsedGoals"
|
||||
/// 기록 시 메모 작성 창을 띄울 행동 — Action.uuid 문자열을 쉼표로 연결
|
||||
static let notePromptActions = "local.notePromptActions"
|
||||
/// 애플워치 컴플리케이션 목록에 노출할 목표 — Goal.uuid 문자열을 쉼표로 연결.
|
||||
/// 기본은 비어 있음(모두 꺼짐) — 목표·다짐이 많아지면 컴플리케이션 목록이 폭증하므로 옵트인.
|
||||
/// 워치는 이 아이폰과 페어링되므로 기기 로컬이 하드웨어 현실과 일치한다.
|
||||
static let watchGoals = "local.watchGoals"
|
||||
}
|
||||
|
||||
nonisolated enum LocalPrefs {
|
||||
@ -112,6 +116,18 @@ nonisolated enum LocalPrefs {
|
||||
defaults.set(toggling(id, in: raw), forKey: LocalPrefsKeys.notePromptActions)
|
||||
}
|
||||
|
||||
// MARK: 애플워치 컴플리케이션에 보일 목표 (스냅숏 생성 시점에 뷰 밖에서도 읽음)
|
||||
|
||||
static func showsOnWatch(_ id: UUID) -> Bool {
|
||||
contains(id, in: defaults.string(forKey: LocalPrefsKeys.watchGoals) ?? "")
|
||||
}
|
||||
|
||||
static func setShowsOnWatch(_ id: UUID, enabled: Bool) {
|
||||
let raw = defaults.string(forKey: LocalPrefsKeys.watchGoals) ?? ""
|
||||
guard contains(id, in: raw) != enabled else { return }
|
||||
defaults.set(toggling(id, in: raw), forKey: LocalPrefsKeys.watchGoals)
|
||||
}
|
||||
|
||||
// MARK: 구버전 @Model 속성 값 1회 이관
|
||||
|
||||
/// 각 키가 아직 없을 때만, 지금까지 모델에 저장돼 있던 값으로 채운다 (앱 시작 시 호출).
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user