fix(widget): NowRunning stop button opened the app instead of stopping

- ⑥ 현재 진행 중 위젯의 종료 버튼 탭이 인텐트 대신 앱 열기로 새던 원인 = 버튼 라벨(정지 아이콘)에 붙어 있던 invalidatableContent() — §10에 문서화된 금지 패턴(invalidatable 래스터 서브트리는 별도 레이어로 분리돼 Button(intent:) 히트 영역에서 빠짐, ③ 링 실기기 버그와 동일 기전)을 이 버튼만 어기고 있었음
- 아이콘의 invalidatableContent() 한 줄 제거 (외형·레이아웃 불변) — 탭 접수 깜빡임 피드백은 바로 옆 타이머 텍스트의 invalidatableContent가 기존대로 담당(①③과 같은 패턴). 소형·중형 대표 카드·대형 나열 행 모두 같은 NowRunningStopButton이라 한 번에 수정
- CLAUDE.md §10 경고문에 ⑥ 재현 사례 병기
- 검증: Debug·Store·워치 3스킴 빌드 성공, 위젯 미리보기 렌더 수정 전과 동일. 히트 영역 자체는 홈 화면 UI라 실기기 탭 확인 필요

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-16 09:06:18 +09:00
parent 232e8c504d
commit df6c03e18c
2 changed files with 6 additions and 3 deletions

View File

@ -267,7 +267,7 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f
- 슬롯 규칙: 미선택이면 기본 대상 채움, 선택했으면 순서 존중 + 모자란 슬롯은 점선 빈 칸. '선택 안 함' 센티널 `NoneEntityID` - 슬롯 규칙: 미선택이면 기본 대상 채움, 선택했으면 순서 존중 + 모자란 슬롯은 점선 빈 칸. '선택 안 함' 센티널 `NoneEntityID`
- **타임라인 예산 전략**(`WidgetRefresh`): 주기 폴링 없음. 값 변경은 `DataChange.commit`/인텐트의 reloadAllTimelines가 담당. 측정 중일 때만 10분 간격 미래 엔트리 1시간치+.atEnd, 평상시엔 하루 경계(최대 4h) 안전망 - **타임라인 예산 전략**(`WidgetRefresh`): 주기 폴링 없음. 값 변경은 `DataChange.commit`/인텐트의 reloadAllTimelines가 담당. 측정 중일 때만 10분 간격 미래 엔트리 1시간치+.atEnd, 평상시엔 하루 경계(최대 4h) 안전망
- **버튼 탭 즉시 피드백**: ①③의 변하는 값 **텍스트**에 `invalidatableContent()` — 탭 즉시 '갱신 중' 표시로 바뀌어 새 타임라인 도착(1~3초, WidgetKit 구조상 단축 불가) 전에도 탭 접수를 보여 줌(중복 탭 방지). ⚠️ **도형·아이콘 ZStack에는 금지** — invalidatable로 표시된 래스터 서브트리는 별도 레이어로 분리돼 `Button(intent:)` 히트 영역에서 빠지고, 그 위 탭이 앱 열기로 샌다(③ 링이 그랬던 실기기 버그. contentShape(.rect)로도 못 살림 — 텍스트 값에만 붙일 것) - **버튼 탭 즉시 피드백**: ①③의 변하는 값 **텍스트**에 `invalidatableContent()` — 탭 즉시 '갱신 중' 표시로 바뀌어 새 타임라인 도착(1~3초, WidgetKit 구조상 단축 불가) 전에도 탭 접수를 보여 줌(중복 탭 방지). ⚠️ **도형·아이콘 ZStack에는 금지** — invalidatable로 표시된 래스터 서브트리는 별도 레이어로 분리돼 `Button(intent:)` 히트 영역에서 빠지고, 그 위 탭이 앱 열기로 샌다(③ 링, ⑥ 종료 버튼 아이콘에서 실기기 재현·수정된 버그. contentShape(.rect)로도 못 살림 — 텍스트 값에만 붙일 것)
- **버튼 히트 영역**: `Button(intent:)` 라벨 끝에 `.contentShape(.rect)` 필수 — plain 버튼은 라벨의 투명 영역(Spacer·여백·링 안쪽)을 탭 판정에서 빼는 경우가 있어, 그 지점 탭이 인텐트 대신 위젯 기본 동작(앱 열기)으로 새는 버그가 있었다. ③의 꼬리표 대상 다짐 셀은 버튼이 없어(표시 전용) 셀 전체가 앱 열기 — 의도된 동작 - **버튼 히트 영역**: `Button(intent:)` 라벨 끝에 `.contentShape(.rect)` 필수 — plain 버튼은 라벨의 투명 영역(Spacer·여백·링 안쪽)을 탭 판정에서 빼는 경우가 있어, 그 지점 탭이 인텐트 대신 위젯 기본 동작(앱 열기)으로 새는 버그가 있었다. ③의 꼬리표 대상 다짐 셀은 버튼이 없어(표시 전용) 셀 전체가 앱 열기 — 의도된 동작
- 확장 프로세스는 `IntentStore.refresh()`로 작업마다 컨테이너를 새로 열어 최신 데이터 보장 (실패 시 직전 컨테이너 유지 — 크래시 방지) - 확장 프로세스는 `IntentStore.refresh()`로 작업마다 컨테이너를 새로 열어 최신 데이터 보장 (실패 시 직전 컨테이너 유지 — 크래시 방지)
- `RunActionIntent`는 AppEntity 대신 **UUID 문자열 파라미터** — 버튼 탭 지연·유실 방지의 핵심 - `RunActionIntent`는 AppEntity 대신 **UUID 문자열 파라미터** — 버튼 탭 지연·유실 방지의 핵심

View File

@ -210,8 +210,11 @@ struct NowRunningStopButton: View {
: AnyShapeStyle(Color.white.opacity(0.14)), : AnyShapeStyle(Color.white.opacity(0.14)),
in: Circle() in: Circle()
) )
// ' ' ( ) // invalidatableContent() (§10) ·
.invalidatableContent() // Button(intent:) ,
// (
// . contentShape ).
// invalidatableContent .
// (§10 ) // (§10 )
.contentShape(.rect) .contentShape(.rect)
} }