Skip to content

Commit a9a0e74

Browse files
committed
refactor: HomeViewCoorinator 구현 및 적용
1 parent d15d953 commit a9a0e74

4 files changed

Lines changed: 127 additions & 82 deletions

File tree

DevLog/UI/Home/HomeView.swift

Lines changed: 51 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,10 @@
88
import SwiftUI
99

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

1716
var body: some View {
1817
List {
@@ -24,116 +23,107 @@ struct HomeView: View {
2423
.navigationTitle(String(localized: "nav_home"))
2524
.toolbar { toolbar }
2625
.sheet(isPresented: Binding(
27-
get: { viewModel.state.reorderTodo },
28-
set: { viewModel.send(.setPresentation(.reorderTodo, $0)) }
26+
get: { coordinator.viewModel.state.reorderTodo },
27+
set: { coordinator.viewModel.send(.setPresentation(.reorderTodo, $0)) }
2928
)) {
3029
TodoManageView(
31-
viewModel: TodoManageViewModel(viewModel.state.preferences),
30+
viewModel: coordinator.makeTodoManageViewModel(),
3231
onDismiss: { array in
33-
viewModel.send(.setPresentation(.reorderTodo, false))
32+
coordinator.viewModel.send(.setPresentation(.reorderTodo, false))
3433
withAnimation {
35-
viewModel.send(.orderTodoCategory(array))
34+
coordinator.viewModel.send(.orderTodoCategory(array))
3635
}
3736
}
3837
)
3938
}
4039
.sheet(isPresented: Binding(
41-
get: { viewModel.state.showContentPicker },
40+
get: { coordinator.viewModel.state.showContentPicker },
4241
set: { _, _ in }
4342
)) {
4443
contentPicker
4544
}
4645
.fullScreenCover(isPresented: Binding(
47-
get: { viewModel.state.showTodoEditor },
48-
set: { viewModel.send(.setPresentation(.todoEditor, $0)) }
46+
get: { coordinator.viewModel.state.showTodoEditor },
47+
set: { coordinator.viewModel.send(.setPresentation(.todoEditor, $0)) }
4948
)) {
50-
if let selectedCategory = viewModel.state.selectedTodoCategory {
49+
if let selectedCategory = coordinator.viewModel.state.selectedTodoCategory {
5150
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)) }
51+
viewModel: coordinator.makeTodoEditorViewModel(category: selectedCategory),
52+
onSubmit: { coordinator.viewModel.send(.addTodo($0)) }
5853
)
5954
}
6055
}
6156
.fullScreenCover(isPresented: Binding(
62-
get: { viewModel.state.showSearchView },
63-
set: { viewModel.send(.setPresentation(.searchView, $0)) }
57+
get: { coordinator.viewModel.state.showSearchView },
58+
set: { coordinator.viewModel.send(.setPresentation(.searchView, $0)) }
6459
)) {
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-
))
60+
SearchView(viewModel: coordinator.makeSearchViewModel())
7161
}
7262
.alert(
73-
viewModel.state.alertTitle,
63+
coordinator.viewModel.state.alertTitle,
7464
isPresented: Binding(
75-
get: { viewModel.state.showAlert },
76-
set: { viewModel.send(.setAlert(isPresented: $0)) }
65+
get: { coordinator.viewModel.state.showAlert },
66+
set: { coordinator.viewModel.send(.setAlert(isPresented: $0)) }
7767
)
7868
) {
7969
alertButtons
8070
} message: {
81-
Text(viewModel.state.alertMessage)
71+
Text(coordinator.viewModel.state.alertMessage)
8272
}
8373
.toast(
8474
isPresented: Binding(
85-
get: { viewModel.state.showToast },
86-
set: { viewModel.send(.setToast(isPresented: $0)) }
75+
get: { coordinator.viewModel.state.showToast },
76+
set: { coordinator.viewModel.send(.setToast(isPresented: $0)) }
8777
),
8878
duration: 5,
89-
action: { viewModel.send(.undoDeleteWebPage) }
79+
action: { coordinator.viewModel.send(.undoDeleteWebPage) }
9080
) {
91-
Label(viewModel.state.toastMessage, systemImage: "arrow.uturn.left")
81+
Label(coordinator.viewModel.state.toastMessage, systemImage: "arrow.uturn.left")
9282
.font(.caption)
9383
.multilineTextAlignment(.center)
9484
}
9585
.onAppear {
96-
viewModel.send(.onAppear)
86+
coordinator.viewModel.send(.onAppear)
9787
}
9888
.overlay {
99-
if viewModel.state.isAppending {
89+
if coordinator.viewModel.state.isAppending {
10090
LoadingView()
10191
}
10292
}
10393
}
10494

10595
@ViewBuilder
10696
private var alertButtons: some View {
107-
switch viewModel.state.alertType {
97+
switch coordinator.viewModel.state.alertType {
10898
case .webPageInput:
10999
TextField(
110100
"https://",
111101
text: Binding(
112-
get: { viewModel.state.webPageURLInput },
113-
set: { viewModel.send(.updateWebPageURLInput($0)) }
102+
get: { coordinator.viewModel.state.webPageURLInput },
103+
set: { coordinator.viewModel.send(.updateWebPageURLInput($0)) }
114104
)
115105
)
116106
.textInputAutocapitalization(.never)
117107
.keyboardType(.URL)
118108
Button(String(localized: "home_add")) {
119-
viewModel.send(.addWebPage)
109+
coordinator.viewModel.send(.addWebPage)
120110
}
121111
Button(String(localized: "common_cancel"), role: .cancel) {
122-
viewModel.send(.setAlert(isPresented: false))
112+
coordinator.viewModel.send(.setAlert(isPresented: false))
123113
}
124114
case .invalidURL, .error, .none:
125115
Button(String(localized: "common_close"), role: .cancel) {
126-
viewModel.send(.setAlert(isPresented: false))
116+
coordinator.viewModel.send(.setAlert(isPresented: false))
127117
}
128118
}
129119
}
130120

131121
private var todoSection: some View {
132122
Section(content: {
133-
if viewModel.state.isPreferencesLoading {
123+
if coordinator.viewModel.state.isPreferencesLoading {
134124
LoadingView()
135125
} else {
136-
let preferences = viewModel.state.preferences
126+
let preferences = coordinator.viewModel.state.preferences
137127
ForEach(preferences.filter { $0.isVisible }, id: \.id) { item in
138128
todoCategoryRow(item)
139129
}
@@ -146,7 +136,7 @@ struct HomeView: View {
146136
.bold()
147137
Spacer()
148138
Button(action: {
149-
viewModel.send(.setPresentation(.reorderTodo, true))
139+
coordinator.viewModel.send(.setPresentation(.reorderTodo, true))
150140
}) {
151141
Image(systemName: "ellipsis")
152142
.font(.title2)
@@ -159,17 +149,17 @@ struct HomeView: View {
159149

160150
private var recentTodoSection: some View {
161151
Section {
162-
if viewModel.state.isRecentTodosLoading {
152+
if coordinator.viewModel.state.isRecentTodosLoading {
163153
LoadingView()
164-
} else if viewModel.state.recentTodos.isEmpty {
154+
} else if coordinator.viewModel.state.recentTodos.isEmpty {
165155
HStack {
166156
Spacer()
167157
Text(String(localized: "home_recent_empty"))
168158
.font(.callout)
169159
Spacer()
170160
}
171161
} else {
172-
ForEach(viewModel.state.recentTodos, id: \.id) { todo in
162+
ForEach(coordinator.viewModel.state.recentTodos, id: \.id) { todo in
173163
recentTodoRow(todo)
174164
}
175165
}
@@ -186,13 +176,13 @@ struct HomeView: View {
186176

187177
private var webPageSection: some View {
188178
Section {
189-
let webPages = viewModel.state.webPages.filter { !$0.isHidden }
190-
if viewModel.state.isWebPageLoading {
179+
let webPages = coordinator.viewModel.state.webPages.filter { !$0.isHidden }
180+
if coordinator.viewModel.state.isWebPageLoading {
191181
LoadingView()
192182
.id(UUID()) // id 부여를 통해 렌더링 강제
193-
} else if viewModel.state.needsWebPageRefresh {
183+
} else if coordinator.viewModel.state.needsWebPageRefresh {
194184
Button {
195-
viewModel.send(.refreshWebPages)
185+
coordinator.viewModel.send(.refreshWebPages)
196186
} label: {
197187
HStack {
198188
Spacer()
@@ -231,18 +221,18 @@ struct HomeView: View {
231221
private var toolbar: some ToolbarContent {
232222
ToolbarItem(placement: .topBarTrailing) {
233223
Button {
234-
viewModel.send(.setPresentation(.contentPicker, true))
224+
coordinator.viewModel.send(.setPresentation(.contentPicker, true))
235225
} label: {
236226
Image(systemName: "plus")
237227
}
238-
.disabled(!viewModel.state.isNetworkConnected)
228+
.disabled(!coordinator.viewModel.state.isNetworkConnected)
239229
}
240230
if #available(iOS 26.0, *) {
241231
ToolbarSpacer(.fixed, placement: .topBarTrailing)
242232
}
243233
ToolbarItemGroup(placement: .topBarTrailing) {
244234
Button {
245-
viewModel.send(.setPresentation(.searchView, true))
235+
coordinator.viewModel.send(.setPresentation(.searchView, true))
246236
} label: {
247237
Image(systemName: "magnifyingglass")
248238
}
@@ -309,7 +299,7 @@ struct HomeView: View {
309299
}
310300
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
311301
Button(role: .destructive) {
312-
viewModel.send(.deleteWebPage(item))
302+
coordinator.viewModel.send(.deleteWebPage(item))
313303
} label: {
314304
Label(String(localized: "common_delete"), systemImage: "trash")
315305
}
@@ -320,14 +310,14 @@ struct HomeView: View {
320310
NavigationStack {
321311
List {
322312
Section {
323-
if viewModel.state.isPreferencesLoading {
313+
if coordinator.viewModel.state.isPreferencesLoading {
324314
LoadingView()
325315
} else {
326-
let preferences = viewModel.state.preferences.filter(\.isVisible)
316+
let preferences = coordinator.viewModel.state.preferences.filter(\.isVisible)
327317
ForEach(preferences, id: \.id) { item in
328318
Button {
329319
DispatchQueue.main.async {
330-
viewModel.send(.tapTodoCategory(item.category))
320+
coordinator.viewModel.send(.tapTodoCategory(item.category))
331321
}
332322
} label: {
333323
labelImage(
@@ -346,7 +336,7 @@ struct HomeView: View {
346336
Section {
347337
Button {
348338
DispatchQueue.main.async {
349-
viewModel.send(.setAlert(isPresented: true, type: .webPageInput))
339+
coordinator.viewModel.send(.setAlert(isPresented: true, type: .webPageInput))
350340
}
351341
} label: {
352342
labelImage(
@@ -365,7 +355,7 @@ struct HomeView: View {
365355
.toolbar {
366356
ToolbarItem(placement: .topBarLeading) {
367357
Button {
368-
viewModel.send(.setPresentation(.contentPicker, false))
358+
coordinator.viewModel.send(.setPresentation(.contentPicker, false))
369359
} label: {
370360
Image(systemName: "xmark")
371361
.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)