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
    }
}
⚠️ **GitHub.com Fallback** ⚠️