import Foundation import Combine final class ContentOverviewViewModel: ObservableObject { private let repository = ContentOverviewRepository() private var subscription = Set() private let pageSize = 20 private var latestRequestId = 0 let type: ContentOverviewType @Published var items = [ContentOverviewItemResponse]() @Published var page = 0 @Published var size = 20 @Published var hasNext = false @Published var hasLoaded = false @Published var isLoading = false @Published var isLoadingNextPage = false @Published var errorMessage = "" @Published var isShowPopup = false var shouldShowEmptyState: Bool { hasLoaded && !isLoading && items.isEmpty } init(type: ContentOverviewType = .newAndHotAudio) { self.type = type } func fetchFirstPageIfNeeded() { guard !hasLoaded else { return } fetchFirstPage() } func fetchFirstPage() { fetchContents(page: 0, isNextPage: false) } func fetchNextPageIfNeeded(currentItem: ContentOverviewItemResponse) { guard items.last?.contentId == currentItem.contentId else { return } fetchNextPageIfPossible() } private func fetchNextPageIfPossible() { guard hasNext, isLoading == false, isLoadingNextPage == false else { return } fetchContents(page: page + 1, isNextPage: true) } private func fetchContents(page requestPage: Int, isNextPage: Bool) { latestRequestId += 1 let requestId = latestRequestId if isNextPage { isLoadingNextPage = true } else { isLoading = true } repository.getContents(type: type, page: requestPage, size: pageSize) .sink { [weak self] result in guard let self else { return } guard requestId == self.latestRequestId else { return } switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) self.applyFailureState() } } receiveValue: { [weak self] response in guard let self else { return } guard requestId == self.latestRequestId else { return } do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponse.self, from: response.data) if let data = decoded.data, decoded.success { self.applySuccessState(data, isNextPage: isNextPage) } else { self.errorMessage = decoded.message ?? I18n.MainContentAll.loadFailedMessage self.isShowPopup = true } } catch { ERROR_LOG(error.localizedDescription) self.errorMessage = I18n.MainContentAll.loadFailedMessage self.isShowPopup = true } self.hasLoaded = true self.isLoading = false self.isLoadingNextPage = false } .store(in: &subscription) } private func applySuccessState(_ data: ContentOverviewPageResponse, isNextPage: Bool) { page = data.page size = data.size hasNext = data.hasNext errorMessage = "" if isNextPage { items.append(contentsOf: data.items) } else { items = data.items } } private func applyFailureState() { errorMessage = I18n.MainContentAll.loadFailedMessage isShowPopup = true hasLoaded = true isLoading = false isLoadingNextPage = false } }