Skip to content

Commit 748d569

Browse files
authored
[#396] 위젯 UI를 구성한다 (#402)
* docs: 각종 플로우 작성 * refactor: 달 기반 데이터에서 분기 기반으로 데이터 형태 수정 * feat: 히트맵 UI 구현 * ui: 타이틀에서 'Todo' 제거 * ui: 투데이 위젯 수정 * refactor: 위젯에 데이터가 더 빨리 뜰 수 있도록 개선 * ui: 데이터를 받아오지 않았을 때의 뷰 수정 * ui: 투데이 위젯이 컴팩트일 경우 Todo 번호와 중요 표시까지 보이도록 수정 * ui: Todo 위젯의 플레이스홀더를 실제 보여주는 UI와 비슷하도록 수정 * ui: 준비 중 텍스트 제거 * refactor: WidgetHeatmapLayout를 공유하는 플레이스홀더 전용 그리드 형태로 정리 * ui: 플레이스홀더일 때 월, 수, 금 라벨 제거 * ui: 의도적인 높이 제한 제거
1 parent 92b52be commit 748d569

17 files changed

Lines changed: 1399 additions & 137 deletions

DevLog/Widget/Heatmap/HeatmapWidgetSnapshot.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@ import Foundation
99

1010
struct HeatmapWidgetSnapshot: Codable, Equatable {
1111
let generatedAt: Date
12-
let monthStart: Date
12+
let quarterStart: Date
1313
let selectedActivityKindRawValues: [String]
1414
let maxCount: Int
15+
let months: [WidgetHeatmapMonthSnapshot]
16+
}
17+
18+
struct WidgetHeatmapMonthSnapshot: Codable, Equatable {
19+
let monthStart: Date
1520
let weeks: [WidgetHeatmapWeekSnapshot]
1621
}
1722

DevLog/Widget/Heatmap/HeatmapWidgetSnapshotFactory.swift

Lines changed: 58 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -36,30 +36,40 @@ struct HeatmapWidgetSnapshotFactory {
3636
completedTodos: [Todo],
3737
deletedTodos: [Todo],
3838
selectedActivityKinds: Set<ActivityKind>,
39-
monthStart: Date,
39+
quarterStart: Date,
4040
now: Date = Date()
4141
) -> HeatmapWidgetSnapshot {
42-
let normalizedMonthStart = startOfMonth(for: monthStart)
42+
let normalizedQuarterStart = startOfQuarter(for: quarterStart)
43+
guard let nextQuarterStart = calendar.date(byAdding: .month, value: 3, to: normalizedQuarterStart) else {
44+
return HeatmapWidgetSnapshot(
45+
generatedAt: now,
46+
quarterStart: normalizedQuarterStart,
47+
selectedActivityKindRawValues: orderedActivityKinds(from: selectedActivityKinds).map(\.rawValue),
48+
maxCount: 0,
49+
months: []
50+
)
51+
}
4352
let dailyCountsByDate = makeDailyCountsByDate(
4453
createdTodos: createdTodos,
4554
completedTodos: completedTodos,
4655
deletedTodos: deletedTodos,
47-
monthStart: normalizedMonthStart
56+
quarterStart: normalizedQuarterStart,
57+
nextQuarterStart: nextQuarterStart
4858
)
49-
let weeks = makeWeeks(
50-
monthStart: normalizedMonthStart,
59+
let months = makeMonths(
60+
quarterStart: normalizedQuarterStart,
5161
dailyCountsByDate: dailyCountsByDate
5262
)
5363

5464
return HeatmapWidgetSnapshot(
5565
generatedAt: now,
56-
monthStart: normalizedMonthStart,
66+
quarterStart: normalizedQuarterStart,
5767
selectedActivityKindRawValues: orderedActivityKinds(from: selectedActivityKinds).map(\.rawValue),
5868
maxCount: maxCount(
59-
from: weeks,
69+
from: months,
6070
selectedActivityKinds: selectedActivityKinds
6171
),
62-
weeks: weeks
72+
months: months
6373
)
6474
}
6575
}
@@ -69,15 +79,17 @@ private extension HeatmapWidgetSnapshotFactory {
6979
createdTodos: [Todo],
7080
completedTodos: [Todo],
7181
deletedTodos: [Todo],
72-
monthStart: Date
82+
quarterStart: Date,
83+
nextQuarterStart: Date
7384
) -> [Date: DailyCounts] {
7485
var dailyCountsByDate = [Date: DailyCounts]()
7586

7687
for todo in createdTodos {
7788
appendCount(
7889
activityKind: .created,
7990
occurredAt: todo.createdAt,
80-
monthStart: monthStart,
91+
quarterStart: quarterStart,
92+
nextQuarterStart: nextQuarterStart,
8193
dailyCountsByDate: &dailyCountsByDate
8294
)
8395
}
@@ -87,7 +99,8 @@ private extension HeatmapWidgetSnapshotFactory {
8799
appendCount(
88100
activityKind: .completed,
89101
occurredAt: completedAt,
90-
monthStart: monthStart,
102+
quarterStart: quarterStart,
103+
nextQuarterStart: nextQuarterStart,
91104
dailyCountsByDate: &dailyCountsByDate
92105
)
93106
}
@@ -97,7 +110,8 @@ private extension HeatmapWidgetSnapshotFactory {
97110
appendCount(
98111
activityKind: .deleted,
99112
occurredAt: deletedAt,
100-
monthStart: monthStart,
113+
quarterStart: quarterStart,
114+
nextQuarterStart: nextQuarterStart,
101115
dailyCountsByDate: &dailyCountsByDate
102116
)
103117
}
@@ -108,17 +122,37 @@ private extension HeatmapWidgetSnapshotFactory {
108122
func appendCount(
109123
activityKind: ActivityKind,
110124
occurredAt: Date,
111-
monthStart: Date,
125+
quarterStart: Date,
126+
nextQuarterStart: Date,
112127
dailyCountsByDate: inout [Date: DailyCounts]
113128
) {
114-
guard isDateInMonth(occurredAt, monthStart: monthStart) else { return }
129+
guard quarterStart <= occurredAt && occurredAt < nextQuarterStart else { return }
115130

116131
let dayStart = calendar.startOfDay(for: occurredAt)
117132
var dailyCounts = dailyCountsByDate[dayStart] ?? DailyCounts()
118133
dailyCounts.increment(activityKind)
119134
dailyCountsByDate[dayStart] = dailyCounts
120135
}
121136

137+
func makeMonths(
138+
quarterStart: Date,
139+
dailyCountsByDate: [Date: DailyCounts]
140+
) -> [WidgetHeatmapMonthSnapshot] {
141+
let monthStarts = (0..<3).compactMap {
142+
calendar.date(byAdding: .month, value: $0, to: quarterStart)
143+
}
144+
145+
return monthStarts.map { monthStart in
146+
WidgetHeatmapMonthSnapshot(
147+
monthStart: monthStart,
148+
weeks: makeWeeks(
149+
monthStart: monthStart,
150+
dailyCountsByDate: dailyCountsByDate
151+
)
152+
)
153+
}
154+
}
155+
122156
func makeWeeks(
123157
monthStart: Date,
124158
dailyCountsByDate: [Date: DailyCounts]
@@ -173,29 +207,21 @@ private extension HeatmapWidgetSnapshotFactory {
173207
return weeks
174208
}
175209

176-
func startOfMonth(for date: Date) -> Date {
177-
guard let monthInterval = calendar.dateInterval(of: .month, for: date) else {
178-
return calendar.startOfDay(for: date)
179-
}
180-
return monthInterval.start
181-
}
182-
183-
func isDateInMonth(
184-
_ date: Date,
185-
monthStart: Date
186-
) -> Bool {
187-
calendar.isDate(
188-
calendar.startOfDay(for: date),
189-
equalTo: monthStart,
190-
toGranularity: .month
191-
)
210+
func startOfQuarter(for date: Date) -> Date {
211+
let month = calendar.component(.month, from: date)
212+
let startMonth = ((month - 1) / 3) * 3 + 1
213+
var components = calendar.dateComponents([.year], from: date)
214+
components.month = startMonth
215+
components.day = 1
216+
return calendar.date(from: components) ?? calendar.startOfDay(for: date)
192217
}
193218

194219
func maxCount(
195-
from weeks: [WidgetHeatmapWeekSnapshot],
220+
from months: [WidgetHeatmapMonthSnapshot],
196221
selectedActivityKinds: Set<ActivityKind>
197222
) -> Int {
198-
weeks
223+
months
224+
.flatMap(\.weeks)
199225
.flatMap(\.days)
200226
.filter(\.isVisible)
201227
.map { day in

DevLog/Widget/Heatmap/HeatmapWidgetSyncCoordinator.swift

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,16 @@ final class HeatmapWidgetSyncCoordinator {
3131
selectedActivityKinds: Set<ActivityKind>,
3232
now: Date = Date()
3333
) async {
34-
let monthStart = startOfMonth(for: now)
35-
guard let nextMonthStart = calendar.date(byAdding: .month, value: 1, to: monthStart) else {
34+
let quarterStart = startOfQuarter(for: now)
35+
guard let nextQuarterStart = calendar.date(byAdding: .month, value: 3, to: quarterStart) else {
3636
return
3737
}
3838

3939
do {
4040
async let createdTodoPage = fetchTodosUseCase.execute(
4141
TodoQuery(
42-
sortDateFrom: monthStart,
43-
sortDateTo: nextMonthStart,
42+
sortDateFrom: quarterStart,
43+
sortDateTo: nextQuarterStart,
4444
includesDeleted: true,
4545
sortTarget: .createdAt,
4646
pageSize: 100,
@@ -50,8 +50,8 @@ final class HeatmapWidgetSyncCoordinator {
5050
)
5151
async let completedTodoPage = fetchTodosUseCase.execute(
5252
TodoQuery(
53-
sortDateFrom: monthStart,
54-
sortDateTo: nextMonthStart,
53+
sortDateFrom: quarterStart,
54+
sortDateTo: nextQuarterStart,
5555
includesDeleted: true,
5656
sortTarget: .completedAt,
5757
pageSize: 100,
@@ -61,8 +61,8 @@ final class HeatmapWidgetSyncCoordinator {
6161
)
6262
async let deletedTodoPage = fetchTodosUseCase.execute(
6363
TodoQuery(
64-
sortDateFrom: monthStart,
65-
sortDateTo: nextMonthStart,
64+
sortDateFrom: quarterStart,
65+
sortDateTo: nextQuarterStart,
6666
includesDeleted: true,
6767
sortTarget: .deletedAt,
6868
pageSize: 100,
@@ -76,7 +76,7 @@ final class HeatmapWidgetSyncCoordinator {
7676
completedTodos: try await completedTodoPage.items,
7777
deletedTodos: try await deletedTodoPage.items,
7878
selectedActivityKinds: selectedActivityKinds,
79-
monthStart: monthStart,
79+
quarterStart: quarterStart,
8080
now: now
8181
)
8282

@@ -94,11 +94,12 @@ final class HeatmapWidgetSyncCoordinator {
9494
}
9595

9696
private extension HeatmapWidgetSyncCoordinator {
97-
func startOfMonth(for date: Date) -> Date {
98-
guard let monthInterval = calendar.dateInterval(of: .month, for: date) else {
99-
return calendar.startOfDay(for: date)
100-
}
101-
102-
return monthInterval.start
97+
func startOfQuarter(for date: Date) -> Date {
98+
let month = calendar.component(.month, from: date)
99+
let startMonth = ((month - 1) / 3) * 3 + 1
100+
var components = calendar.dateComponents([.year], from: date)
101+
components.month = startMonth
102+
components.day = 1
103+
return calendar.date(from: components) ?? calendar.startOfDay(for: date)
103104
}
104105
}

DevLogWidget/Common/WidgetPlaceholderCard.swift

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import SwiftUI
99

1010
struct WidgetPlaceholderCard: View {
11-
let title: String
1211
let message: String
1312

1413
var body: some View {
@@ -18,11 +17,7 @@ struct WidgetPlaceholderCard: View {
1817
Text(message)
1918
.font(.caption)
2019
.foregroundStyle(.secondary)
21-
}
22-
.overlay(alignment: .topLeading) {
23-
Text(title)
24-
.font(.headline)
25-
.padding(12)
20+
.multilineTextAlignment(.center)
2621
}
2722
}
2823
}

DevLogWidget/Heatmap/HeatmapWidget.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ struct HeatmapWidget: Widget {
2222
.containerBackground(.fill.tertiary, for: .widget)
2323
}
2424
.configurationDisplayName("Heatmap")
25-
.description("이번 달 활동 히트맵을 표시합니다.")
26-
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
25+
.description("활동 히트맵을 표시합니다.")
26+
.supportedFamilies([.systemSmall, .systemMedium])
2727
}
2828
}

DevLogWidget/Heatmap/HeatmapWidgetConfigurationIntent.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ import WidgetKit
1010

1111
struct HeatmapWidgetConfigurationIntent: WidgetConfigurationIntent {
1212
static var title: LocalizedStringResource = "Heatmap"
13-
static var description = IntentDescription("이번 달 활동 히트맵을 표시합니다.")
13+
static var description = IntentDescription("활동 히트맵을 표시합니다.")
1414
}

0 commit comments

Comments
 (0)