Skip to content

Commit ee41ec6

Browse files
authored
카테고리 추가·수정 시트에 Figma 디자인을 적용한다 (#935)
* feat: 카테고리 이름 중복 여부와 안내 문구 상태 추가 * ui: 카테고리 추가·수정 시트에 Figma 디자인 적용
1 parent 4e5ebfc commit ee41ec6

4 files changed

Lines changed: 386 additions & 63 deletions

File tree

‎Application/Presentation/HomeTab/Sources/Category/CategoryManageFeature.swift‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ struct CategoryManageFeature {
4242
var categoryNameCountText: String {
4343
"\(category.name.count)/20"
4444
}
45-
var canSubmitUserCategory: Bool {
45+
var isDuplicatedName: Bool {
4646
let name = category.name.trimmingCharacters(in: .whitespacesAndNewlines)
4747
if name.isEmpty {
4848
return false
@@ -51,7 +51,7 @@ struct CategoryManageFeature {
5151
if SystemTodoCategory.allCases.contains(where: {
5252
$0.rawValue.caseInsensitiveCompare(name) == .orderedSame
5353
}) {
54-
return false
54+
return true
5555
}
5656

5757
if preferences.contains(where: { item in
@@ -61,6 +61,23 @@ struct CategoryManageFeature {
6161

6262
return userCategory.name.caseInsensitiveCompare(name) == .orderedSame
6363
}) {
64+
return true
65+
}
66+
67+
return false
68+
}
69+
var nameMessage: String {
70+
isDuplicatedName
71+
? String(localized: "todo_manage_name_duplicate", bundle: PresentationResources.bundle)
72+
: String(localized: "todo_manage_name_hint", bundle: PresentationResources.bundle)
73+
}
74+
var canSubmitUserCategory: Bool {
75+
let name = category.name.trimmingCharacters(in: .whitespacesAndNewlines)
76+
if name.isEmpty {
77+
return false
78+
}
79+
80+
if isDuplicatedName {
6481
return false
6582
}
6683

‎Application/Presentation/HomeTab/Sources/Category/CategoryManageView.swift‎

Lines changed: 197 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -26,23 +26,8 @@ struct CategoryManageView: View {
2626
}
2727
.margins(.horizontal, 16)
2828
.margins(.vertical, 8),
29-
footer: UIHostingConfiguration {
30-
Button {
31-
store.send(.tapAddUserCategory)
32-
} label: {
33-
Label(
34-
String(localized: "todo_manage_add_category_title", bundle: PresentationResources.bundle),
35-
systemImage: "plus.circle.fill"
36-
)
37-
.font(.headline)
38-
.foregroundStyle(Color.onPrimaryContainer)
39-
.frame(maxWidth: .infinity)
40-
.padding(.vertical, 10)
41-
}
42-
.adaptiveButtonStyle(shape: RoundedRectangle(cornerRadius: 16), color: .primaryContainer)
43-
}
44-
.margins(.horizontal, 16)
45-
.margins(.vertical, 24),
29+
footer: UIHostingConfiguration { EmptyView() }
30+
.margins(.vertical, 8),
4631
row: { item in
4732
UIHostingConfiguration {
4833
CategoryManageRow(
@@ -60,8 +45,25 @@ struct CategoryManageView: View {
6045
}
6146
)
6247
}
63-
.ignoresSafeArea(edges: .bottom)
6448
.safeAreaInset(edge: .top, spacing: 0) { toolBar }
49+
.safeAreaInset(edge: .bottom, spacing: 0) {
50+
Button {
51+
store.send(.tapAddUserCategory)
52+
} label: {
53+
Label(
54+
String(localized: "todo_manage_add_category_title", bundle: PresentationResources.bundle),
55+
systemImage: "plus.circle.fill"
56+
)
57+
.font(.headline)
58+
.foregroundStyle(Color.white)
59+
.frame(maxWidth: .infinity)
60+
.padding(.vertical, 10)
61+
}
62+
.adaptiveButtonStyle(shape: RoundedRectangle(cornerRadius: 16), color: .accent)
63+
.padding(.horizontal)
64+
.padding(.vertical, 12)
65+
.background(Color.surface, ignoresSafeAreaEdges: .bottom)
66+
}
6567
.background(Color.appBackground.ignoresSafeArea())
6668
.sheet(
6769
item: $store.scope(state: \.categorySheet, action: \.categorySheet)
@@ -110,7 +112,7 @@ private struct CategoryManageRow: View {
110112

111113
var body: some View {
112114
if item.isUserCategory {
113-
content
115+
CategoryManageRowContent(item: item, onToggle: onToggle)
114116
.background(Color.surface)
115117
.itemActions {
116118
ItemActionButton(
@@ -126,11 +128,16 @@ private struct CategoryManageRow: View {
126128
)
127129
}
128130
} else {
129-
content
131+
CategoryManageRowContent(item: item, onToggle: onToggle)
130132
}
131133
}
134+
}
135+
136+
private struct CategoryManageRowContent: View {
137+
let item: TodoCategoryItem
138+
let onToggle: () -> Void
132139

133-
private var content: some View {
140+
var body: some View {
134141
HStack(spacing: 12) {
135142
Image(systemName: item.symbolName)
136143
.font(.headline)
@@ -154,60 +161,189 @@ private struct CategoryManageRow: View {
154161
}
155162
}
156163

164+
private let categoryColorHexValues = [
165+
"#FF3B30", "#FF9500", "#34C759", "#1EA0A6", "#007AFF", "#AF52DE", "#FF2D55", "#8E8E93"
166+
]
167+
157168
private struct CategoryManageSheet: View {
158169
@Bindable var store: Store<CategoryManageFeature.CategorySheetState, CategoryManageFeature.Action.CategorySheet>
159170

160171
var body: some View {
161-
NavigationStack {
162-
Form {
163-
Section {
164-
HStack(spacing: 8) {
165-
TextField(
166-
"",
167-
text: $store.category.name,
168-
prompt: Text(store.placeholder).foregroundStyle(.secondary)
169-
)
170-
.frame(height: UIFont.preferredFont(forTextStyle: .body).lineHeight)
171-
172-
Text(store.categoryNameCountText)
173-
.font(.footnote)
174-
.foregroundStyle(.secondary)
175-
.monospacedDigit()
172+
ScrollView {
173+
LazyVStack(spacing: 24) {
174+
nameCard
175+
colorCard
176+
previewCard
177+
}
178+
.padding(.horizontal, 16)
179+
.padding(.bottom, 16)
180+
}
181+
.contentMargins(.top, 16, for: .scrollContent)
182+
.safeAreaInset(edge: .top, spacing: 0) { toolBar }
183+
.safeAreaInset(edge: .bottom, spacing: 0) {
184+
Button {
185+
store.send(.tapSaveButton)
186+
} label: {
187+
Text(store.submitTitle)
188+
.font(.headline)
189+
.foregroundStyle(Color.white)
190+
.frame(maxWidth: .infinity)
191+
.padding(.vertical, 10)
192+
}
193+
.adaptiveButtonStyle(shape: RoundedRectangle(cornerRadius: 16), color: .accent)
194+
.disabled(!store.canSubmitUserCategory)
195+
.padding(.horizontal)
196+
.padding(.vertical, 12)
197+
.background(Color.surface, ignoresSafeAreaEdges: .bottom)
198+
}
199+
.background(Color.appBackground.ignoresSafeArea())
200+
.presentationDragIndicator(.visible)
201+
}
202+
203+
private var toolBar: some View {
204+
ZStack {
205+
Text(store.navigationTitle)
206+
.font(.headline)
207+
HStack {
208+
Button {
209+
store.send(.tapCloseButton)
210+
} label: {
211+
if #available(iOS 26.0, *) {
212+
Image(systemName: "xmark")
213+
} else {
214+
Text(String(localized: "common_close", bundle: PresentationResources.bundle))
176215
}
177216
}
217+
.topBarButtonStyle()
178218

179-
Section {
180-
ColorPicker(selection: $store.category.colorHex.colorValue, supportsOpacity: false) {
181-
Text(store.category.colorHex.isEmpty ? "#" : store.category.colorHex)
182-
.overlay(alignment: .bottom) {
183-
Rectangle()
184-
.frame(height: 1)
185-
.offset(y: 1)
186-
}
187-
.foregroundStyle(store.category.colorHex.colorValue)
188-
.onTapGesture {
189-
store.send(.tapRandomColorButton)
190-
}
191-
}
192-
.pickerStyle(.palette)
219+
Spacer()
220+
}
221+
}
222+
.padding(.horizontal, 16)
223+
.padding(.vertical, 12)
224+
.background(Color.appBackground, ignoresSafeAreaEdges: .top)
225+
}
226+
227+
private var nameCard: some View {
228+
VStack(alignment: .leading, spacing: 12) {
229+
Text(String(localized: "todo_manage_name_placeholder", bundle: PresentationResources.bundle))
230+
.font(.headline)
231+
.padding(.horizontal, 16)
232+
233+
HStack(spacing: 8) {
234+
TextField(
235+
"",
236+
text: $store.category.name,
237+
prompt: Text(store.placeholder).foregroundStyle(.secondary)
238+
)
239+
.font(.body)
240+
241+
Text(store.categoryNameCountText)
242+
.font(.footnote)
243+
.foregroundStyle(Color.textSecondary)
244+
.monospacedDigit()
245+
}
246+
.padding()
247+
.background(Color.surface, in: .rect(cornerRadius: 16))
248+
.tint(Color.accent)
249+
250+
Text(store.nameMessage)
251+
.font(.footnote)
252+
.foregroundStyle(store.isDuplicatedName ? Color.danger : Color.textSecondary)
253+
.padding(.horizontal, 16)
254+
}
255+
}
256+
257+
private var colorCard: some View {
258+
VStack(alignment: .leading, spacing: 12) {
259+
HStack {
260+
Text(String(localized: "todo_manage_color", bundle: PresentationResources.bundle))
261+
.font(.headline)
262+
263+
Spacer()
264+
265+
Button {
266+
store.send(.tapRandomColorButton)
267+
} label: {
268+
Label(
269+
String(localized: "todo_manage_random_color", bundle: PresentationResources.bundle),
270+
systemImage: "shuffle"
271+
)
272+
.font(.subheadline)
273+
.foregroundStyle(Color.accent)
193274
}
275+
.buttonStyle(.plain)
194276
}
195-
.navigationTitle(store.navigationTitle)
196-
.navigationBarTitleDisplayMode(.inline)
197-
.toolbar {
198-
ToolbarItem(placement: .navigationBarLeading) {
199-
Button(String(localized: "common_close", bundle: PresentationResources.bundle)) {
200-
store.send(.tapCloseButton)
277+
.padding(.horizontal, 16)
278+
279+
VStack(spacing: 16) {
280+
Image(systemName: store.todoCategoryItem.symbolName)
281+
.font(.largeTitle)
282+
.frame(width: 88, height: 88)
283+
.iconStyle(color: store.todoCategoryItem.color, in: Circle())
284+
285+
HStack(spacing: 4) {
286+
ForEach(categoryColorHexValues, id: \.self) { colorHex in
287+
let color = Color(hexString: colorHex) ?? .gray
288+
let isSelected = store.category.colorHex.caseInsensitiveCompare(colorHex) == .orderedSame
289+
290+
Button {
291+
store.send(.binding(.set(\.category.colorHex, colorHex)))
292+
} label: {
293+
Circle()
294+
.fill(color)
295+
.padding(3)
296+
.overlay {
297+
if isSelected {
298+
Image(systemName: "checkmark")
299+
.font(.caption.bold())
300+
.foregroundStyle(.white)
301+
Circle()
302+
.strokeBorder(color, lineWidth: 2)
303+
}
304+
}
305+
.aspectRatio(1, contentMode: .fit)
306+
}
307+
.buttonStyle(.plain)
308+
.frame(maxWidth: .infinity)
201309
}
310+
311+
ColorPicker(
312+
store.category.colorHex.isEmpty ? "#" : store.category.colorHex,
313+
selection: $store.category.colorHex.colorValue,
314+
supportsOpacity: false
315+
)
316+
.labelsHidden()
317+
.frame(maxWidth: .infinity)
202318
}
319+
}
320+
.padding(16)
321+
.frame(maxWidth: .infinity)
322+
.background(Color.surface, in: .rect(cornerRadius: 16))
323+
}
324+
}
203325

204-
ToolbarItem(placement: .navigationBarTrailing) {
205-
Button(store.submitTitle) {
206-
store.send(.tapSaveButton)
207-
}
208-
.disabled(!store.canSubmitUserCategory)
326+
private var previewCard: some View {
327+
VStack(alignment: .leading, spacing: 12) {
328+
Text(String(localized: "todo_preview", bundle: PresentationResources.bundle))
329+
.font(.headline)
330+
.padding(.horizontal, 16)
331+
332+
VStack(alignment: .leading, spacing: 12) {
333+
Text(String(localized: "todo_manage_preview_description", bundle: PresentationResources.bundle))
334+
.font(.footnote)
335+
.foregroundStyle(Color.textSecondary)
336+
337+
HStack(spacing: 12) {
338+
CategoryManageRowContent(item: store.todoCategoryItem, onToggle: {})
339+
Image(systemName: "line.3.horizontal")
340+
.foregroundStyle(Color.textTertiary)
209341
}
342+
.allowsHitTesting(false)
210343
}
344+
.padding(16)
345+
.frame(maxWidth: .infinity, alignment: .leading)
346+
.background(Color.surface, in: .rect(cornerRadius: 16))
211347
}
212348
}
213349
}

0 commit comments

Comments
 (0)