From 045f0b6f749206149dad585ed4b7caea9a14cb2e Mon Sep 17 00:00:00 2001 From: songyc macbook Date: Wed, 8 Jul 2026 16:40:59 +0900 Subject: [PATCH] feat(action): enhance icon UI and add memo prompt options - Enlarge the count number displayed on the main tab action icon - Enhance the click feedback animation on the main tab for better visibility - Add an option to toggle memo prompts during action creation - Conditionally show memo prompts based on settings when stopping a timer or incrementing a count --- .../UserInterfaceState.xcuserstate | Bin 21210 -> 21210 bytes myApp/Haru_Danim/IOS/Core/DebugSeed.swift | 6 + myApp/Haru_Danim/IOS/Core/Models.swift | 4 + myApp/Haru_Danim/IOS/Localizable.xcstrings | 18 +++ myApp/Haru_Danim/IOS/Views/ActionViews.swift | 26 ++++ myApp/Haru_Danim/IOS/Views/HistoryView.swift | 7 + myApp/Haru_Danim/IOS/Views/MainView.swift | 127 +++++++++++++++--- .../Haru_Danim/IOS/Views/RecordEditors.swift | 24 +++- 8 files changed, 189 insertions(+), 23 deletions(-) diff --git a/myApp/Haru_Danim/Haru_Danim.xcodeproj/project.xcworkspace/xcuserdata/ceuak.xcuserdatad/UserInterfaceState.xcuserstate b/myApp/Haru_Danim/Haru_Danim.xcodeproj/project.xcworkspace/xcuserdata/ceuak.xcuserdatad/UserInterfaceState.xcuserstate index f6bd72e42647229e402e87443f7c87875e41e04b..7fd135648761c935a94fae8ed75edf210fff656d 100644 GIT binary patch delta 337 zcmWO0PbdUo7{~GFF|S!e%#<~DH~(UqgJ=BNU3Rs(Nud&y^&J3h(w~5$lbwNNiNLg+o#{@_nyP|93Ed4dCFM@^RXInS2QOV2fk9DFCwNq zm$iZcCT@a(Teyuokax8UCNJkIPqM!l)Hjh9AYLzU0MV zrnz5dMj#H8 zFb>Pm;C9h%2gD!@k%l%H60^Odk~CJ85SM*<79FGoTA?JZ(i){`ozj${JvyQjI-_&C cpliCNJ9?l;dZQ2ep z81@5M^BL3{>Kb?(_<^{oVPeCi24^5^36QTcIZ;J^GP9w|F;*cpDiF z8YVO{8aAk0Vi92AXAzv7E1|-EiA4m+7M;wfqaz`~BF!MSh((g|SR = [] @State private var showingSymbolPicker = false @State private var showingNewTag = false @@ -388,6 +397,20 @@ struct ActionEditorView: View { .pickerStyle(.inline) .labelsHidden() } + Section { + Toggle(isOn: $promptsForNote) { + Label { + Text("기록 시 메모 창 표시") + } icon: { + Image(systemName: "note.text") + .foregroundStyle(AppTheme.green) + } + } + } footer: { + Text(trackingType == .time + ? "켜면 시간 측정을 종료할 때 메모 창이 떠요. 건너뛸 수 있어요." + : "켜면 횟수를 추가할 때마다 메모 창이 떠요. 건너뛸 수 있어요.") + } } .navigationTitle(action == nil ? "행동 추가" : "행동 수정") .navigationBarTitleDisplayMode(.inline) @@ -415,6 +438,7 @@ struct ActionEditorView: View { name = action.name symbolName = action.symbolName trackingType = action.trackingType + promptsForNote = action.promptsForNote selectedTags = Set(action.tags.map(\.persistentModelID)) } @@ -433,6 +457,7 @@ struct ActionEditorView: View { action.name = trimmed action.symbolName = symbolName action.trackingType = trackingType + action.promptsForNote = promptsForNote action.tags = tags } else { let maxOrder = allActions.map(\.sortOrder).max() ?? -1 @@ -442,6 +467,7 @@ struct ActionEditorView: View { trackingType: trackingType, sortOrder: maxOrder + 1 ) + newAction.promptsForNote = promptsForNote newAction.tags = tags context.insert(newAction) } diff --git a/myApp/Haru_Danim/IOS/Views/HistoryView.swift b/myApp/Haru_Danim/IOS/Views/HistoryView.swift index 2138e4f..9a94728 100644 --- a/myApp/Haru_Danim/IOS/Views/HistoryView.swift +++ b/myApp/Haru_Danim/IOS/Views/HistoryView.swift @@ -279,6 +279,13 @@ struct HistoryView: View { Text(Format.time(item.entry.timestamp)) .font(.caption) .foregroundStyle(.secondary) + if !item.entry.note.isEmpty { + Label(item.entry.note, systemImage: "note.text") + .font(.caption) + .foregroundStyle(AppTheme.yellow) + .lineLimit(2) + .padding(.top, 1) + } } Spacer() Text("+\(item.entry.amount)") diff --git a/myApp/Haru_Danim/IOS/Views/MainView.swift b/myApp/Haru_Danim/IOS/Views/MainView.swift index 23caaa0..201daa6 100644 --- a/myApp/Haru_Danim/IOS/Views/MainView.swift +++ b/myApp/Haru_Danim/IOS/Views/MainView.swift @@ -30,6 +30,7 @@ struct MainView: View { @State private var recordsAction: Action? @State private var deletingAction: Action? @State private var memoSession: TimeSession? + @State private var memoEntry: CountEntry? private var anyRunning: Bool { !runningSessions.isEmpty } @@ -71,6 +72,9 @@ struct MainView: View { .sheet(item: $memoSession) { session in SessionMemoSheet(session: session) } + .sheet(item: $memoEntry) { entry in + CountMemoSheet(entry: entry) + } .alert( "행동 삭제", isPresented: Binding( @@ -219,18 +223,24 @@ struct MainView: View { LiveActivityManager.sync(context: context) } case .count: - context.insert(CountEntry(action: action, timestamp: .now)) + let entry = CountEntry(action: action, timestamp: .now) + context.insert(entry) + if action.promptsForNote { + memoEntry = entry + } } } /// 세션 종료. "짧은 기록 무시" 설정보다 짧으면 기록하지 않고 삭제 (실수 방지) - /// 정상 기록된 세션은 메모 작성 시트를 띄운다 (건너뛰기 가능). + /// 정상 기록된 세션은 행동의 메모 옵션이 켜져 있으면 메모 작성 시트를 띄운다 (건너뛰기 가능). private func finish(_ session: TimeSession) { if minSessionSeconds > 0, session.duration() < Double(minSessionSeconds) { context.delete(session) } else { session.endAt = .now - memoSession = session + if session.action?.promptsForNote == true { + memoSession = session + } } LiveActivityManager.sync(context: context) } @@ -309,6 +319,63 @@ struct SessionMemoSheet: View { } } +// MARK: - 횟수 기록 메모 시트 + +struct CountMemoSheet: View { + @Environment(\.dismiss) private var dismiss + let entry: CountEntry + + @State private var text = "" + @FocusState private var focused: Bool + + var body: some View { + NavigationStack { + VStack(alignment: .leading, spacing: 12) { + if let action = entry.action { + HStack(spacing: 10) { + Image(systemName: action.symbolName) + .font(.system(size: 15, weight: .semibold)) + .foregroundStyle(.white) + .frame(width: 32, height: 32) + .background(action.color, in: RoundedRectangle(cornerRadius: 9, style: .continuous)) + VStack(alignment: .leading, spacing: 1) { + Text(action.name) + .font(.subheadline.weight(.semibold)) + Text("+\(entry.amount) 기록 완료") + .font(.caption) + .foregroundStyle(.secondary) + } + } + } + TextField("이번 기록에 대한 메모 (선택)", text: $text, axis: .vertical) + .lineLimit(3...5) + .padding(10) + .background(AppTheme.surface, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) + .focused($focused) + Spacer() + } + .padding() + .background(AppTheme.background) + .navigationTitle("메모 남기기") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("건너뛰기") { dismiss() } + } + ToolbarItem(placement: .confirmationAction) { + Button("저장") { + entry.note = text.trimmingCharacters(in: .whitespacesAndNewlines) + dismiss() + } + .disabled(text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + } + } + } + .presentationDetents([.height(280)]) + .onAppear { text = entry.note } + } +} + // MARK: - 지글(흔들림) 애니메이션 struct JiggleEffect: ViewModifier { @@ -414,15 +481,14 @@ struct ActionButtonCell: View { var body: some View { Button { onTap() - if action.trackingType == .count { - bounce.toggle() - } + bounce.toggle() } label: { VStack(alignment: .leading, spacing: compact ? 3 : 6) { HStack { Image(systemName: action.symbolName) .font(.system(size: compact ? 17 : 26, weight: .medium)) .foregroundStyle(.white) + .symbolEffect(.bounce, value: bounce) Spacer() if action.isRunning && !compact { Image(systemName: "record.circle") @@ -454,7 +520,7 @@ struct ActionButtonCell: View { } } .buttonStyle(PressableButtonStyle()) - .sensoryFeedback(.impact(weight: .light), trigger: bounce) + .sensoryFeedback(.impact(weight: .medium), trigger: bounce) .disabled(isEditing) } @@ -475,30 +541,49 @@ struct ActionButtonCell: View { .lineLimit(1) } case .count: - Text(todayLabel) - .font(.caption2) - .foregroundStyle(.white.opacity(0.85)) - .lineLimit(1) + HStack(alignment: .firstTextBaseline, spacing: compact ? 2 : 3) { + if !compact { + Text("오늘") + .font(.caption2) + .foregroundStyle(.white.opacity(0.75)) + } + Text("\(todayCount)") + .font(.system(size: compact ? 18 : 27, weight: .heavy, design: .rounded)) + .monospacedDigit() + .foregroundStyle(.white) + .contentTransition(.numericText(value: Double(todayCount))) + Text("회") + .font((compact ? Font.caption2 : Font.caption).weight(.semibold)) + .foregroundStyle(.white.opacity(0.85)) + } + .lineLimit(1) + .animation(.snappy(duration: 0.3), value: todayCount) } } private var todayLabel: String { let agg = Aggregator() - switch action.trackingType { - case .time: - let seconds = agg.seconds(for: action, in: agg.math.dayRange(containing: .now)) - return seconds > 0 ? "오늘 \(Format.durationShort(seconds))" : "오늘 0분" - case .count: - let count = agg.count(for: action, in: agg.math.dayRange(containing: .now)) - return "오늘 \(count)회" - } + let seconds = agg.seconds(for: action, in: agg.math.dayRange(containing: .now)) + return seconds > 0 ? "오늘 \(Format.durationShort(seconds))" : "오늘 0분" + } + + private var todayCount: Int { + let agg = Aggregator() + return agg.count(for: action, in: agg.math.dayRange(containing: .now)) } } +/// 누르면 크게 줄어들었다가 놓으면 튀어오르듯 복귀 (감쇠 낮은 스프링으로 오버슈트) struct PressableButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label - .scaleEffect(configuration.isPressed ? 0.93 : 1) - .animation(.spring(duration: 0.2), value: configuration.isPressed) + .scaleEffect(configuration.isPressed ? 0.85 : 1) + .brightness(configuration.isPressed ? 0.08 : 0) + .animation( + configuration.isPressed + ? .spring(duration: 0.12) + : .spring(response: 0.35, dampingFraction: 0.45), + value: configuration.isPressed + ) } } diff --git a/myApp/Haru_Danim/IOS/Views/RecordEditors.swift b/myApp/Haru_Danim/IOS/Views/RecordEditors.swift index 8f3edb7..bf8030f 100644 --- a/myApp/Haru_Danim/IOS/Views/RecordEditors.swift +++ b/myApp/Haru_Danim/IOS/Views/RecordEditors.swift @@ -149,6 +149,12 @@ struct CountRowLabel: View { .foregroundStyle(.secondary) Text(Format.time(entry.timestamp)) .font(.body) + if !entry.note.isEmpty { + Label(entry.note, systemImage: "note.text") + .font(.caption) + .foregroundStyle(AppTheme.yellow) + .lineLimit(2) + } } Spacer() Text("+\(entry.amount)") @@ -278,6 +284,7 @@ struct CountEntryEditorView: View { @State private var timestamp: Date = .now @State private var amount = 1 + @State private var note = "" var body: some View { NavigationStack { @@ -290,6 +297,10 @@ struct CountEntryEditorView: View { Text("\(amount)회").foregroundStyle(.secondary) } } + Section("메모") { + TextField("이 기록에 대한 메모 (선택)", text: $note, axis: .vertical) + .lineLimit(2...5) + } Section { Button("기록 삭제 (증가 취소)", role: .destructive) { context.delete(entry) @@ -307,6 +318,7 @@ struct CountEntryEditorView: View { Button("저장") { entry.timestamp = timestamp entry.amount = amount + entry.note = note.trimmingCharacters(in: .whitespacesAndNewlines) dismiss() } } @@ -314,6 +326,7 @@ struct CountEntryEditorView: View { .onAppear { timestamp = entry.timestamp amount = entry.amount + note = entry.note } } } @@ -329,6 +342,7 @@ struct CountAddView: View { @State private var amount = 1 @State private var useNow = true @State private var timestamp: Date = .now + @State private var note = "" var body: some View { NavigationStack { @@ -350,6 +364,10 @@ struct CountAddView: View { DatePicker("시각", selection: $timestamp) } } + Section("메모") { + TextField("이 기록에 대한 메모 (선택)", text: $note, axis: .vertical) + .lineLimit(2...5) + } } .navigationTitle("횟수 추가") .navigationBarTitleDisplayMode(.inline) @@ -359,11 +377,13 @@ struct CountAddView: View { } ToolbarItem(placement: .confirmationAction) { Button("추가") { - context.insert(CountEntry( + let entry = CountEntry( action: action, timestamp: useNow ? .now : timestamp, amount: amount - )) + ) + entry.note = note.trimmingCharacters(in: .whitespacesAndNewlines) + context.insert(entry) dismiss() } }