Code Demo Seminar - TiepNC/Note GitHub Wiki
Repository
protocol PopularMoviesRepository {
func fetchPopularMoviesUseCase(page: Int,
completion: @escaping (Result<MoviesPage, Error>) -> Void)
}
final class DefaultPopularMoviesRepository: PopularMoviesRepository {
private let persistance: PopularMoviesStorage
private let dataTransferService: DataTransferService
init(persistance: PopularMoviesStorage, dataTransferService: DataTransferService) {
self.persistance = persistance
self.dataTransferService = dataTransferService
}
func fetchPopularMoviesUseCase(page: Int, completion: @escaping (Result<MoviesPage, Error>) -> Void) {
persistance.getResponse(page: page) { [weak self] (result) in
guard let self = self else { return }
if case let .success(moviesResponseCached) = result, let moviesResponse = moviesResponseCached {
completion(.success(moviesResponse.toDomain()))
return
}
let endpoint = APIEndpoints.getTrendingMovies(page: page)
self.dataTransferService.request(with: endpoint) { result in
switch result {
case .success(let moviesResponseDTO):
self.persistance.save(moviesPage: moviesResponseDTO)
completion(.success(moviesResponseDTO.toDomain()))
case .failure(let error):
completion(.failure(error))
}
}
}
}
}
protocol PopularMoviesStorage {
func getResponse(page: Int, completion: @escaping (Result<MoviesResponseDTO?, Error>) -> Void)
func save(moviesPage: MoviesResponseDTO)
}
class MemoryPopularMoviesStorage: PopularMoviesStorage {
private var moviesResponses: [MoviesResponseDTO] = []
func getResponse(page: Int, completion: @escaping (Result<MoviesResponseDTO?, Error>) -> Void) {
completion(.success(moviesResponses.first(where: { $0.page == page })))
}
func save(moviesPage: MoviesResponseDTO) {
moviesResponses.removeAll(where: { $0.page == moviesPage.page })
moviesResponses.append(moviesPage)
}
}
class UserDefaultsPopularMoviesStorage: PopularMoviesStorage {
private var userDefaults: UserDefaults
private let decoder = JSONDecoder()
private let encoder = JSONEncoder()
init(userDefaults: UserDefaults = UserDefaults.standard) {
self.userDefaults = userDefaults
}
func getResponse(page: Int, completion: @escaping (Result<MoviesResponseDTO?, Error>) -> Void) {
if let data = userDefaults.value(forKey: "popularMoviesPage\(page)") as? Data {
do {
let moviesResponses = try decoder.decode(MoviesResponseDTO.self, from: data)
completion(.success(moviesResponses))
} catch {
completion(.failure(error))
}
} else {
completion(.success(nil))
}
}
func save(moviesPage: MoviesResponseDTO) {
if let encoded = try? encoder.encode(moviesPage) {
userDefaults.set(encoded, forKey: "popularMoviesPage\(moviesPage.page)")
}
}
}
protocol FetchPopularMoviesUseCase {
func excute(requestValue: FetchPopularMoviesUseCaseRequestValue,
completion: @escaping (Result<MoviesPage, Error>) -> Void)
}
final class DefaultFetchPopularMoviesUseCase: FetchPopularMoviesUseCase {
private let popularMoviesRepository: PopularMoviesRepository
init(popularMoviesRepository: PopularMoviesRepository) {
self.popularMoviesRepository = popularMoviesRepository
}
func excute(requestValue: FetchPopularMoviesUseCaseRequestValue,
completion: @escaping (Result<MoviesPage, Error>) -> Void) {
popularMoviesRepository.fetchPopularMoviesUseCase(page: requestValue.page, completion: completion)
}
}
struct FetchPopularMoviesUseCaseRequestValue {
let page: Int
}
enum TrendingViewModelLoading {
case fullScreen
case nextPage
}
protocol TrendingViewModelInput {
func loadMovies()
}
protocol TrendingViewModelOutput {
var items: Observable<[Movie]> { get }
var loading: Observable<TrendingViewModelLoading?> { get }
var error: Observable<String> { get }
}
protocol PopularViewModel: TrendingViewModelInput, TrendingViewModelOutput { }
final class DefaultTrendingViewModel: PopularViewModel {
private let fetchPopularMoviesUseCase: FetchPopularMoviesUseCase
private var currentPage = 1
// MARK: - Output
let items: Observable<[Movie]> = Observable([])
let loading: Observable<TrendingViewModelLoading?> = Observable(nil)
let error: Observable<String> = Observable("")
// MARK: - Init
init(fetchPopularMoviesUseCase: FetchPopularMoviesUseCase) {
self.fetchPopularMoviesUseCase = fetchPopularMoviesUseCase
}
}
// MARK: - Input
extension DefaultTrendingViewModel {
func loadMovies() {
loading.value = .fullScreen
fetchPopularMoviesUseCase.excute(requestValue: .init(page: currentPage)) { [ weak self] (result) in
switch result {
case .success(let moviePage):
self?.appendPage(moviePage)
case .failure(let error):
self?.handle(error: error)
}
self?.loading.value = .none
}
}
func appendPage(_ moviesPage: MoviesPage) {
items.value = moviesPage.movies
}
private func handle(error: Error) {
self.error.value = error.localizedDescription
}
}
class PopularViewController: UITableViewController, Alertable {
private var viewModel: PopularViewModel!
static func create(viewModel: PopularViewModel) -> PopularViewController {
let viewController = PopularViewController()
viewController.viewModel = viewModel
return viewController
}
override func viewDidLoad() {
super.viewDidLoad()
bind(to: viewModel)
viewModel.loadMovies()
}
private func bind(to viewModel: PopularViewModel) {
viewModel.items.observe(on: self, observerBlock: { [weak self] _ in self?.tableView.reloadData() })
viewModel.loading.observe(on: self, observerBlock: { [weak self] in self?.updateLoading($0) })
viewModel.error.observe(on: self, observerBlock: { [weak self] error in self?.showError(error) })
}
private func updateLoading(_ loading: TrendingViewModelLoading?) {
LoadingView.hide()
switch loading {
case .fullScreen: LoadingView.show()
case .nextPage: LoadingView.show()
case .none:
break
}
}
private func showError(_ error: String) {
guard !error.isEmpty else { return }
showAlert(message: error)
}
}
extension PopularViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.items.value.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = viewModel.items.value[indexPath.row].title
return cell
}
}