-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSearchUserViewModel.swift
More file actions
172 lines (137 loc) · 5.27 KB
/
Copy pathSearchUserViewModel.swift
File metadata and controls
172 lines (137 loc) · 5.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
//
// SearchUserViewModel.swift
// FeatureSearch
//
// Created by 전성훈 on 7/14/26.
// Copyright © 2026 com.seonghun.gitsearchmicro. All rights reserved.
//
import Foundation
import Combine
import CoreDesignSystem
import FeatureSearchInterface
struct SearchUserViewModelInput: ViewModelInput {
let searchUser: AnyPublisher<String, Never>
let loadNextPage: AnyPublisher<Void, Never>
let backToLogin: AnyPublisher<Void, Never>
}
struct SearchUserViewModelOutput: ViewModelOutput {
let state: AnyPublisher<SearchViewState, Never>
let error: AnyPublisher<Error, Never>
}
@MainActor
final class SearchUserViewModel: BaseViewModel {
typealias Input = SearchUserViewModelInput
typealias Output = SearchUserViewModelOutput
private let searchUsersUseCase: any SearchUsersUseCase
private let fetchProfileUseCase: any FetchProfileUseCase
private let paginator = SearchPaginator()
private weak let actions: FeatureSearchCoordinatorActions?
private var cancellables = Set<AnyCancellable>()
private let searchTask = ManagedTask()
private let loadNextPageTask = ManagedTask()
private let stateSubject = CurrentValueSubject<SearchViewState, Never>(.idle)
private let errorSubject = PassthroughSubject<Error, Never>()
/// 검색/페이징이 실패했을 때 화면을 되돌릴 상태
private var lastStableState: SearchViewState = .idle
init(
searchUsersUseCase: any SearchUsersUseCase,
fetchProfileUseCase: any FetchProfileUseCase,
actions: FeatureSearchCoordinatorActions?
) {
self.searchUsersUseCase = searchUsersUseCase
self.fetchProfileUseCase = fetchProfileUseCase
self.actions = actions
}
func transform(
input: SearchUserViewModelInput
) -> SearchUserViewModelOutput {
input.searchUser
.filter { !$0.isEmpty }
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.sink(with: self, in: &cancellables) { owner, query in
owner.searchUsers(query: query)
}
input.loadNextPage
.filter { [weak self] in
self?.paginator.hasNextPage ?? false
}
.sink(with: self, in: &cancellables) { owner, _ in
owner.loadNextPage()
}
input.backToLogin
.sink(with: self, in: &cancellables) { owner, _ in
owner.actions?.searchNeedsLogin()
}
return Output(
state: stateSubject
.removeDuplicates()
.eraseToAnyPublisher(),
error: errorSubject
.eraseToAnyPublisher()
)
}
func profileData(for user: GithubUser) async throws -> Data {
try await fetchProfileUseCase.execute(
imagePath: user.profilePath,
id: user.id
)
}
// MARK: - 상태 전이
private func send(_ state: SearchViewState) {
stateSubject.send(state)
switch state {
case .idle, .empty:
lastStableState = state
case .searching:
break
case .loaded(let users, _):
lastStableState = .loaded(users: users, isPagingNext: false)
}
}
private func searchUsers(query: String) {
loadNextPageTask.cancel()
send(.searching)
searchTask.replace(
with: self,
onError: { owner, error in
// 실패 -> 직전에 보던 화면으로 복귀
owner.send(owner.lastStableState)
owner.errorSubject.send(error)
}
) { owner in
let firstPage = owner.paginator.begin(query: query)
let page = try await owner.searchUsersUseCase.execute(
query: query,
page: firstPage,
perPage: owner.paginator.perPage
)
guard !Task.isCancelled else { return }
owner.paginator.apply(page)
owner.send(page.users.isEmpty ? .empty : .loaded(users: page.users, isPagingNext: false))
}
}
private func loadNextPage() {
guard case .loaded(let users, false) = stateSubject.value else { return }
send(.loaded(users: users, isPagingNext: true))
loadNextPageTask.runIfIdle(
with: self,
onError: { owner, error in
owner.send(owner.lastStableState)
owner.errorSubject.send(error)
}
) { owner in
let nextPage = try owner.paginator.nextPage()
let page = try await owner.searchUsersUseCase.execute(
query: owner.paginator.currentQuery ?? "",
page: nextPage,
perPage: owner.paginator.perPage
)
guard !Task.isCancelled else { return }
owner.paginator.apply(page)
// 서버가 페이지 경계에서 같은 사용자를 중복으로 줄 수 있다 — id로 방어.
let existingIDs = Set(users.map(\.id))
let uniqueUsers = page.users.filter { !existingIDs.contains($0.id) }
owner.send(.loaded(users: users + uniqueUsers, isPagingNext: false))
}
}
}