diff --git a/Application/DevLogApp/Sources/App/DevLogApp.swift b/Application/DevLogApp/Sources/App/DevLogApp.swift index 3737e02a..1a2dca6c 100644 --- a/Application/DevLogApp/Sources/App/DevLogApp.swift +++ b/Application/DevLogApp/Sources/App/DevLogApp.swift @@ -16,6 +16,7 @@ struct DevLogApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate @Environment(\.diContainer) var container: DIContainer @Environment(\.scenePhase) var scenePhase + @State private var windowEvent = TodoEditorWindowEvent() init() { AppAssembler().assemble(AppDIContainer.shared) @@ -29,6 +30,7 @@ struct DevLogApp: App { systemThemeUseCase: container.resolve(ObserveSystemThemeUseCase.self), trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self), widgetURLTab: { MainTab(widgetURL: $0) }, + windowEvent: windowEvent, pushNotificationTodoIdPublisher: PushNotificationRoute.shared.observe(), clearPushNotificationRoute: { PushNotificationRoute.shared.clear() } ) @@ -38,5 +40,19 @@ struct DevLogApp: App { container.resolve(WidgetSyncEventBus.self).publish(.syncRequested) } } + WindowGroup(id: TodoEditorWindowValue.sceneId, for: TodoEditorWindowValue.self) { value in + if let value = value.wrappedValue { + TodoEditorWindowView( + value: value, + windowEvent: windowEvent + ) + .autocorrectionDisabled() + } else { + ContentUnavailableView( + String(localized: "todo_edit"), + systemImage: "square.and.pencil" + ) + } + } } } diff --git a/Application/DevLogPresentation/Sources/Extension/EnvironmentValues+.swift b/Application/DevLogPresentation/Sources/Extension/EnvironmentValues+.swift index fd30488e..7b8eb91a 100644 --- a/Application/DevLogPresentation/Sources/Extension/EnvironmentValues+.swift +++ b/Application/DevLogPresentation/Sources/Extension/EnvironmentValues+.swift @@ -21,6 +21,10 @@ extension EnvironmentValues { self[SceneHeightKey.self] } + var isiOSAppOnMac: Bool { + self[IOSAppOnMacKey.self] + } + private struct SafeAreaInsetsKey: EnvironmentKey { static var defaultValue: EdgeInsets { guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, @@ -48,6 +52,12 @@ extension EnvironmentValues { return windowScene.screen.bounds.height } } + + private struct IOSAppOnMacKey: EnvironmentKey { + static var defaultValue: Bool { + ProcessInfo.processInfo.isiOSAppOnMac + } + } } extension UIEdgeInsets { diff --git a/Application/DevLogPresentation/Sources/Home/Home/HomeView.swift b/Application/DevLogPresentation/Sources/Home/Home/HomeView.swift index 3627f29d..ee3040c0 100644 --- a/Application/DevLogPresentation/Sources/Home/Home/HomeView.swift +++ b/Application/DevLogPresentation/Sources/Home/Home/HomeView.swift @@ -9,6 +9,8 @@ import SwiftUI import DevLogDomain struct HomeView: View { + @Environment(\.openWindow) private var openWindow + @Environment(\.isiOSAppOnMac) private var isiOSAppOnMac @ScaledMetric(relativeTo: .largeTitle) private var labelWidth = CGFloat(34) let coordinator: HomeViewCoordinator let isCompactLayout: Bool @@ -48,8 +50,7 @@ struct HomeView: View { )) { if let selectedCategory = coordinator.viewModel.state.selectedTodoCategory { TodoEditorView( - viewModel: coordinator.makeTodoEditorViewModel(category: selectedCategory), - onSubmit: { coordinator.viewModel.send(.addTodo($0)) } + viewModel: coordinator.makeTodoEditorViewModel(category: selectedCategory) ) } } @@ -272,6 +273,7 @@ struct HomeView: View { } label: { RecentTodoRow(todo: item) .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(.rect) } .buttonStyle(.plain) } @@ -290,6 +292,7 @@ struct HomeView: View { } label: { WebItemRow(item: item, showsChevron: false) .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(.rect) } .buttonStyle(.plain) } @@ -314,7 +317,7 @@ struct HomeView: View { ForEach(preferences, id: \.id) { item in Button { DispatchQueue.main.async { - coordinator.viewModel.send(.tapTodoCategory(item.category)) + openTodoEditor(for: item.category) } } label: { labelImage( @@ -384,6 +387,18 @@ struct HomeView: View { .contentShape(.rect) } + private func openTodoEditor(for todoCategory: TodoCategory) { + if isiOSAppOnMac { + coordinator.viewModel.send(.setPresentation(.contentPicker, false)) + openWindow( + id: TodoEditorWindowValue.sceneId, + value: TodoEditorWindowValue(todoCategory: todoCategory, source: .home) + ) + } else { + coordinator.viewModel.send(.tapTodoCategory(todoCategory)) + } + } + } enum HomeRoute: Hashable { diff --git a/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift b/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift index cfb78512..fbbc9410 100644 --- a/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift +++ b/Application/DevLogPresentation/Sources/Home/Home/HomeViewCoordinator.swift @@ -5,6 +5,7 @@ // Created by opfic on 5/10/26. // +import Combine import Foundation import DevLogCore import DevLogDomain @@ -14,33 +15,20 @@ import DevLogDomain final class HomeViewCoordinator { let viewModel: HomeViewModel let router = NavigationRouter() - private let fetchTodoCategoryPreferencesUseCase: FetchTodoCategoryPreferencesUseCase - private let fetchReferenceItemsUseCase: FetchReferenceItemsUseCase - private let fetchWebPagesUseCase: FetchWebPagesUseCase - private let fetchTodosUseCase: FetchTodosUseCase - private let fetchRecentSearchQueriesUseCase: FetchRecentSearchQueriesUseCase - private let updateRecentSearchQueriesUseCase: UpdateRecentSearchQueriesUseCase + private let container: DIContainer + @ObservationIgnored + private var cancellable: AnyCancellable? init(container: DIContainer) { - let fetchTodoCategoryPreferencesUseCase = container.resolve(FetchTodoCategoryPreferencesUseCase.self) - let fetchWebPagesUseCase = container.resolve(FetchWebPagesUseCase.self) - let fetchTodosUseCase = container.resolve(FetchTodosUseCase.self) - - self.fetchTodoCategoryPreferencesUseCase = fetchTodoCategoryPreferencesUseCase - self.fetchReferenceItemsUseCase = container.resolve(FetchReferenceItemsUseCase.self) - self.fetchWebPagesUseCase = fetchWebPagesUseCase - self.fetchTodosUseCase = fetchTodosUseCase - self.fetchRecentSearchQueriesUseCase = container.resolve(FetchRecentSearchQueriesUseCase.self) - self.updateRecentSearchQueriesUseCase = container.resolve(UpdateRecentSearchQueriesUseCase.self) + self.container = container self.viewModel = HomeViewModel( - fetchPreferencesUseCase: fetchTodoCategoryPreferencesUseCase, + fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), updatePreferencesUseCase: container.resolve(UpdateTodoCategoryPreferencesUseCase.self), addWebPageUseCase: container.resolve(AddWebPageUseCase.self), deleteWebPageUseCase: container.resolve(DeleteWebPageUseCase.self), undoDeleteWebPageUseCase: container.resolve(UndoDeleteWebPageUseCase.self), - upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), - fetchTodosUseCase: fetchTodosUseCase, - fetchWebPagesUseCase: fetchWebPagesUseCase, + fetchTodosUseCase: container.resolve(FetchTodosUseCase.self), + fetchWebPagesUseCase: container.resolve(FetchWebPagesUseCase.self), networkConnectivityUseCase: container.resolve(ObserveNetworkConnectivityUseCase.self), trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self) ) @@ -50,6 +38,16 @@ final class HomeViewCoordinator { viewModel.send(.fetchData) } + func bindWindowEvent(_ windowEvent: TodoEditorWindowEvent) { + guard cancellable == nil else { return } + + cancellable = windowEvent.submits + .sink { [weak self] submit in + guard submit.value.matchesCreate(source: .home) else { return } + self?.viewModel.send(.fetchData) + } + } + func makeTodoManageViewModel() -> TodoManageViewModel { TodoManageViewModel(viewModel.state.preferences) } @@ -57,17 +55,23 @@ final class HomeViewCoordinator { func makeTodoEditorViewModel(category: TodoCategory) -> TodoEditorViewModel { TodoEditorViewModel( category: category, - fetchPreferencesUseCase: fetchTodoCategoryPreferencesUseCase, - fetchReferenceItemsUseCase: fetchReferenceItemsUseCase + fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), + fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), + upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), + trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self), + onUpsertSuccess: { [weak self] _ in + self?.viewModel.send(.setPresentation(.todoEditor, false)) + self?.viewModel.send(.fetchData) + } ) } func makeSearchViewModel() -> SearchViewModel { SearchViewModel( - fetchWebPagesUseCase: fetchWebPagesUseCase, - fetchTodosUseCase: fetchTodosUseCase, - fetchRecentSearchQueriesUseCase: fetchRecentSearchQueriesUseCase, - updateRecentSearchQueriesUseCase: updateRecentSearchQueriesUseCase + fetchWebPagesUseCase: container.resolve(FetchWebPagesUseCase.self), + fetchTodosUseCase: container.resolve(FetchTodosUseCase.self), + fetchRecentSearchQueriesUseCase: container.resolve(FetchRecentSearchQueriesUseCase.self), + updateRecentSearchQueriesUseCase: container.resolve(UpdateRecentSearchQueriesUseCase.self) ) } } diff --git a/Application/DevLogPresentation/Sources/Home/Home/HomeViewModel.swift b/Application/DevLogPresentation/Sources/Home/Home/HomeViewModel.swift index c492ae5d..1fa8b7ce 100644 --- a/Application/DevLogPresentation/Sources/Home/Home/HomeViewModel.swift +++ b/Application/DevLogPresentation/Sources/Home/Home/HomeViewModel.swift @@ -50,7 +50,6 @@ final class HomeViewModel: Store { case tapTodoCategory(TodoCategory) case orderTodoCategory([TodoCategoryItem]) case setTodoCategory([TodoCategoryItem]) - case addTodo(Todo) case updateRecentTodos([RecentTodoItem]) case updateWebPageURLInput(String) case addWebPage @@ -60,7 +59,6 @@ final class HomeViewModel: Store { } enum SideEffect { - case addTodo(Todo) case addWebPage(String) case deleteWebPage(WebPageItem) case undoDeleteWebPage(String) @@ -103,7 +101,6 @@ final class HomeViewModel: Store { private(set) var state = State() private let fetchPreferencesUseCase: FetchTodoCategoryPreferencesUseCase private let updatePreferencesUseCase: UpdateTodoCategoryPreferencesUseCase - private let upsertTodoUseCase: UpsertTodoUseCase private let addWebPageUseCase: AddWebPageUseCase private let deleteWebPageUseCase: DeleteWebPageUseCase private let undoDeleteWebPageUseCase: UndoDeleteWebPageUseCase @@ -121,7 +118,6 @@ final class HomeViewModel: Store { addWebPageUseCase: AddWebPageUseCase, deleteWebPageUseCase: DeleteWebPageUseCase, undoDeleteWebPageUseCase: UndoDeleteWebPageUseCase, - upsertTodoUseCase: UpsertTodoUseCase, fetchTodosUseCase: FetchTodosUseCase, fetchWebPagesUseCase: FetchWebPagesUseCase, networkConnectivityUseCase: ObserveNetworkConnectivityUseCase, @@ -132,7 +128,6 @@ final class HomeViewModel: Store { self.addWebPageUseCase = addWebPageUseCase self.deleteWebPageUseCase = deleteWebPageUseCase self.undoDeleteWebPageUseCase = undoDeleteWebPageUseCase - self.upsertTodoUseCase = upsertTodoUseCase self.fetchTodosUseCase = fetchTodosUseCase self.fetchWebPagesUseCase = fetchWebPagesUseCase self.networkConnectivityUseCase = networkConnectivityUseCase @@ -149,7 +144,7 @@ final class HomeViewModel: Store { case .networkStatusChanged(let isConnected): state.isNetworkConnected = isConnected case .fetchData, .setPresentation, .setAlert, .setToast, .refreshWebPages, - .tapTodoCategory, .orderTodoCategory, .addTodo, .updateWebPageURLInput, + .tapTodoCategory, .orderTodoCategory, .updateWebPageURLInput, .addWebPage, .deleteWebPage, .undoDeleteWebPage: effects = reduceByView(action, state: &state) @@ -183,23 +178,6 @@ final class HomeViewModel: Store { send(.setAlert(isPresented: true, type: .error)) } } - case .addTodo(let todo): - beginLoading(for: .overlay, mode: .delayed) - Task { - do { - defer { endLoading(for: .overlay, mode: .delayed) } - try await upsertTodoUseCase.execute(todo) - trackAnalyticsEventUseCase.execute(.todoCreate) - let page = try await fetchRecentTodos() - let items = page.items - .filter { $0.createdAt != $0.updatedAt } - .prefix(5) - .compactMap { RecentTodoItem(from: $0) } - send(.updateRecentTodos(items)) - } catch { - send(.setAlert(isPresented: true, type: .error)) - } - } case .fetchRecentTodos: beginLoading(for: .recentTodos, mode: .immediate) Task { @@ -305,8 +283,6 @@ private extension HomeViewModel { state.preferences = preferences state.recentTodos = syncRecentTodos(state.recentTodos, preferences: preferences) return [.updateTodoCategoryPreferences(preferences)] - case .addTodo(let todo): - return [.addTodo(todo)] case .updateWebPageURLInput(let text): state.webPageURLInput = text case .addWebPage: diff --git a/Application/DevLogPresentation/Sources/Home/TodoDetailView.swift b/Application/DevLogPresentation/Sources/Home/TodoDetailView.swift index 62bb6d64..93d9a62e 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoDetailView.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoDetailView.swift @@ -11,6 +11,8 @@ import DevLogDomain struct TodoDetailView: View { @Environment(\.diContainer) private var container: DIContainer + @Environment(\.openWindow) private var openWindow + @Environment(\.isiOSAppOnMac) private var isiOSAppOnMac @State var viewModel: TodoDetailViewModel var body: some View { @@ -44,7 +46,6 @@ struct TodoDetailView: View { TodoDetailView(viewModel: TodoDetailViewModel( fetchTodoUseCase: container.resolve(FetchTodoByIdUseCase.self), fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), - upsertUseCase: container.resolve(UpsertTodoUseCase.self), todoId: item.id, showEditButton: false )) @@ -66,9 +67,13 @@ struct TodoDetailView: View { viewModel: TodoEditorViewModel( todo: todo, fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), - fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self) - ), - onSubmit: { viewModel.send(.upsertTodo($0)) } + fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), + upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), + onUpsertSuccess: { todo in + viewModel.send(.setShowEditor(false)) + viewModel.send(.setTodo(todo)) + } + ) ) } } @@ -90,7 +95,7 @@ struct TodoDetailView: View { } ToolbarItem(placement: .topBarTrailing) { Button { - viewModel.send(.setShowEditor(true)) + openTodoEditor() } label: { Text(String(localized: "todo_edit")) } @@ -98,6 +103,18 @@ struct TodoDetailView: View { } } + private func openTodoEditor() { + if isiOSAppOnMac { + guard let todo = viewModel.state.todo else { return } + openWindow( + id: TodoEditorWindowValue.sceneId, + value: TodoEditorWindowValue(todo: todo) + ) + } else { + viewModel.send(.setShowEditor(true)) + } + } + @ViewBuilder private var sheetContent: some View { if let todo = viewModel.state.todo { diff --git a/Application/DevLogPresentation/Sources/Home/TodoDetailViewModel.swift b/Application/DevLogPresentation/Sources/Home/TodoDetailViewModel.swift index d5d034cc..dc068268 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoDetailViewModel.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoDetailViewModel.swift @@ -31,13 +31,11 @@ final class TodoDetailViewModel: Store { case setTodo(Todo) case setReferenceItems([Int: TodoReferenceItem]) case setLoading(Bool) - case upsertTodo(Todo) } enum SideEffect { case fetchTodo case resolveMarkdown(String) - case upsertTodo(Todo) } private(set) var state: State = .init() @@ -45,19 +43,16 @@ final class TodoDetailViewModel: Store { let showEditButton: Bool private let fetchTodoUseCase: FetchTodoByIdUseCase private let fetchReferenceItemsUseCase: FetchReferenceItemsUseCase - private let upsertUseCase: UpsertTodoUseCase private let loadingState = LoadingState() init( fetchTodoUseCase: FetchTodoByIdUseCase, fetchReferenceItemsUseCase: FetchReferenceItemsUseCase, - upsertUseCase: UpsertTodoUseCase, todoId: String, showEditButton: Bool = true ) { self.fetchTodoUseCase = fetchTodoUseCase self.fetchReferenceItemsUseCase = fetchReferenceItemsUseCase - self.upsertUseCase = upsertUseCase self.todoId = todoId self.showEditButton = showEditButton } @@ -85,8 +80,6 @@ final class TodoDetailViewModel: Store { state.referenceItems = items case .setLoading(let value): state.isLoading = value - case .upsertTodo(let todo): - effects = [.upsertTodo(todo)] } if self.state != state { self.state = state } @@ -122,17 +115,6 @@ final class TodoDetailViewModel: Store { send(.setReferenceItems(referenceItems)) } - case .upsertTodo(let todo): - beginLoading(.delayed) - Task { - do { - defer { endLoading(.delayed) } - try await upsertUseCase.execute(todo) - send(.setTodo(todo)) - } catch { - send(.setAlert(true)) - } - } } } } diff --git a/Application/DevLogPresentation/Sources/Home/TodoEditorView.swift b/Application/DevLogPresentation/Sources/Home/TodoEditorView.swift index b7b1e4fa..2d01eeae 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoEditorView.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoEditorView.swift @@ -15,15 +15,16 @@ struct TodoEditorView: View { @State var viewModel: TodoEditorViewModel @Environment(\.diContainer) private var container: DIContainer @Environment(\.dismiss) private var dismiss + @Environment(\.isiOSAppOnMac) private var isiOSAppOnMac @FocusState private var field: Field? private let calendar = Calendar.current - var onSubmit: ((Todo) -> Void)? + var onClose: (() -> Void)? var body: some View { NavigationStack { ScrollView { LazyVStack(spacing: 10) { - titleField + titleSection LazyVStack( alignment: .leading, spacing: 0, @@ -32,7 +33,10 @@ struct TodoEditorView: View { Section { tabView } header: { - tabViewSelector + if !isiOSAppOnMac { + tabPicker + .padding(.horizontal) + } } } } @@ -60,7 +64,6 @@ struct TodoEditorView: View { TodoDetailView(viewModel: TodoDetailViewModel( fetchTodoUseCase: container.resolve(FetchTodoByIdUseCase.self), fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), - upsertUseCase: container.resolve(UpsertTodoUseCase.self), todoId: item.id, showEditButton: false )) @@ -74,7 +77,9 @@ struct TodoEditorView: View { .presentationDragIndicator(.visible) } .toolbar { - ToolbarLeadingButton { dismiss() } + if !isiOSAppOnMac { + ToolbarLeadingButton { close() } + } ToolbarItem(placement: .topBarTrailing) { Button { viewModel.send(.setShowInfo(true)) @@ -85,9 +90,36 @@ struct TodoEditorView: View { ToolbarTrailingButton { submit() } - .disabled(!viewModel.isReadyToSubmit) + .disabled(!viewModel.isReadyToSubmit || viewModel.state.isLoading) + } + .alert( + viewModel.state.alertTitle, + isPresented: Binding( + get: { viewModel.state.showAlert }, + set: { viewModel.send(.setAlert($0)) } + ) + ) { + Button(String(localized: "common_close"), role: .cancel) { } + } message: { + Text(viewModel.state.alertMessage) + } + } + } + + @ViewBuilder + private var titleSection: some View { + Group { + if isiOSAppOnMac { + HStack(spacing: 12) { + titleField + tabPicker + .frame(width: 180) + } + } else { + titleField } } + .padding(.horizontal) } private var titleField: some View { @@ -102,10 +134,9 @@ struct TodoEditorView: View { .font(.title2) .frame(height: 30) .focused($field, equals: .title) - .padding(.horizontal) } - private var tabViewSelector: some View { + private var tabPicker: some View { Picker( "", selection: Binding( @@ -126,7 +157,6 @@ struct TodoEditorView: View { .tag(TodoEditorViewModel.Tag.preview) } .pickerStyle(.segmented) - .padding(.horizontal) } private var tabView: some View { @@ -179,8 +209,15 @@ struct TodoEditorView: View { private func submit() { let todo = viewModel.makeTodo() - onSubmit?(todo) - dismiss() + viewModel.send(.upsertTodo(todo)) + } + + private func close() { + if let onClose { + onClose() + } else { + dismiss() + } } private func transitionToPreview() { diff --git a/Application/DevLogPresentation/Sources/Home/TodoEditorViewModel.swift b/Application/DevLogPresentation/Sources/Home/TodoEditorViewModel.swift index 691f1a85..82fac208 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoEditorViewModel.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoEditorViewModel.swift @@ -54,6 +54,10 @@ final class TodoEditorViewModel: Store { var referenceItems: [Int: TodoReferenceItem] = [:] var dueDate: Date? var showInfo: Bool = false + var showAlert: Bool = false + var alertTitle: String = "" + var alertMessage: String = "" + var isLoading: Bool = false var tags: OrderedSet = [] var tagText: String = "" var focusOnEditor: Bool = false @@ -77,6 +81,8 @@ final class TodoEditorViewModel: Store { case setCompleted(Bool) case setDueDate(Date?) case setCategory(TodoCategoryItem) + case setAlert(Bool) + case setLoading(Bool) case setPinned(Bool) case setShowInfo(Bool) case setSelectedTodoId(TodoIdItem?) @@ -85,17 +91,22 @@ final class TodoEditorViewModel: Store { case setTitle(String) case setCategories([TodoCategoryItem]) case setReferenceItems([Int: TodoReferenceItem]) + case upsertTodo(Todo) } enum SideEffect { case fetchCategories case resolveMarkdown(String) + case upsertTodo(Todo) } private(set) var state = State() private let calendar = Calendar.current private let fetchPreferencesUseCase: FetchTodoCategoryPreferencesUseCase private let fetchReferenceItemsUseCase: FetchReferenceItemsUseCase + private let upsertTodoUseCase: UpsertTodoUseCase + private let trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase? + private let onUpsertSuccess: ((Todo) -> Void)? private let id: String private let isCompleted: Bool private let isChecked: Bool @@ -128,10 +139,16 @@ final class TodoEditorViewModel: Store { init( category: TodoCategory, fetchPreferencesUseCase: FetchTodoCategoryPreferencesUseCase, - fetchReferenceItemsUseCase: FetchReferenceItemsUseCase + fetchReferenceItemsUseCase: FetchReferenceItemsUseCase, + upsertTodoUseCase: UpsertTodoUseCase, + trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase? = nil, + onUpsertSuccess: ((Todo) -> Void)? = nil ) { self.fetchPreferencesUseCase = fetchPreferencesUseCase self.fetchReferenceItemsUseCase = fetchReferenceItemsUseCase + self.upsertTodoUseCase = upsertTodoUseCase + self.trackAnalyticsEventUseCase = trackAnalyticsEventUseCase + self.onUpsertSuccess = onUpsertSuccess self.id = UUID().uuidString self.isCompleted = false self.isChecked = false @@ -147,10 +164,16 @@ final class TodoEditorViewModel: Store { init( todo: Todo, fetchPreferencesUseCase: FetchTodoCategoryPreferencesUseCase, - fetchReferenceItemsUseCase: FetchReferenceItemsUseCase + fetchReferenceItemsUseCase: FetchReferenceItemsUseCase, + upsertTodoUseCase: UpsertTodoUseCase, + trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase? = nil, + onUpsertSuccess: ((Todo) -> Void)? = nil ) { self.fetchPreferencesUseCase = fetchPreferencesUseCase self.fetchReferenceItemsUseCase = fetchReferenceItemsUseCase + self.upsertTodoUseCase = upsertTodoUseCase + self.trackAnalyticsEventUseCase = trackAnalyticsEventUseCase + self.onUpsertSuccess = onUpsertSuccess self.id = todo.id self.isCompleted = todo.isCompleted self.isChecked = todo.isChecked @@ -201,6 +224,10 @@ final class TodoEditorViewModel: Store { state.isCompleted = isCompleted case .setCategory(let todoCategoryItem): state.category = todoCategoryItem + case .setAlert(let isPresented): + setAlert(&state, isPresented: isPresented) + case .setLoading(let value): + state.isLoading = value case .setPinned(let isPinned): state.isPinned = isPinned case .setShowInfo(let isPresented): @@ -216,6 +243,8 @@ final class TodoEditorViewModel: Store { state.categories = categories case .setReferenceItems(let items): state.referenceItems = items + case .upsertTodo(let todo): + effects = [.upsertTodo(todo)] } if self.state != state { self.state = state } @@ -247,6 +276,20 @@ final class TodoEditorViewModel: Store { send(.setReferenceItems(referenceItems)) } + case .upsertTodo(let todo): + send(.setLoading(true)) + Task { + do { + defer { send(.setLoading(false)) } + try await upsertTodoUseCase.execute(todo) + if originalDraft == nil { + trackAnalyticsEventUseCase?.execute(.todoCreate) + } + onUpsertSuccess?(todo) + } catch { + send(.setAlert(true)) + } + } } } } @@ -269,6 +312,15 @@ extension TodoEditorViewModel { } } + private func setAlert( + _ state: inout State, + isPresented: Bool + ) { + state.alertTitle = String(localized: "common_error_title") + state.alertMessage = String(localized: "common_error_message") + state.showAlert = isPresented + } + func makeTodo() -> Todo { let date = Date() return Todo( diff --git a/Application/DevLogPresentation/Sources/Home/TodoEditorWindowEvent.swift b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowEvent.swift new file mode 100644 index 00000000..c76f9750 --- /dev/null +++ b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowEvent.swift @@ -0,0 +1,26 @@ +// +// TodoEditorWindowEvent.swift +// DevLogPresentation +// +// Created by opfic on 5/31/26. +// + +import Combine +import DevLogDomain + +public final class TodoEditorWindowEvent { + private let subject = PassthroughSubject() + + var submits: AnyPublisher { + subject.eraseToAnyPublisher() + } + + public init() { } + + func submit( + value: TodoEditorWindowValue, + todo: Todo + ) { + subject.send(TodoEditorWindowSubmit(value: value, todo: todo)) + } +} diff --git a/Application/DevLogPresentation/Sources/Home/TodoEditorWindowValue.swift b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowValue.swift new file mode 100644 index 00000000..ec27f458 --- /dev/null +++ b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowValue.swift @@ -0,0 +1,162 @@ +// +// TodoEditorWindowValue.swift +// DevLogPresentation +// +// Created by opfic on 5/31/26. +// + +import Foundation +import DevLogDomain + +public enum TodoEditorWindowValue: Codable, Hashable { + case create(TodoEditorWindowCategory, TodoEditorWindowSource) + case edit(TodoEditorWindowTodo) + + public static let sceneId = "todo-editor" + + public init( + todoCategory: TodoCategory, + source: TodoEditorWindowSource + ) { + self = .create(TodoEditorWindowCategory(todoCategory: todoCategory), source) + } + + init(todo: Todo) { + self = .edit(TodoEditorWindowTodo(todo: todo)) + } +} + +public enum TodoEditorWindowSource: String, Codable, Hashable { + case home + case list +} + +public struct TodoEditorWindowCategory: Codable, Hashable { + private enum Kind: String, Codable { + case system + case user + } + + private let kind: Kind + private let id: String + private let name: String + private let colorHex: String + + init(todoCategory: TodoCategory) { + switch todoCategory { + case .system(let systemTodoCategory): + self.kind = .system + self.id = systemTodoCategory.rawValue + self.name = "" + self.colorHex = "" + case .user(let userTodoCategory): + self.kind = .user + self.id = userTodoCategory.id + self.name = userTodoCategory.name + self.colorHex = userTodoCategory.colorHex + } + } + + var todoCategory: TodoCategory { + switch kind { + case .system: + let systemTodoCategory = SystemTodoCategory(rawValue: id) ?? .etc + return .system(systemTodoCategory) + case .user: + return .user(UserTodoCategory( + id: id, + name: name, + colorHex: colorHex + )) + } + } +} + +public struct TodoEditorWindowTodo: Codable, Hashable { + private let id: String + private let isPinned: Bool + private let isCompleted: Bool + private let isChecked: Bool + private let number: Int? + private let title: String + private let content: String + private let createdAt: Date + private let updatedAt: Date + private let completedAt: Date? + private let deletedAt: Date? + private let dueDate: Date? + private let tags: [String] + private let category: TodoEditorWindowCategory + + init(todo: Todo) { + self.id = todo.id + self.isPinned = todo.isPinned + self.isCompleted = todo.isCompleted + self.isChecked = todo.isChecked + self.number = todo.number + self.title = todo.title + self.content = todo.content + self.createdAt = todo.createdAt + self.updatedAt = todo.updatedAt + self.completedAt = todo.completedAt + self.deletedAt = todo.deletedAt + self.dueDate = todo.dueDate + self.tags = todo.tags + self.category = TodoEditorWindowCategory(todoCategory: todo.category) + } + + public static func == ( + lhs: TodoEditorWindowTodo, + rhs: TodoEditorWindowTodo + ) -> Bool { + lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + var todo: Todo { + Todo( + id: id, + isPinned: isPinned, + isCompleted: isCompleted, + isChecked: isChecked, + number: number, + title: title, + content: content, + createdAt: createdAt, + updatedAt: updatedAt, + completedAt: completedAt, + deletedAt: deletedAt, + dueDate: dueDate, + tags: tags, + category: category.todoCategory + ) + } +} + +struct TodoEditorWindowSubmit: Equatable { + let id = UUID() + let value: TodoEditorWindowValue + let todo: Todo +} + +extension TodoEditorWindowValue { + func matchesCreate( + category: TodoCategory? = nil, + source: TodoEditorWindowSource + ) -> Bool { + guard case .create(let windowCategory, let windowSource) = self, + windowSource == source else { return false } + if let category { + return windowCategory.todoCategory == category + } + return true + } + + func matchesEdit(todoId: String) -> Bool { + guard case .edit(let windowTodo) = self else { return false } + return windowTodo.todo.id == todoId + } +} diff --git a/Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift new file mode 100644 index 00000000..fa95eac0 --- /dev/null +++ b/Application/DevLogPresentation/Sources/Home/TodoEditorWindowView.swift @@ -0,0 +1,92 @@ +// +// TodoEditorWindowView.swift +// DevLogPresentation +// +// Created by opfic on 5/31/26. +// + +import SwiftUI +import DevLogCore +import DevLogDomain + +public struct TodoEditorWindowView: View { + @Environment(\.diContainer) private var container: DIContainer + @State private var windowScene: UIWindowScene? + private let value: TodoEditorWindowValue + private let windowEvent: TodoEditorWindowEvent + + public init( + value: TodoEditorWindowValue, + windowEvent: TodoEditorWindowEvent + ) { + self.value = value + self.windowEvent = windowEvent + } + + public var body: some View { + Group { + switch value { + case .create(let windowCategory, _): + TodoEditorView( + viewModel: TodoEditorViewModel( + category: windowCategory.todoCategory, + fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), + fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), + upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), + trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self), + onUpsertSuccess: upsert + ), + onClose: closeWindow + ) + case .edit(let windowTodo): + TodoEditorView( + viewModel: TodoEditorViewModel( + todo: windowTodo.todo, + fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), + fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), + upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), + onUpsertSuccess: upsert + ), + onClose: closeWindow + ) + } + } + .background { + WindowSceneReader { windowScene = $0 } + } + } + + private func upsert(_ todo: Todo) { + windowEvent.submit(value: value, todo: todo) + closeWindow() + } + + private func closeWindow() { + guard let windowScene else { return } + UIApplication.shared.requestSceneSessionDestruction( + windowScene.session, + options: nil, + errorHandler: nil + ) + } +} + +private struct WindowSceneReader: UIViewRepresentable { + let onResolve: (UIWindowScene?) -> Void + + func makeUIView(context: Context) -> UIView { + let view = UIView() + resolve(from: view) + return view + } + + func updateUIView(_ view: UIView, context: Context) { + resolve(from: view) + } + + private func resolve(from view: UIView) { + DispatchQueue.main.async { + onResolve(view.window?.windowScene) + } + } +} diff --git a/Application/DevLogPresentation/Sources/Home/TodoListView.swift b/Application/DevLogPresentation/Sources/Home/TodoListView.swift index ec0a44ff..a8c2ab11 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoListView.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoListView.swift @@ -13,6 +13,8 @@ struct TodoListView: View { @Environment(NavigationRouter.self) private var router @Environment(\.diContainer) var container: DIContainer @Environment(\.colorScheme) private var colorScheme + @Environment(\.openWindow) private var openWindow + @Environment(\.isiOSAppOnMac) private var isiOSAppOnMac @ScaledMetric(relativeTo: .body) private var headerHeight = 41 @State private var headerOffset: CGFloat = .zero @State private var isScrollTrackingEnabled = false @@ -82,15 +84,20 @@ struct TodoListView: View { viewModel: TodoEditorViewModel( category: viewModel.category, fetchPreferencesUseCase: container.resolve(FetchTodoCategoryPreferencesUseCase.self), - fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self) - ), - onSubmit: { viewModel.send(.upsertTodo($0)) } + fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), + upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), + trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self), + onUpsertSuccess: { _ in + viewModel.send(.setShowEditor(false)) + viewModel.send(.refresh) + } + ) ) } .toolbar { ToolbarItem(placement: .topBarTrailing) { Button { - viewModel.send(.setShowEditor(true)) + openTodoEditor() } label: { Image(systemName: "plus") } @@ -230,6 +237,17 @@ struct TodoListView: View { ) } + private func openTodoEditor() { + if isiOSAppOnMac { + openWindow( + id: TodoEditorWindowValue.sceneId, + value: TodoEditorWindowValue(todoCategory: viewModel.category, source: .list) + ) + } else { + viewModel.send(.setShowEditor(true)) + } + } + @ViewBuilder private var searchResultsContent: some View { let searchResults = viewModel.state.searchResults.filter { !$0.isHidden } diff --git a/Application/DevLogPresentation/Sources/Home/TodoListViewModel.swift b/Application/DevLogPresentation/Sources/Home/TodoListViewModel.swift index ebab4b66..9467e77b 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoListViewModel.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoListViewModel.swift @@ -50,7 +50,6 @@ final class TodoListViewModel: Store { case loadNextPage case setSearchText(String) case setToast(isPresented: Bool) - case upsertTodo(Todo) // Run case applySearchQuery(String) @@ -70,7 +69,6 @@ final class TodoListViewModel: Store { case fetch case loadNextPage case search(String) - case upsert(Todo) case delete(TodoListItem) case undoDelete(String) case toggleCompleted(TodoListItem) @@ -138,7 +136,7 @@ final class TodoListViewModel: Store { .setShowAllSearchResults, .tapToggleCompleted, .tapTogglePinned, .undoDelete: effects = reduceByUser(action, state: &state) - case .onAppear, .loadNextPage, .setSearchText, .setToast, .upsertTodo: + case .onAppear, .loadNextPage, .setSearchText, .setToast: effects = reduceByView(action, state: &state) case .applySearchQuery, .fetchSearchResults, .didToggleCompleted, .didTogglePinned, @@ -206,18 +204,6 @@ final class TodoListViewModel: Store { } } searchTasks[.request] = requestTask - case .upsert(let item): - beginLoading(.delayed) - Task { - do { - defer { endLoading(.delayed) } - try await upsertTodoUseCase.execute(item) - trackAnalyticsEventUseCase.execute(.todoCreate) - send(.refresh) - } catch { - send(.setAlert(true)) - } - } case .toggleCompleted(let item): beginLoading(.delayed) Task { @@ -369,8 +355,6 @@ private extension TodoListViewModel { state.searchResults.removeAll { $0.isHidden } self.undoTodoId = nil } - case .upsertTodo(let todo): - return [.upsert(todo)] default: break } diff --git a/Application/DevLogPresentation/Sources/Home/TodoWindowCoordinator.swift b/Application/DevLogPresentation/Sources/Home/TodoWindowCoordinator.swift index bea4f7ce..525daf60 100644 --- a/Application/DevLogPresentation/Sources/Home/TodoWindowCoordinator.swift +++ b/Application/DevLogPresentation/Sources/Home/TodoWindowCoordinator.swift @@ -5,6 +5,7 @@ // Created by opfic on 5/31/26. // +import Combine import Foundation import DevLogCore import DevLogDomain @@ -12,14 +13,25 @@ import DevLogDomain @MainActor @Observable final class TodoWindowCoordinator { - private let diContainer: DIContainer + private let container: DIContainer @ObservationIgnored private var listViewModel: TodoListViewModel? @ObservationIgnored private var detailViewModel: TodoDetailViewModel? + @ObservationIgnored + private var cancellable: AnyCancellable? init(container: DIContainer) { - self.diContainer = container + self.container = container + } + + func bindWindowEvent(_ windowEvent: TodoEditorWindowEvent) { + guard cancellable == nil else { return } + + cancellable = windowEvent.submits + .sink { [weak self] submit in + self?.handleTodoEditorSubmit(submit) + } } func makeListViewModel(category: TodoCategory) -> TodoListViewModel { @@ -29,12 +41,12 @@ final class TodoWindowCoordinator { } let listViewModel = TodoListViewModel( - fetchTodosUseCase: diContainer.resolve(FetchTodosUseCase.self), - fetchTodoByIdUseCase: diContainer.resolve(FetchTodoByIdUseCase.self), - upsertTodoUseCase: diContainer.resolve(UpsertTodoUseCase.self), - deleteTodoUseCase: diContainer.resolve(DeleteTodoUseCase.self), - undoDeleteTodoUseCase: diContainer.resolve(UndoDeleteTodoUseCase.self), - trackAnalyticsEventUseCase: diContainer.resolve(TrackAnalyticsEventUseCase.self), + fetchTodosUseCase: container.resolve(FetchTodosUseCase.self), + fetchTodoByIdUseCase: container.resolve(FetchTodoByIdUseCase.self), + upsertTodoUseCase: container.resolve(UpsertTodoUseCase.self), + deleteTodoUseCase: container.resolve(DeleteTodoUseCase.self), + undoDeleteTodoUseCase: container.resolve(UndoDeleteTodoUseCase.self), + trackAnalyticsEventUseCase: container.resolve(TrackAnalyticsEventUseCase.self), category: category ) self.listViewModel = listViewModel @@ -52,13 +64,24 @@ final class TodoWindowCoordinator { } let detailViewModel = TodoDetailViewModel( - fetchTodoUseCase: diContainer.resolve(FetchTodoByIdUseCase.self), - fetchReferenceItemsUseCase: diContainer.resolve(FetchReferenceItemsUseCase.self), - upsertUseCase: diContainer.resolve(UpsertTodoUseCase.self), + fetchTodoUseCase: container.resolve(FetchTodoByIdUseCase.self), + fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), todoId: todoId, showEditButton: showEditButton ) self.detailViewModel = detailViewModel return detailViewModel } + + private func handleTodoEditorSubmit(_ submit: TodoEditorWindowSubmit) { + if let listViewModel, + submit.value.matchesCreate(category: listViewModel.category, source: .list) { + listViewModel.send(.refresh) + } + + if let detailViewModel, + submit.value.matchesEdit(todoId: detailViewModel.todoId) { + detailViewModel.send(.setTodo(submit.todo)) + } + } } diff --git a/Application/DevLogPresentation/Sources/Main/MainView.swift b/Application/DevLogPresentation/Sources/Main/MainView.swift index 355708af..fef09bd7 100644 --- a/Application/DevLogPresentation/Sources/Main/MainView.swift +++ b/Application/DevLogPresentation/Sources/Main/MainView.swift @@ -18,9 +18,11 @@ struct MainView: View { @State private var pushNotificationListViewCoordinator: PushNotificationListViewCoordinator @State private var profileViewCoordinator: ProfileViewCoordinator @Binding var selectedTab: MainTab? + private let windowEvent: TodoEditorWindowEvent init( container: DIContainer, + windowEvent: TodoEditorWindowEvent, selectedTab: Binding ) { self._coordinator = State(initialValue: MainViewCoordinator(container: container)) @@ -31,6 +33,7 @@ struct MainView: View { initialValue: PushNotificationListViewCoordinator(container: container) ) self._profileViewCoordinator = State(initialValue: ProfileViewCoordinator(container: container)) + self.windowEvent = windowEvent self._selectedTab = selectedTab } @@ -46,6 +49,8 @@ struct MainView: View { } .onAppear { coordinator.viewModel.send(.onAppear) + homeViewCoordinator.bindWindowEvent(windowEvent) + todoWindowCoordinator.bindWindowEvent(windowEvent) } .onChange(of: selectedTab, initial: true) { _, newValue in guard let newValue else { return } diff --git a/Application/DevLogPresentation/Sources/Profile/ProfileViewCoordinator.swift b/Application/DevLogPresentation/Sources/Profile/ProfileViewCoordinator.swift index e1cefe4d..2a90f684 100644 --- a/Application/DevLogPresentation/Sources/Profile/ProfileViewCoordinator.swift +++ b/Application/DevLogPresentation/Sources/Profile/ProfileViewCoordinator.swift @@ -61,7 +61,6 @@ final class ProfileViewCoordinator { TodoDetailViewModel( fetchTodoUseCase: container.resolve(FetchTodoByIdUseCase.self), fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), - upsertUseCase: container.resolve(UpsertTodoUseCase.self), todoId: todoId, showEditButton: false ) diff --git a/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListViewCoordinator.swift b/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListViewCoordinator.swift index fe8f9b85..23b9bee8 100644 --- a/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListViewCoordinator.swift +++ b/Application/DevLogPresentation/Sources/PushNotification/PushNotificationListViewCoordinator.swift @@ -14,12 +14,12 @@ import DevLogDomain final class PushNotificationListViewCoordinator { let viewModel: PushNotificationListViewModel var todoIdToPresent: TodoIdItem? - private let diContainer: DIContainer + private let container: DIContainer @ObservationIgnored private var todoDetailViewModel: TodoDetailViewModel? init(container: DIContainer) { - self.diContainer = container + self.container = container self.viewModel = PushNotificationListViewModel( fetchUseCase: container.resolve(FetchPushNotificationsUseCase.self), deleteUseCase: container.resolve(DeletePushNotificationUseCase.self), @@ -42,9 +42,8 @@ final class PushNotificationListViewCoordinator { } let todoDetailViewModel = TodoDetailViewModel( - fetchTodoUseCase: diContainer.resolve(FetchTodoByIdUseCase.self), - fetchReferenceItemsUseCase: diContainer.resolve(FetchReferenceItemsUseCase.self), - upsertUseCase: diContainer.resolve(UpsertTodoUseCase.self), + fetchTodoUseCase: container.resolve(FetchTodoByIdUseCase.self), + fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), todoId: todoId, showEditButton: false ) diff --git a/Application/DevLogPresentation/Sources/Root/RootView.swift b/Application/DevLogPresentation/Sources/Root/RootView.swift index 0ba4bec5..b74658b2 100644 --- a/Application/DevLogPresentation/Sources/Root/RootView.swift +++ b/Application/DevLogPresentation/Sources/Root/RootView.swift @@ -16,6 +16,7 @@ public struct RootView: View { @State private var selectedRoute: Route? @State private var selectedMainTab: MainTab? private let widgetURLTab: (URL) -> MainTab? + private let windowEvent: TodoEditorWindowEvent private let pushNotificationTodoIdPublisher: AnyPublisher private let clearPushNotificationRoute: () -> Void @@ -25,6 +26,7 @@ public struct RootView: View { systemThemeUseCase: ObserveSystemThemeUseCase, trackAnalyticsEventUseCase: TrackAnalyticsEventUseCase, widgetURLTab: @escaping (URL) -> MainTab?, + windowEvent: TodoEditorWindowEvent, pushNotificationTodoIdPublisher: AnyPublisher, clearPushNotificationRoute: @escaping () -> Void ) { @@ -35,6 +37,7 @@ public struct RootView: View { trackAnalyticsEventUseCase: trackAnalyticsEventUseCase )) self.widgetURLTab = widgetURLTab + self.windowEvent = windowEvent self.pushNotificationTodoIdPublisher = pushNotificationTodoIdPublisher self.clearPushNotificationRoute = clearPushNotificationRoute } @@ -46,6 +49,7 @@ public struct RootView: View { if signIn { MainView( container: container, + windowEvent: windowEvent, selectedTab: $selectedMainTab ) } else { @@ -91,7 +95,6 @@ public struct RootView: View { TodoDetailView(viewModel: TodoDetailViewModel( fetchTodoUseCase: container.resolve(FetchTodoByIdUseCase.self), fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), - upsertUseCase: container.resolve(UpsertTodoUseCase.self), todoId: todoId, showEditButton: false )) diff --git a/Application/DevLogPresentation/Sources/Search/SearchView.swift b/Application/DevLogPresentation/Sources/Search/SearchView.swift index 3d07a4a5..27616391 100644 --- a/Application/DevLogPresentation/Sources/Search/SearchView.swift +++ b/Application/DevLogPresentation/Sources/Search/SearchView.swift @@ -24,7 +24,6 @@ struct SearchView: View { TodoDetailView(viewModel: TodoDetailViewModel( fetchTodoUseCase: container.resolve(FetchTodoByIdUseCase.self), fetchReferenceItemsUseCase: container.resolve(FetchReferenceItemsUseCase.self), - upsertUseCase: container.resolve(UpsertTodoUseCase.self), todoId: todoId )) case .web(let page):