diff --git a/Movie-App-Group-7/Movie-App-Group-7.xcodeproj/project.pbxproj b/Movie-App-Group-7/Movie-App-Group-7.xcodeproj/project.pbxproj index 93222e6..e2d949b 100644 --- a/Movie-App-Group-7/Movie-App-Group-7.xcodeproj/project.pbxproj +++ b/Movie-App-Group-7/Movie-App-Group-7.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + A12FE7232902380800F6166F /* Typealias.swift in Sources */ = {isa = PBXBuildFile; fileRef = A12FE7222902380800F6166F /* Typealias.swift */; }; A14043FC28FFE12F0095BFB6 /* DetailMovieViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = A14043FB28FFE12F0095BFB6 /* DetailMovieViewController.xib */; }; A158A74E2900EF880096F08E /* MovieResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A158A74D2900EF880096F08E /* MovieResponse.swift */; }; A158A7522900EFC90096F08E /* MovieNetworkModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A158A7512900EFC90096F08E /* MovieNetworkModel.swift */; }; @@ -35,6 +36,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + A12FE7222902380800F6166F /* Typealias.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Typealias.swift; sourceTree = ""; }; A14043FB28FFE12F0095BFB6 /* DetailMovieViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DetailMovieViewController.xib; sourceTree = ""; }; A158A74D2900EF880096F08E /* MovieResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieResponse.swift; sourceTree = ""; }; A158A7512900EFC90096F08E /* MovieNetworkModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieNetworkModel.swift; sourceTree = ""; }; @@ -75,6 +77,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + A12FE721290237E300F6166F /* Typealias */ = { + isa = PBXGroup; + children = ( + A12FE7222902380800F6166F /* Typealias.swift */, + ); + path = Typealias; + sourceTree = ""; + }; A14043FD28FFE4D70095BFB6 /* Extension */ = { isa = PBXGroup; children = ( @@ -196,6 +206,7 @@ A15ADAE228FF14B100468D32 /* Utils */ = { isa = PBXGroup; children = ( + A12FE721290237E300F6166F /* Typealias */, A14043FD28FFE4D70095BFB6 /* Extension */, ); path = Utils; @@ -399,6 +410,7 @@ BF20612228B1194E00F0915C /* LoginViewController.swift in Sources */, A15ADADD28FF052300468D32 /* ProfileViewController.swift in Sources */, BF20611E28B1194E00F0915C /* AppDelegate.swift in Sources */, + A12FE7232902380800F6166F /* Typealias.swift in Sources */, A158A765290127050096F08E /* UIView+Extension.swift in Sources */, A15ADAE028FF094400468D32 /* DetailMovieViewController.swift in Sources */, A158A760290113830096F08E /* ProfileViewModel.swift in Sources */, diff --git a/Movie-App-Group-7/Movie-App-Group-7/Core/Data/Local Data Source/User Default/UserDefaultModel.swift b/Movie-App-Group-7/Movie-App-Group-7/Core/Data/Local Data Source/User Default/UserDefaultModel.swift index 0dbc4c1..a05b21f 100644 --- a/Movie-App-Group-7/Movie-App-Group-7/Core/Data/Local Data Source/User Default/UserDefaultModel.swift +++ b/Movie-App-Group-7/Movie-App-Group-7/Core/Data/Local Data Source/User Default/UserDefaultModel.swift @@ -18,10 +18,6 @@ class UserDefaultModel { userDefaults = UserDefaults.standard } - init(userDefaults: UserDefaults) { - self.userDefaults = userDefaults - } - func login(username: String) { userDefaults.set(username, forKey: usernameUserDefaultsKey) userDefaults.set(true, forKey: loggedInUserDefaultsKey) diff --git a/Movie-App-Group-7/Movie-App-Group-7/Core/Data/Remote Data Source/Network/MovieNetworkModel.swift b/Movie-App-Group-7/Movie-App-Group-7/Core/Data/Remote Data Source/Network/MovieNetworkModel.swift index 4b44a09..de7f685 100644 --- a/Movie-App-Group-7/Movie-App-Group-7/Core/Data/Remote Data Source/Network/MovieNetworkModel.swift +++ b/Movie-App-Group-7/Movie-App-Group-7/Core/Data/Remote Data Source/Network/MovieNetworkModel.swift @@ -13,7 +13,7 @@ enum MovieNetworkResult { } enum DetailMovieNetworkResult { - case success(Movie?) + case success(Movie) case failure(String) } @@ -24,64 +24,44 @@ protocol MovieNetworkModel { final class MovieDefaultNetworkModel : MovieNetworkModel { func getMovies(completion: @escaping (MovieNetworkResult) -> ()) { - guard let url = URL(string: "https://ghibliapi.herokuapp.com/films") else { - DispatchQueue.main.async { - completion(.failure("Bad URL")) - } - return - } + let url = URL(string: "https://ghibliapi.herokuapp.com/films")! URLSession.shared.dataTask(with: url) { (data, response, error) in - DispatchQueue.global(qos: .background).async { + DispatchQueue.main.async { guard let data = data else { - DispatchQueue.main.async { - completion(.success([])) - } + completion(.failure("Data not found")) return } - + do { let result = try JSONDecoder().decode([MovieResponse].self, from: data) let response = ObjectMapper().mapMoviesResponseToMoviesDomain(moviesResponse: result) - DispatchQueue.main.async { - completion(.success(response)) - } + completion(.success(response)) } catch { - DispatchQueue.main.async { - completion(.failure("Failed to convert")) - } + completion(.failure("Failed to convert")) } } + + }.resume() } func getMovie(movieId: String, completion: @escaping (DetailMovieNetworkResult) -> ()) { - guard let url = URL(string: "https://ghibliapi.herokuapp.com/films/\(movieId)") else { - DispatchQueue.main.async { - completion(.failure("Bad URL")) - } - return - } + let url = URL(string: "https://ghibliapi.herokuapp.com/films/\(movieId)")! URLSession.shared.dataTask(with: url) { (data, response, error) in - DispatchQueue.global(qos: .background).async { + DispatchQueue.main.async { guard let data = data else { - DispatchQueue.main.async { - completion(.success(nil)) - } + completion(.failure("Data not found")) return } - + do { let result = try JSONDecoder().decode(MovieResponse.self, from: data) let response = ObjectMapper().mapMovieResponseToMovieDomain(movieResponse: result) - DispatchQueue.main.async { - completion(.success(response)) - } + completion(.success(response)) } catch { - DispatchQueue.main.async { - completion(.failure("Failed to convert")) - } + completion(.failure("Failed to convert")) } } }.resume() diff --git a/Movie-App-Group-7/Movie-App-Group-7/UI/Detail Movie/DetailMovieViewController.swift b/Movie-App-Group-7/Movie-App-Group-7/UI/Detail Movie/DetailMovieViewController.swift index 63ee0cc..2b5a138 100644 --- a/Movie-App-Group-7/Movie-App-Group-7/UI/Detail Movie/DetailMovieViewController.swift +++ b/Movie-App-Group-7/Movie-App-Group-7/UI/Detail Movie/DetailMovieViewController.swift @@ -21,6 +21,8 @@ class DetailMovieViewController: UIViewController { @IBOutlet private weak var movieDirectorLabel: UILabel! @IBOutlet private weak var movieDescriptionLabel: UILabel! @IBOutlet private weak var scrollView: UIScrollView! + @IBOutlet private weak var errorLabel: UILabel! + @IBOutlet private weak var contentView: UIView! private let viewModel = DetailMovieViewModel(movieNetworkModel: MovieDefaultNetworkModel()) @@ -29,12 +31,13 @@ class DetailMovieViewController: UIViewController { self.tabBarController?.tabBar.isHidden = true guard let movieId = movieId else { - self.navigationController?.popViewController(animated: true) + self.showError(message: "Movie ID not found", true) return } + bindObservers() + viewModel.retrieveMovie(movieId: movieId) configureScrollView() - retrieveDetailMovie(movieId) } override func viewWillDisappear(_ animated: Bool) { @@ -42,20 +45,20 @@ class DetailMovieViewController: UIViewController { self.tabBarController?.tabBar.isHidden = false } - private func retrieveDetailMovie(_ movieId: String) { + private func bindObservers() { + viewModel.movieObservable = { [weak self] movie in + self?.bindView(movie) + } - viewModel.retrieveMovie(movieId: movieId) { [weak self] result in - switch result { - case .success(let optionalMovie): - guard let movie = optionalMovie else { - self?.navigationController?.popViewController(animated: true) - return - } - self?.bindView(movie) - case .failure(let message): - print(message) + viewModel.showErrorMessage = { [weak self] message in + if let message = message { + self?.showError(message: message, true) } } + + viewModel.showLoading = { [weak self] isLoading in + self?.contentView.isShimmering = isLoading + } } private func configureScrollView() { @@ -77,48 +80,32 @@ class DetailMovieViewController: UIViewController { } private func loadImageUsingKingfisher(url: URL, urlPoster: URL) { - isDownloadingImage(true, movieImageContainerView) - isDownloadingImage(true, movieBannerContainerView) - - movieImageView.kf.setImage( - with: url, - placeholder: nil, - options: nil, - progressBlock: { [weak self] _, _ in - self?.isDownloadingImage(true, self?.movieImageContainerView) - }, - completionHandler: { [weak self] result in - switch result { - case .success(_): - self?.isDownloadingImage(false, self?.movieImageContainerView) - case .failure(let error): - self?.isDownloadingImage(false, self?.movieImageContainerView) - print(error) - } - } - ) + downloadingImage(for: movieImageView, with: url, movieImageContainerView) + downloadingImage(for: movieBannerImageView, with: urlPoster, movieBannerContainerView) + } + + private func downloadingImage(for imageView: UIImageView, with url: URL, _ view: UIView?) { + isDownloadingImage(true, view) - movieBannerImageView.kf.setImage( - with: urlPoster, - placeholder: nil, - options: nil, - progressBlock: { [weak self] _, _ in - self?.isDownloadingImage(true, self?.movieBannerContainerView) - }, - completionHandler: { [weak self] result in - switch result { - case .success(_): - self?.isDownloadingImage(false, self?.movieBannerContainerView) - case .failure(let error): - self?.isDownloadingImage(false, self?.movieBannerContainerView) - print(error) - } + imageView.kf.setImage(with: url) { [weak self] result in + switch result { + case .success(_): + self?.isDownloadingImage(false, view) + case .failure(let error): + self?.isDownloadingImage(false, view) + print(error) } - ) + } } private func isDownloadingImage(_ isDownloading: Bool, _ view: UIView?) { view?.isShimmering = isDownloading view?.backgroundColor = isDownloading ? .gray : .clear } + + private func showError(message: String? = nil, _ isError: Bool = false) { + errorLabel.isHidden = !isError + scrollView.isHidden = isError + errorLabel.text = message + } } diff --git a/Movie-App-Group-7/Movie-App-Group-7/UI/Detail Movie/DetailMovieViewController.xib b/Movie-App-Group-7/Movie-App-Group-7/UI/Detail Movie/DetailMovieViewController.xib index 6584f63..6b1e029 100644 --- a/Movie-App-Group-7/Movie-App-Group-7/UI/Detail Movie/DetailMovieViewController.xib +++ b/Movie-App-Group-7/Movie-App-Group-7/UI/Detail Movie/DetailMovieViewController.xib @@ -10,6 +10,8 @@ + + @@ -29,11 +31,17 @@ + - + @@ -52,7 +60,7 @@ - + @@ -69,7 +77,7 @@ - + @@ -160,5 +170,8 @@ + + + diff --git a/Movie-App-Group-7/Movie-App-Group-7/UI/Detail Movie/ViewModel/DetailMovieViewModel.swift b/Movie-App-Group-7/Movie-App-Group-7/UI/Detail Movie/ViewModel/DetailMovieViewModel.swift index fe4091a..f6abb4a 100644 --- a/Movie-App-Group-7/Movie-App-Group-7/UI/Detail Movie/ViewModel/DetailMovieViewModel.swift +++ b/Movie-App-Group-7/Movie-App-Group-7/UI/Detail Movie/ViewModel/DetailMovieViewModel.swift @@ -7,25 +7,29 @@ import Foundation -enum RetrievingDetailMovieState { - case success(Movie?) - case failure(String) -} - class DetailMovieViewModel { + var showLoading: Observer? + var showErrorMessage: Observer? + var movieObservable: Observer? + private let movieNetworkModel : MovieNetworkModel init(movieNetworkModel: MovieNetworkModel) { self.movieNetworkModel = movieNetworkModel } - func retrieveMovie(movieId: String, completion : @escaping (RetrievingDetailMovieState) -> ()) { - movieNetworkModel.getMovie(movieId: movieId) { result in + func retrieveMovie(movieId: String) { + showLoading?(true) + showErrorMessage?(nil) + + movieNetworkModel.getMovie(movieId: movieId) { [weak self] result in switch result { case .success(let movie): - completion(.success(movie)) + self?.showLoading?(false) + self?.movieObservable?(movie) case .failure(let message): - completion(.failure(message)) + self?.showLoading?(false) + self?.showErrorMessage?(message) } } diff --git a/Movie-App-Group-7/Movie-App-Group-7/UI/Home/HomeViewController.swift b/Movie-App-Group-7/Movie-App-Group-7/UI/Home/HomeViewController.swift index e5f7d2f..65ddd3c 100644 --- a/Movie-App-Group-7/Movie-App-Group-7/UI/Home/HomeViewController.swift +++ b/Movie-App-Group-7/Movie-App-Group-7/UI/Home/HomeViewController.swift @@ -16,31 +16,30 @@ class HomeViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + bindObservers() configureTableView() configureRefreshControl() registerTableViewCell() - retrieveMovies() - self.tabBarController?.tabBar.isHidden = false + viewModel.retrieveMovies() } - private func retrieveMovies(isRefreshing: Bool = false) { - if isRefreshing { - self.movieTableView.refreshControl?.beginRefreshing() + private func bindObservers() { + viewModel.moviesObservable = { [weak self] movies in + self?.showError() + self?.movieTableView.reloadData() } - viewModel.retrieveMovies { [weak self] result in - switch result { - case .failure(_): - if isRefreshing { - self?.movieTableView.refreshControl?.endRefreshing() - } - self?.showError(false) - case .success: - if isRefreshing { - self?.movieTableView.refreshControl?.endRefreshing() - } - self?.showError() - self?.movieTableView.reloadData() + viewModel.showErrorMessage = { [weak self] message in + if let message = message { + self?.showError(true, with: message) + } + } + + viewModel.showLoading = { [weak self] isLoading in + if isLoading { + self?.movieTableView.refreshControl?.beginRefreshing() + } else { + self?.movieTableView.refreshControl?.endRefreshing() } } } @@ -58,15 +57,16 @@ class HomeViewController: UIViewController { } @objc private func pullToRefresh() { - retrieveMovies(isRefreshing: true) + viewModel.retrieveMovies(isRefreshing: true) } private func registerTableViewCell() { movieTableView.register(UINib(nibName: "MovieTableViewCell", bundle: nil), forCellReuseIdentifier: "MovieTableViewCell") } - private func showError(_ isError: Bool = true) { - errorLabel.isHidden = isError + private func showError(_ isError: Bool = false, with message: String? = nil) { + errorLabel.isHidden = !isError + errorLabel.text = message } } diff --git a/Movie-App-Group-7/Movie-App-Group-7/UI/Home/TableViewCell/MovieTableViewCell.swift b/Movie-App-Group-7/Movie-App-Group-7/UI/Home/TableViewCell/MovieTableViewCell.swift index 42d0858..3c6692e 100644 --- a/Movie-App-Group-7/Movie-App-Group-7/UI/Home/TableViewCell/MovieTableViewCell.swift +++ b/Movie-App-Group-7/Movie-App-Group-7/UI/Home/TableViewCell/MovieTableViewCell.swift @@ -25,21 +25,14 @@ class MovieTableViewCell: UITableViewCell { func loadImageUsingKingfisher(url: URL) { isDownloadingImage(true) - movieImageView.kf.setImage( - with: url, - placeholder: nil, - options: nil, - progressBlock: nil, - completionHandler: { [weak self] result in - switch result { - case .success(_): - self?.isDownloadingImage(false) - case .failure(let error): - self?.isDownloadingImage(false) - print(error) - } + movieImageView.kf.setImage(with: url) { [weak self] result in + switch result { + case .success(_): + self?.isDownloadingImage(false) + case .failure(_): + self?.isDownloadingImage(false) } - ) + } } func cancelDownloadingImage() { diff --git a/Movie-App-Group-7/Movie-App-Group-7/UI/Home/ViewModel/HomeViewModel.swift b/Movie-App-Group-7/Movie-App-Group-7/UI/Home/ViewModel/HomeViewModel.swift index d9a4d86..d324763 100644 --- a/Movie-App-Group-7/Movie-App-Group-7/UI/Home/ViewModel/HomeViewModel.swift +++ b/Movie-App-Group-7/Movie-App-Group-7/UI/Home/ViewModel/HomeViewModel.swift @@ -7,12 +7,11 @@ import Foundation -enum RetrievingMoviesState { - case success - case failure(String) -} - class HomeViewModel { + var showLoading: Observer? + var showErrorMessage: Observer? + var moviesObservable: Observer<[Movie]>? + private var movies = [Movie]() private let movieNetworkModel : MovieNetworkModel @@ -20,14 +19,19 @@ class HomeViewModel { self.movieNetworkModel = movieNetworkModel } - func retrieveMovies(completion: @escaping (RetrievingMoviesState) -> ()) { - movieNetworkModel.getMovies { result in + func retrieveMovies(isRefreshing: Bool = false) { + showLoading?(isRefreshing) + showErrorMessage?(nil) + + movieNetworkModel.getMovies { [weak self] result in switch result { case .failure(let message): - completion(.failure(message)) + self?.showLoading?(false) + self?.showErrorMessage?(message) case .success(let movies): - self.movies = movies.shuffled() - completion(.success) + self?.showLoading?(false) + self?.movies = movies.shuffled() + self?.moviesObservable?(movies.shuffled()) } } } diff --git a/Movie-App-Group-7/Movie-App-Group-7/UI/Login/LoginViewController.swift b/Movie-App-Group-7/Movie-App-Group-7/UI/Login/LoginViewController.swift index adfec37..7629859 100644 --- a/Movie-App-Group-7/Movie-App-Group-7/UI/Login/LoginViewController.swift +++ b/Movie-App-Group-7/Movie-App-Group-7/UI/Login/LoginViewController.swift @@ -47,21 +47,7 @@ class LoginViewController: UIViewController { private func showDialog() { let alert = UIAlertController(title: "Failed", message: "Wrong Credentials", preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: { action in - switch action.style { - case .default: - print("default") - - case .cancel: - print("cancel") - - case .destructive: - print("destructive") - - default: - print("default") - } - })) + alert.addAction(UIAlertAction(title: "Ok", style: .default)) self.present(alert, animated: true, completion: nil) } diff --git a/Movie-App-Group-7/Movie-App-Group-7/Utils/Typealias/Typealias.swift b/Movie-App-Group-7/Movie-App-Group-7/Utils/Typealias/Typealias.swift new file mode 100644 index 0000000..b2da480 --- /dev/null +++ b/Movie-App-Group-7/Movie-App-Group-7/Utils/Typealias/Typealias.swift @@ -0,0 +1,10 @@ +// +// Typealias.swift +// Movie-App-Group-7 +// +// Created by Mohammad Azri on 21/10/22. +// + +import Foundation + +typealias Observer = (T) -> Void