Skip to content

Commit c24e237

Browse files
authored
[#207] 작은 기기로 ProfileView를 렌더링 했을 때 히트맵 UI가 넘치는 현상을 해결한다 (#209)
* fix: screenWidth와 safeAreaInset을 통해 width를 정의 후 수식으로 셀 크기를 정의하도록 변경 * style: 주석 추가
1 parent 11508e2 commit c24e237

4 files changed

Lines changed: 89 additions & 37 deletions

File tree

DevLog/Presentation/Structure/Profile/ProfileCompletionDay.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@ struct ProfileCompletionDay: Hashable {
1111
let date: Date
1212
let createdCount: Int
1313
let completedCount: Int
14-
let isInMonth: Bool
14+
let isVisible: Bool
1515
}

DevLog/Presentation/Structure/Profile/ProfileCompletionQuarter.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ struct ProfileCompletionQuarter: Identifiable, Hashable {
2020
months
2121
.flatMap { $0.weeks }
2222
.flatMap { $0 }
23-
.filter { $0.isInMonth }
23+
.filter { $0.isVisible }
2424
.map { $0.createdCount + $0.completedCount }
2525
.max() ?? 0
2626
}
@@ -32,7 +32,7 @@ struct ProfileCompletionQuarter: Identifiable, Hashable {
3232
let days = months
3333
.flatMap(\.weeks)
3434
.flatMap { $0 }
35-
.filter(\.isInMonth)
35+
.filter(\.isVisible)
3636
let groupedByWeekStart = Dictionary(grouping: days) { day in
3737
calendar.dateInterval(of: .weekOfYear, for: day.date)?.start
3838
?? calendar.startOfDay(for: day.date)

DevLog/Presentation/ViewModel/ProfileViewModel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ private extension ProfileViewModel {
440440
date: normalizedDate,
441441
createdCount: createdCount,
442442
completedCount: completedCount,
443-
isInMonth: isInMonth
443+
isVisible: isInMonth
444444
)
445445
)
446446
guard let nextDay = calendar.date(byAdding: .day, value: 1, to: cursor) else { break }

DevLog/UI/Profile/ProfileHeatmapView.swift

Lines changed: 85 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,35 @@
88
import SwiftUI
99

1010
struct ProfileHeatmapView: View {
11+
@Environment(\.safeAreaInsets) private var safeAreaInsets
12+
@Environment(\.sceneWidth) private var sceneWidth
1113
let quarter: ProfileCompletionQuarter
1214
let selectedActivityTypes: Set<ProfileActivityType>
1315
let selectedDay: ProfileCompletionDay?
1416
let onSelectDay: (ProfileCompletionDay) -> Void
1517

1618
var body: some View {
19+
let layout = ProfileHeatmapLayout(
20+
availableWidth: availableWidth,
21+
weekCounts: quarter.months.map(\.weeks.count)
22+
)
23+
1724
VStack(alignment: .leading, spacing: 10) {
1825
Text("활동 히트맵")
1926
.font(.subheadline)
2027
.bold()
2128
HStack(alignment: .top, spacing: 0) {
22-
weekdayLabel
23-
.padding(.trailing, 10)
24-
let months = quarter.months
25-
ForEach(Array(zip(months.indices, months)), id: \.1) { index, month in
26-
MonthCompactHeatmapView(
27-
month: month,
28-
maxCount: quarter.maxCount,
29-
selectedActivityTypes: selectedActivityTypes,
30-
selectedDay: selectedDay,
31-
onSelectDay: onSelectDay
32-
)
33-
if index < months.count - 1 {
34-
Spacer()
29+
weekdayLabel(layout: layout)
30+
HStack(alignment: .top, spacing: layout.monthSpacing) {
31+
ForEach(quarter.months) { month in
32+
MonthCompactHeatmapView(
33+
month: month,
34+
maxCount: quarter.maxCount,
35+
layout: layout,
36+
selectedActivityTypes: selectedActivityTypes,
37+
selectedDay: selectedDay,
38+
onSelectDay: onSelectDay
39+
)
3540
}
3641
}
3742
}
@@ -40,74 +45,121 @@ struct ProfileHeatmapView: View {
4045
}
4146

4247
@ViewBuilder
43-
private var weekdayLabel: some View {
48+
private func weekdayLabel(layout: ProfileHeatmapLayout) -> some View {
4449
let labels: [Int: String] = [
4550
2: "",
4651
4: "",
4752
6: ""
4853
]
4954
let orderedWeekdays = Array(1...7)
50-
let cellSize: CGFloat = 16
5155

52-
VStack(alignment: .leading, spacing: 4) {
56+
VStack(alignment: .leading, spacing: layout.cellSpacing) {
5357
ForEach(orderedWeekdays, id: \.self) { weekday in
5458
Group {
5559
if let label = labels[weekday] {
5660
Text(label)
5761
.font(.caption2)
5862
.foregroundStyle(.secondary)
59-
.frame(width: cellSize, height: cellSize)
63+
.frame(
64+
width: layout.cellSize,
65+
height: layout.cellSize,
66+
alignment: .leading
67+
)
6068
} else {
6169
Color.clear
62-
.frame(width: cellSize, height: cellSize)
70+
.frame(
71+
width: layout.cellSize,
72+
height: layout.cellSize
73+
)
6374
}
6475
}
6576
}
6677
}
67-
.padding(.top, 22)
78+
.padding(.top, layout.weekdayTopPadding)
79+
}
80+
81+
private var availableWidth: CGFloat {
82+
// ProfileView의 바깥 가로 패딩(16)과 히트맵 카드 내부 패딩(12)을 합한 값
83+
let horizontalPadding: CGFloat = 16 + 12
84+
return max(
85+
0,
86+
sceneWidth
87+
- safeAreaInsets.leading
88+
- safeAreaInsets.trailing
89+
- (horizontalPadding * 2)
90+
)
91+
}
92+
}
93+
94+
private struct ProfileHeatmapLayout {
95+
let cellSize: CGFloat
96+
let cellSpacing: CGFloat = 4
97+
let monthSpacing: CGFloat = 12
98+
let monthTitleSpacing: CGFloat = 6
99+
100+
init(availableWidth: CGFloat, weekCounts: [Int]) {
101+
let sanitizedWeekCounts = weekCounts.filter { 0 < $0 }
102+
let totalColumns = max(sanitizedWeekCounts.reduce(0, +), 1)
103+
let totalColumnSpacings = sanitizedWeekCounts.reduce(0) { partialResult, count in
104+
partialResult + max(count - 1, 0)
105+
}
106+
let fixedWidth = monthSpacing * CGFloat(max(sanitizedWeekCounts.count - 1, 0))
107+
+ cellSpacing * CGFloat(totalColumnSpacings)
108+
cellSize = max(0, availableWidth - fixedWidth) / CGFloat(totalColumns + 1)
109+
}
110+
111+
var weekdayTopPadding: CGFloat {
112+
cellSize + monthTitleSpacing
113+
}
114+
115+
var cellCornerRadius: CGFloat {
116+
max(2, cellSize * 0.2)
117+
}
118+
119+
var innerSelectionLineWidth: CGFloat {
120+
max(1.2, cellSize * 0.12)
68121
}
69122
}
70123

71124
private struct MonthCompactHeatmapView: View {
72125
@Environment(\.colorScheme) private var colorScheme
73126
let month: ProfileCompletionMonth
74127
let maxCount: Int
128+
let layout: ProfileHeatmapLayout
75129
let selectedActivityTypes: Set<ProfileActivityType>
76130
let selectedDay: ProfileCompletionDay?
77131
let onSelectDay: (ProfileCompletionDay) -> Void
78132
private let orderedWeekdays = Array(1...7)
79-
private let cellSize: CGFloat = 16
80-
private let cellSpacing: CGFloat = 4
81133

82134
var body: some View {
83-
VStack(alignment: .leading, spacing: 6) {
135+
VStack(alignment: .leading, spacing: layout.monthTitleSpacing) {
84136
Text(month.monthStart.formatted(.dateTime.month(.abbreviated)))
85-
.frame(height: cellSize)
137+
.frame(height: layout.cellSize)
86138
.font(.caption)
87139
.foregroundStyle(.secondary)
88140

89-
VStack(alignment: .leading, spacing: cellSpacing) {
141+
VStack(alignment: .leading, spacing: layout.cellSpacing) {
90142
ForEach(orderedWeekdays, id: \.self) { weekday in
91-
HStack(spacing: cellSpacing) {
143+
HStack(spacing: layout.cellSpacing) {
92144
ForEach(month.weeks.indices, id: \.self) { weekIndex in
93145
let day = month.weeks[weekIndex].first {
94146
Calendar.current.component(.weekday, from: $0.date) == weekday
95147
}
96148

97-
RoundedRectangle(cornerRadius: 3)
149+
RoundedRectangle(cornerRadius: layout.cellCornerRadius)
98150
.fill(fillColor(for: day, with: maxCount))
99-
.overlay(
100-
RoundedRectangle(cornerRadius: 3)
101-
.stroke(selectionInnerBorderColor(for: day), lineWidth: 2)
151+
.stroke(
152+
selectionInnerBorderColor(for: day),
153+
lineWidth: layout.innerSelectionLineWidth
102154
)
103155
.overlay(
104-
RoundedRectangle(cornerRadius: 4)
156+
RoundedRectangle(cornerRadius: layout.cellCornerRadius + 1)
105157
.stroke(selectionOuterBorderColor(for: day), lineWidth: 0.8)
106158
.padding(-1)
107159
)
108-
.frame(width: cellSize, height: cellSize)
160+
.frame(width: layout.cellSize, height: layout.cellSize)
109161
.onTapGesture {
110-
if let day, day.isInMonth {
162+
if let day, day.isVisible {
111163
onSelectDay(day)
112164
}
113165
}
@@ -135,7 +187,7 @@ private struct MonthCompactHeatmapView: View {
135187
}
136188

137189
private func fillColor(for day: ProfileCompletionDay?, with maxCount: Int) -> Color {
138-
guard let day, day.isInMonth else { return .clear }
190+
guard let day, day.isVisible else { return .clear }
139191
let count = dayCount(for: day)
140192
if count == 0 {
141193
return Color(.systemGray5)

0 commit comments

Comments
 (0)