feat(ux): 잠금화면 다짐 점 링 방식 + 즐겨찾기 순서 변경 시트 (실기기 피드백 2건)
첫 출시 전 반영 요청된 사용성 개선 — 둘 다 표시 계층·기기 로컬 설정만 건드리는 안전 범주. ① 잠금화면 위젯 다짐 점: '아래에서 차오르는 물높이' 채움은 5~6pt에서 90%↔100% 차이(위쪽 1px)가 배경에 따라 사실상 안 보임 — **미완료 = 링(호) 진행(시작 최소 10%, 완료 직전에도 틈이 보이게 최대 88%까지만 감김) / 완료 = 꽉 찬 원반**으로 교체. 링 끝의 틈은 둘레에 생겨 눈에 띄고, 완료는 면적(빈 링 vs 원반) 자체가 달라 어떤 배경에서도 구분된다. ratio 그대로라 '이하 유지'는 한도 안=원반/초과=빈 트랙으로 의미 유지 (§10 문서화) ② 즐겨찾기 순서 변경: 행동 탭 ⋯ 메뉴에 '즐겨찾기 순서 변경' 시트 추가(2개 이상일 때, 꼬리표 순서 시트와 동일 패턴 — 닫힐 때 1회 커밋). LocalPrefs.reorderFavorites는 전역 배치에서 즐겨찾기가 차지한 자리들만 재배열해 나머지 행동·위젯 기본 슬롯 무영향. 모음 탭 즐겨찾기 섹션·애플워치 즐겨찾기가 같은 순서를 읽으므로 세 표면이 자동 일관 (§6.2·§6.1·§11 문서화). move(fromOffsets:)는 SwiftUI 확장이라 Foundation 계층에 동일 의미로 직접 구현 - 도움말 '즐겨찾기 모아 보기' 본문에 순서 변경 경로·워치 적용 추가 - 신규 문구 4키 en/ja 완역, 옛 본문 stale 정리 (missing/stale 0) - §14에 -actionShowFavoriteOrder 검증 인자 추가 - 검증: Debug/Store 빌드, 잠금 위젯 프리뷰·순서 시트 스크린샷 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
14b18bc22a
commit
1dea41b472
@ -175,7 +175,7 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f
|
||||
### 6.1 모음 탭 (`MainView`)
|
||||
- 상단: 노출 목표 진행 현황 카드(1개=단독, 2개+=스냅 페이징 가로 스크롤 + 페이지 점, iPad=그리드). 카드 스타일 설정: 다짐별 각각(perQuest, 접힘 시 3개+더보기) / 전체 합산(combined)
|
||||
- "현재 진행 중" 영역: 측정 중 세션 나열 + 실시간 타이머 + 종료 버튼
|
||||
- **즐겨찾기·나머지 섹션**(`favoritesSection`/`othersSection`, `!favoriteActions.isEmpty && !isEditing`일 때 — 없으면 기존 통짜 그리드): `Action.isFavorite`(행동 탭과 동일 필드, §6.2) 켠 행동이 **그리드의 첫 섹션**(별 헤더)으로 올라오고, 나머지는 "나머지 행동 · N개" 섹션으로 나뉜다. 두 섹션 모두 통짜 그리드와 같은 `actionGrid(_:)` 빌더를 써서 셀 크기·열 규칙·맥 보정이 항상 동일(시각적으로 하나의 그리드). **v1(별도 카드+컴팩트 셀 가로 스크롤 줄, 2026-07-21)은 그리드와 이질적이고 같은 행동이 두 번 보여 "따로 노는 느낌"이라는 실사용 피드백(2026-07-22)으로 v2로 교체**. **'나머지 행동' 헤더를 탭하면 접힘/펼침**(기기별 `local.mainOthersCollapsed`, 기본 펼침, 셰브론 회전) — 행동이 많아도 접으면 즐겨찾기만 남는다. **숨기는 행동은 없음**(전부 같은 화면, 위치만 승격 — 그리드에서 완전히 숨기는 방식은 기각 유지). 편집(배치)도 같은 섹션 구조 유지(아래 '배치 편집' 항목). 각 섹션 순서는 기기별 배치 순서(`orderedActions`)를 그대로 따르고, **새로 즐겨찾기 지정하면(롱프레스 메뉴·행동 탭 스와이프·상세 토글 어느 경로든) 배치 순서상 즐겨찾기 블록 끝으로 이동**(`LocalPrefs.placeNewFavoriteAtEnd` — "지정한 순서대로 뒤에 붙는" 규칙. 해제는 위치 무이동이라 방금 뺀 행동이 나머지 상단 근처에 남고, 첫 즐겨찾기도 무이동). 탭 동작(`handleTap`)과 롱프레스 메뉴(`actionContextMenu(for:)` — 기록 확인/통계 보기/기록 직접 입력·수정/**즐겨찾기 지정·해제**/행동 설정 수정/삭제)는 전 섹션 공유. 즐겨찾기 지정·해제는 이 메뉴 또는 행동 탭에서 가능(§6.2)
|
||||
- **즐겨찾기·나머지 섹션**(`favoritesSection`/`othersSection`, `!favoriteActions.isEmpty && !isEditing`일 때 — 없으면 기존 통짜 그리드): `Action.isFavorite`(행동 탭과 동일 필드, §6.2) 켠 행동이 **그리드의 첫 섹션**(별 헤더)으로 올라오고, 나머지는 "나머지 행동 · N개" 섹션으로 나뉜다. 두 섹션 모두 통짜 그리드와 같은 `actionGrid(_:)` 빌더를 써서 셀 크기·열 규칙·맥 보정이 항상 동일(시각적으로 하나의 그리드). **v1(별도 카드+컴팩트 셀 가로 스크롤 줄, 2026-07-21)은 그리드와 이질적이고 같은 행동이 두 번 보여 "따로 노는 느낌"이라는 실사용 피드백(2026-07-22)으로 v2로 교체**. **'나머지 행동' 헤더를 탭하면 접힘/펼침**(기기별 `local.mainOthersCollapsed`, 기본 펼침, 셰브론 회전) — 행동이 많아도 접으면 즐겨찾기만 남는다. **숨기는 행동은 없음**(전부 같은 화면, 위치만 승격 — 그리드에서 완전히 숨기는 방식은 기각 유지). 편집(배치)도 같은 섹션 구조 유지(아래 '배치 편집' 항목). 각 섹션 순서는 기기별 배치 순서(`orderedActions`)를 그대로 따르고(행동 탭 ⋯ '즐겨찾기 순서 변경' 시트로도 재배열 가능 — §6.2), **새로 즐겨찾기 지정하면(롱프레스 메뉴·행동 탭 스와이프·상세 토글 어느 경로든) 배치 순서상 즐겨찾기 블록 끝으로 이동**(`LocalPrefs.placeNewFavoriteAtEnd` — "지정한 순서대로 뒤에 붙는" 규칙. 해제는 위치 무이동이라 방금 뺀 행동이 나머지 상단 근처에 남고, 첫 즐겨찾기도 무이동). 탭 동작(`handleTap`)과 롱프레스 메뉴(`actionContextMenu(for:)` — 기록 확인/통계 보기/기록 직접 입력·수정/**즐겨찾기 지정·해제**/행동 설정 수정/삭제)는 전 섹션 공유. 즐겨찾기 지정·해제는 이 메뉴 또는 행동 탭에서 가능(§6.2)
|
||||
- 행동 버튼 그리드: 꼬리표 색 그라데이션 배경 + 아이콘 + 이름 + 오늘 누적(시간형) 또는 오늘 횟수(횟수형, 숫자 전환 애니메이션). 측정 중이면 노란 테두리 + record 점. 탭 = 시작/종료 토글 또는 +1 (스프링 눌림 + 햅틱)
|
||||
- 한 줄 개수: iPhone 2~6개 설정(5개 이상은 compact 셀), iPad는 화면 폭 자동 채움 — **iPad의 목표 카드·행동 그리드는 LazyVGrid가 아니라 비-lazy `AdaptiveColumnsLayout`**(MainView 내 커스텀 Layout, LazyVGrid(.adaptive)와 같은 열 계산). lazy 높이 추정이 높이 제각각인 목표 카드 + 1초 타이머 갱신과 결합해 맨 아래 스크롤 시 화면이 무한 진동(터치 불능)하는 실기기 버그의 수정 — iPhone(고정 열·페이징 높이 고정)은 LazyVGrid 유지. iPad 모음 그리드를 다시 lazy로 되돌리지 말 것. **행동 셀 높이 통일 규칙**: 시간형·횟수형 푸터 높이가 달라(횟수형 27pt 숫자) 셀 자연 높이가 달랐음 — 푸터에 숫자 크기의 hidden 템플릿(Text "0")을 깔아 두 유형의 자연 높이를 맞추고, AdaptiveColumnsLayout.place가 LazyVGrid처럼 행 높이를 제안해 행 안 셀도 균일(측정 패스는 불변이라 진동 버그와 무관, 고정 높이 목표 카드 무영향)
|
||||
- 롱프레스 메뉴: 기록 확인(기록 탭 필터 이동) / 통계 보기 / 기록 직접 입력·수정 / **즐겨찾기 지정·해제** / 행동 설정 수정 / 삭제
|
||||
@ -186,9 +186,9 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f
|
||||
- 빈 상태(행동 0개): 개념 한 줄 소개 + '첫 행동 만들기'(추가 시트 직행) + '예시 행동으로 시작해 보기'(꼬리표 생활·건강 + 독서/운동/물 마시기 생성)
|
||||
|
||||
### 6.2 행동 탭 (`ActionViews`)
|
||||
- 즐겨찾기 섹션(스와이프로 토글) → 꼬리표별 그룹 → 꼬리표 없음. 탭하면 상세(정보·즐겨찾기·메모 창 토글·누적 통계·삭제)
|
||||
- 즐겨찾기 섹션(스와이프로 토글, **표시 순서 = 모음 탭 배치의 즐겨찾기 부분수열**) → 꼬리표별 그룹 → 꼬리표 없음. 탭하면 상세(정보·즐겨찾기·메모 창 토글·누적 통계·삭제)
|
||||
- 추가/수정: 이름, 아이콘(SF Symbols 검색+카테고리 선택기 `SymbolPickerView`/`SymbolCatalog`), 꼬리표 복수 선택(즉석 추가 가능), 추적 방식(**생성 시에만 선택 — 수정에서는 비활성**, 기존 기록의 표시·집계가 뒤섞이는 것 방지), 메모 창 여부. 무료 한도 초과 시 프리미엄 안내 얼럿
|
||||
- ⋯ 메뉴: 꼬리표 순서 변경 시트
|
||||
- ⋯ 메뉴: 꼬리표 순서 변경 시트 + **즐겨찾기 순서 변경 시트**(2개 이상일 때 — `LocalPrefs.reorderFavorites`가 전역 배치에서 즐겨찾기가 차지한 자리들만 재배열해 나머지 행동·위젯 기본 슬롯 무영향. 모음 탭 즐겨찾기 섹션·애플워치 즐겨찾기에 같은 순서로 반영, 닫힐 때 1회 커밋. 2026-07-22 실기기 피드백)
|
||||
|
||||
### 6.3 꼬리표 탭 (`TagViews`): 목록(행동 수 표시)·드래그 정렬·수정(이름/색: ColorPicker+프리셋 12색)·스와이프 삭제(행동은 유지)
|
||||
|
||||
@ -277,7 +277,7 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f
|
||||
| ④ 다짐 현황 `HaruGoalQuestGridWidget` | 표시 전용 링 그리드 + 목표 헤더. 소형 2×2 / 중형 4×2 / 대형: 다짐16 / 목표4 / 목표2 |
|
||||
| ⑤ 행동 통계 `HaruStatsChartWidget` | 꺾은선(행동 색). 기간: 일주일/한 달(일별, 소형 제외→주별 대체)/한 달(주별). 선 수 제한 소2/중4/대6 |
|
||||
| ⑥ 현재 진행 중 `HaruNowRunningWidget` | 측정 중인 대표 행동(설정 '대표 시간 기준' — 다이나믹 아일랜드와 같은 규칙)을 꼬리표 색 배경 + 실시간 타이머(`Text(style:.timer)`) + 종료 버튼(`RunActionIntent` 토글의 종료 방향)으로 표시. 소형/중형: 대표 1개 + "+N개 함께 추적 중". 대형: 대표 카드 + 함께 측정 중 나열(각각 종료 버튼, 4개 초과분은 "외 N개") / 단독이면 오늘 누적(실시간)·시작 시각 카드. 측정 중 아니면 빈 상태 안내, 갤러리 미리보기는 샘플 표시 |
|
||||
| 잠금화면 `HaruLockGoalWidget` | 목표 1개 달성률 — 원형 게이지/숫자만/다짐 점(**진행률만큼 아래에서부터 차오름**, 달성=완전 채움 — ratio 사용이라 이하 유지는 기존대로 한도 안=채움/초과=빈 점). circular/rectangular/inline |
|
||||
| 잠금화면 `HaruLockGoalWidget` | 목표 1개 달성률 — 원형 게이지/숫자만/다짐 점(**미완료=링이 진행률만큼 감김[시작 최소 10%·완료 직전에도 틈이 보이게 최대 88%], 완료=꽉 찬 원반** — 물높이 채움은 5~6pt에서 90%↔100% 구분 불가라 교체[2026-07-22 실기기 피드백]. ratio 사용이라 이하 유지는 한도 안=원반/초과=빈 트랙). circular/rectangular/inline |
|
||||
| 제어 센터 `HaruActionRunControl` | iOS 18 ControlWidget — 행동 1개 토글(시간형 시작/종료, 횟수형 +1). `ToggleActionControlIntent`(SetValueIntent+LiveActivityIntent, 앱 프로세스 실행) |
|
||||
|
||||
- 슬롯 규칙: 미선택이면 기본 대상 채움, 선택했으면 순서 존중 + 모자란 슬롯은 점선 빈 칸. '선택 안 함' 센티널 `NoneEntityID`
|
||||
@ -291,7 +291,7 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f
|
||||
|
||||
## 11. 애플워치 (프리미엄)
|
||||
|
||||
- **워치 앱**: 첫 화면 = [측정 중 섹션 — **행을 탭하면 대표 측정 즉시 종료**(행동 목록과 같은 run 명령 재사용, 오른쪽 정지 아이콘+'누르면 측정이 종료돼요' 푸터. 대표 식별은 `WatchSnapshot.runningActionID`(optional, 구버전 캐시는 이름 매칭 폴백 — `runningAction` 헬퍼)] + **즐겨찾기**(앱에서 즐겨찾기한 행동 직행, WatchActionInfo.isFavorite는 구버전 캐시 호환 위해 optional) + 꼬리표 목록 → 행동 목록 → 탭으로 실행. iPhone과 실시간 연동
|
||||
- **워치 앱**: 첫 화면 = [측정 중 섹션 — **행을 탭하면 대표 측정 즉시 종료**(행동 목록과 같은 run 명령 재사용, 오른쪽 정지 아이콘+'누르면 측정이 종료돼요' 푸터. 대표 식별은 `WatchSnapshot.runningActionID`(optional, 구버전 캐시는 이름 매칭 폴백 — `runningAction` 헬퍼)] + **즐겨찾기**(앱에서 즐겨찾기한 행동 직행, 나열 순서 = 아이폰 모음 탭 배치 = 즐겨찾기 순서 시트와 동일, WatchActionInfo.isFavorite는 구버전 캐시 호환 위해 optional) + 꼬리표 목록 → 행동 목록 → 탭으로 실행. iPhone과 실시간 연동
|
||||
- **통신** (`WatchSyncManager`(iOS) ↔ `WatchStore`(워치), 페이로드 `WatchShared/WatchPayload.swift`):
|
||||
- iPhone→워치: `updateApplicationContext`(기본) + `transferCurrentComplicationUserInfo`(측정 상태 변경 또는 30분 경과 시만 — 일일 예산 절약)
|
||||
- 워치→iPhone: `sendMessage`(응답=최신 스냅숏), 실패/미활성 시 `transferUserInfo` 큐 폴백 (5분 지난 큐 명령은 무시)
|
||||
@ -337,7 +337,7 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f
|
||||
| 측정 알림 | `-settings.longSessionAlertHours <N>`(설정 주입) `-longSessionAlertTestSeconds <N>`(시간 대신 N초 발화, provisional 권한 자동) `-alertDump`(예약 상태 → Documents/session-alerts.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) `-recordShowEditor <session|count>`(최신 기록 수정 시트) |
|
||||
| 설정/기타 | `-settingsScrollGoal` `-settingsScrollPremium` `-settingsScrollMeasure`(측정 섹션 — 짧은 기록 무시 푸터 검증) `-settingsShowDayStartWheel`(하루 시작 시간 휠 펼침 시작) `-premiumPreview` `-helpPreview`(+`-helpExpandAll`: 전 주제 펼침, `-helpScrollTo <help-main 등>`: 이미지 주제로 스크롤) `-dataExportPreview`(+`-dataExportRun`이면 CSV 자동 생성 후 Documents 복사) `-widgetPreview YES|lock` `-widgetPreviewScroll <앵커>` `-widgetRenderMode accented|vibrant`(틴트/투명 홈 렌더링 분기 검증) `cloudSync`(standard bool로 강제) `-intentSmokeTest`(시리 인텐트 10개 시나리오 실행 → Documents/intent-smoke-test.txt, -seedDemo·-premium과 함께) |
|
||||
| 설정/기타 | `-settingsScrollGoal` `-settingsScrollPremium` `-settingsScrollMeasure`(측정 섹션 — 짧은 기록 무시 푸터 검증) `-settingsShowDayStartWheel`(하루 시작 시간 휠 펼침 시작) `-premiumPreview` `-helpPreview`(+`-helpExpandAll`: 전 주제 펼침, `-helpScrollTo <help-main 등>`: 이미지 주제로 스크롤) `-dataExportPreview`(+`-dataExportRun`이면 CSV 자동 생성 후 Documents 복사) `-actionShowFavoriteOrder`(행동 탭 즐겨찾기 순서 시트 — `-startTab action`과 함께) `-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 <탭>`과 함께) |
|
||||
| 일기 | `-diarySeed`(필기 획 2줄 포함 — 달력 화면에서 먼저 시드 후 재실행으로 열 것) `-diaryOpenToday`(**`-startTab diary` 필수**) `-diaryPage <N>` `-diaryZoomScale <배>`(2.5초 뒤 fit×N 프로그램 줌 — 획 유지·선명도 확인) `-diaryShowConfig` `-diaryShowExport`(달력 화면 onAppear — `-diaryOpenToday`와 함께 쓰면 안 뜸) `-diaryExportRun pdf|image`(결과를 Documents로 복사) `-diaryShowCalendarPicker` `-diarySeedEvents` `-diaryHideFirstAction` `-diaryShowActionFilter` `-diaryHideFirstGoal` `-diaryShowGoalFilter` `-diarySeedPhoto`(오늘=이모지+사진·어제=사진만 기분 사진 시드 — 달력 백레이어 검증) `-diarySeedTemplate`(2쪽 샘플 PDF 양식 생성) `-diaryShowTemplates`(양식 관리 시트) `-diaryShowPageChooser`(페이지 추가 선택 시트) `-diaryApplyTemplate`(첫 양식으로 페이지 추가) / 섹션 강제: `-diary.sectionOrder "timetable,hero,goals"` `-diary.hiddenSections "mood,todos,records,bars"` |
|
||||
| 워치 | `-complicationPreview` `-complicationScroll` `-complicationScrollTo <stale\|quest>` `-autoRunFirstAction` `-autoStopRunning`(측정 중 행 탭과 동일 경로로 대표 측정 종료 — 페어링 심 E2E용) |
|
||||
|
||||
@ -6376,6 +6376,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"손잡이를 끌어 순서를 바꿔요. 모음 탭 '즐겨찾기'와 애플워치의 즐겨찾기 목록에 같은 순서로 적용돼요." : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Drag the handles to reorder. The same order applies to the Home tab's Favorites and the favorites list on Apple Watch."
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ハンドルをドラッグして順序を変更します。ホームタブの「お気に入り」とApple Watchのお気に入りリストにも同じ順序で適用されます。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"수" : {
|
||||
"comment" : "요일 축약(수요일)",
|
||||
"localizations" : {
|
||||
@ -9851,6 +9867,38 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"즐겨찾기 순서" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Favorites Order"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "お気に入りの順序"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"즐겨찾기 순서 변경" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Reorder Favorites"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "お気に入りの並べ替え"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"즐겨찾기 해제" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -12633,18 +12681,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"행동을 즐겨찾기로 지정하면 모음 탭 행동 목록이 '즐겨찾기'와 '나머지 행동' 두 부분으로 나뉘고, 즐겨찾기가 항상 맨 위에 와요. 새로 지정한 행동은 즐겨찾기 맨 뒤에 붙어요. 순서를 바꾸려면 '배치 편집'에서 버튼을 길게 눌러 끌어 옮기면 되고, 즐겨찾기와 나머지 사이로 끌어서 지정·해제할 수도 있어요. '나머지 행동' 제목을 누르면 그 아래를 접어 둘 수 있어서 행동이 많아져도 자주 쓰는 것만 남길 수 있어요(접힘은 기기마다 저장). 지정은 행동 버튼을 길게 눌러 '즐겨찾기'를 선택하거나, 행동 탭에서 행동을 열어 별 아이콘을 눌러도 돼요." : {
|
||||
"행동을 즐겨찾기로 지정하면 모음 탭 행동 목록이 '즐겨찾기'와 '나머지 행동' 두 부분으로 나뉘고, 즐겨찾기가 항상 맨 위에 와요. 새로 지정한 행동은 즐겨찾기 맨 뒤에 붙어요. 순서를 바꾸려면 '배치 편집'에서 버튼을 길게 눌러 끌어 옮기거나, 행동 탭 오른쪽 위 ⋯ 메뉴의 '즐겨찾기 순서 변경'을 쓰면 돼요 — 이 순서는 애플워치의 즐겨찾기 목록에도 똑같이 적용돼요. 배치 편집에서는 즐겨찾기와 나머지 사이로 끌어서 지정·해제할 수도 있어요. '나머지 행동' 제목을 누르면 그 아래를 접어 둘 수 있어서 행동이 많아져도 자주 쓰는 것만 남길 수 있어요(접힘은 기기마다 저장). 지정은 행동 버튼을 길게 눌러 '즐겨찾기'를 선택하거나, 행동 탭에서 행동을 열어 별 아이콘을 눌러도 돼요." : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Mark actions as favorites and the Home tab list splits into two parts — 'Favorites' and 'Other actions' — with favorites always at the top. Newly favorited actions are added to the end of the favorites. To reorder, use 'Edit Layout' and drag buttons around; you can also drag between the two sections to favorite or unfavorite. Tap the 'Other actions' title to collapse everything below it, so even with lots of actions only the ones you use most stay in view (the collapsed state is saved per device). To mark a favorite, long-press an action button and choose 'Favorites', or open the action in the Actions tab and tap the star icon."
|
||||
"value" : "Mark actions as favorites and the Home tab list splits into two parts — 'Favorites' and 'Other actions' — with favorites always at the top. Newly favorited actions are added to the end of the favorites. To reorder, drag buttons in 'Edit Layout', or use 'Reorder Favorites' in the ⋯ menu at the top right of the Actions tab — the same order also applies to the favorites list on Apple Watch. In Edit Layout you can also drag between the two sections to favorite or unfavorite. Tap the 'Other actions' title to collapse everything below it, so even with lots of actions only the ones you use most stay in view (the collapsed state is saved per device). To mark a favorite, long-press an action button and choose 'Favorites', or open the action in the Actions tab and tap the star icon."
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "アクションをお気に入りに指定すると、ホームタブの一覧が「お気に入り」と「その他のアクション」の2つに分かれ、お気に入りが常に一番上に表示されます。新しく指定したアクションはお気に入りの最後に追加されます。順序を変えるには「配置を編集」でボタンをドラッグして動かします。お気に入りとその他の間へドラッグして指定・解除することもできます。「その他のアクション」のタイトルをタップすると下を折りたたむことができ、アクションが増えてもよく使うものだけを残せます(折りたたみ状態は端末ごとに保存)。指定はアクションボタンを長押しして「お気に入り」を選ぶか、アクションタブでアクションを開いて星アイコンをタップしてもできます。"
|
||||
"value" : "アクションをお気に入りに指定すると、ホームタブの一覧が「お気に入り」と「その他のアクション」の2つに分かれ、お気に入りが常に一番上に表示されます。新しく指定したアクションはお気に入りの最後に追加されます。順序を変えるには「配置を編集」でボタンをドラッグするか、アクションタブ右上の⋯メニューの「お気に入りの並べ替え」を使います — この順序はApple Watchのお気に入りリストにも同じように適用されます。配置編集では、お気に入りとその他の間へドラッグして指定・解除することもできます。「その他のアクション」のタイトルをタップすると下を折りたたむことができ、アクションが増えてもよく使うものだけを残せます(折りたたみ状態は端末ごとに保存)。指定はアクションボタンを長押しして「お気に入り」を選ぶか、アクションタブでアクションを開いて星アイコンをタップしてもできます。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +20,13 @@ struct ActionListView: View {
|
||||
@State private var showLimitAlert = false
|
||||
@State private var showingPremiumSheet = false
|
||||
@State private var showingTagOrder = false
|
||||
@State private var showingFavoriteOrder: Bool = {
|
||||
#if DEBUG
|
||||
// 검증용: -actionShowFavoriteOrder YES → 즐겨찾기 순서 시트 바로 표시
|
||||
if UserDefaults.standard.bool(forKey: "actionShowFavoriteOrder") { return true }
|
||||
#endif
|
||||
return false
|
||||
}()
|
||||
|
||||
// 행동 표시 순서는 모음 탭 배치와 같은 기기별 로컬 설정을 따른다 (LocalPrefs 참고)
|
||||
@AppStorage(LocalPrefsKeys.actionOrder, store: AppGroup.defaults) private var actionOrderRaw = ""
|
||||
@ -89,6 +96,14 @@ struct ActionListView: View {
|
||||
} label: {
|
||||
Label("꼬리표 순서 변경", systemImage: "arrow.up.arrow.down")
|
||||
}
|
||||
// 순서 변경은 2개 이상일 때만 의미가 있다
|
||||
if favoriteActions.count >= 2 {
|
||||
Button {
|
||||
showingFavoriteOrder = true
|
||||
} label: {
|
||||
Label("즐겨찾기 순서 변경", systemImage: "star.fill")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
}
|
||||
@ -113,6 +128,9 @@ struct ActionListView: View {
|
||||
.sheet(isPresented: $showingTagOrder) {
|
||||
TagOrderSheet()
|
||||
}
|
||||
.sheet(isPresented: $showingFavoriteOrder) {
|
||||
FavoriteOrderSheet()
|
||||
}
|
||||
.alert("무료 사용 한도", isPresented: $showLimitAlert) {
|
||||
Button("프리미엄 알아보기") { showingPremiumSheet = true }
|
||||
Button("확인", role: .cancel) {}
|
||||
@ -222,6 +240,63 @@ struct TagOrderSheet: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 즐겨찾기 순서 변경 시트 (모음 탭 즐겨찾기 섹션·애플워치 즐겨찾기에 반영)
|
||||
|
||||
struct FavoriteOrderSheet: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.modelContext) private var context
|
||||
@Query(sort: \Action.createdAt) private var allActions: [Action]
|
||||
// 이동 즉시 목록이 새 순서로 그려지도록 배치 순서를 관찰한다
|
||||
@AppStorage(LocalPrefsKeys.actionOrder, store: AppGroup.defaults) private var actionOrderRaw = ""
|
||||
|
||||
/// 드래그로 순서가 바뀌었는지 — 시트가 닫힐 때 한 번만 커밋해
|
||||
/// 드래그마다 위젯·워치 갱신이 연발되지 않게 한다 (TagOrderSheet와 동일 패턴)
|
||||
@State private var orderChanged = false
|
||||
|
||||
private var favorites: [Action] {
|
||||
LocalPrefs.orderedActions(allActions, raw: actionOrderRaw).filter(\.isFavorite)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
Section {
|
||||
ForEach(favorites) { action in
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: action.symbolName)
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 28, height: 28)
|
||||
.background(action.color, in: RoundedRectangle(cornerRadius: 8, style: .continuous))
|
||||
Text(action.name)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
.onMove { source, destination in
|
||||
LocalPrefs.reorderFavorites(from: source, to: destination, context: context)
|
||||
orderChanged = true
|
||||
}
|
||||
} footer: {
|
||||
Text("손잡이를 끌어 순서를 바꿔요. 모음 탭 '즐겨찾기'와 애플워치의 즐겨찾기 목록에 같은 순서로 적용돼요.")
|
||||
}
|
||||
}
|
||||
.environment(\.editMode, .constant(.active))
|
||||
.navigationTitle("즐겨찾기 순서")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("완료") { dismiss() }
|
||||
}
|
||||
}
|
||||
}
|
||||
.presentationDetents([.medium, .large])
|
||||
.onDisappear {
|
||||
// 순서는 기기 로컬 설정이지만 위젯·워치 스냅숏이 이를 읽는다 — 닫힐 때 갱신 경로 1회
|
||||
if orderChanged { DataChange.commit(context: context) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 행동 상세
|
||||
|
||||
struct ActionDetailView: View {
|
||||
|
||||
@ -189,7 +189,7 @@ struct HelpView: View {
|
||||
HelpTopic(
|
||||
symbol: "star.fill",
|
||||
title: "즐겨찾기 모아 보기",
|
||||
body: "행동을 즐겨찾기로 지정하면 모음 탭 행동 목록이 '즐겨찾기'와 '나머지 행동' 두 부분으로 나뉘고, 즐겨찾기가 항상 맨 위에 와요. 새로 지정한 행동은 즐겨찾기 맨 뒤에 붙어요. 순서를 바꾸려면 '배치 편집'에서 버튼을 길게 눌러 끌어 옮기면 되고, 즐겨찾기와 나머지 사이로 끌어서 지정·해제할 수도 있어요. '나머지 행동' 제목을 누르면 그 아래를 접어 둘 수 있어서 행동이 많아져도 자주 쓰는 것만 남길 수 있어요(접힘은 기기마다 저장). 지정은 행동 버튼을 길게 눌러 '즐겨찾기'를 선택하거나, 행동 탭에서 행동을 열어 별 아이콘을 눌러도 돼요."
|
||||
body: "행동을 즐겨찾기로 지정하면 모음 탭 행동 목록이 '즐겨찾기'와 '나머지 행동' 두 부분으로 나뉘고, 즐겨찾기가 항상 맨 위에 와요. 새로 지정한 행동은 즐겨찾기 맨 뒤에 붙어요. 순서를 바꾸려면 '배치 편집'에서 버튼을 길게 눌러 끌어 옮기거나, 행동 탭 오른쪽 위 ⋯ 메뉴의 '즐겨찾기 순서 변경'을 쓰면 돼요 — 이 순서는 애플워치의 즐겨찾기 목록에도 똑같이 적용돼요. 배치 편집에서는 즐겨찾기와 나머지 사이로 끌어서 지정·해제할 수도 있어요. '나머지 행동' 제목을 누르면 그 아래를 접어 둘 수 있어서 행동이 많아져도 자주 쓰는 것만 남길 수 있어요(접힘은 기기마다 저장). 지정은 행동 버튼을 길게 눌러 '즐겨찾기'를 선택하거나, 행동 탭에서 행동을 열어 별 아이콘을 눌러도 돼요."
|
||||
),
|
||||
HelpTopic(
|
||||
symbol: "hand.tap.fill",
|
||||
|
||||
@ -119,6 +119,26 @@ nonisolated enum LocalPrefs {
|
||||
saveActionOrder(ordered)
|
||||
}
|
||||
|
||||
/// 즐겨찾기 순서 변경 시트(행동 탭 ⋯ 메뉴)의 이동 반영 — 전역 배치에서 즐겨찾기가
|
||||
/// 차지한 '자리들'은 그대로 두고, 그 자리에 들어갈 즐겨찾기끼리만 순서를 바꾼다.
|
||||
/// 나머지 행동의 절대 위치가 유지되므로 위젯 기본 슬롯(배치 앞쪽 N개) 같은 파생
|
||||
/// 순서에 영향이 없다. 모음 탭 즐겨찾기 섹션·애플워치 즐겨찾기가 같은 순서를 읽는다 (§6.1 v2·§11)
|
||||
static func reorderFavorites(from source: IndexSet, to destination: Int, context: ModelContext) {
|
||||
let all = (try? context.fetch(FetchDescriptor<Action>())) ?? []
|
||||
var ordered = orderedActions(all)
|
||||
let slots = ordered.indices.filter { ordered[$0].isFavorite }
|
||||
var favorites = slots.map { ordered[$0] }
|
||||
// move(fromOffsets:toOffset:)는 SwiftUI의 배열 확장이라 이 파일(Foundation 계층)에서는
|
||||
// 같은 의미(destination은 제거 전 기준 인덱스)로 직접 구현한다
|
||||
let moving = source.sorted(by: >).map { favorites.remove(at: $0) }.reversed()
|
||||
let insertAt = destination - source.count(where: { $0 < destination })
|
||||
favorites.insert(contentsOf: moving, at: insertAt)
|
||||
for (slot, action) in zip(slots, favorites) {
|
||||
ordered[slot] = action
|
||||
}
|
||||
saveActionOrder(ordered)
|
||||
}
|
||||
|
||||
// MARK: 메모 창 팝업 여부 (기록 시점에 뷰 밖에서도 읽음)
|
||||
|
||||
static func promptsForNote(_ id: UUID) -> Bool {
|
||||
|
||||
@ -214,27 +214,33 @@ struct LockGoalWidgetView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// 다짐 진행률 점 표시 — 진행률만큼 아래에서부터 차오른다 (달성 = 완전히 채움).
|
||||
/// ratio를 그대로 쓰므로 '이하 유지'는 기존과 동일하게 한도 안=채움/초과=빈 점.
|
||||
/// 작은 크기에서는 부채꼴보다 바닥부터 차는 물높이 방식이 읽기 쉽다.
|
||||
/// 다짐 진행률 점 표시 — 미완료는 링(호)이 진행률만큼 감기고, 완료는 꽉 찬 원반.
|
||||
/// 예전 '아래에서 차오르는 물높이' 방식은 5~6pt 크기에서 90%와 100%의 차이(위쪽 1px 남짓)가
|
||||
/// 사실상 안 보였다 — 링은 끝의 '틈'이 둘레에 생겨 눈에 띄고(완료 직전에도 틈이 남게
|
||||
/// 미완료는 최대 88%까지만 감는다), 완료는 면적 자체(빈 링 vs 원반)가 달라 어떤 배경에서도
|
||||
/// 구분된다. ratio를 그대로 쓰므로 '이하 유지'는 한도 안=원반 / 초과=빈 트랙으로 의미 유지.
|
||||
private func questDots(_ goal: GoalSnapshot, size: CGFloat) -> some View {
|
||||
HStack(spacing: 3) {
|
||||
ForEach(goal.quests.prefix(6)) { quest in
|
||||
let fill = min(max(quest.ratio, 0), 1)
|
||||
Circle()
|
||||
.strokeBorder(.primary, lineWidth: 1)
|
||||
.background(
|
||||
GeometryReader { geo in
|
||||
VStack(spacing: 0) {
|
||||
Spacer(minLength: 0)
|
||||
Rectangle()
|
||||
.fill(.primary)
|
||||
.frame(height: geo.size.height * fill)
|
||||
}
|
||||
}
|
||||
.clipShape(Circle())
|
||||
)
|
||||
.frame(width: size, height: size)
|
||||
let ratio = min(max(quest.ratio, 0), 1)
|
||||
let lineWidth = max(size * 0.3, 1.4)
|
||||
ZStack {
|
||||
if ratio >= 0.999 {
|
||||
Circle()
|
||||
.fill(.primary)
|
||||
} else {
|
||||
Circle()
|
||||
.inset(by: lineWidth / 2)
|
||||
.stroke(.primary.opacity(0.35), lineWidth: lineWidth)
|
||||
Circle()
|
||||
.inset(by: lineWidth / 2)
|
||||
// 시작도 보이게 최소 10%, 완료 직전에도 틈이 남게 최대 88%
|
||||
.trim(from: 0, to: ratio > 0 ? min(max(ratio, 0.1), 0.88) : 0)
|
||||
.stroke(.primary, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round))
|
||||
.rotationEffect(.degrees(-90))
|
||||
}
|
||||
}
|
||||
.frame(width: size, height: size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user