세 가지가 전부 투표 게시글 목록 조회와 연관되기 때문에 묶어서 작성하려고 한다.
import Combine
import SwiftUI
final class ConsiderationViewModel: ObservableObject {
@Published var isLoading = true
@Published var error: NetworkError?
@Published var currentVote = 0
private var cancellables: Set<AnyCancellable> = []
private var isLastPage = false
private var page = 0
private var appLoginState: AppLoginState
init(appLoginState: AppLoginState) {
self.appLoginState = appLoginState
}
func fetchPosts(page: Int = 0,
size: Int = 5,
visibilityScope: VisibilityScopeType,
isFirstFetch: Bool = true,
isRefresh: Bool = false) {
appLoginState.serviceRoot.apimanager
.request(.postService(.getPosts(page: page,
size: size,
visibilityScope: visibilityScope.rawValue)),
decodingType: [PostModel].self)
.compactMap(\.data)
.receive(on: DispatchQueue.main)
.sink { completion in
switch completion {
case .finished:
break
case .failure(let failure):
print(failure)
}
} receiveValue: { data in
self.appLoginState.appData.postManager.posts.append(contentsOf: data)
self.isLastPage = data.count < 5
self.isLoading = false
}
.store(in: &cancellables)
}
func fetchMorePosts(_ visibilityScope: VisibilityScopeType) {
guard !isLastPage else { return }
page += 1
fetchPosts(page: page,
visibilityScope: visibilityScope,
isFirstFetch: false)
}
func votePost(postId: Int,
choice: Bool,
index: Int) {
// 투표하기 API 연동
}
// ...
}
import Combine
import SwiftUI
final class VoteListViewModel: ObservableObject {
enum Action {
case loadVotes
case loadMoreVotes
case calculateRatio(voteCount: Int, agreeCount: Int)
case vote(selection: Bool)
}
@Published var isLoading: Bool = true
@Published var currentVote: Int = 0
@Published var votes: [VoteModel] = []
@Published var agreeRatio: Double?
@Published var disagreeRatio: Double?
private var cancellables: Set<AnyCancellable> = []
private var page: Int = 0
private let voteUseCase: VoteUseCaseType
init(voteUseCase: VoteUseCaseType) {
self.voteUseCase = voteUseCase
}
func send(action: Action) {
switch action {
case .loadVotes:
isLoading = true
voteUseCase.loadVotes(page: 0, size: 5, scope: .global)
.sink { [weak self] completion in
self?.isLoading = false
} receiveValue: { [weak self] votes in
self?.isLoading = false
self?.votes = votes
}
.store(in: &cancellables)
case .loadMoreVotes:
page += 1
voteUseCase.loadVotes(page: page, size: 5, scope: .global)
.sink { _ in
} receiveValue: { [weak self] votes in
self?.votes.append(contentsOf: votes)
}
.store(in: &cancellables)
case let .calculateRatio(voteCounts, agreeCount):
// TODO: 비율 계산
case let .vote(selection):
// TODO: 투표하기 API 연동
return
}
}
}
protocol VoteUseCaseType {
func loadVotes(page: Int, size: Int, scope: VisibilityScopeType) -> AnyPublisher<[VoteModel], WoteError>
}
final class VoteUseCase: VoteUseCaseType {
private let voteRepository: VoteRepositoryType
init(voteRepository: VoteRepositoryType) {
self.voteRepository = voteRepository
}
func loadVotes(page: Int, size: Int, scope: VisibilityScopeType) -> AnyPublisher<[VoteModel], WoteError> {
voteRepository.getVotes(page: page, size: size, scope: scope)
.eraseToAnyPublisher()
}
}
final class VoteRepository: VoteRepositoryType {
private let voteDataSource: VoteDataSourceType
init(voteDataSource: VoteDataSourceType) {
self.voteDataSource = voteDataSource
}
func getVotes(page: Int, size: Int, scope: VisibilityScopeType) -> AnyPublisher<[VoteModel], WoteError> {
let requestObject: VoteRequestObject = .init(
page: page,
size: size,
visibilityScope: scope.rawValue
)
return voteDataSource.getVotes(requestObject)
.map { $0.map { $0.toModel() }}
.mapError { WoteError.error($0) }
.eraseToAnyPublisher()
}
}
protocol VoteDataSourceType {
func getVotes(_ object: VoteRequestObject) -> AnyPublisher<[PostResponseObject], APIError>
}
final class VoteDataSource: VoteDataSourceType {
private let provider = MoyaProvider<VoteAPI>()
func getVotes(_ object: VoteRequestObject) -> AnyPublisher<[PostResponseObject], APIError> {
provider.requestPublisher(.getVotes(object))
.tryMap {
try JSONDecoder().decode(GeneralResponse<[PostResponseObject]>.self, from: $0.data)
}
.compactMap { $0.data }
.mapError { APIError.error($0) }
.eraseToAnyPublisher()
}
}
View 단에서도 리팩토링을 진행했다.
ForEach(Array(zip(datas.indices,
datas)), id: \.0) { index, item in
VStack(spacing: 0) {
VoteContentCell(viewModel: viewModel,
data: item,
index: index)
nextVoteButton
.padding(.top, 16)
}
.tag(index)
.onAppear {
if (index == datas.count - 2) {
viewModel.fetchMorePosts(visibilityScope)
}
}
리팩토링 전에는 이렇게 투표 목록들을 보여주는 cell에서 viewModel을 직접 주입했었다.
이유는 이 투표 목록에서도 화면 전환이나, 투표를 하는 등의 여러 action이 있었기 때문이었다.
하지만 cell마다 이렇게 viewModel을 넣어주는 것은 많이 비효율적이라고 생각했고,
VoteContentCell(
vote: item,
agreeRatio: viewModel.agreeRatio ?? 0,
disagreeRatio: viewModel.disagreeRatio ?? 0,
voteTapped: {
viewModel.send(action: .vote(selection: $0))
}, detailTapped: {
voteRouter.push(to: VoteTabDestination.voteDetail(postId: item.id))
}
)
위와 같이 클로저를 만들어서 외부의 viewModel을 사용하도록 변경했다.
오늘의 일지 끗