Skip to content

Commit 33480c6

Browse files
committed
ui: 시트로 태그 확인 및 수정 처리
1 parent e1db90d commit 33480c6

3 files changed

Lines changed: 68 additions & 152 deletions

File tree

DevLog/Resource/Localizable.xcstrings

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -462,12 +462,6 @@
462462
},
463463
"태그 없음" : {
464464

465-
},
466-
"태그 입력" : {
467-
468-
},
469-
"태그 편집" : {
470-
471465
},
472466
"테마" : {
473467

DevLog/UI/Common/Component/Tag+.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ struct Tag: View {
5454
)
5555

5656
}
57+
.buttonStyle(.plain)
5758
}
5859
}
5960
.background {

DevLog/UI/Home/TodoEditorView.swift

Lines changed: 67 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ struct TodoEditorView: View {
4343
.sheet(isPresented: Binding(
4444
get: { viewModel.state.showInfo },
4545
set: { viewModel.send(.setShowInfo($0)) }
46+
)) {
47+
TodoEditorInfoSheetView(viewModel: viewModel) {
48+
viewModel.send(.setShowInfo(false))
49+
}
4650
}
4751
.toolbar {
4852
ToolbarLeadingButton { dismiss() }
@@ -154,7 +158,24 @@ struct TodoEditorView: View {
154158
.padding(.vertical, 8)
155159
}
156160

157-
private var editorInfoSheet: some View {
161+
private func submit() {
162+
let todo = viewModel.makeTodo()
163+
onSubmit?(todo)
164+
dismiss()
165+
}
166+
167+
private enum Field: Hashable {
168+
case title, content
169+
}
170+
}
171+
172+
private struct TodoEditorInfoSheetView: View {
173+
@Bindable var viewModel: TodoEditorViewModel
174+
let onClose: () -> Void
175+
@FocusState private var isTagFieldFocused: Bool
176+
private let calendar = Calendar.current
177+
178+
var body: some View {
158179
NavigationStack {
159180
List {
160181
Section("옵션") {
@@ -187,28 +208,51 @@ struct TodoEditorView: View {
187208
}
188209

189210
Section("태그") {
190-
TagEditor(
191-
tags: viewModel.state.tags,
192-
addAction: { viewModel.send(.addTag($0)) },
193-
deleteAction: { viewModel.send(.removeTag($0)) }
194-
) {
195-
Label("태그 편집", systemImage: "tag")
211+
HStack(spacing: 12) {
212+
TextField(
213+
"추가",
214+
text: Binding(
215+
get: { viewModel.state.tagText },
216+
set: { viewModel.send(.setTagText($0)) }
217+
)
218+
)
219+
.frame(height: UIFont.preferredFont(forTextStyle: .title2).lineHeight)
220+
.textInputAutocapitalization(.never)
221+
.focused($isTagFieldFocused)
222+
.onSubmit {
223+
submitTag()
224+
}
225+
226+
if isTagFieldFocused {
227+
Button {
228+
submitTag()
229+
} label: {
230+
Image(systemName: "plus.circle.fill")
231+
.font(.title2)
232+
.foregroundStyle(canSubmitTag ? .blue : .secondary)
233+
}
234+
.disabled(!canSubmitTag)
235+
}
196236
}
197237

198238
if viewModel.state.tags.isEmpty {
199239
Text("태그 없음")
200240
.foregroundStyle(.secondary)
201-
} else {
202-
TagList(viewModel.state.tags)
203241
.padding(.vertical, 4)
242+
} else {
243+
TagList(
244+
viewModel.state.tags,
245+
isEditing: isTagFieldFocused,
246+
action: { viewModel.send(.removeTag($0)) }
247+
)
204248
}
205249
}
206-
.alignmentGuide(.listRowSeparatorLeading) { $0[.leading] }
207250
}
208251
.navigationTitle("세부 정보")
209252
.navigationBarTitleDisplayMode(.inline)
210253
.toolbar {
211254
ToolbarLeadingButton {
255+
onClose()
212256
}
213257
}
214258
}
@@ -220,7 +264,7 @@ struct TodoEditorView: View {
220264
set: { viewModel.send(.setDueDate($0)) }
221265
)) {
222266
HStack {
223-
Label("마감일", systemImage: "calendar")
267+
Text("마감일")
224268
.foregroundStyle(.primary)
225269

226270
Spacer()
@@ -238,14 +282,20 @@ struct TodoEditorView: View {
238282
}
239283
}
240284

241-
private func submit() {
242-
let todo = viewModel.makeTodo()
243-
onSubmit?(todo)
244-
dismiss()
285+
private func submitTag() {
286+
guard canSubmitTag else { return }
287+
288+
let tagText = normalizedTagText
289+
viewModel.send(.addTag(tagText))
290+
viewModel.send(.setTagText(""))
245291
}
246292

247-
private enum Field: Hashable {
248-
case title, description, tag
293+
private var normalizedTagText: String {
294+
viewModel.state.tagText.trimmingCharacters(in: .whitespacesAndNewlines)
295+
}
296+
297+
private var canSubmitTag: Bool {
298+
!normalizedTagText.isEmpty && !viewModel.state.tags.contains(normalizedTagText)
249299
}
250300

251301
private func dueDateText(for dueDate: Date) -> String {
@@ -264,135 +314,6 @@ struct TodoEditorView: View {
264314
}
265315
}
266316

267-
private struct TagEditor<Content: View>: View {
268-
@Environment(\.safeAreaInsets) private var safeAreaInsets
269-
@State private var isPresented: Bool = false
270-
@State private var sheetHeight: CGFloat = .pi
271-
@State private var tagsHeight: CGFloat = 0
272-
@State private var fieldHeight: CGFloat = 0
273-
@State private var tag = ""
274-
@ViewBuilder private var content: () -> Content
275-
private let tags: OrderedSet<String>
276-
private let addAction: (String) -> Void
277-
private let deleteAction: (String) -> Void
278-
private let spacing: CGFloat = 8
279-
280-
init(
281-
tags: OrderedSet<String>,
282-
addAction: @escaping (String) -> Void = { _ in },
283-
deleteAction: @escaping (String) -> Void = { _ in },
284-
@ViewBuilder content: @escaping () -> Content
285-
) {
286-
self.tags = tags
287-
self.addAction = addAction
288-
self.deleteAction = deleteAction
289-
self.content = content
290-
}
291-
292-
var body: some View {
293-
Button {
294-
isPresented = true
295-
} label: {
296-
content()
297-
}
298-
.sheet(
299-
isPresented: $isPresented,
300-
onDismiss: { tag = "" }
301-
) {
302-
VStack(spacing: tags.isEmpty ? 0 : spacing) {
303-
ScrollView {
304-
TagList(tags, isEditing: true, action: deleteAction)
305-
.background {
306-
GeometryReader { geometry in
307-
Color.clear
308-
.onAppear {
309-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
310-
tagsHeight = geometry.size.height
311-
sheetHeight += tagsHeight + (tagsHeight == 0 ? 0 : spacing)
312-
}
313-
}
314-
.onChange(of: tags) { _, newTags in
315-
DispatchQueue.main.async {
316-
tagsHeight = geometry.size.height
317-
sheetHeight = fieldHeight + tagsHeight + (newTags.isEmpty ? 0 : spacing)
318-
}
319-
}
320-
}
321-
}
322-
}
323-
.scrollIndicators(.hidden)
324-
.frame(maxHeight: tagsHeight)
325-
.padding(.top, tags.isEmpty ? 0 : 8)
326-
327-
tagField
328-
.background {
329-
GeometryReader { geometry in
330-
Color.clear
331-
.onAppear {
332-
fieldHeight = geometry.size.height + 16
333-
sheetHeight = fieldHeight
334-
}
335-
}
336-
}
337-
338-
}
339-
.padding(.horizontal)
340-
.presentationDragIndicator(.hidden)
341-
.presentationDetents([.height(sheetHeight)])
342-
}
343-
}
344-
345-
private var tagField: some View {
346-
HStack {
347-
HStack {
348-
TextField("태그 입력", text: $tag)
349-
.keyboardType(.webSearch)
350-
.padding(tag.isEmpty ? .all : [.leading, .vertical])
351-
.onSubmit {
352-
isPresented = false
353-
}
354-
355-
if !tag.isEmpty {
356-
Button {
357-
tag = ""
358-
} label: {
359-
Image(systemName: "xmark.circle.fill")
360-
.font(.title)
361-
.symbolRenderingMode(.palette)
362-
.foregroundStyle(
363-
Color(.label),
364-
Color(.systemBackground)
365-
)
366-
}
367-
.padding(.trailing)
368-
}
369-
}
370-
.background {
371-
Capsule()
372-
.fill(.ultraThinMaterial)
373-
.overlay {
374-
Capsule()
375-
.stroke(Color.white.opacity(0.2), lineWidth: 1)
376-
}
377-
}
378-
379-
Button {
380-
addAction(tag)
381-
tag = ""
382-
} label: {
383-
Image(systemName: "plus")
384-
.font(.largeTitle)
385-
.foregroundStyle(Color.white)
386-
.adaptiveButtonStyle(
387-
shape: .circle,
388-
color: (!tag.isEmpty && !tags.contains(tag)) ? Color.blue : .gray.opacity(0.4)
389-
)
390-
}
391-
.disabled(tag.isEmpty || tags.contains(tag))
392-
}
393-
}
394-
}
395-
396317
private struct DueDatePicker<Content: View>: View {
397318
@Environment(\.safeAreaInsets) private var safeAreaInsets
398319
@State private var isPresented: Bool = false

0 commit comments

Comments
 (0)