Skip to content

Commit 2322a33

Browse files
authored
[#288] TodoEditorView에서 Todo의 완료 여부를 토글할 수 있는 UI를 구현한다 (#296)
* feat: 토글로 완료 여부를 선택할 수 있도록 추가 * ui: TodoDetailView의 시트를 TodoEditorView의 시트 형태로 변경 * fix: 기존에 완료된 후 해제하고 새로 완료했을 경우 기존의 완료 시간으로 업데이트 되는 현상 해결
1 parent 389efce commit 2322a33

3 files changed

Lines changed: 123 additions & 13 deletions

File tree

DevLog/Presentation/ViewModel/TodoEditorViewModel.swift

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import OrderedCollections
1111
@Observable
1212
final class TodoEditorViewModel: Store {
1313
private struct Draft: Equatable {
14+
let isCompleted: Bool
15+
let completedAt: Date?
1416
let isPinned: Bool
1517
let title: String
1618
let content: String
@@ -19,6 +21,8 @@ final class TodoEditorViewModel: Store {
1921
let kind: TodoKind
2022

2123
init(todo: Todo) {
24+
self.isCompleted = todo.isCompleted
25+
self.completedAt = todo.completedAt
2226
self.isPinned = todo.isPinned
2327
self.title = todo.title
2428
self.content = todo.content
@@ -28,6 +32,8 @@ final class TodoEditorViewModel: Store {
2832
}
2933

3034
init(state: State) {
35+
self.isCompleted = state.isCompleted
36+
self.completedAt = state.completedAt
3137
self.isPinned = state.isPinned
3238
self.title = state.title
3339
self.content = state.content
@@ -38,6 +44,8 @@ final class TodoEditorViewModel: Store {
3844
}
3945

4046
struct State: Equatable {
47+
var isCompleted: Bool = false
48+
var completedAt: Date?
4149
var isPinned: Bool = false
4250
var title: String = ""
4351
var content: String = ""
@@ -61,6 +69,7 @@ final class TodoEditorViewModel: Store {
6169
case addTag(String)
6270
case removeTag(String)
6371
case setContent(String)
72+
case setCompleted(Bool)
6473
case setDueDate(Date?)
6574
case setKind(TodoKind)
6675
case setPinned(Bool)
@@ -78,7 +87,6 @@ final class TodoEditorViewModel: Store {
7887
private let isCompleted: Bool
7988
private let isChecked: Bool
8089
private let createdAt: Date?
81-
private let completedAt: Date?
8290
private let originalDraft: Draft?
8391

8492
var navigationTitle: String {
@@ -104,7 +112,6 @@ final class TodoEditorViewModel: Store {
104112
self.isCompleted = false
105113
self.isChecked = false
106114
self.createdAt = nil
107-
self.completedAt = nil
108115
self.originalDraft = nil
109116
state.kind = kind
110117
}
@@ -115,8 +122,9 @@ final class TodoEditorViewModel: Store {
115122
self.isCompleted = todo.isCompleted
116123
self.isChecked = todo.isChecked
117124
self.createdAt = todo.createdAt
118-
self.completedAt = todo.completedAt
119125
self.originalDraft = Draft(todo: todo)
126+
state.isCompleted = todo.isCompleted
127+
state.completedAt = todo.completedAt
120128
state.isPinned = todo.isPinned
121129
state.title = todo.title
122130
state.content = todo.content
@@ -145,6 +153,11 @@ final class TodoEditorViewModel: Store {
145153
} else {
146154
state.dueDate = nil
147155
}
156+
case .setCompleted(let isCompleted):
157+
if state.isCompleted != isCompleted {
158+
state.completedAt = isCompleted ? Date() : nil
159+
}
160+
state.isCompleted = isCompleted
148161
case .setKind(let todoKind):
149162
state.kind = todoKind
150163
case .setPinned(let isPinned):
@@ -183,13 +196,13 @@ extension TodoEditorViewModel {
183196
return Todo(
184197
id: self.id,
185198
isPinned: state.isPinned,
186-
isCompleted: self.isCompleted,
199+
isCompleted: state.isCompleted,
187200
isChecked: self.isChecked,
188201
title: state.title,
189202
content: state.content,
190203
createdAt: self.createdAt ?? date,
191204
updatedAt: date,
192-
completedAt: self.completedAt,
205+
completedAt: state.completedAt,
193206
dueDate: state.dueDate,
194207
tags: state.tags.map { $0 },
195208
kind: state.kind

DevLog/UI/Home/TodoDetailView.swift

Lines changed: 96 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,104 @@ struct TodoDetailView: View {
7070
@ViewBuilder
7171
private var sheetContent: some View {
7272
if let todo = viewModel.state.todo {
73-
TodoInfoSheetView(
74-
createdAt: todo.createdAt,
75-
completedAt: todo.completedAt,
76-
dueDate: todo.dueDate,
77-
tags: todo.tags
78-
) {
73+
TodoDetailInfoSheetView(todo: todo) {
7974
viewModel.send(.setShowInfo(false))
8075
}
8176
}
8277
}
8378
}
79+
80+
private struct TodoDetailInfoSheetView: View {
81+
let todo: Todo
82+
let onClose: () -> Void
83+
private let calendar = Calendar.current
84+
85+
var body: some View {
86+
NavigationStack {
87+
List {
88+
Section("옵션") {
89+
HStack {
90+
Text("카테고리")
91+
Spacer()
92+
Text(todo.kind.localizedName)
93+
.foregroundStyle(.secondary)
94+
}
95+
96+
statusRow(
97+
title: "완료",
98+
systemImage: todo.isCompleted ? "checkmark.circle.fill" : "circle",
99+
color: todo.isCompleted ? .green : .secondary
100+
)
101+
102+
statusRow(
103+
title: "중요 표시",
104+
systemImage: todo.isPinned ? "star.fill" : "star",
105+
color: todo.isPinned ? .orange : .secondary
106+
)
107+
108+
HStack {
109+
Text("마감일")
110+
111+
Spacer()
112+
113+
if let dueDate = todo.dueDate {
114+
Tag(dueDateText(for: dueDate), isEditing: false)
115+
.padding(.vertical, -4)
116+
} else {
117+
Text("없음")
118+
.foregroundStyle(.secondary)
119+
}
120+
}
121+
}
122+
123+
Section("태그") {
124+
if todo.tags.isEmpty {
125+
Text("태그 없음")
126+
.foregroundStyle(.secondary)
127+
.padding(.vertical, 4)
128+
} else {
129+
TagList(todo.tags)
130+
}
131+
}
132+
}
133+
.navigationTitle("세부 정보")
134+
.navigationBarTitleDisplayMode(.inline)
135+
.toolbar {
136+
ToolbarLeadingButton {
137+
onClose()
138+
}
139+
}
140+
}
141+
}
142+
143+
@ViewBuilder
144+
private func statusRow(
145+
title: String,
146+
systemImage: String,
147+
color: Color
148+
) -> some View {
149+
HStack {
150+
Text(title)
151+
152+
Spacer()
153+
154+
Image(systemName: systemImage)
155+
.foregroundStyle(color)
156+
}
157+
}
158+
159+
private func dueDateText(for dueDate: Date) -> String {
160+
let currentYear = calendar.component(.year, from: Date())
161+
let dueDateYear = calendar.component(.year, from: dueDate)
162+
163+
if currentYear == dueDateYear {
164+
return dueDate.formatted(
165+
.dateTime.month(.defaultDigits).day(.defaultDigits)
166+
)
167+
}
168+
169+
return dueDate.formatted(
170+
.dateTime.year(.twoDigits).month(.defaultDigits).day(.defaultDigits)
171+
)
172+
}
173+
}

DevLog/UI/Home/TodoEditorView.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,15 @@ private struct TodoEditorInfoSheetView: View {
192192
}
193193
}
194194

195+
Toggle(
196+
"완료",
197+
isOn: Binding(
198+
get: { viewModel.state.isCompleted },
199+
set: { viewModel.send(.setCompleted($0)) }
200+
)
201+
)
202+
.tint(.blue)
203+
195204
Toggle(
196205
"중요 표시",
197206
isOn: Binding(
@@ -263,9 +272,7 @@ private struct TodoEditorInfoSheetView: View {
263272
HStack {
264273
Text("마감일")
265274
.foregroundStyle(.primary)
266-
267275
Spacer()
268-
269276
if let dueDate = viewModel.state.dueDate {
270277
Tag(dueDateText(for: dueDate), isEditing: true) {
271278
viewModel.send(.setDueDate(nil))

0 commit comments

Comments
 (0)