Skip to content

Commit 3609008

Browse files
authored
[#534] TodoEditor에서 Todo를 수정하면 HomeView에서 최근 수정 섹션에 해당 Todo가 반영되지 않는 현상을 해결한다 (#554)
* feat: Todo 수정 및 삭제를 감지하는 버스 구현 * feat: TodoMutationEvent 방출 * feat: TodoMutationEvent 기반 홈 화면 갱신 * refactor: HomeViewCoordinator stream task 관리 개선 * refactor: self 처리
1 parent d98d39f commit 3609008

10 files changed

Lines changed: 172 additions & 12 deletions

File tree

Application/DevLogData/Sources/DataAssembler.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,16 @@ public final class DataAssembler: Assembler {
3232
)
3333
}
3434

35+
container.register(TodoMutationEventBus.self) {
36+
TodoMutationEventBusImpl()
37+
}
38+
3539
container.register(TodoRepository.self) {
3640
TodoRepositoryImpl(
3741
todoService: container.resolve(TodoService.self),
3842
todoCategoryService: container.resolve(TodoCategoryService.self),
39-
widgetSyncEventBus: container.resolve(WidgetSyncEventBus.self)
43+
widgetSyncEventBus: container.resolve(WidgetSyncEventBus.self),
44+
todoMutationEventBus: container.resolve(TodoMutationEventBus.self)
4045
)
4146
}
4247

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//
2+
// TodoMutationEventBusImpl.swift
3+
// DevLogData
4+
//
5+
// Created by opfic on 6/6/26.
6+
//
7+
8+
import Foundation
9+
import DevLogDomain
10+
11+
actor TodoMutationEventBusImpl: TodoMutationEventBus {
12+
private var continuations = [UUID: AsyncStream<TodoMutationEvent>.Continuation]()
13+
14+
func publish(_ event: TodoMutationEvent) async {
15+
continuations.values.forEach { $0.yield(event) }
16+
}
17+
18+
func events() -> AsyncStream<TodoMutationEvent> {
19+
let id = UUID()
20+
let (stream, continuation) = AsyncStream.makeStream(of: TodoMutationEvent.self)
21+
22+
continuations[id] = continuation
23+
continuation.onTermination = { [weak self] _ in
24+
Task {
25+
await self?.removeContinuation(id: id)
26+
}
27+
}
28+
29+
return stream
30+
}
31+
}
32+
33+
private extension TodoMutationEventBusImpl {
34+
func removeContinuation(id: UUID) {
35+
continuations[id] = nil
36+
}
37+
}

Application/DevLogData/Sources/Repository/TodoRepositoryImpl.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,18 @@ final class TodoRepositoryImpl: TodoRepository {
1313
private let todoService: TodoService
1414
private let todoCategoryService: TodoCategoryService
1515
private let widgetSyncEventBus: WidgetSyncEventBus
16+
private let todoMutationEventBus: TodoMutationEventBus
1617

1718
init(
1819
todoService: TodoService,
1920
todoCategoryService: TodoCategoryService,
20-
widgetSyncEventBus: WidgetSyncEventBus
21+
widgetSyncEventBus: WidgetSyncEventBus,
22+
todoMutationEventBus: TodoMutationEventBus
2123
) {
2224
self.todoService = todoService
2325
self.todoCategoryService = todoCategoryService
2426
self.widgetSyncEventBus = widgetSyncEventBus
27+
self.todoMutationEventBus = todoMutationEventBus
2528
}
2629

2730
func fetchTodos(_ query: TodoQuery, cursor: TodoCursor?) async throws -> TodoPage {
@@ -107,6 +110,7 @@ final class TodoRepositoryImpl: TodoRepository {
107110
func upsertTodo(_ todo: Todo) async throws {
108111
let todoRequest = TodoRequest.fromDomain(todo)
109112
try await upsertTodo(todoRequest)
113+
await todoMutationEventBus.publish(.updated(todo.id))
110114
}
111115

112116
func upsertTodo(_ todoDraft: TodoDraft) async throws {
@@ -127,6 +131,7 @@ final class TodoRepositoryImpl: TodoRepository {
127131
do {
128132
try await todoService.deleteTodo(todoId: todoId)
129133
widgetSyncEventBus.publish(.syncRequested)
134+
await todoMutationEventBus.publish(.deleted(todoId))
130135
} catch {
131136
throw error.toDomain()
132137
}
@@ -136,6 +141,7 @@ final class TodoRepositoryImpl: TodoRepository {
136141
do {
137142
try await todoService.undoDeleteTodo(todoId: todoId)
138143
widgetSyncEventBus.publish(.syncRequested)
144+
await todoMutationEventBus.publish(.restored(todoId))
139145
} catch {
140146
throw error.toDomain()
141147
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//
2+
// TodoMutationEventBusImplTests.swift
3+
// DevLogDataTests
4+
//
5+
// Created by opfic on 6/6/26.
6+
//
7+
8+
import Testing
9+
import DevLogDomain
10+
@testable import DevLogData
11+
12+
struct TodoMutationEventBusImplTests {
13+
@Test("TodoMutationEventBus는 발행된 이벤트를 관찰자에게 전달한다")
14+
func todoMutationEventBus는_발행된_이벤트를_관찰자에게_전달한다() async {
15+
let bus = TodoMutationEventBusImpl()
16+
let events = await bus.events()
17+
let task = Task {
18+
var iterator = events.makeAsyncIterator()
19+
return await iterator.next()
20+
}
21+
22+
await bus.publish(.updated("todo-id"))
23+
24+
let event = await task.value
25+
#expect(event == .updated("todo-id"))
26+
}
27+
}

Application/DevLogData/Tests/Repository/TodoRepositoryImplTests.swift

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ import Foundation
1010
import Testing
1111
import DevLogCore
1212
import DevLogDomain
13-
@testable import DevLogData
13+
@testable @preconcurrency import DevLogData
1414

1515
struct TodoRepositoryImplTests {
16-
@Test("Todo 변경 성공 시 위젯 동기화 이벤트를 발행한다")
17-
func todo_변경_성공_시_위젯_동기화_이벤트를_발행한다() async throws {
16+
@Test("Todo 변경 성공 시 위젯 동기화와 mutation 이벤트를 발행한다")
17+
func todo_변경_성공_시_위젯_동기화와_mutation_이벤트를_발행한다() async throws {
1818
let fixture = makeFixture()
1919
let todo = makeTodo()
2020

@@ -24,10 +24,13 @@ struct TodoRepositoryImplTests {
2424

2525
let events = fixture.widgetSyncEventBus.events
2626
#expect(events == [.syncRequested, .syncRequested, .syncRequested])
27+
28+
let mutationEvents = await fixture.todoMutationEventBus.publishedEvents()
29+
#expect(mutationEvents == [.updated(todo.id), .deleted(todo.id), .restored(todo.id)])
2730
}
2831

29-
@Test("Todo 변경 실패 시 위젯 동기화 이벤트를 발행하지 않는다")
30-
func todo_변경_실패_시_위젯_동기화_이벤트를_발행하지_않는다() async throws {
32+
@Test("Todo 변경 실패 시 위젯 동기화와 mutation 이벤트를 발행하지 않는다")
33+
func todo_변경_실패_시_위젯_동기화와_mutation_이벤트를_발행하지_않는다() async throws {
3134
let fixture = makeFixture()
3235
let todo = makeTodo()
3336

@@ -52,24 +55,30 @@ struct TodoRepositoryImplTests {
5255
#expect(error as? TodoRepositoryImplTestsError == .serviceFailed)
5356
}
5457

55-
let events = fixture.widgetSyncEventBus.events
56-
#expect(events.isEmpty)
58+
let syncEvents = fixture.widgetSyncEventBus.events
59+
#expect(syncEvents.isEmpty)
60+
61+
let mutationEvents = await fixture.todoMutationEventBus.publishedEvents()
62+
#expect(mutationEvents.isEmpty)
5763
}
5864

5965
private func makeFixture() -> Fixture {
6066
let todoService = TodoServiceSpy()
6167
let todoCategoryService = TodoCategoryServiceSpy()
6268
let widgetSyncEventBus = WidgetSyncEventBusSpy()
69+
let todoMutationEventBus = TodoMutationEventBusSpy()
6370
let repository = TodoRepositoryImpl(
6471
todoService: todoService,
6572
todoCategoryService: todoCategoryService,
66-
widgetSyncEventBus: widgetSyncEventBus
73+
widgetSyncEventBus: widgetSyncEventBus,
74+
todoMutationEventBus: todoMutationEventBus
6775
)
6876

6977
return Fixture(
7078
repository: repository,
7179
todoService: todoService,
72-
widgetSyncEventBus: widgetSyncEventBus
80+
widgetSyncEventBus: widgetSyncEventBus,
81+
todoMutationEventBus: todoMutationEventBus
7382
)
7483
}
7584

@@ -97,6 +106,7 @@ private struct Fixture {
97106
let repository: TodoRepositoryImpl
98107
let todoService: TodoServiceSpy
99108
let widgetSyncEventBus: WidgetSyncEventBusSpy
109+
let todoMutationEventBus: TodoMutationEventBusSpy
100110
}
101111

102112
private actor TodoServiceSpy: TodoService {
@@ -159,6 +169,24 @@ private final class WidgetSyncEventBusSpy: WidgetSyncEventBus {
159169
}
160170
}
161171

172+
private actor TodoMutationEventBusSpy: TodoMutationEventBus {
173+
private var capturedEvents = [TodoMutationEvent]()
174+
175+
func publish(_ event: TodoMutationEvent) async {
176+
capturedEvents.append(event)
177+
}
178+
179+
func publishedEvents() -> [TodoMutationEvent] {
180+
capturedEvents
181+
}
182+
183+
func events() async -> AsyncStream<TodoMutationEvent> {
184+
AsyncStream { continuation in
185+
continuation.finish()
186+
}
187+
}
188+
}
189+
162190
private enum TodoRepositoryImplTestsError: Error, Equatable {
163191
case serviceFailed
164192
case unexpectedCall
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//
2+
// TodoMutationEvent.swift
3+
// DevLogDomain
4+
//
5+
// Created by opfic on 6/6/26.
6+
//
7+
8+
public enum TodoMutationEvent: Equatable, Sendable {
9+
case updated(String)
10+
case deleted(String)
11+
case restored(String)
12+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//
2+
// TodoMutationEventBus.swift
3+
// DevLogDomain
4+
//
5+
// Created by opfic on 6/6/26.
6+
//
7+
8+
public protocol TodoMutationEventBus: Sendable {
9+
func publish(_ event: TodoMutationEvent) async
10+
func events() async -> AsyncStream<TodoMutationEvent>
11+
}

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,17 @@ import DevLogDomain
1313
@MainActor
1414
@Observable
1515
final class HomeViewCoordinator {
16+
private enum AsyncStreamTaskID {
17+
case todoMutationEvent
18+
}
19+
1620
let viewModel: HomeViewModel
1721
let router = NavigationRouter<HomeRoute>()
1822
private let container: DIContainer
1923
@ObservationIgnored
2024
private var cancellable: AnyCancellable?
25+
@ObservationIgnored
26+
private var streamTasks = [AsyncStreamTaskID: Task<Void, Never>]()
2127

2228
init(container: DIContainer) {
2329
self.container = container
@@ -34,10 +40,34 @@ final class HomeViewCoordinator {
3440
)
3541
}
3642

43+
deinit {
44+
streamTasks.values.forEach { $0.cancel() }
45+
}
46+
3747
func fetchData() {
3848
viewModel.send(.fetchData)
3949
}
4050

51+
func refreshRecentTodos() {
52+
viewModel.send(.refreshRecentTodos)
53+
}
54+
55+
func bindTodoMutationEvent() {
56+
guard streamTasks[.todoMutationEvent] == nil else { return }
57+
58+
let bus = container.resolve(TodoMutationEventBus.self)
59+
streamTasks[.todoMutationEvent] = Task { [weak self] in
60+
let events = await bus.events()
61+
for await event in events {
62+
guard let self else { break }
63+
switch event {
64+
case .updated, .deleted, .restored:
65+
self.refreshRecentTodos()
66+
}
67+
}
68+
}
69+
}
70+
4171
func bindWindowEvent(_ windowEvent: TodoEditorWindowEvent) {
4272
guard cancellable == nil else { return }
4373

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ final class HomeViewModel: StorePattern {
3636

3737
enum Action {
3838
case fetchData
39+
case refreshRecentTodos
3940
case networkStatusChanged(Bool)
4041
case setPresentation(Presentation, Bool)
4142
case setAlert(isPresented: Bool, type: AlertType? = nil)
@@ -136,7 +137,7 @@ final class HomeViewModel: StorePattern {
136137
switch action {
137138
case .networkStatusChanged(let isConnected):
138139
state.isNetworkConnected = isConnected
139-
case .fetchData, .setPresentation, .setAlert, .refreshWebPages,
140+
case .fetchData, .refreshRecentTodos, .setPresentation, .setAlert, .refreshWebPages,
140141
.tapTodoCategory, .orderTodoCategory, .updateWebPageURLInput,
141142
.addWebPage, .deleteWebPage, .undoDeleteWebPage, .finishDeleteWebPageToast:
142143
effects = reduceByView(action, state: &state)
@@ -252,6 +253,8 @@ private extension HomeViewModel {
252253
switch action {
253254
case .fetchData:
254255
return [.fetchTodoCategoryPreferences, .fetchRecentTodos, .fetchWebPages]
256+
case .refreshRecentTodos:
257+
return [.fetchRecentTodos]
255258
case .refreshWebPages:
256259
return [.fetchWebPages]
257260
case .setPresentation(let presentation, let isPresented):

Application/DevLogPresentation/Sources/Main/MainView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ struct MainView: View {
4848
.onAppear {
4949
coordinator.viewModel.send(.onAppear)
5050
homeViewCoordinator.bindWindowEvent(windowEvent)
51+
homeViewCoordinator.bindTodoMutationEvent()
5152
todoWindowCoordinator.bindWindowEvent(windowEvent)
5253
}
5354
.onChange(of: selectedTab, initial: true) { _, newValue in

0 commit comments

Comments
 (0)