From 459c85700f66b9b4335c5afcec2e1fc39bfaf747 Mon Sep 17 00:00:00 2001 From: songyc macbook Date: Sun, 19 Jul 2026 09:59:33 +0900 Subject: [PATCH] fix(ui): smooth expand/collapse animation for time input rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 시각 입력 값 행의 펼침/접힘이 "확" 스냅된다는 실기기 피드백 → 조건부 삽입(if) 대신 높이 0↔216pt 프레임 보간(.smooth 0.3s, 바운스 없음)으로 변경 — List/Form 행 높이가 매 프레임 따라와 부드럽게 열리고 닫힘. 공용 CollapsibleTimeWheel 한 곳 수정으로 6곳(기록 편집 4시트·마감 시각·하루 시작 시간) 전부 적용 - 휠은 Color.clear의 overlay(top 고정)로 띄움 — UIDatePicker의 고유 높이(216)가 레이아웃에 개입해 접힘 상태에 빈 공간이 남던 문제(1차 구현에서 발견) 방지. 접힘 시 clipped+터치 차단+보이스오버 숨김 - 값 반영 경로·검증(종료<시작 경고 등) 불변, 새 문자열 없음(카탈로그 클린) - 검증: 세션 수정 접힘(빈 공간 없음)·설정 펼침 시뮬 스크린샷, Debug/Store 빌드 성공. CLAUDE.md §6.5에 보간 구조 유지 규칙 기재 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_014CkLDi8Lr21vGqo4SjZaTp --- myApp/HaruDanim/CLAUDE.md | 2 +- myApp/HaruDanim/IOS/Views/RecordEditors.swift | 22 +++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/myApp/HaruDanim/CLAUDE.md b/myApp/HaruDanim/CLAUDE.md index c4c020f..09b1093 100644 --- a/myApp/HaruDanim/CLAUDE.md +++ b/myApp/HaruDanim/CLAUDE.md @@ -201,7 +201,7 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f ### 6.5 기록 탭 (`HistoryView`, `RecordEditors`) - **범위 조회**: 기록은 전량 로드하지 않고 선택 날짜가 속한 **주 범위**만 @Query로 가져온다(`HistoryRecordsView` — SwiftData 동적 쿼리 패턴: 날짜가 바뀌면 새 조건으로 재생성). 하루 경계 걸침·진행 중 세션은 "시작 < 범위끝 AND (종료 없음 OR 종료 > 범위시작)" 겹침 조건으로 포함 - 날짜 헤더(화살표 이동, 날짜 탭=달력 팝오버, "오늘" 버튼) + 목록/타임테이블 세그먼트 -- 목록: 시간 기록(시작~종료·총 시간·메모)·횟수 기록(시각·+n·메모), 탭하면 수정 시트. **수정·추가 시트의 시각 입력은 상시 노출 휠 다이얼**(`RecordEditors.swift` RecordTimeWheel) — 컴팩트 팝오버는 다이얼 감속 중 닫히면 정착값이 유실되는 실기기 보고가 있어 폐기. `in:` 범위 제약도 하한 근처 선택을 소리 없이 되감아 금지(검증은 종료<시작 경고 문구+저장 비활성). 편집 상태는 분 단위 절사(flooredToMinute) — 실측정 기록의 초 찌꺼기(14:32:47)가 남으면 다이얼로 맞춘 1시간이 59분으로 표시되는 어긋남 방지. **앱의 모든 시각 입력은 `CollapsibleTimeWheel`**(RecordEditors.swift) — 평소엔 값 행(라벨+현재 값+셰브론), 탭하면 인라인 휠(RecordTimeWheel) 펼침. 상시 노출 휠이 어수선하다는 실기기 피드백으로 기록 편집 4시트·다짐 '마감 시각'·설정 '하루 시작 시간' 전부 통일(기본 접힘). 팝오버 금지 이유(감속 중 닫힘 시 정착값 유실)와 범위 제약 금지·분 절사 규칙은 그대로 +- 목록: 시간 기록(시작~종료·총 시간·메모)·횟수 기록(시각·+n·메모), 탭하면 수정 시트. **수정·추가 시트의 시각 입력은 상시 노출 휠 다이얼**(`RecordEditors.swift` RecordTimeWheel) — 컴팩트 팝오버는 다이얼 감속 중 닫히면 정착값이 유실되는 실기기 보고가 있어 폐기. `in:` 범위 제약도 하한 근처 선택을 소리 없이 되감아 금지(검증은 종료<시작 경고 문구+저장 비활성). 편집 상태는 분 단위 절사(flooredToMinute) — 실측정 기록의 초 찌꺼기(14:32:47)가 남으면 다이얼로 맞춘 1시간이 59분으로 표시되는 어긋남 방지. **앱의 모든 시각 입력은 `CollapsibleTimeWheel`**(RecordEditors.swift) — 평소엔 값 행(라벨+현재 값+셰브론), 탭하면 인라인 휠(RecordTimeWheel) 펼침. 상시 노출 휠이 어수선하다는 실기기 피드백으로 기록 편집 4시트·다짐 '마감 시각'·설정 '하루 시작 시간' 전부 통일(기본 접힘). **펼침/접힘은 조건부 삽입(if)이 아니라 높이 0↔216 프레임 보간**(.smooth 0.3s) — 삽입 방식은 List 행 높이가 스냅되어 '확' 열리는 느낌(실기기 피드백). 휠은 Color.clear의 overlay로 띄워 UIDatePicker 고유 높이가 레이아웃에 개입하지 않게 한다(접힘 상태 빈 공간 버그 방지 — 이 구조를 유지할 것). 팝오버 금지 이유(감속 중 닫힘 시 정착값 유실)와 범위 제약 금지·분 절사 규칙은 그대로 - 타임테이블: 24시간 축(하루 시작 시간 기준), 하루/일주일 전환(주간 보기에서 화살표는 7일씩 점프). 시간=색 블록, 횟수=색 점. 블록/점 탭=수정 - 필터: `RecordFilterSheet`(공용) — 꼬리표 아래 행동이 그룹된 트리, **"제외한 행동 ID" 집합**으로 관리해 기본이 전체 선택. 활성 시 초록 칩 표시 - 내보내기: 현재 날짜/모드/필터 그대로 `ExportBuilder.history` 스냅숏 → 이미지 시트 diff --git a/myApp/HaruDanim/IOS/Views/RecordEditors.swift b/myApp/HaruDanim/IOS/Views/RecordEditors.swift index cc32331..d7679fe 100644 --- a/myApp/HaruDanim/IOS/Views/RecordEditors.swift +++ b/myApp/HaruDanim/IOS/Views/RecordEditors.swift @@ -54,10 +54,16 @@ struct CollapsibleTimeWheel: View { self._isExpanded = State(initialValue: initiallyExpanded) } + /// UIDatePicker 휠의 고유 높이 (역사적으로 고정 216pt) — 펼침/접힘을 + /// 조건부 삽입(if) 대신 0↔이 높이의 프레임 보간으로 애니메이션하기 위한 상수. + /// List/Form 행에서 조건부 삽입은 행 높이가 스냅되어 "확" 열리고 닫히는 + /// 느낌이 나는데(실기기 피드백), 높이 보간은 행 높이가 매 프레임 따라와 부드럽다. + private static let wheelHeight: CGFloat = 216 + var body: some View { VStack(alignment: .leading, spacing: 0) { Button { - withAnimation(.snappy(duration: 0.25)) { + withAnimation(.smooth(duration: 0.3)) { isExpanded.toggle() } } label: { @@ -77,9 +83,17 @@ struct CollapsibleTimeWheel: View { .buttonStyle(.plain) .accessibilityLabel(Text(label)) .accessibilityValue(Text(selection, format: valueFormat)) - if isExpanded { - RecordTimeWheel(label: label, selection: $selection, components: components) - } + // 높이는 Color.clear가 결정(0↔휠 높이 보간)하고 휠 자체는 overlay로 띄운다 — + // UIDatePicker의 고유 높이가 레이아웃에 개입해 접힘 상태에 빈 공간이 남는 것 방지 + Color.clear + .frame(height: isExpanded ? Self.wheelHeight : 0) + .overlay(alignment: .top) { + RecordTimeWheel(label: label, selection: $selection, components: components) + } + .clipped() + .opacity(isExpanded ? 1 : 0) + .allowsHitTesting(isExpanded) + .accessibilityHidden(!isExpanded) } }