Skip to content

Commit 9335ec8

Browse files
authored
[#440] 맥으로 Todo를 작성 혹은 수정 시 시트 대신 새 창이 뜨도록 수정한다 (#505)
* feat: 맥에서 앱을 실행했을 때 구분할 수 있는 방법 구현 * feat: Mac Todo 편집 윈도우 추가 * refactor: Todo 편집 윈도우 이벤트 전달 정리 * ui: 새창 / 풀스크린 버전 ui 분리 * fix: 버튼을 정확하게 탭하지 않았을 때 탭 인식이 안되는 현상 수정 * fix: Todo 편집 윈도우 source 매칭 오류 수정 * refactor: HomeCoordinator 경량화 * refactor: Todo 추가 / 수정 책임을 에디터로 이동 * fix: TodoEditor에서 Todo를 생성, 수정하면서 윈도우가 닫히지 않는 현상 해결 * refactor: Todo 편집 윈도우 이벤트 전달 구조 개선 * refactor: 컨테이너 변수 이름 수정
1 parent a536573 commit 9335ec8

20 files changed

Lines changed: 549 additions & 130 deletions

Application/DevLogApp/Sources/App/DevLogApp.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ struct DevLogApp: App {
1616
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
1717
@Environment(\.diContainer) var container: DIContainer
1818
@Environment(\.scenePhase) var scenePhase
19+
@State private var windowEvent = TodoEditorWindowEvent()
1920

2021
init() {
2122
AppAssembler().assemble(AppDIContainer.shared)
@@ -29,6 +30,7 @@ struct DevLogApp: App {
2930
systemThemeUseCase: container.resolve(ObserveSystemThemeUseCase.self),
3031
trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self),
3132
widgetURLTab: { MainTab(widgetURL: $0) },
33+
windowEvent: windowEvent,
3234
pushNotificationTodoIdPublisher: PushNotificationRoute.shared.observe(),
3335
clearPushNotificationRoute: { PushNotificationRoute.shared.clear() }
3436
)
@@ -38,5 +40,19 @@ struct DevLogApp: App {
3840
container.resolve(WidgetSyncEventBus.self).publish(.syncRequested)
3941
}
4042
}
43+
WindowGroup(id: TodoEditorWindowValue.sceneId, for: TodoEditorWindowValue.self) { value in
44+
if let value = value.wrappedValue {
45+
TodoEditorWindowView(
46+
value: value,
47+
windowEvent: windowEvent
48+
)
49+
.autocorrectionDisabled()
50+
} else {
51+
ContentUnavailableView(
52+
String(localized: "todo_edit"),
53+
systemImage: "square.and.pencil"
54+
)
55+
}
56+
}
4157
}
4258
}

Application/DevLogPresentation/Sources/Extension/EnvironmentValues+.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ extension EnvironmentValues {
2121
self[SceneHeightKey.self]
2222
}
2323

24+
var isiOSAppOnMac: Bool {
25+
self[IOSAppOnMacKey.self]
26+
}
27+
2428
private struct SafeAreaInsetsKey: EnvironmentKey {
2529
static var defaultValue: EdgeInsets {
2630
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
@@ -48,6 +52,12 @@ extension EnvironmentValues {
4852
return windowScene.screen.bounds.height
4953
}
5054
}
55+
56+
private struct IOSAppOnMacKey: EnvironmentKey {
57+
static var defaultValue: Bool {
58+
ProcessInfo.processInfo.isiOSAppOnMac
59+
}
60+
}
5161
}
5262

5363
extension UIEdgeInsets {

Application/DevLogPresentation/Sources/Home/Home/HomeView.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import SwiftUI
99
import DevLogDomain
1010

1111
struct HomeView: View {
12+
@Environment(\.openWindow) private var openWindow
13+
@Environment(\.isiOSAppOnMac) private var isiOSAppOnMac
1214
@ScaledMetric(relativeTo: .largeTitle) private var labelWidth = CGFloat(34)
1315
let coordinator: HomeViewCoordinator
1416
let isCompactLayout: Bool
@@ -48,8 +50,7 @@ struct HomeView: View {
4850
)) {
4951
if let selectedCategory = coordinator.viewModel.state.selectedTodoCategory {
5052
TodoEditorView(
51-
viewModel: coordinator.makeTodoEditorViewModel(category: selectedCategory),
52-
onSubmit: { coordinator.viewModel.send(.addTodo($0)) }
53+
viewModel: coordinator.makeTodoEditorViewModel(category: selectedCategory)
5354
)
5455
}
5556
}
@@ -272,6 +273,7 @@ struct HomeView: View {
272273
} label: {
273274
RecentTodoRow(todo: item)
274275
.frame(maxWidth: .infinity, alignment: .leading)
276+
.contentShape(.rect)
275277
}
276278
.buttonStyle(.plain)
277279
}
@@ -290,6 +292,7 @@ struct HomeView: View {
290292
} label: {
291293
WebItemRow(item: item, showsChevron: false)
292294
.frame(maxWidth: .infinity, alignment: .leading)
295+
.contentShape(.rect)
293296
}
294297
.buttonStyle(.plain)
295298
}
@@ -314,7 +317,7 @@ struct HomeView: View {
314317
ForEach(preferences, id: \.id) { item in
315318
Button {
316319
DispatchQueue.main.async {
317-
coordinator.viewModel.send(.tapTodoCategory(item.category))
320+
openTodoEditor(for: item.category)
318321
}
319322
} label: {
320323
labelImage(
@@ -384,6 +387,18 @@ struct HomeView: View {
384387
.contentShape(.rect)
385388
}
386389

390+
private func openTodoEditor(for todoCategory: TodoCategory) {
391+
if isiOSAppOnMac {
392+
coordinator.viewModel.send(.setPresentation(.contentPicker, false))
393+
openWindow(
394+
id: TodoEditorWindowValue.sceneId,
395+
value: TodoEditorWindowValue(todoCategory: todoCategory, source: .home)
396+
)
397+
} else {
398+
coordinator.viewModel.send(.tapTodoCategory(todoCategory))
399+
}
400+
}
401+
387402
}
388403

389404
enum HomeRoute: Hashable {

Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Created by opfic on 5/10/26.
66
//
77

8+
import Combine
89
import Foundation
910
import DevLogCore
1011
import DevLogDomain
@@ -14,33 +15,20 @@ import DevLogDomain
1415
final class HomeViewCoordinator {
1516
let viewModel: HomeViewModel
1617
let router = NavigationRouter<HomeRoute>()
17-
private let fetchTodoCategoryPreferencesUseCase: FetchTodoCategoryPreferencesUseCase
18-
private let fetchReferenceItemsUseCase: FetchReferenceItemsUseCase
19-
private let fetchWebPagesUseCase: FetchWebPagesUseCase
20-
private let fetchTodosUseCase: FetchTodosUseCase
21-
private let fetchRecentSearchQueriesUseCase: FetchRecentSearchQueriesUseCase
22-
private let updateRecentSearchQueriesUseCase: UpdateRecentSearchQueriesUseCase
18+
private let container: DIContainer
19+
@ObservationIgnored
20+
private var cancellable: AnyCancellable?
2321

2422
init(container: DIContainer) {
25-
let fetchTodoCategoryPreferencesUseCase = container.resolve(FetchTodoCategoryPreferencesUseCase.self)
26-
let fetchWebPagesUseCase = container.resolve(FetchWebPagesUseCase.self)
27-
let fetchTodosUseCase = container.resolve(FetchTodosUseCase.self)
28-
29-
self.fetchTodoCategoryPreferencesUseCase = fetchTodoCategoryPreferencesUseCase
30-
self.fetchReferenceItemsUseCase = container.resolve(FetchReferenceItemsUseCase.self)
31-
self.fetchWebPagesUseCase = fetchWebPagesUseCase
32-
self.fetchTodosUseCase = fetchTodosUseCase
33-
self.fetchRecentSearchQueriesUseCase = container.resolve(FetchRecentSearchQueriesUseCase.self)
34-
self.updateRecentSearchQueriesUseCase = container.resolve(UpdateRecentSearchQueriesUseCase.self)
23+
self.container = container
3524
self.viewModel = HomeViewModel(
36-
fetchPreferencesUseCase: fetchTodoCategoryPreferencesUseCase,
25+
fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self),
3726
updatePreferencesUseCase: container.resolve(UpdateTodoCategoryPreferencesUseCase.self),
3827
addWebPageUseCase: container.resolve(AddWebPageUseCase.self),
3928
deleteWebPageUseCase: container.resolve(DeleteWebPageUseCase.self),
4029
undoDeleteWebPageUseCase: container.resolve(UndoDeleteWebPageUseCase.self),
41-
upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self),
42-
fetchTodosUseCase: fetchTodosUseCase,
43-
fetchWebPagesUseCase: fetchWebPagesUseCase,
30+
fetchTodosUseCase: container.resolve(FetchTodosUseCase.self),
31+
fetchWebPagesUseCase: container.resolve(FetchWebPagesUseCase.self),
4432
networkConnectivityUseCase: container.resolve(ObserveNetworkConnectivityUseCase.self),
4533
trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self)
4634
)
@@ -50,24 +38,40 @@ final class HomeViewCoordinator {
5038
viewModel.send(.fetchData)
5139
}
5240

41+
func bindWindowEvent(_ windowEvent: TodoEditorWindowEvent) {
42+
guard cancellable == nil else { return }
43+
44+
cancellable = windowEvent.submits
45+
.sink { [weak self] submit in
46+
guard submit.value.matchesCreate(source: .home) else { return }
47+
self?.viewModel.send(.fetchData)
48+
}
49+
}
50+
5351
func makeTodoManageViewModel() -> TodoManageViewModel {
5452
TodoManageViewModel(viewModel.state.preferences)
5553
}
5654

5755
func makeTodoEditorViewModel(category: TodoCategory) -> TodoEditorViewModel {
5856
TodoEditorViewModel(
5957
category: category,
60-
fetchPreferencesUseCase: fetchTodoCategoryPreferencesUseCase,
61-
fetchReferenceItemsUseCase: fetchReferenceItemsUseCase
58+
fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self),
59+
fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self),
60+
upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self),
61+
trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self),
62+
onUpsertSuccess: { [weak self] _ in
63+
self?.viewModel.send(.setPresentation(.todoEditor, false))
64+
self?.viewModel.send(.fetchData)
65+
}
6266
)
6367
}
6468

6569
func makeSearchViewModel() -> SearchViewModel {
6670
SearchViewModel(
67-
fetchWebPagesUseCase: fetchWebPagesUseCase,
68-
fetchTodosUseCase: fetchTodosUseCase,
69-
fetchRecentSearchQueriesUseCase: fetchRecentSearchQueriesUseCase,
70-
updateRecentSearchQueriesUseCase: updateRecentSearchQueriesUseCase
71+
fetchWebPagesUseCase: container.resolve(FetchWebPagesUseCase.self),
72+
fetchTodosUseCase: container.resolve(FetchTodosUseCase.self),
73+
fetchRecentSearchQueriesUseCase: container.resolve(FetchRecentSearchQueriesUseCase.self),
74+
updateRecentSearchQueriesUseCase: container.resolve(UpdateRecentSearchQueriesUseCase.self)
7175
)
7276
}
7377
}

Application/DevLogPresentation/Sources/Home/Home/HomeViewModel.swift

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ final class HomeViewModel: Store {
5050
case tapTodoCategory(TodoCategory)
5151
case orderTodoCategory([TodoCategoryItem])
5252
case setTodoCategory([TodoCategoryItem])
53-
case addTodo(Todo)
5453
case updateRecentTodos([RecentTodoItem])
5554
case updateWebPageURLInput(String)
5655
case addWebPage
@@ -60,7 +59,6 @@ final class HomeViewModel: Store {
6059
}
6160

6261
enum SideEffect {
63-
case addTodo(Todo)
6462
case addWebPage(String)
6563
case deleteWebPage(WebPageItem)
6664
case undoDeleteWebPage(String)
@@ -103,7 +101,6 @@ final class HomeViewModel: Store {
103101
private(set) var state = State()
104102
private let fetchPreferencesUseCase: FetchTodoCategoryPreferencesUseCase
105103
private let updatePreferencesUseCase: UpdateTodoCategoryPreferencesUseCase
106-
private let upsertTodoUseCase: UpsertTodoUseCase
107104
private let addWebPageUseCase: AddWebPageUseCase
108105
private let deleteWebPageUseCase: DeleteWebPageUseCase
109106
private let undoDeleteWebPageUseCase: UndoDeleteWebPageUseCase
@@ -121,7 +118,6 @@ final class HomeViewModel: Store {
121118
addWebPageUseCase: AddWebPageUseCase,
122119
deleteWebPageUseCase: DeleteWebPageUseCase,
123120
undoDeleteWebPageUseCase: UndoDeleteWebPageUseCase,
124-
upsertTodoUseCase: UpsertTodoUseCase,
125121
fetchTodosUseCase: FetchTodosUseCase,
126122
fetchWebPagesUseCase: FetchWebPagesUseCase,
127123
networkConnectivityUseCase: ObserveNetworkConnectivityUseCase,
@@ -132,7 +128,6 @@ final class HomeViewModel: Store {
132128
self.addWebPageUseCase = addWebPageUseCase
133129
self.deleteWebPageUseCase = deleteWebPageUseCase
134130
self.undoDeleteWebPageUseCase = undoDeleteWebPageUseCase
135-
self.upsertTodoUseCase = upsertTodoUseCase
136131
self.fetchTodosUseCase = fetchTodosUseCase
137132
self.fetchWebPagesUseCase = fetchWebPagesUseCase
138133
self.networkConnectivityUseCase = networkConnectivityUseCase
@@ -149,7 +144,7 @@ final class HomeViewModel: Store {
149144
case .networkStatusChanged(let isConnected):
150145
state.isNetworkConnected = isConnected
151146
case .fetchData, .setPresentation, .setAlert, .setToast, .refreshWebPages,
152-
.tapTodoCategory, .orderTodoCategory, .addTodo, .updateWebPageURLInput,
147+
.tapTodoCategory, .orderTodoCategory, .updateWebPageURLInput,
153148
.addWebPage, .deleteWebPage, .undoDeleteWebPage:
154149
effects = reduceByView(action, state: &state)
155150

@@ -183,23 +178,6 @@ final class HomeViewModel: Store {
183178
send(.setAlert(isPresented: true, type: .error))
184179
}
185180
}
186-
case .addTodo(let todo):
187-
beginLoading(for: .overlay, mode: .delayed)
188-
Task {
189-
do {
190-
defer { endLoading(for: .overlay, mode: .delayed) }
191-
try await upsertTodoUseCase.execute(todo)
192-
trackAnalyticsEventUseCase.execute(.todoCreate)
193-
let page = try await fetchRecentTodos()
194-
let items = page.items
195-
.filter { $0.createdAt != $0.updatedAt }
196-
.prefix(5)
197-
.compactMap { RecentTodoItem(from: $0) }
198-
send(.updateRecentTodos(items))
199-
} catch {
200-
send(.setAlert(isPresented: true, type: .error))
201-
}
202-
}
203181
case .fetchRecentTodos:
204182
beginLoading(for: .recentTodos, mode: .immediate)
205183
Task {
@@ -305,8 +283,6 @@ private extension HomeViewModel {
305283
state.preferences = preferences
306284
state.recentTodos = syncRecentTodos(state.recentTodos, preferences: preferences)
307285
return [.updateTodoCategoryPreferences(preferences)]
308-
case .addTodo(let todo):
309-
return [.addTodo(todo)]
310286
case .updateWebPageURLInput(let text):
311287
state.webPageURLInput = text
312288
case .addWebPage:

Application/DevLogPresentation/Sources/Home/TodoDetailView.swift

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import DevLogDomain
1111

1212
struct TodoDetailView: View {
1313
@Environment(\.diContainer) private var container: DIContainer
14+
@Environment(\.openWindow) private var openWindow
15+
@Environment(\.isiOSAppOnMac) private var isiOSAppOnMac
1416
@State var viewModel: TodoDetailViewModel
1517

1618
var body: some View {
@@ -44,7 +46,6 @@ struct TodoDetailView: View {
4446
TodoDetailView(viewModel: TodoDetailViewModel(
4547
fetchTodoUseCase: container.resolve(FetchTodoByIdUseCase.self),
4648
fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self),
47-
upsertUseCase: container.resolve(UpsertTodoUseCase.self),
4849
todoId: item.id,
4950
showEditButton: false
5051
))
@@ -66,9 +67,13 @@ struct TodoDetailView: View {
6667
viewModel: TodoEditorViewModel(
6768
todo: todo,
6869
fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self),
69-
fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self)
70-
),
71-
onSubmit: { viewModel.send(.upsertTodo($0)) }
70+
fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self),
71+
upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self),
72+
onUpsertSuccess: { todo in
73+
viewModel.send(.setShowEditor(false))
74+
viewModel.send(.setTodo(todo))
75+
}
76+
)
7277
)
7378
}
7479
}
@@ -90,14 +95,26 @@ struct TodoDetailView: View {
9095
}
9196
ToolbarItem(placement: .topBarTrailing) {
9297
Button {
93-
viewModel.send(.setShowEditor(true))
98+
openTodoEditor()
9499
} label: {
95100
Text(String(localized: "todo_edit"))
96101
}
97102
}
98103
}
99104
}
100105

106+
private func openTodoEditor() {
107+
if isiOSAppOnMac {
108+
guard let todo = viewModel.state.todo else { return }
109+
openWindow(
110+
id: TodoEditorWindowValue.sceneId,
111+
value: TodoEditorWindowValue(todo: todo)
112+
)
113+
} else {
114+
viewModel.send(.setShowEditor(true))
115+
}
116+
}
117+
101118
@ViewBuilder
102119
private var sheetContent: some View {
103120
if let todo = viewModel.state.todo {

0 commit comments

Comments
 (0)