Skip to content

Commit ed65075

Browse files
authored
[#434] HomeView에 Coordinator을 구성한다 (#442)
* refactor: HomeViewCoorinator 구현 및 적용 * refactor: NavigationRouter 의존성 제거 * refactor: show(_:) -> replace(with:)로 수정
1 parent d15d953 commit ed65075

6 files changed

Lines changed: 132 additions & 88 deletions

File tree

DevLog/UI/Common/NavigationRouter.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ final class NavigationRouter<Route: Hashable> {
2828
}
2929
}
3030

31-
func show(_ route: Route) {
31+
func replace(with route: Route) {
3232
path = [route]
3333
}
3434

DevLog/UI/Home/HomeView.swift

Lines changed: 54 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,9 @@
88
import SwiftUI
99

1010
struct HomeView: View {
11-
@Environment(\.diContainer) var container: any DIContainer
12-
@Environment(NavigationRouter<HomeRoute>.self) private var router
13-
@State var viewModel: HomeViewModel
14-
let isCompactLayout: Bool
1511
@ScaledMetric(relativeTo: .largeTitle) private var labelWidth = CGFloat(34)
12+
let coordinator: HomeViewCoordinator
13+
let isCompactLayout: Bool
1614

1715
var body: some View {
1816
List {
@@ -24,116 +22,107 @@ struct HomeView: View {
2422
.navigationTitle(String(localized: "nav_home"))
2523
.toolbar { toolbar }
2624
.sheet(isPresented: Binding(
27-
get: { viewModel.state.reorderTodo },
28-
set: { viewModel.send(.setPresentation(.reorderTodo, $0)) }
25+
get: { coordinator.viewModel.state.reorderTodo },
26+
set: { coordinator.viewModel.send(.setPresentation(.reorderTodo, $0)) }
2927
)) {
3028
TodoManageView(
31-
viewModel: TodoManageViewModel(viewModel.state.preferences),
29+
viewModel: coordinator.makeTodoManageViewModel(),
3230
onDismiss: { array in
33-
viewModel.send(.setPresentation(.reorderTodo, false))
31+
coordinator.viewModel.send(.setPresentation(.reorderTodo, false))
3432
withAnimation {
35-
viewModel.send(.orderTodoCategory(array))
33+
coordinator.viewModel.send(.orderTodoCategory(array))
3634
}
3735
}
3836
)
3937
}
4038
.sheet(isPresented: Binding(
41-
get: { viewModel.state.showContentPicker },
39+
get: { coordinator.viewModel.state.showContentPicker },
4240
set: { _, _ in }
4341
)) {
4442
contentPicker
4543
}
4644
.fullScreenCover(isPresented: Binding(
47-
get: { viewModel.state.showTodoEditor },
48-
set: { viewModel.send(.setPresentation(.todoEditor, $0)) }
45+
get: { coordinator.viewModel.state.showTodoEditor },
46+
set: { coordinator.viewModel.send(.setPresentation(.todoEditor, $0)) }
4947
)) {
50-
if let selectedCategory = viewModel.state.selectedTodoCategory {
48+
if let selectedCategory = coordinator.viewModel.state.selectedTodoCategory {
5149
TodoEditorView(
52-
viewModel: TodoEditorViewModel(
53-
category: selectedCategory,
54-
fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self),
55-
fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self)
56-
),
57-
onSubmit: { viewModel.send(.addTodo($0)) }
50+
viewModel: coordinator.makeTodoEditorViewModel(category: selectedCategory),
51+
onSubmit: { coordinator.viewModel.send(.addTodo($0)) }
5852
)
5953
}
6054
}
6155
.fullScreenCover(isPresented: Binding(
62-
get: { viewModel.state.showSearchView },
63-
set: { viewModel.send(.setPresentation(.searchView, $0)) }
56+
get: { coordinator.viewModel.state.showSearchView },
57+
set: { coordinator.viewModel.send(.setPresentation(.searchView, $0)) }
6458
)) {
65-
SearchView(viewModel: SearchViewModel(
66-
fetchWebPagesUseCase: container.resolve(FetchWebPagesUseCase.self),
67-
fetchTodosUseCase: container.resolve(FetchTodosUseCase.self),
68-
fetchRecentSearchQueriesUseCase: container.resolve(FetchRecentSearchQueriesUseCase.self),
69-
updateRecentSearchQueriesUseCase: container.resolve(UpdateRecentSearchQueriesUseCase.self)
70-
))
59+
SearchView(viewModel: coordinator.makeSearchViewModel())
7160
}
7261
.alert(
73-
viewModel.state.alertTitle,
62+
coordinator.viewModel.state.alertTitle,
7463
isPresented: Binding(
75-
get: { viewModel.state.showAlert },
76-
set: { viewModel.send(.setAlert(isPresented: $0)) }
64+
get: { coordinator.viewModel.state.showAlert },
65+
set: { coordinator.viewModel.send(.setAlert(isPresented: $0)) }
7766
)
7867
) {
7968
alertButtons
8069
} message: {
81-
Text(viewModel.state.alertMessage)
70+
Text(coordinator.viewModel.state.alertMessage)
8271
}
8372
.toast(
8473
isPresented: Binding(
85-
get: { viewModel.state.showToast },
86-
set: { viewModel.send(.setToast(isPresented: $0)) }
74+
get: { coordinator.viewModel.state.showToast },
75+
set: { coordinator.viewModel.send(.setToast(isPresented: $0)) }
8776
),
8877
duration: 5,
89-
action: { viewModel.send(.undoDeleteWebPage) }
78+
action: { coordinator.viewModel.send(.undoDeleteWebPage) }
9079
) {
91-
Label(viewModel.state.toastMessage, systemImage: "arrow.uturn.left")
80+
Label(coordinator.viewModel.state.toastMessage, systemImage: "arrow.uturn.left")
9281
.font(.caption)
9382
.multilineTextAlignment(.center)
9483
}
9584
.onAppear {
96-
viewModel.send(.onAppear)
85+
coordinator.viewModel.send(.onAppear)
9786
}
9887
.overlay {
99-
if viewModel.state.isAppending {
88+
if coordinator.viewModel.state.isAppending {
10089
LoadingView()
10190
}
10291
}
10392
}
10493

10594
@ViewBuilder
10695
private var alertButtons: some View {
107-
switch viewModel.state.alertType {
96+
switch coordinator.viewModel.state.alertType {
10897
case .webPageInput:
10998
TextField(
11099
"https://",
111100
text: Binding(
112-
get: { viewModel.state.webPageURLInput },
113-
set: { viewModel.send(.updateWebPageURLInput($0)) }
101+
get: { coordinator.viewModel.state.webPageURLInput },
102+
set: { coordinator.viewModel.send(.updateWebPageURLInput($0)) }
114103
)
115104
)
116105
.textInputAutocapitalization(.never)
117106
.keyboardType(.URL)
118107
Button(String(localized: "home_add")) {
119-
viewModel.send(.addWebPage)
108+
coordinator.viewModel.send(.addWebPage)
120109
}
121110
Button(String(localized: "common_cancel"), role: .cancel) {
122-
viewModel.send(.setAlert(isPresented: false))
111+
coordinator.viewModel.send(.setAlert(isPresented: false))
123112
}
124113
case .invalidURL, .error, .none:
125114
Button(String(localized: "common_close"), role: .cancel) {
126-
viewModel.send(.setAlert(isPresented: false))
115+
coordinator.viewModel.send(.setAlert(isPresented: false))
127116
}
128117
}
129118
}
130119

131120
private var todoSection: some View {
132121
Section(content: {
133-
if viewModel.state.isPreferencesLoading {
122+
if coordinator.viewModel.state.isPreferencesLoading {
134123
LoadingView()
135124
} else {
136-
let preferences = viewModel.state.preferences
125+
let preferences = coordinator.viewModel.state.preferences
137126
ForEach(preferences.filter { $0.isVisible }, id: \.id) { item in
138127
todoCategoryRow(item)
139128
}
@@ -146,7 +135,7 @@ struct HomeView: View {
146135
.bold()
147136
Spacer()
148137
Button(action: {
149-
viewModel.send(.setPresentation(.reorderTodo, true))
138+
coordinator.viewModel.send(.setPresentation(.reorderTodo, true))
150139
}) {
151140
Image(systemName: "ellipsis")
152141
.font(.title2)
@@ -159,17 +148,17 @@ struct HomeView: View {
159148

160149
private var recentTodoSection: some View {
161150
Section {
162-
if viewModel.state.isRecentTodosLoading {
151+
if coordinator.viewModel.state.isRecentTodosLoading {
163152
LoadingView()
164-
} else if viewModel.state.recentTodos.isEmpty {
153+
} else if coordinator.viewModel.state.recentTodos.isEmpty {
165154
HStack {
166155
Spacer()
167156
Text(String(localized: "home_recent_empty"))
168157
.font(.callout)
169158
Spacer()
170159
}
171160
} else {
172-
ForEach(viewModel.state.recentTodos, id: \.id) { todo in
161+
ForEach(coordinator.viewModel.state.recentTodos, id: \.id) { todo in
173162
recentTodoRow(todo)
174163
}
175164
}
@@ -186,13 +175,13 @@ struct HomeView: View {
186175

187176
private var webPageSection: some View {
188177
Section {
189-
let webPages = viewModel.state.webPages.filter { !$0.isHidden }
190-
if viewModel.state.isWebPageLoading {
178+
let webPages = coordinator.viewModel.state.webPages.filter { !$0.isHidden }
179+
if coordinator.viewModel.state.isWebPageLoading {
191180
LoadingView()
192181
.id(UUID()) // id 부여를 통해 렌더링 강제
193-
} else if viewModel.state.needsWebPageRefresh {
182+
} else if coordinator.viewModel.state.needsWebPageRefresh {
194183
Button {
195-
viewModel.send(.refreshWebPages)
184+
coordinator.viewModel.send(.refreshWebPages)
196185
} label: {
197186
HStack {
198187
Spacer()
@@ -231,18 +220,18 @@ struct HomeView: View {
231220
private var toolbar: some ToolbarContent {
232221
ToolbarItem(placement: .topBarTrailing) {
233222
Button {
234-
viewModel.send(.setPresentation(.contentPicker, true))
223+
coordinator.viewModel.send(.setPresentation(.contentPicker, true))
235224
} label: {
236225
Image(systemName: "plus")
237226
}
238-
.disabled(!viewModel.state.isNetworkConnected)
227+
.disabled(!coordinator.viewModel.state.isNetworkConnected)
239228
}
240229
if #available(iOS 26.0, *) {
241230
ToolbarSpacer(.fixed, placement: .topBarTrailing)
242231
}
243232
ToolbarItemGroup(placement: .topBarTrailing) {
244233
Button {
245-
viewModel.send(.setPresentation(.searchView, true))
234+
coordinator.viewModel.send(.setPresentation(.searchView, true))
246235
} label: {
247236
Image(systemName: "magnifyingglass")
248237
}
@@ -261,7 +250,7 @@ struct HomeView: View {
261250
}
262251
} else {
263252
Button {
264-
router.show(.category(item))
253+
coordinator.router.replace(with: .category(item))
265254
} label: {
266255
labelImage(
267256
text: item.localizedName,
@@ -281,7 +270,7 @@ struct HomeView: View {
281270
}
282271
} else {
283272
Button {
284-
router.show(.todo(TodoIdItem(id: item.id)))
273+
coordinator.router.replace(with: .todo(TodoIdItem(id: item.id)))
285274
} label: {
286275
RecentTodoRow(todo: item)
287276
.frame(maxWidth: .infinity, alignment: .leading)
@@ -299,7 +288,7 @@ struct HomeView: View {
299288
}
300289
} else {
301290
Button {
302-
router.show(.webPage(item))
291+
coordinator.router.replace(with: .webPage(item))
303292
} label: {
304293
WebItemRow(item: item, showsChevron: false)
305294
.frame(maxWidth: .infinity, alignment: .leading)
@@ -309,7 +298,7 @@ struct HomeView: View {
309298
}
310299
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
311300
Button(role: .destructive) {
312-
viewModel.send(.deleteWebPage(item))
301+
coordinator.viewModel.send(.deleteWebPage(item))
313302
} label: {
314303
Label(String(localized: "common_delete"), systemImage: "trash")
315304
}
@@ -320,14 +309,14 @@ struct HomeView: View {
320309
NavigationStack {
321310
List {
322311
Section {
323-
if viewModel.state.isPreferencesLoading {
312+
if coordinator.viewModel.state.isPreferencesLoading {
324313
LoadingView()
325314
} else {
326-
let preferences = viewModel.state.preferences.filter(\.isVisible)
315+
let preferences = coordinator.viewModel.state.preferences.filter(\.isVisible)
327316
ForEach(preferences, id: \.id) { item in
328317
Button {
329318
DispatchQueue.main.async {
330-
viewModel.send(.tapTodoCategory(item.category))
319+
coordinator.viewModel.send(.tapTodoCategory(item.category))
331320
}
332321
} label: {
333322
labelImage(
@@ -346,7 +335,7 @@ struct HomeView: View {
346335
Section {
347336
Button {
348337
DispatchQueue.main.async {
349-
viewModel.send(.setAlert(isPresented: true, type: .webPageInput))
338+
coordinator.viewModel.send(.setAlert(isPresented: true, type: .webPageInput))
350339
}
351340
} label: {
352341
labelImage(
@@ -365,7 +354,7 @@ struct HomeView: View {
365354
.toolbar {
366355
ToolbarItem(placement: .topBarLeading) {
367356
Button {
368-
viewModel.send(.setPresentation(.contentPicker, false))
357+
coordinator.viewModel.send(.setPresentation(.contentPicker, false))
369358
} label: {
370359
Image(systemName: "xmark")
371360
.bold()
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//
2+
// HomeViewCoordinator.swift
3+
// DevLog
4+
//
5+
// Created by opfic on 5/10/26.
6+
//
7+
8+
import Foundation
9+
10+
@MainActor
11+
@Observable
12+
final class HomeViewCoordinator {
13+
let viewModel: HomeViewModel
14+
let router = NavigationRouter<HomeRoute>()
15+
private let fetchTodoCategoryPreferencesUseCase: FetchTodoCategoryPreferencesUseCase
16+
private let fetchReferenceItemsUseCase: FetchReferenceItemsUseCase
17+
private let fetchWebPagesUseCase: FetchWebPagesUseCase
18+
private let fetchTodosUseCase: FetchTodosUseCase
19+
private let fetchRecentSearchQueriesUseCase: FetchRecentSearchQueriesUseCase
20+
private let updateRecentSearchQueriesUseCase: UpdateRecentSearchQueriesUseCase
21+
22+
init(container: DIContainer) {
23+
let fetchTodoCategoryPreferencesUseCase = container.resolve(FetchTodoCategoryPreferencesUseCase.self)
24+
let fetchWebPagesUseCase = container.resolve(FetchWebPagesUseCase.self)
25+
let fetchTodosUseCase = container.resolve(FetchTodosUseCase.self)
26+
27+
self.fetchTodoCategoryPreferencesUseCase = fetchTodoCategoryPreferencesUseCase
28+
self.fetchReferenceItemsUseCase = container.resolve(FetchReferenceItemsUseCase.self)
29+
self.fetchWebPagesUseCase = fetchWebPagesUseCase
30+
self.fetchTodosUseCase = fetchTodosUseCase
31+
self.fetchRecentSearchQueriesUseCase = container.resolve(FetchRecentSearchQueriesUseCase.self)
32+
self.updateRecentSearchQueriesUseCase = container.resolve(UpdateRecentSearchQueriesUseCase.self)
33+
self.viewModel = HomeViewModel(
34+
fetchPreferencesUseCase: fetchTodoCategoryPreferencesUseCase,
35+
updatePreferencesUseCase: container.resolve(UpdateTodoCategoryPreferencesUseCase.self),
36+
addWebPageUseCase: container.resolve(AddWebPageUseCase.self),
37+
deleteWebPageUseCase: container.resolve(DeleteWebPageUseCase.self),
38+
undoDeleteWebPageUseCase: container.resolve(UndoDeleteWebPageUseCase.self),
39+
upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self),
40+
fetchTodosUseCase: fetchTodosUseCase,
41+
fetchWebPagesUseCase: fetchWebPagesUseCase,
42+
networkConnectivityUseCase: container.resolve(ObserveNetworkConnectivityUseCase.self)
43+
)
44+
}
45+
46+
func makeTodoManageViewModel() -> TodoManageViewModel {
47+
TodoManageViewModel(viewModel.state.preferences)
48+
}
49+
50+
func makeTodoEditorViewModel(category: TodoCategory) -> TodoEditorViewModel {
51+
TodoEditorViewModel(
52+
category: category,
53+
fetchPreferencesUseCase: fetchTodoCategoryPreferencesUseCase,
54+
fetchReferenceItemsUseCase: fetchReferenceItemsUseCase
55+
)
56+
}
57+
58+
func makeSearchViewModel() -> SearchViewModel {
59+
SearchViewModel(
60+
fetchWebPagesUseCase: fetchWebPagesUseCase,
61+
fetchTodosUseCase: fetchTodosUseCase,
62+
fetchRecentSearchQueriesUseCase: fetchRecentSearchQueriesUseCase,
63+
updateRecentSearchQueriesUseCase: updateRecentSearchQueriesUseCase
64+
)
65+
}
66+
}

0 commit comments

Comments
 (0)