From 75ca7a6043055955c842cf16d0ef40776e173b14 Mon Sep 17 00:00:00 2001 From: alveshmarcos Date: Sat, 23 Oct 2021 22:01:38 -0300 Subject: [PATCH 1/2] Add RxSwift --- .../xcschemes/xcschememanagement.plist | 2 +- .../Repository/GithubMainRepository.swift | 34 +++---- .../Data/Repository/GithubRepository.swift | 13 +-- .../Scenes/Search/SearchCoordinator.swift | 2 +- .../Search/View/SearchViewController.swift | 91 ++++++++++--------- .../Search/ViewModel/SearchViewModel.swift | 48 +++++----- .../Scenes/Search/ViewModel/ViewModel.swift | 8 -- Podfile | 2 + Podfile.lock | 16 +++- 9 files changed, 109 insertions(+), 107 deletions(-) diff --git a/GithubRepo.xcodeproj/xcuserdata/alvesmarcos.xcuserdatad/xcschemes/xcschememanagement.plist b/GithubRepo.xcodeproj/xcuserdata/alvesmarcos.xcuserdatad/xcschemes/xcschememanagement.plist index 36efa3e..e2101dc 100644 --- a/GithubRepo.xcodeproj/xcuserdata/alvesmarcos.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/GithubRepo.xcodeproj/xcuserdata/alvesmarcos.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ GithubRepo.xcscheme_^#shared#^_ orderHint - 4 + 7 diff --git a/GithubRepo/Data/Repository/GithubMainRepository.swift b/GithubRepo/Data/Repository/GithubMainRepository.swift index 18a5996..c275ef5 100644 --- a/GithubRepo/Data/Repository/GithubMainRepository.swift +++ b/GithubRepo/Data/Repository/GithubMainRepository.swift @@ -6,47 +6,37 @@ // import Foundation +import RxRelay +import RxSwift class GithubMainRepository: GithubRepository { private let dataSource: GithubDataSource - private(set) var repositories: [Repository] - private(set) var loading: Bool - private(set) var error: Bool - weak var delegate: GithubRepositoryDelegate? + private(set) var repositories: BehaviorRelay<[Repository]> + private(set) var state: BehaviorRelay init(dataSource: GithubDataSource = RemoteGithubDataSource()) { self.dataSource = dataSource - self.repositories = [] - self.loading = false - self.error = false + self.repositories = BehaviorRelay<[Repository]>(value: []) + self.state = BehaviorRelay(value: .inital) } func handleChangeRepositories(_ repo: [Repository]) { - self.repositories = repo - delegate?.didChangeRepositories(repositories: repo) + self.repositories.accept(repo) } - func handleChangeLoading(_ loading: Bool) { - self.loading = loading - delegate?.didChangeLoading(loading: loading) - } - - func handleChangeError(_ error: Bool) { - self.error = error - delegate?.didChangeError(error: error) + func handleChangeState(_ state: FetchState) { + self.state.accept(state) } func fetchRepositories(with query: String) { - handleChangeLoading(true) + handleChangeState(.loading) self.dataSource.getRepositories(with: query) { [weak self] response in switch response { case .success(let repositoriesResponse): self?.handleChangeRepositories(repositoriesResponse) - self?.handleChangeLoading(false) - self?.handleChangeError(false) + self?.handleChangeState(repositoriesResponse.isEmpty ? .empty: .content) case .failure: - self?.handleChangeLoading(false) - self?.handleChangeError(true) + self?.handleChangeState(.error) } } } diff --git a/GithubRepo/Data/Repository/GithubRepository.swift b/GithubRepo/Data/Repository/GithubRepository.swift index b3a95c3..ddb16b4 100644 --- a/GithubRepo/Data/Repository/GithubRepository.swift +++ b/GithubRepo/Data/Repository/GithubRepository.swift @@ -6,18 +6,15 @@ // import Foundation +import RxRelay -protocol GithubRepositoryDelegate: AnyObject { - func didChangeLoading(loading: Bool) - func didChangeRepositories(repositories: [Repository]) - func didChangeError(error: Bool) +enum FetchState { + case loading, error, content, empty, inital } protocol GithubRepository { - var delegate: GithubRepositoryDelegate? { get set } - var repositories: [Repository] { get } - var loading: Bool { get } - var error: Bool { get } + var repositories: BehaviorRelay<[Repository]> { get } + var state: BehaviorRelay { get } func fetchRepositories(with query: String) } diff --git a/GithubRepo/Scenes/Search/SearchCoordinator.swift b/GithubRepo/Scenes/Search/SearchCoordinator.swift index aedd556..459bfaf 100644 --- a/GithubRepo/Scenes/Search/SearchCoordinator.swift +++ b/GithubRepo/Scenes/Search/SearchCoordinator.swift @@ -10,7 +10,7 @@ import UIKit class SearchCoordinator: NavigationCoordinator { var isCompleted: (() -> Void)? - + var rootViewController: UINavigationController var childCoordinators = [Coordinator]() diff --git a/GithubRepo/Scenes/Search/View/SearchViewController.swift b/GithubRepo/Scenes/Search/View/SearchViewController.swift index 0ca1496..95af4c7 100644 --- a/GithubRepo/Scenes/Search/View/SearchViewController.swift +++ b/GithubRepo/Scenes/Search/View/SearchViewController.swift @@ -5,6 +5,9 @@ // Created by Marcos Alves on 07/09/21. // +import RxCocoa +import RxRelay +import RxSwift import UIKit class SearchViewController: UIViewController { @@ -18,6 +21,7 @@ class SearchViewController: UIViewController { // MARK: - Attributes + private let disposeBag = DisposeBag() private var searchViewModel: SearchViewModel? private var searchTimer: Timer? @@ -48,6 +52,8 @@ class SearchViewController: UIViewController { super.viewDidLoad() registerTableViewCell() + subscribeSearchState() + subscribeTableData() prepareUI() } @@ -55,7 +61,6 @@ class SearchViewController: UIViewController { func bindViewModel(to viewModel: SearchViewModel) { self.searchViewModel = viewModel - self.searchViewModel?.delegate = self } // MARK: - Setup @@ -71,8 +76,6 @@ class SearchViewController: UIViewController { } private func registerTableViewCell() { - tableView?.delegate = self - tableView?.dataSource = self tableView?.register( UINib( nibName: RepositoryTableViewCell.kTableViewCellIdentifier, @@ -85,74 +88,72 @@ class SearchViewController: UIViewController { // MARK: - Helper Methods private func handleSearchInitialState() { + self.spinner.stopAnimating() self.stateView?.isHidden = false self.stateTextView?.text = kInitialSearchStateText self.stateImageView?.image = UIImage(named: "Bookmark") } private func handleSearchEmptyState() { + self.spinner.stopAnimating() self.stateView?.isHidden = false self.stateTextView?.text = kEmptySearchStateText self.stateImageView?.image = UIImage(named: "BookmarkMad") } private func handleSearchErrorState() { + self.spinner.stopAnimating() self.stateView?.isHidden = false self.stateTextView?.text = kErrorSearchStateText self.stateImageView?.image = UIImage(named: "BookmarkError") } -} - -// MARK: - Notifications from View Model -extension SearchViewController: SearchViewModelDelegate { - func onChangeSearchError(error: Bool) { - DispatchQueue.main.async { - if error { - self.handleSearchErrorState() - } - } - } - - func onChangeSearchLoadingState(isLoading: Bool) { - DispatchQueue.main.async { - if isLoading { - self.spinner.startAnimating() - } else { - self.spinner.stopAnimating() - } - } + private func handleSearchLoadingState() { + self.spinner.startAnimating() + self.stateView?.isHidden = true } - func onChangeSearchRepository(repoCellViewModels: [RepositoryCellViewModel]) { - DispatchQueue.main.async { - if repoCellViewModels.isEmpty { - self.handleSearchEmptyState() - } else { - self.stateView?.isHidden = true - self.tableView?.reloadData() - } - } + private func handleSearchContentState() { + self.spinner.stopAnimating() + self.stateView?.isHidden = true } } -// MARK: - Table View Extension - -extension SearchViewController: UITableViewDataSource, UITableViewDelegate { - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return searchViewModel?.repositoryCellViewModels.count ?? 0 +// MARK: - Handle Notifications from View Model + +extension SearchViewController { + func subscribeSearchState() { + searchViewModel?.state.subscribe(onNext: { value -> Void in + DispatchQueue.main.async { [weak self] in + switch value { + case .inital: + self?.handleSearchInitialState() + case .loading: + self?.handleSearchLoadingState() + case .error: + self?.handleSearchErrorState() + case .empty: + self?.handleSearchEmptyState() + case .content: + self?.handleSearchContentState() + } + } + }) + .disposed(by: disposeBag) } - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell( - withIdentifier: RepositoryTableViewCell.kTableViewCellIdentifier - ) as? RepositoryTableViewCell, indexPath.row < self.searchViewModel?.repositoryCellViewModels.count ?? 0 else { - return RepositoryTableViewCell() + func subscribeTableData() { + guard let table = tableView else { + return } - if let repositoryViewModel = self.searchViewModel?.repositoryCellViewModels[indexPath.row] { - cell.setupCell(with: repositoryViewModel) + searchViewModel?.repositoryCellViewModels.bind( + to: table.rx.items( + cellIdentifier: RepositoryTableViewCell.kTableViewCellIdentifier, + cellType: RepositoryTableViewCell.self + )) { _, item, cell in + cell.setupCell(with: item) } - return cell + .disposed(by: disposeBag) } } diff --git a/GithubRepo/Scenes/Search/ViewModel/SearchViewModel.swift b/GithubRepo/Scenes/Search/ViewModel/SearchViewModel.swift index 127961e..5972eb3 100644 --- a/GithubRepo/Scenes/Search/ViewModel/SearchViewModel.swift +++ b/GithubRepo/Scenes/Search/ViewModel/SearchViewModel.swift @@ -6,26 +6,27 @@ // import Foundation +import RxRelay +import RxSwift class SearchViewModel: ViewModelSearching { // MARK: - Attributes + private let disposeBag = DisposeBag() private var githubRepository: GithubRepository - private(set) var repositoryCellViewModels: [RepositoryCellViewModel] - private(set) var loading: Bool - private(set) var error: Bool + private(set) var repositoryCellViewModels: BehaviorRelay<[RepositoryCellViewModel]> + private(set) var state: BehaviorRelay private weak var coordinator: SearchCoordinator? - weak var delegate: SearchViewModelDelegate? // MARK: - Constructors init(coordinator: SearchCoordinator, repository: GithubRepository = GithubMainRepository()) { self.coordinator = coordinator - self.repositoryCellViewModels = [] - self.loading = false - self.error = false + self.repositoryCellViewModels = BehaviorRelay(value: []) + self.state = BehaviorRelay(value: .inital) self.githubRepository = repository - self.githubRepository.delegate = self + + self.bind() } // MARK: - Methods @@ -33,23 +34,28 @@ class SearchViewModel: ViewModelSearching { func fetchRepositories(query: String) { githubRepository.fetchRepositories(with: query) } -} - -// MARK: - Notifications from Repository -extension SearchViewModel: GithubRepositoryDelegate { - func didChangeLoading(loading: Bool) { - self.loading = loading - self.delegate?.onChangeSearchLoadingState(isLoading: loading) + private func bind() { + githubRepository.state.subscribe(onNext: { + self.onChangeState(state: $0) + }) + .disposed(by: disposeBag) + githubRepository.repositories.subscribe(onNext: { + self.onChangeRepositories(repos: $0) + }) + .disposed(by: disposeBag) } +} + +// MARK: - Handle Notifications from Repository - func didChangeRepositories(repositories: [Repository]) { - self.repositoryCellViewModels = repositories.map { RepositoryCellViewModel(repository: $0) } - self.delegate?.onChangeSearchRepository(repoCellViewModels: self.repositoryCellViewModels) +extension SearchViewModel { + private func onChangeState(state: FetchState) { + self.state.accept(state) } - func didChangeError(error: Bool) { - self.error = error - self.delegate?.onChangeSearchError(error: error) + private func onChangeRepositories(repos: [Repository]) { + let repositoriesCell = repos.map { RepositoryCellViewModel(repository: $0) } + self.repositoryCellViewModels.accept(repositoriesCell) } } diff --git a/GithubRepo/Scenes/Search/ViewModel/ViewModel.swift b/GithubRepo/Scenes/Search/ViewModel/ViewModel.swift index 9c1746c..ed5c89c 100644 --- a/GithubRepo/Scenes/Search/ViewModel/ViewModel.swift +++ b/GithubRepo/Scenes/Search/ViewModel/ViewModel.swift @@ -7,14 +7,6 @@ import Foundation -protocol SearchViewModelDelegate: AnyObject { - func onChangeSearchLoadingState(isLoading: Bool) - func onChangeSearchRepository(repoCellViewModels: [RepositoryCellViewModel]) - func onChangeSearchError(error: Bool) -} - protocol ViewModelSearching { - var delegate: SearchViewModelDelegate? { get set } - func fetchRepositories(query: String) } diff --git a/Podfile b/Podfile index f33d5e9..d830547 100644 --- a/Podfile +++ b/Podfile @@ -9,4 +9,6 @@ target 'GithubRepo' do pod 'Alamofire' pod 'Kingfisher' pod 'SwiftLint' + pod 'RxSwift' + pod 'RxCocoa' end diff --git a/Podfile.lock b/Podfile.lock index b9655dc..e051f49 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,24 +1,38 @@ PODS: - Alamofire (5.4.3) - Kingfisher (6.3.1) + - RxCocoa (6.2.0): + - RxRelay (= 6.2.0) + - RxSwift (= 6.2.0) + - RxRelay (6.2.0): + - RxSwift (= 6.2.0) + - RxSwift (6.2.0) - SwiftLint (0.44.0) DEPENDENCIES: - Alamofire - Kingfisher + - RxCocoa + - RxSwift - SwiftLint SPEC REPOS: trunk: - Alamofire - Kingfisher + - RxCocoa + - RxRelay + - RxSwift - SwiftLint SPEC CHECKSUMS: Alamofire: e447a2774a40c996748296fa2c55112fdbbc42f9 Kingfisher: 016c8b653a35add51dd34a3aba36b580041acc74 + RxCocoa: 4baf94bb35f2c0ab31bc0cb9f1900155f646ba42 + RxRelay: e72dbfd157807478401ef1982e1c61c945c94b2f + RxSwift: d356ab7bee873611322f134c5f9ef379fa183d8f SwiftLint: e96c0a8c770c7ebbc4d36c55baf9096bb65c4584 -PODFILE CHECKSUM: 505c925431ff309d40347527ff94565ab266bf2c +PODFILE CHECKSUM: f0341a13a10837fbe4089f8a432af401cc988f07 COCOAPODS: 1.10.1 From 66f196c63df030ca97be509b3ca7afb9ba276cd7 Mon Sep 17 00:00:00 2001 From: alveshmarcos Date: Wed, 27 Oct 2021 01:20:26 -0300 Subject: [PATCH 2/2] Apply PR suggestions --- GithubRepo.xcodeproj/project.pbxproj | 8 ---- .../xcschemes/xcschememanagement.plist | 2 +- GithubRepo/Application/AppCoordinator.swift | 2 +- .../Data/DataSource/GithubDataSource.swift | 3 +- .../DataSource/LocalGithubDataSource.swift | 28 ------------ .../DataSource/RemoteGithubDataSource.swift | 14 +++--- .../Repository/GithubMainRepository.swift | 28 +++++++----- GithubRepo/Data/Services/GithubFetcher.swift | 3 +- .../Data/Services/MockGithubFetcher.swift | 44 ------------------- .../Data/Services/RemoteGithubFetcher.swift | 17 +++---- .../Scenes/About/AboutCoordinator.swift | 2 +- .../Search/View/SearchViewController.swift | 10 ++--- .../Search/ViewModel/SearchViewModel.swift | 34 +++++--------- Podfile | 1 + Podfile.lock | 10 ++++- 15 files changed, 59 insertions(+), 147 deletions(-) delete mode 100644 GithubRepo/Data/DataSource/LocalGithubDataSource.swift delete mode 100644 GithubRepo/Data/Services/MockGithubFetcher.swift diff --git a/GithubRepo.xcodeproj/project.pbxproj b/GithubRepo.xcodeproj/project.pbxproj index 3db703a..99ecbf0 100644 --- a/GithubRepo.xcodeproj/project.pbxproj +++ b/GithubRepo.xcodeproj/project.pbxproj @@ -43,13 +43,11 @@ 0DAFDE3326E71433009FF757 /* SearchViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0DAFDE3126E71433009FF757 /* SearchViewController.xib */; }; 0DB25D6C271A420C00B2968C /* GithubFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB25D6B271A420C00B2968C /* GithubFetcher.swift */; }; 0DB25D6E271A43E500B2968C /* RemoteGithubFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB25D6D271A43E500B2968C /* RemoteGithubFetcher.swift */; }; - 0DB25D70271A47D300B2968C /* MockGithubFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB25D6F271A47D300B2968C /* MockGithubFetcher.swift */; }; 0DB25D73271A4DF500B2968C /* DataMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB25D72271A4DF500B2968C /* DataMapper.swift */; }; 0DB25D75271A517700B2968C /* RepositoryResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB25D74271A517700B2968C /* RepositoryResponse.swift */; }; 0DB25D77271A519600B2968C /* OwnerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB25D76271A519600B2968C /* OwnerResponse.swift */; }; 0DB25D7A271A5A1700B2968C /* GithubDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB25D79271A5A1700B2968C /* GithubDataSource.swift */; }; 0DB25D7C271A5A8F00B2968C /* RemoteGithubDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB25D7B271A5A8F00B2968C /* RemoteGithubDataSource.swift */; }; - 0DB25D7E271A5BDE00B2968C /* LocalGithubDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB25D7D271A5BDE00B2968C /* LocalGithubDataSource.swift */; }; 0DB25D81271A5F5500B2968C /* GithubRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB25D80271A5F5500B2968C /* GithubRepository.swift */; }; 0DB25D83271A623A00B2968C /* GithubMainRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB25D82271A623A00B2968C /* GithubMainRepository.swift */; }; 0DC0572B2708F95A00EE6F5C /* UIColors+RandomColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC0572A2708F95A00EE6F5C /* UIColors+RandomColor.swift */; }; @@ -95,13 +93,11 @@ 0DAFDE3126E71433009FF757 /* SearchViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SearchViewController.xib; sourceTree = ""; }; 0DB25D6B271A420C00B2968C /* GithubFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubFetcher.swift; sourceTree = ""; }; 0DB25D6D271A43E500B2968C /* RemoteGithubFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteGithubFetcher.swift; sourceTree = ""; }; - 0DB25D6F271A47D300B2968C /* MockGithubFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockGithubFetcher.swift; sourceTree = ""; }; 0DB25D72271A4DF500B2968C /* DataMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataMapper.swift; sourceTree = ""; }; 0DB25D74271A517700B2968C /* RepositoryResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryResponse.swift; sourceTree = ""; }; 0DB25D76271A519600B2968C /* OwnerResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OwnerResponse.swift; sourceTree = ""; }; 0DB25D79271A5A1700B2968C /* GithubDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubDataSource.swift; sourceTree = ""; }; 0DB25D7B271A5A8F00B2968C /* RemoteGithubDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteGithubDataSource.swift; sourceTree = ""; }; - 0DB25D7D271A5BDE00B2968C /* LocalGithubDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalGithubDataSource.swift; sourceTree = ""; }; 0DB25D80271A5F5500B2968C /* GithubRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubRepository.swift; sourceTree = ""; }; 0DB25D82271A623A00B2968C /* GithubMainRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubMainRepository.swift; sourceTree = ""; }; 0DC0572A2708F95A00EE6F5C /* UIColors+RandomColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColors+RandomColor.swift"; sourceTree = ""; }; @@ -311,7 +307,6 @@ children = ( 0DB25D6B271A420C00B2968C /* GithubFetcher.swift */, 0DB25D6D271A43E500B2968C /* RemoteGithubFetcher.swift */, - 0DB25D6F271A47D300B2968C /* MockGithubFetcher.swift */, ); path = Services; sourceTree = ""; @@ -332,7 +327,6 @@ children = ( 0DB25D79271A5A1700B2968C /* GithubDataSource.swift */, 0DB25D7B271A5A8F00B2968C /* RemoteGithubDataSource.swift */, - 0DB25D7D271A5BDE00B2968C /* LocalGithubDataSource.swift */, ); path = DataSource; sourceTree = ""; @@ -510,14 +504,12 @@ buildActionMask = 2147483647; files = ( 0DB25D73271A4DF500B2968C /* DataMapper.swift in Sources */, - 0DB25D70271A47D300B2968C /* MockGithubFetcher.swift in Sources */, 0D5E5BB8271B98A5009CD366 /* Coordinator.swift in Sources */, 0DAFDE3226E71433009FF757 /* SearchViewController.swift in Sources */, 0D5C740B2707FAAC0052FADF /* Int+ConventIntoAFriendlyKMAbbr.swift in Sources */, 0D9A650E27050C420061CD8F /* Owner.swift in Sources */, 0D69A9ED271E3042004A973A /* SearchCoordinator.swift in Sources */, 0DB25D77271A519600B2968C /* OwnerResponse.swift in Sources */, - 0DB25D7E271A5BDE00B2968C /* LocalGithubDataSource.swift in Sources */, 0DB25D6E271A43E500B2968C /* RemoteGithubFetcher.swift in Sources */, 0D9A6516270529090061CD8F /* RepositoryTableViewCell.swift in Sources */, 0DAFDE0D26E6EFC3009FF757 /* AppDelegate.swift in Sources */, diff --git a/GithubRepo.xcodeproj/xcuserdata/alvesmarcos.xcuserdatad/xcschemes/xcschememanagement.plist b/GithubRepo.xcodeproj/xcuserdata/alvesmarcos.xcuserdatad/xcschemes/xcschememanagement.plist index e2101dc..93034f3 100644 --- a/GithubRepo.xcodeproj/xcuserdata/alvesmarcos.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/GithubRepo.xcodeproj/xcuserdata/alvesmarcos.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ GithubRepo.xcscheme_^#shared#^_ orderHint - 7 + 8 diff --git a/GithubRepo/Application/AppCoordinator.swift b/GithubRepo/Application/AppCoordinator.swift index b6d1b7d..fe80ee8 100644 --- a/GithubRepo/Application/AppCoordinator.swift +++ b/GithubRepo/Application/AppCoordinator.swift @@ -10,7 +10,7 @@ import UIKit class AppCoordinator: Coordinator { var isCompleted: (() -> Void)? - + var childCoordinators = [Coordinator]() private let window: UIWindow diff --git a/GithubRepo/Data/DataSource/GithubDataSource.swift b/GithubRepo/Data/DataSource/GithubDataSource.swift index 765d119..c8faeb5 100644 --- a/GithubRepo/Data/DataSource/GithubDataSource.swift +++ b/GithubRepo/Data/DataSource/GithubDataSource.swift @@ -6,7 +6,8 @@ // import Foundation +import RxSwift protocol GithubDataSource { - func getRepositories(with query: String, completion: @escaping (Result<[Repository], Error>) -> Void) + func getRepositories(with query: String) -> Single<[Repository]> } diff --git a/GithubRepo/Data/DataSource/LocalGithubDataSource.swift b/GithubRepo/Data/DataSource/LocalGithubDataSource.swift deleted file mode 100644 index 2dfbae7..0000000 --- a/GithubRepo/Data/DataSource/LocalGithubDataSource.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// LocalGithubDataSource.swift -// GithubRepo -// -// Created by Marcos Alves on 15/10/21. -// - -import Foundation - -class MockGithubDataSource: GithubDataSource { - let service: GithubFetcher - - init(service: GithubFetcher = MockGithubFetcher()) { - self.service = service - } - - func getRepositories(with query: String, completion: @escaping (Result<[Repository], Error>) -> Void) { - service.fetchRepositories(with: query) { response in - switch response { - case .success(let searchRepositoriesResponse): - let repositories = searchRepositoriesResponse.items.map { RepositoryResponseMapper.map($0) } - completion(.success(repositories)) - case .failure(let error): - completion(.failure(error)) - } - } - } -} diff --git a/GithubRepo/Data/DataSource/RemoteGithubDataSource.swift b/GithubRepo/Data/DataSource/RemoteGithubDataSource.swift index 9080f6d..9a7364d 100644 --- a/GithubRepo/Data/DataSource/RemoteGithubDataSource.swift +++ b/GithubRepo/Data/DataSource/RemoteGithubDataSource.swift @@ -6,6 +6,7 @@ // import Foundation +import RxSwift class RemoteGithubDataSource: GithubDataSource { private let service: GithubFetcher @@ -14,15 +15,10 @@ class RemoteGithubDataSource: GithubDataSource { self.service = service } - func getRepositories(with query: String, completion: @escaping (Result<[Repository], Error>) -> Void) { - service.fetchRepositories(with: query) { response in - switch response { - case .success(let searchRepositoriesResponse): - let repositories = searchRepositoriesResponse.items.map { RepositoryResponseMapper.map($0) } - completion(.success(repositories)) - case .failure(let error): - completion(.failure(error)) + func getRepositories(with query: String) -> Single<[Repository]> { + return self.service.fetchRepositories(with: query) + .map { response in + response.1.items.map { RepositoryResponseMapper.map($0) } } - } } } diff --git a/GithubRepo/Data/Repository/GithubMainRepository.swift b/GithubRepo/Data/Repository/GithubMainRepository.swift index c275ef5..e3c4805 100644 --- a/GithubRepo/Data/Repository/GithubMainRepository.swift +++ b/GithubRepo/Data/Repository/GithubMainRepository.swift @@ -11,8 +11,10 @@ import RxSwift class GithubMainRepository: GithubRepository { private let dataSource: GithubDataSource - private(set) var repositories: BehaviorRelay<[Repository]> - private(set) var state: BehaviorRelay + private let disposeBag = DisposeBag() + + let repositories: BehaviorRelay<[Repository]> + let state: BehaviorRelay init(dataSource: GithubDataSource = RemoteGithubDataSource()) { self.dataSource = dataSource @@ -29,15 +31,17 @@ class GithubMainRepository: GithubRepository { } func fetchRepositories(with query: String) { - handleChangeState(.loading) - self.dataSource.getRepositories(with: query) { [weak self] response in - switch response { - case .success(let repositoriesResponse): - self?.handleChangeRepositories(repositoriesResponse) - self?.handleChangeState(repositoriesResponse.isEmpty ? .empty: .content) - case .failure: - self?.handleChangeState(.error) - } - } + self.handleChangeState(.loading) + self.dataSource.getRepositories(with: query) + .subscribe( + onSuccess: { [weak self] in + self?.handleChangeRepositories($0) + self?.handleChangeState($0.isEmpty ? .empty: .content) + }, + onFailure: { [weak self] _ in + self?.handleChangeState(.error) + } + ) + .disposed(by: disposeBag) } } diff --git a/GithubRepo/Data/Services/GithubFetcher.swift b/GithubRepo/Data/Services/GithubFetcher.swift index fd746d2..c8bb12a 100644 --- a/GithubRepo/Data/Services/GithubFetcher.swift +++ b/GithubRepo/Data/Services/GithubFetcher.swift @@ -6,7 +6,8 @@ // import Foundation +import RxSwift protocol GithubFetcher { - func fetchRepositories(with query: String, completion: @escaping(Result) -> Void) + func fetchRepositories(with query: String) -> Single<(HTTPURLResponse, SearchRepoResponse)> } diff --git a/GithubRepo/Data/Services/MockGithubFetcher.swift b/GithubRepo/Data/Services/MockGithubFetcher.swift deleted file mode 100644 index 7d93fdf..0000000 --- a/GithubRepo/Data/Services/MockGithubFetcher.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// LocalGithubFetcher.swift -// GithubRepo -// -// Created by Marcos Alves on 15/10/21. -// - -import Foundation - -struct MockGithubFetcher: GithubFetcher { - func fetchRepositories(with query: String, completion: @escaping (Result) -> Void) { - let searchResponseMock = SearchRepoResponse( - totalCount: 218_807, incompleteResults: false, items: [ - RepositoryResponse( - id: 44_838_949, - name: "swift", - description: "The Swift Programming Language", - language: "C++", - forks: 9_202, - stars: 57_387, - owner: OwnerResponse( - id: 10_639_145, - login: "apple", - avatarUrl: "https://avatars.githubusercontent.com/u/10639145?v=4" - ) - ), - RepositoryResponse( - id: 94_066_125, - name: "Swift", - description: "🥇Swift基础知识大全,🚀Swift学习从简单到复杂,不断地完善与更新, 欢迎Star❤️,欢迎Fork", - language: "C", - forks: 417, - stars: 1_464, - owner: OwnerResponse( - id: 27_724_501, - login: "iOS-Swift-Developers", - avatarUrl: "https://avatars.githubusercontent.com/u/27724501?v=4" - ) - ) - ] - ) - completion(.success(searchResponseMock)) - } -} diff --git a/GithubRepo/Data/Services/RemoteGithubFetcher.swift b/GithubRepo/Data/Services/RemoteGithubFetcher.swift index 0c48ced..b03e60a 100644 --- a/GithubRepo/Data/Services/RemoteGithubFetcher.swift +++ b/GithubRepo/Data/Services/RemoteGithubFetcher.swift @@ -7,25 +7,18 @@ import Alamofire import Foundation +import RxAlamofire +import RxSwift struct GithubFetcherConstants { static let kUrl = "https://api.github.com/search/repositories" } struct RemoteGithubFetcher: GithubFetcher { - func fetchRepositories(with query: String, completion: @escaping(Result) -> Void) { + func fetchRepositories(with query: String) -> Single<(HTTPURLResponse, SearchRepoResponse)> { guard let url = URL(string: GithubFetcherConstants.kUrl) else { - completion(.failure(URLError(.badURL))) - return + return Single.error(URLError(.badURL)) } - AF.request(url, method: .get, parameters: ["q": query]) - .responseDecodable(of: SearchRepoResponse.self) { response in - switch response.result { - case .success(let data): - completion(.success(data)) - case .failure(let error): - completion(.failure(error)) - } - } + return RxAlamofire.requestDecodable(.get, url, parameters: ["q": query]).asSingle() } } diff --git a/GithubRepo/Scenes/About/AboutCoordinator.swift b/GithubRepo/Scenes/About/AboutCoordinator.swift index 7079348..7f28d1d 100644 --- a/GithubRepo/Scenes/About/AboutCoordinator.swift +++ b/GithubRepo/Scenes/About/AboutCoordinator.swift @@ -10,7 +10,7 @@ import UIKit class AboutCoordinator: NavigationCoordinator { var isCompleted: (() -> Void)? - + var rootViewController: UINavigationController var childCoordinators = [Coordinator]() diff --git a/GithubRepo/Scenes/Search/View/SearchViewController.swift b/GithubRepo/Scenes/Search/View/SearchViewController.swift index 95af4c7..5647f6f 100644 --- a/GithubRepo/Scenes/Search/View/SearchViewController.swift +++ b/GithubRepo/Scenes/Search/View/SearchViewController.swift @@ -123,8 +123,9 @@ class SearchViewController: UIViewController { extension SearchViewController { func subscribeSearchState() { - searchViewModel?.state.subscribe(onNext: { value -> Void in - DispatchQueue.main.async { [weak self] in + searchViewModel?.state + .asDriver() + .drive { [weak self] value in switch value { case .inital: self?.handleSearchInitialState() @@ -138,16 +139,15 @@ extension SearchViewController { self?.handleSearchContentState() } } - }) .disposed(by: disposeBag) } func subscribeTableData() { - guard let table = tableView else { + guard let tableView = self.tableView else { return } searchViewModel?.repositoryCellViewModels.bind( - to: table.rx.items( + to: tableView.rx.items( cellIdentifier: RepositoryTableViewCell.kTableViewCellIdentifier, cellType: RepositoryTableViewCell.self )) { _, item, cell in diff --git a/GithubRepo/Scenes/Search/ViewModel/SearchViewModel.swift b/GithubRepo/Scenes/Search/ViewModel/SearchViewModel.swift index 5972eb3..5c9e767 100644 --- a/GithubRepo/Scenes/Search/ViewModel/SearchViewModel.swift +++ b/GithubRepo/Scenes/Search/ViewModel/SearchViewModel.swift @@ -26,7 +26,7 @@ class SearchViewModel: ViewModelSearching { self.state = BehaviorRelay(value: .inital) self.githubRepository = repository - self.bind() + self.bindRepository() } // MARK: - Methods @@ -35,27 +35,15 @@ class SearchViewModel: ViewModelSearching { githubRepository.fetchRepositories(with: query) } - private func bind() { - githubRepository.state.subscribe(onNext: { - self.onChangeState(state: $0) - }) - .disposed(by: disposeBag) - githubRepository.repositories.subscribe(onNext: { - self.onChangeRepositories(repos: $0) - }) - .disposed(by: disposeBag) - } -} - -// MARK: - Handle Notifications from Repository - -extension SearchViewModel { - private func onChangeState(state: FetchState) { - self.state.accept(state) - } - - private func onChangeRepositories(repos: [Repository]) { - let repositoriesCell = repos.map { RepositoryCellViewModel(repository: $0) } - self.repositoryCellViewModels.accept(repositoriesCell) + private func bindRepository() { + githubRepository.state + .bind(to: self.state) + .disposed(by: disposeBag) + githubRepository.repositories + .map({ repos in + repos.map { RepositoryCellViewModel(repository: $0) } + }) + .bind(to: self.repositoryCellViewModels) + .disposed(by: disposeBag) } } diff --git a/Podfile b/Podfile index d830547..02e9ed1 100644 --- a/Podfile +++ b/Podfile @@ -11,4 +11,5 @@ target 'GithubRepo' do pod 'SwiftLint' pod 'RxSwift' pod 'RxCocoa' + pod 'RxAlamofire' end diff --git a/Podfile.lock b/Podfile.lock index e051f49..b4cd567 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,6 +1,11 @@ PODS: - Alamofire (5.4.3) - Kingfisher (6.3.1) + - RxAlamofire (6.1.1): + - RxAlamofire/Core (= 6.1.1) + - RxAlamofire/Core (6.1.1): + - Alamofire (~> 5.4) + - RxSwift (~> 6.0) - RxCocoa (6.2.0): - RxRelay (= 6.2.0) - RxSwift (= 6.2.0) @@ -12,6 +17,7 @@ PODS: DEPENDENCIES: - Alamofire - Kingfisher + - RxAlamofire - RxCocoa - RxSwift - SwiftLint @@ -20,6 +26,7 @@ SPEC REPOS: trunk: - Alamofire - Kingfisher + - RxAlamofire - RxCocoa - RxRelay - RxSwift @@ -28,11 +35,12 @@ SPEC REPOS: SPEC CHECKSUMS: Alamofire: e447a2774a40c996748296fa2c55112fdbbc42f9 Kingfisher: 016c8b653a35add51dd34a3aba36b580041acc74 + RxAlamofire: beb75a1c452d0de225651db4903f5d29d034a620 RxCocoa: 4baf94bb35f2c0ab31bc0cb9f1900155f646ba42 RxRelay: e72dbfd157807478401ef1982e1c61c945c94b2f RxSwift: d356ab7bee873611322f134c5f9ef379fa183d8f SwiftLint: e96c0a8c770c7ebbc4d36c55baf9096bb65c4584 -PODFILE CHECKSUM: f0341a13a10837fbe4089f8a432af401cc988f07 +PODFILE CHECKSUM: a7941cc001e74f124a43f0a2fa494b25445e7d9b COCOAPODS: 1.10.1