From fd9ba964a4835468475ce323eeca712eb55b6bd4 Mon Sep 17 00:00:00 2001 From: Pietro Caselani Date: Tue, 1 Jan 2019 17:30:30 -0200 Subject: [PATCH 1/4] Change Search module to view state - CouchTrackerCore ok --- CouchTracker.xcodeproj/project.pbxproj | 16 +- .../Result/SearchResultDefaultPresenter.swift | 2 +- .../Search/SearchAPIRepository.swift | 21 -- CouchTrackerCore/Search/SearchContract.swift | 29 +-- .../Search/SearchDefaultPresenter.swift | 38 ++-- CouchTrackerCore/Search/SearchService.swift | 16 +- CouchTrackerCore/Search/SearchStates.swift | 24 ++ .../NetworkMock/TraktProviderMock.swift | 4 + .../Search/SearchAPIRepositoryTest.swift | 40 ---- .../Search/SearchInteractorTest.swift | 50 ++--- .../Search/SearchMocks.swift | 112 ++-------- .../Search/SearchPresenterTest.swift | 107 +++++---- .../Trending/TrendingMocks.swift | 1 - .../Contents.swift | 205 +----------------- .../timeline.xctimeline | 6 - 15 files changed, 170 insertions(+), 501 deletions(-) delete mode 100644 CouchTrackerCore/Search/SearchAPIRepository.swift create mode 100644 CouchTrackerCore/Search/SearchStates.swift delete mode 100644 CouchTrackerCoreTests/Search/SearchAPIRepositoryTest.swift delete mode 100644 CouchTrackerPlayground.playground/timeline.xctimeline diff --git a/CouchTracker.xcodeproj/project.pbxproj b/CouchTracker.xcodeproj/project.pbxproj index 06056db7..9a2a2fd5 100644 --- a/CouchTracker.xcodeproj/project.pbxproj +++ b/CouchTracker.xcodeproj/project.pbxproj @@ -75,7 +75,6 @@ 4F0F5766203D5FA000B86CB8 /* WatchedSeasonEntityRealm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F202F62202F10FE0040185F /* WatchedSeasonEntityRealm.swift */; }; 4F0F5767203D5FA000B86CB8 /* WatchedShowEntityRealm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 645322F62024CF5000449B0A /* WatchedShowEntityRealm.swift */; }; 4F0F5768203D5FA000B86CB8 /* Schedulers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 641D497420222EDC002A4EAD /* Schedulers.swift */; }; - 4F0F5769203D5FA000B86CB8 /* SearchAPIRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D29C3D1F55A96C008E344E /* SearchAPIRepository.swift */; }; 4F0F576B203D5FA000B86CB8 /* SearchContract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648390E41F504EE500911BA0 /* SearchContract.swift */; }; 4F0F576C203D5FA000B86CB8 /* SearchDefaultPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D29C3B1F55A968008E344E /* SearchDefaultPresenter.swift */; }; 4F0F576E203D5FA000B86CB8 /* SearchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D29C3C1F55A968008E344E /* SearchService.swift */; }; @@ -249,7 +248,6 @@ 4F0F58B8203D7B0A00B86CB8 /* WatchedSeasonEntityRealm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F202F62202F10FE0040185F /* WatchedSeasonEntityRealm.swift */; }; 4F0F58B9203D7B0A00B86CB8 /* WatchedShowEntityRealm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 645322F62024CF5000449B0A /* WatchedShowEntityRealm.swift */; }; 4F0F58BA203D7B0A00B86CB8 /* Schedulers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 641D497420222EDC002A4EAD /* Schedulers.swift */; }; - 4F0F58BB203D7B0A00B86CB8 /* SearchAPIRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D29C3D1F55A96C008E344E /* SearchAPIRepository.swift */; }; 4F0F58BD203D7B0A00B86CB8 /* SearchContract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648390E41F504EE500911BA0 /* SearchContract.swift */; }; 4F0F58BE203D7B0A00B86CB8 /* SearchDefaultPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D29C3B1F55A968008E344E /* SearchDefaultPresenter.swift */; }; 4F0F58C0203D7B0A00B86CB8 /* SearchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D29C3C1F55A968008E344E /* SearchService.swift */; }; @@ -358,7 +356,6 @@ 4F458B1C20DD86E3008595D8 /* SyncMovieResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F458B1B20DD86E3008595D8 /* SyncMovieResult.swift */; }; 4F458B1D20DD86E3008595D8 /* SyncMovieResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F458B1B20DD86E3008595D8 /* SyncMovieResult.swift */; }; 4F56336320514AD200923D3D /* ShowManagerDefaultDataSourceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F56336220514AD200923D3D /* ShowManagerDefaultDataSourceTest.swift */; }; - 4F56581C2059944500D3D56B /* SearchAPIRepositoryTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F56581B2059944500D3D56B /* SearchAPIRepositoryTest.swift */; }; 4F566C8820A724800003ACD7 /* ShowProgressListState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F566C8720A724800003ACD7 /* ShowProgressListState.swift */; }; 4F566C8920A724800003ACD7 /* ShowProgressListState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F566C8720A724800003ACD7 /* ShowProgressListState.swift */; }; 4F6102232052791D0079EDBA /* ShowManagerMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6102222052791D0079EDBA /* ShowManagerMocks.swift */; }; @@ -533,6 +530,8 @@ 827EB79E21DAED9300B02001 /* AppConfigurationsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F202F6E202F208F0040185F /* AppConfigurationsStore.swift */; }; 827EB7A021DAEDD800B02001 /* AppConfigurationsViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 827EB79F21DAEDD800B02001 /* AppConfigurationsViewState.swift */; }; 827EB7A121DAEDD800B02001 /* AppConfigurationsViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 827EB79F21DAEDD800B02001 /* AppConfigurationsViewState.swift */; }; + 827EB7A321DBE2C700B02001 /* SearchStates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 827EB7A221DBE2C700B02001 /* SearchStates.swift */; }; + 827EB7A421DBE2C900B02001 /* SearchStates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 827EB7A221DBE2C700B02001 /* SearchStates.swift */; }; 82836E98218EAAFE0037A798 /* ShowsSynchronizerContract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82836E97218EAAFE0037A798 /* ShowsSynchronizerContract.swift */; }; 82836E99218EAAFE0037A798 /* ShowsSynchronizerContract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82836E97218EAAFE0037A798 /* ShowsSynchronizerContract.swift */; }; 82836E9B218EAB580037A798 /* DefaultWatchedShowsSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82836E9A218EAB580037A798 /* DefaultWatchedShowsSynchronizer.swift */; }; @@ -723,7 +722,6 @@ 4F562E05202E7A3400CB1AE0 /* WatchedEpisodeEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchedEpisodeEntity.swift; sourceTree = ""; }; 4F562E09202E7A4300CB1AE0 /* WatchedSeasonEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchedSeasonEntity.swift; sourceTree = ""; }; 4F56336220514AD200923D3D /* ShowManagerDefaultDataSourceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowManagerDefaultDataSourceTest.swift; sourceTree = ""; }; - 4F56581B2059944500D3D56B /* SearchAPIRepositoryTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchAPIRepositoryTest.swift; sourceTree = ""; }; 4F566C8720A724800003ACD7 /* ShowProgressListState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowProgressListState.swift; sourceTree = ""; }; 4F6097DB203B40DB006A2B33 /* AppConfigurationsStateTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigurationsStateTest.swift; sourceTree = ""; }; 4F6097DE203B429A006A2B33 /* ShowProgressFilterTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowProgressFilterTest.swift; sourceTree = ""; }; @@ -982,7 +980,6 @@ 64D29C391F55A957008E344E /* TraktGenreRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TraktGenreRepository.swift; sourceTree = ""; }; 64D29C3B1F55A968008E344E /* SearchDefaultPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchDefaultPresenter.swift; sourceTree = ""; }; 64D29C3C1F55A968008E344E /* SearchService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchService.swift; sourceTree = ""; }; - 64D29C3D1F55A96C008E344E /* SearchAPIRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchAPIRepository.swift; sourceTree = ""; }; 64D43BCA1F605DB0005CDBF7 /* PosterCellDefaultPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterCellDefaultPresenter.swift; sourceTree = ""; }; 64D43BCE1F605DE4005CDBF7 /* PosterCellService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterCellService.swift; sourceTree = ""; }; 64D43BD11F605DFB005CDBF7 /* PosterCellContract.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterCellContract.swift; sourceTree = ""; }; @@ -1030,6 +1027,7 @@ 827BCD3C20EFF3F60080B242 /* ShowOverviewViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowOverviewViewState.swift; sourceTree = ""; }; 827BCD3F20EFF5C30080B242 /* ShowOverviewImagesState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowOverviewImagesState.swift; sourceTree = ""; }; 827EB79F21DAEDD800B02001 /* AppConfigurationsViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigurationsViewState.swift; sourceTree = ""; }; + 827EB7A221DBE2C700B02001 /* SearchStates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchStates.swift; sourceTree = ""; }; 82836E97218EAAFE0037A798 /* ShowsSynchronizerContract.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowsSynchronizerContract.swift; sourceTree = ""; }; 82836E9A218EAB580037A798 /* DefaultWatchedShowsSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultWatchedShowsSynchronizer.swift; sourceTree = ""; }; 82836E9D218EABE20037A798 /* SyncOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncOptions.swift; sourceTree = ""; }; @@ -1370,10 +1368,10 @@ isa = PBXGroup; children = ( 4FE6C608205FCA7300444B12 /* Result */, - 64D29C3D1F55A96C008E344E /* SearchAPIRepository.swift */, 648390E41F504EE500911BA0 /* SearchContract.swift */, 64D29C3B1F55A968008E344E /* SearchDefaultPresenter.swift */, 64D29C3C1F55A968008E344E /* SearchService.swift */, + 827EB7A221DBE2C700B02001 /* SearchStates.swift */, ); path = Search; sourceTree = ""; @@ -2080,7 +2078,6 @@ 648390CF1F504E7500911BA0 /* SearchInteractorTest.swift */, 648390D01F504E7500911BA0 /* SearchMocks.swift */, 6483911F1F50606100911BA0 /* SearchPresenterTest.swift */, - 4F56581B2059944500D3D56B /* SearchAPIRepositoryTest.swift */, DF3BF079D00223397D6BEAA9 /* Result */, ); path = Search; @@ -3100,6 +3097,7 @@ 4F0F5721203D5FA000B86CB8 /* AppConfigurationsMoyaNetwork.swift in Sources */, 4F0F5725203D5FA000B86CB8 /* BaseView.swift in Sources */, 4F0F5726203D5FA000B86CB8 /* BuildConfig.swift in Sources */, + 827EB7A321DBE2C700B02001 /* SearchStates.swift in Sources */, 4F0F5727203D5FA000B86CB8 /* ConfigurationRepository.swift in Sources */, 4F0F5728203D5FA000B86CB8 /* ConfigurationCachedRepository.swift in Sources */, 4F0F572B203D5FA000B86CB8 /* EpisodeEntity.swift in Sources */, @@ -3172,7 +3170,6 @@ 82E1DE6C21D9A530006C8EA8 /* ShowManagerViewState.swift in Sources */, 4F0F5767203D5FA000B86CB8 /* WatchedShowEntityRealm.swift in Sources */, 4F0F5768203D5FA000B86CB8 /* Schedulers.swift in Sources */, - 4F0F5769203D5FA000B86CB8 /* SearchAPIRepository.swift in Sources */, 4F0F576B203D5FA000B86CB8 /* SearchContract.swift in Sources */, 82836EA1218EAD730037A798 /* DefaultWatchedShowSynchronizer.swift in Sources */, 4FEF6ADF2125A00100B7CF63 /* ShowWatchedProgressAPIRepository.swift in Sources */, @@ -3324,7 +3321,6 @@ 4F0F57F8203D61B300B86CB8 /* ShowProgressSortTest.swift in Sources */, 4F0F57F9203D61B300B86CB8 /* ShowsManagerDefaultModuleSetupTest.swift in Sources */, 4F0F57FA203D61B300B86CB8 /* ShowsManagerMocks.swift in Sources */, - 4F56581C2059944500D3D56B /* SearchAPIRepositoryTest.swift in Sources */, 4F0F57FB203D61B300B86CB8 /* ShowsManagerPresenterTest.swift in Sources */, 82D2434621C6DBA40003FC3D /* SynchronizerMocks.swift in Sources */, 4F0F5800203D61B300B86CB8 /* TraktLoginInteractorTest.swift in Sources */, @@ -3392,6 +3388,7 @@ 4F0F5867203D7B0A00B86CB8 /* AppConfigurationsDefaultRepository.swift in Sources */, 4FEF6ADD21259F6900B7CF63 /* ShowWatchedProgressRepository.swift in Sources */, 4F0F5868203D7B0A00B86CB8 /* AppConfigurationsDefaultPresenter.swift in Sources */, + 827EB7A421DBE2C900B02001 /* SearchStates.swift in Sources */, 4F0F586D203D7B0A00B86CB8 /* AppConfigurationsService.swift in Sources */, 4F0F586E203D7B0A00B86CB8 /* AppConfigurationsState.swift in Sources */, 4F0F5870203D7B0A00B86CB8 /* AppConfigurationsUserDefaultsDataSource.swift in Sources */, @@ -3482,7 +3479,6 @@ 82930A20216C1C8500A824F6 /* SynchronizerContract.swift in Sources */, 82E1DE6A21D96E2C006C8EA8 /* ShowsManagerViewState.swift in Sources */, 4F10101920A99C64003CA348 /* ShowsProgressListStateDefaultDataSource.swift in Sources */, - 4F0F58BB203D7B0A00B86CB8 /* SearchAPIRepository.swift in Sources */, 4FEF6AE32125A07900B7CF63 /* EpisodeDetailsRepository.swift in Sources */, 4F0F58BD203D7B0A00B86CB8 /* SearchContract.swift in Sources */, 4F0F58BE203D7B0A00B86CB8 /* SearchDefaultPresenter.swift in Sources */, diff --git a/CouchTrackerCore/Search/Result/SearchResultDefaultPresenter.swift b/CouchTrackerCore/Search/Result/SearchResultDefaultPresenter.swift index f275afe6..e77713a0 100644 --- a/CouchTrackerCore/Search/Result/SearchResultDefaultPresenter.swift +++ b/CouchTrackerCore/Search/Result/SearchResultDefaultPresenter.swift @@ -1,7 +1,7 @@ import RxSwift import TraktSwift -public final class SearchResultDefaultPresenter: SearchResultPresenter, SearchResultOutput { +public final class SearchResultDefaultPresenter: SearchResultPresenter { private weak var view: SearchResultView? private let router: SearchResultRouter private var results = [SearchResult]() diff --git a/CouchTrackerCore/Search/SearchAPIRepository.swift b/CouchTrackerCore/Search/SearchAPIRepository.swift deleted file mode 100644 index f5a4c322..00000000 --- a/CouchTrackerCore/Search/SearchAPIRepository.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Moya -import RxSwift -import TraktSwift - -public final class SearchAPIRepository: SearchRepository { - private let trakt: TraktProvider - private let schedulers: Schedulers - - public init(traktProvider: TraktProvider, schedulers: Schedulers) { - trakt = traktProvider - self.schedulers = schedulers - } - - public func search(query: String, types: [SearchType], page: Int, limit: Int) -> Single<[SearchResult]> { - let target = Search.textQuery(types: types, query: query, page: page, limit: limit) - - return trakt.search.rx.request(target) - .observeOn(schedulers.networkScheduler) - .map([SearchResult].self) - } -} diff --git a/CouchTrackerCore/Search/SearchContract.swift b/CouchTrackerCore/Search/SearchContract.swift index 9042c396..246452b8 100644 --- a/CouchTrackerCore/Search/SearchContract.swift +++ b/CouchTrackerCore/Search/SearchContract.swift @@ -1,38 +1,13 @@ import RxSwift import TraktSwift -public protocol SearchView: BaseView { - var presenter: SearchPresenter! { get set } - - func showHint(message: String) -} - -public protocol SearchResultOutput: class { - func searchChangedTo(state: SearchState) - func handleEmptySearchResult() - func handleSearch(results: [SearchResult]) - func handleError(message: String) -} - public protocol SearchPresenter: class { - init(view: SearchView, interactor: SearchInteractor, resultOutput: SearchResultOutput, types: [SearchType]) - - func viewDidLoad() func search(query: String) func cancelSearch() + func observeSearchState() -> Observable + func observeSearchResults() -> Observable } public protocol SearchInteractor: class { - init(repository: SearchRepository) - - func search(query: String, types: [SearchType]) -> Single<[SearchResult]> -} - -public protocol SearchRepository: class { func search(query: String, types: [SearchType], page: Int, limit: Int) -> Single<[SearchResult]> } - -public enum SearchState { - case searching - case notSearching -} diff --git a/CouchTrackerCore/Search/SearchDefaultPresenter.swift b/CouchTrackerCore/Search/SearchDefaultPresenter.swift index f60a0b52..9aaa9523 100644 --- a/CouchTrackerCore/Search/SearchDefaultPresenter.swift +++ b/CouchTrackerCore/Search/SearchDefaultPresenter.swift @@ -2,41 +2,49 @@ import RxSwift import TraktSwift public final class SearchDefaultPresenter: SearchPresenter { - private weak var view: SearchView? - private weak var output: SearchResultOutput? + private let disposeBag = DisposeBag() + private let searchStateSubject = BehaviorSubject(value: .notSearching) + private let searchResultsSubject = BehaviorSubject(value: .emptyResults) private let interactor: SearchInteractor + private let schedulers: Schedulers private let searchTypes: [SearchType] - private let disposeBag = DisposeBag() + private var currentPage = 0 - public init(view: SearchView, interactor: SearchInteractor, resultOutput: SearchResultOutput, types: [SearchType]) { - self.view = view + public init(interactor: SearchInteractor, types: [SearchType], schedulers: Schedulers = DefaultSchedulers.instance) { self.interactor = interactor - output = resultOutput + self.schedulers = schedulers searchTypes = types } - public func viewDidLoad() { - view?.showHint(message: "Type something".localized) + public func observeSearchState() -> Observable { + return searchStateSubject.distinctUntilChanged() + } + + public func observeSearchResults() -> Observable { + return searchResultsSubject.distinctUntilChanged() } public func search(query: String) { - output?.searchChangedTo(state: .searching) + searchStateSubject.onNext(.searching) - interactor.search(query: query, types: searchTypes) - .observeOn(MainScheduler.instance) + interactor.search(query: query, types: searchTypes, page: currentPage, limit: Defaults.itemsPerPage) + .observeOn(schedulers.mainScheduler) .subscribe(onSuccess: { [weak self] results in + self?.searchStateSubject.onNext(.notSearching) + guard results.count > 0 else { - self?.output?.handleEmptySearchResult() + self?.searchResultsSubject.onNext(.emptyResults) return } - self?.output?.handleSearch(results: results) + self?.searchResultsSubject.onNext(.results(results: results)) + }, onError: { [weak self] error in - self?.output?.handleError(message: error.localizedDescription) + self?.searchStateSubject.onNext(.error(error: error)) }).disposed(by: disposeBag) } public func cancelSearch() { - output?.searchChangedTo(state: .notSearching) + searchStateSubject.onNext(.notSearching) } } diff --git a/CouchTrackerCore/Search/SearchService.swift b/CouchTrackerCore/Search/SearchService.swift index 670075fb..5a788fad 100644 --- a/CouchTrackerCore/Search/SearchService.swift +++ b/CouchTrackerCore/Search/SearchService.swift @@ -2,13 +2,19 @@ import RxSwift import TraktSwift public final class SearchService: SearchInteractor { - private let repository: SearchRepository + private let trakt: TraktProvider + private let schedulers: Schedulers - public init(repository: SearchRepository) { - self.repository = repository + public init(traktProvider: TraktProvider, schedulers: Schedulers = DefaultSchedulers.instance) { + trakt = traktProvider + self.schedulers = schedulers } - public func search(query: String, types: [SearchType]) -> Single<[SearchResult]> { - return repository.search(query: query, types: types, page: 0, limit: 50) + public func search(query: String, types: [SearchType], page: Int, limit: Int) -> Single<[SearchResult]> { + let target = Search.textQuery(types: types, query: query, page: page, limit: limit) + + return trakt.search.rx.request(target) + .observeOn(schedulers.networkScheduler) + .map([SearchResult].self) } } diff --git a/CouchTrackerCore/Search/SearchStates.swift b/CouchTrackerCore/Search/SearchStates.swift new file mode 100644 index 00000000..1d1886e7 --- /dev/null +++ b/CouchTrackerCore/Search/SearchStates.swift @@ -0,0 +1,24 @@ +import TraktSwift + +public enum SearchResultState: Hashable { + case emptyResults + case results(results: [SearchResult]) +} + +public enum SearchState: Hashable { + case searching + case notSearching + case error(error: Error) + + public func hash(into hasher: inout Hasher) { + switch self { + case .searching: hasher.combine("SearchState.searching") + case .notSearching: hasher.combine("SearchState.notSearching") + case let .error(error): hasher.combine("SearchState.error-\(error.localizedDescription)") + } + } + + public static func == (lhs: SearchState, rhs: SearchState) -> Bool { + return lhs.hashValue == rhs.hashValue + } +} diff --git a/CouchTrackerCoreTests/NetworkMock/TraktProviderMock.swift b/CouchTrackerCoreTests/NetworkMock/TraktProviderMock.swift index f865c5db..8d944741 100644 --- a/CouchTrackerCoreTests/NetworkMock/TraktProviderMock.swift +++ b/CouchTrackerCoreTests/NetworkMock/TraktProviderMock.swift @@ -39,4 +39,8 @@ final class TraktProviderMock: TraktProvider, TraktAuthenticationProvider { oauth = oauthURL self.error = error } + + func createProvider(stub: @escaping MoyaProvider.StubClosure) -> MoyaProviderMock { + return MoyaProviderMock(stubClosure: stub) + } } diff --git a/CouchTrackerCoreTests/Search/SearchAPIRepositoryTest.swift b/CouchTrackerCoreTests/Search/SearchAPIRepositoryTest.swift deleted file mode 100644 index a6fc1a16..00000000 --- a/CouchTrackerCoreTests/Search/SearchAPIRepositoryTest.swift +++ /dev/null @@ -1,40 +0,0 @@ -import Moya -import TraktSwift -import XCTest - -@testable import CouchTrackerCore - -final class SearchAPIRepositoryTest: XCTestCase { - func testSearchAPIRepository_whenSearch_shouldInvokeNetwork() { - // Given - let trakt = createTraktProviderMock() - let schedulers = TestSchedulers() - let repository = SearchAPIRepository(traktProvider: trakt, schedulers: schedulers) - - // When - _ = schedulers.testScheduler.start { - repository.search(query: "Batman", types: [SearchType.movie, SearchType.show], page: 2, limit: 30).asObservable() - } - - // Then - guard let provider = trakt.search as? MoyaProviderMock else { - XCTFail("Should be an instance of MoyaProviderMock") - return - } - XCTAssertTrue(provider.requestInvoked) - - guard let target = provider.targetInvoked else { - XCTFail("Should not be nil") - return - } - - if case let .textQuery(params) = target { - XCTAssertEqual(params.types, [SearchType.movie, SearchType.show]) - XCTAssertEqual(params.query, "Batman") - XCTAssertEqual(params.page, 2) - XCTAssertEqual(params.limit, 30) - } else { - XCTFail("Should be a text query") - } - } -} diff --git a/CouchTrackerCoreTests/Search/SearchInteractorTest.swift b/CouchTrackerCoreTests/Search/SearchInteractorTest.swift index b482a04e..c5174cb7 100644 --- a/CouchTrackerCoreTests/Search/SearchInteractorTest.swift +++ b/CouchTrackerCoreTests/Search/SearchInteractorTest.swift @@ -5,13 +5,13 @@ import TraktSwift import XCTest final class SearchInteractorTest: XCTestCase { - private var scheduler: TestScheduler! + private var scheduler: TestSchedulers! private var observer: TestableObserver<[SearchResult]>! override func setUp() { super.setUp() - scheduler = TestScheduler(initialClock: 0) + scheduler = TestSchedulers() observer = scheduler.createObserver([SearchResult].self) } @@ -22,46 +22,32 @@ final class SearchInteractorTest: XCTestCase { } func testSearchInteractor_fetchSuccessEmptyData_andEmitsEmptyDataAndOnCompleted() { - let repository = SearchMocks.Repository() - let interactor = SearchService(repository: repository) + let trakt = createTraktProviderMock() + let searchProviderMock = trakt.search as! MoyaProviderMock - let disposable = interactor.search(query: "Cool movie", types: [SearchType.movie]).asObservable().subscribe(observer) + let interactor = SearchService(traktProvider: trakt, schedulers: scheduler) - scheduler.scheduleAt(500) { disposable.dispose() } - - let expectedEvents: [Recorded>] = [Recorded.next(0, [SearchResult]()), Recorded.completed(0)] - - RXAssertEvents(observer, expectedEvents) - XCTAssertTrue(repository.searchInvoked) - - guard let parameters = repository.searchParameters else { - XCTFail("searchParameters can't be nil") - return + let res = scheduler.start { + interactor.search(query: "empty", types: [SearchType.movie], page: 0, limit: 30).asObservable() } - XCTAssertEqual(parameters.query, "Cool movie") - XCTAssertEqual(parameters.types, [SearchType.movie]) - XCTAssertEqual(parameters.page, 0) - XCTAssertEqual(parameters.limit, 50) + let expectedEvents = [Recorded.next(201, [SearchResult]()), Recorded.completed(202)] + XCTAssertEqual(res.events, expectedEvents) + + XCTAssertTrue(searchProviderMock.requestInvoked) } func testSearchInteractor_fetchSuccessReceivesData_andEmitDataAndOnCompleted() { let results = TraktEntitiesMock.createSearchResultsMock() + let trakt = createTraktProviderMock() + let interactor = SearchService(traktProvider: trakt, schedulers: scheduler) - let interactor = SearchService(repository: SearchMocks.Repository(results: results)) - let disposable = interactor.search(query: "Tron", types: [SearchType.movie]).asObservable().subscribe(observer) - - scheduler.scheduleAt(500) { disposable.dispose() } - - let expectedEvents: [Recorded>] = [Recorded.next(0, results), Recorded.completed(0)] - - RXAssertEvents(observer, expectedEvents) - } + let res = scheduler.start { + interactor.search(query: "Tron", types: [SearchType.movie], page: 0, limit: 30).asObservable() + } - func testSearchInteractor_invokeRepositoryWithCorrectParameters() { - let repository = SearchMocks.Repository() - let interactor = SearchService(repository: repository) + let expectedEvents = [Recorded.next(201, results), Recorded.completed(202)] - _ = interactor.search(query: "Query cool", types: [.list, .movie, .person, .none]) + XCTAssertEqual(res.events, expectedEvents) } } diff --git a/CouchTrackerCoreTests/Search/SearchMocks.swift b/CouchTrackerCoreTests/Search/SearchMocks.swift index 3b4a36c5..1459e4e1 100644 --- a/CouchTrackerCoreTests/Search/SearchMocks.swift +++ b/CouchTrackerCoreTests/Search/SearchMocks.swift @@ -3,105 +3,27 @@ import Moya import RxSwift import TraktSwift -final class SearchMocks { - private init() {} - - final class View: SearchView { - var presenter: SearchPresenter! - var invokedShowHint = false - var invokedShowHintParameters: (message: String, Void)? - - func showHint(message: String) { - invokedShowHint = true - invokedShowHintParameters = (message, ()) - } - } - - final class ResultOutput: SearchResultOutput { - var invokedHandleEmptySearchResult = false - var invokedHandleSearch = false - var invokedHandleSearchParameters: (results: [SearchResult], Void)? - var invokedHandleError = false - var invokedHandleErrorParameters: (message: String, Void)? - var searchChangedToInvoked = false - var searchChangedToInvokedCount = 0 - var searchState = SearchState.notSearching - - func handleEmptySearchResult() { - invokedHandleEmptySearchResult = true - } - - func handleSearch(results: [SearchResult]) { - invokedHandleSearch = true - invokedHandleSearchParameters = (results, ()) - } - - func handleError(message: String) { - invokedHandleError = true - invokedHandleErrorParameters = (message, ()) - } - - func searchChangedTo(state: SearchState) { - searchChangedToInvoked = true - searchChangedToInvokedCount += 1 - searchState = state - } - } - - final class ErrorRepository: SearchRepository { - var searchInvoked = false - var searchParameters: (query: String, types: [SearchType], page: Int, limit: Int)? - private let error: Swift.Error - - init(error: Swift.Error) { - self.error = error - } - - func search(query: String, types: [SearchType], page: Int, limit: Int) -> Single<[SearchResult]> { - searchInvoked = true - searchParameters = (query, types, page, limit) - return Single.error(error) - } - } - - final class Repository: SearchRepository { - private let results: [SearchResult] - var searchInvoked = false - var searchParameters: (query: String, types: [SearchType], page: Int, limit: Int)? - - init(results: [SearchResult] = [SearchResult]()) { - self.results = results - } - - func search(query: String, types: [SearchType], page: Int, limit: Int) -> Single<[SearchResult]> { - searchInvoked = true - searchParameters = (query, types, page, limit) - return Single.just(results) - } - } - +enum SearchMocks { final class Interactor: SearchInteractor { var searchInvoked = false var searchParameters: (query: String, types: [SearchType])? - private let repository: SearchRepository - - init(repository: SearchRepository) { - self.repository = repository - } - - func search(query: String, types: [SearchType]) -> Single<[SearchResult]> { - searchInvoked = true - searchParameters = (query, types) - let lowercaseQuery = query.lowercased() - - return repository.search(query: query, types: [.movie], page: 0, limit: 50).map { results -> [SearchResult] in - results.filter { result -> Bool in - let containsMovie = result.movie?.title?.lowercased().contains(lowercaseQuery) ?? false - let containsShow = result.show?.title?.lowercased().contains(lowercaseQuery) ?? false - return containsMovie || containsShow - } - } + func search(query _: String, types _: [SearchType], page _: Int, limit _: Int) -> Single<[SearchResult]> { + return Single.just([SearchResult]()) } } } + +// func search(query: String, types: [SearchType]) -> Single<[SearchResult]> { +// searchInvoked = true +// searchParameters = (query, types) +// +// let lowercaseQuery = query.lowercased() +// +// return repository.search(query: query, types: [.movie], page: 0, limit: 50).map { results -> [SearchResult] in +// results.filter { result -> Bool in +// let containsMovie = result.movie?.title?.lowercased().contains(lowercaseQuery) ?? false +// let containsShow = result.show?.title?.lowercased().contains(lowercaseQuery) ?? false +// return containsMovie || containsShow +// } +// } diff --git a/CouchTrackerCoreTests/Search/SearchPresenterTest.swift b/CouchTrackerCoreTests/Search/SearchPresenterTest.swift index 54e2720e..eac7d8e3 100644 --- a/CouchTrackerCoreTests/Search/SearchPresenterTest.swift +++ b/CouchTrackerCoreTests/Search/SearchPresenterTest.swift @@ -3,82 +3,79 @@ import TraktSwift import XCTest final class SearchPresenterTest: XCTestCase { - var output: SearchMocks.ResultOutput! - var view: SearchMocks.View! - override func setUp() { super.setUp() - output = SearchMocks.ResultOutput() - view = SearchMocks.View() +// output = SearchMocks.ResultOutput() +// view = SearchMocks.View() } override func tearDown() { - output = nil - view = nil +// output = nil +// view = nil super.tearDown() } func testSearchPresenter_viewDidLoad_updateViewHint() { - let store = SearchMocks.Repository() - let interactor = SearchService(repository: store) - let presenter = SearchDefaultPresenter(view: view, interactor: interactor, resultOutput: output, types: [SearchType.movie]) +// let store = SearchMocks.Repository() +// let interactor = SearchService(repository: store) +// let presenter = SearchDefaultPresenter(view: view, interactor: interactor, resultOutput: output, types: [SearchType.movie]) - presenter.viewDidLoad() +// presenter.viewDidLoad() - XCTAssertTrue(view.invokedShowHint) +// XCTAssertTrue(view.invokedShowHint) } func testSearchPresenter_performSearchSuccess_outputsTheResults() { - let searchResultEntities = TraktEntitiesMock.createSearchResultsMock() - let store = SearchMocks.Repository(results: searchResultEntities) - let interactor = SearchService(repository: store) - let presenter = SearchDefaultPresenter(view: view, interactor: interactor, resultOutput: output, types: [SearchType.movie]) - - presenter.search(query: "Tron") - - XCTAssertTrue(output.invokedHandleSearch) - - if output.invokedHandleSearchParameters?.results == nil { - XCTFail("Parameters can't be nil") - } else { - XCTAssertEqual(output.invokedHandleSearchParameters!.results, searchResultEntities) - } +// let searchResultEntities = TraktEntitiesMock.createSearchResultsMock() +// let store = SearchMocks.Repository(results: searchResultEntities) +// let interactor = SearchService(repository: store) +// let presenter = SearchDefaultPresenter(view: view, interactor: interactor, resultOutput: output, types: [SearchType.movie]) +// +// presenter.search(query: "Tron") +// +// XCTAssertTrue(output.invokedHandleSearch) +// +// if output.invokedHandleSearchParameters?.results == nil { +// XCTFail("Parameters can't be nil") +// } else { +// XCTAssertEqual(output.invokedHandleSearchParameters!.results, searchResultEntities) +// } } func testSearchPresenter_performSearchReceivesNoData_notifyOutput() { - let store = SearchMocks.Repository() - let interactor = SearchService(repository: store) - let presenter = SearchDefaultPresenter(view: view, interactor: interactor, resultOutput: output, types: [SearchType.movie]) - - presenter.search(query: "Tron") - - XCTAssertTrue(output.invokedHandleEmptySearchResult) - } - - func testSearchPresenter_performSearchFailure_outputsErrorMessage() { - let userInfo = [NSLocalizedDescriptionKey: "There is no active connection"] - let error = NSError(domain: "io.github.pietrocaselani.CouchTracker", code: 10, userInfo: userInfo) - let store = SearchMocks.ErrorRepository(error: error) - let interactor = SearchService(repository: store) - let presenter = SearchDefaultPresenter(view: view, interactor: interactor, resultOutput: output, types: [SearchType.movie]) - - presenter.search(query: "Tron") - - let expectedMessage = error.localizedDescription - - XCTAssertTrue(output.invokedHandleError) - XCTAssertEqual(output.invokedHandleErrorParameters?.message, expectedMessage) +// let store = SearchMocks.Repository() +// let interactor = SearchService(repository: store) +// let presenter = SearchDefaultPresenter(view: view, interactor: interactor, resultOutput: output, types: [SearchType.movie]) +// +// presenter.search(query: "Tron") +// +// XCTAssertTrue(output.invokedHandleEmptySearchResult) +// } +// +// func testSearchPresenter_performSearchFailure_outputsErrorMessage() { +// let userInfo = [NSLocalizedDescriptionKey: "There is no active connection"] +// let error = NSError(domain: "io.github.pietrocaselani.CouchTracker", code: 10, userInfo: userInfo) +// let store = SearchMocks.ErrorRepository(error: error) +// let interactor = SearchService(repository: store) +// let presenter = SearchDefaultPresenter(view: view, interactor: interactor, resultOutput: output, types: [SearchType.movie]) +// +// presenter.search(query: "Tron") +// +// let expectedMessage = error.localizedDescription +// +// XCTAssertTrue(output.invokedHandleError) +// XCTAssertEqual(output.invokedHandleErrorParameters?.message, expectedMessage) } func testSearchPresenter_performCancel_notifyOutput() { - let store = SearchMocks.Repository() - let interactor = SearchService(repository: store) - let presenter = SearchDefaultPresenter(view: view, interactor: interactor, resultOutput: output, types: [SearchType.movie]) - - presenter.cancelSearch() - - XCTAssertEqual(output.searchState, SearchState.notSearching) +// let store = SearchMocks.Repository() +// let interactor = SearchService(repository: store) +// let presenter = SearchDefaultPresenter(view: view, interactor: interactor, resultOutput: output, types: [SearchType.movie]) +// +// presenter.cancelSearch() +// +// XCTAssertEqual(output.searchState, SearchState.notSearching) } } diff --git a/CouchTrackerCoreTests/Trending/TrendingMocks.swift b/CouchTrackerCoreTests/Trending/TrendingMocks.swift index 30560cfa..fbfad67c 100644 --- a/CouchTrackerCoreTests/Trending/TrendingMocks.swift +++ b/CouchTrackerCoreTests/Trending/TrendingMocks.swift @@ -7,7 +7,6 @@ let trendingRepositoryMock = TrendingRepositoryMock(traktProvider: createTraktPr final class TrendingViewMock: TrendingViewProtocol { var presenter: TrendingPresenter! - var searchView: SearchView! var invokedShowEmptyView = false func showEmptyView() { diff --git a/CouchTrackerPlayground.playground/Contents.swift b/CouchTrackerPlayground.playground/Contents.swift index 52c8e8c8..8347d360 100644 --- a/CouchTrackerPlayground.playground/Contents.swift +++ b/CouchTrackerPlayground.playground/Contents.swift @@ -3,207 +3,26 @@ import UIKit import PlaygroundSupport -import CouchTrackerApp +//import CouchTrackerApp import CouchTrackerCore import TraktSwift import TMDBSwift import TVDBSwift import Kingfisher import Cartography +import RxSwift -public class ShowEpisodeViewDemo: CouchTrackerApp.View { - public var didTouchOnPreview: (() -> Void)? - public var didTouchOnWatch: (() -> Void)? - - // Public Views - - public let posterImageView: UIImageView = { - let imageView = UIImageView() - imageView.contentMode = UIView.ContentMode.scaleAspectFill - return imageView - }() - - public let previewImageView: UIImageView = { - let imageView = UIImageView() - imageView.contentMode = UIView.ContentMode.scaleAspectFill - imageView.isUserInteractionEnabled = true - imageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapOnPreview))) - return imageView - }() - - public let titleLabel: UILabel = { - let label = UILabel() - label.font = UIFont.boldSystemFont(ofSize: 22) - label.textColor = Colors.Text.primaryTextColor - label.numberOfLines = 0 - return label - }() - - public let overviewLabel: UILabel = { - let label = UILabel() - label.textColor = Colors.Text.secondaryTextColor - label.numberOfLines = 0 - return label - }() - - public let releaseDateLabel: UILabel = { - let label = UILabel() - label.textColor = Colors.Text.secondaryTextColor - return label - }() - - public let watchedAtLabel: UILabel = { - let label = UILabel() - label.textColor = Colors.Text.secondaryTextColor - return label - }() - - public let watchButton: UIButton = { - let button = UIButton() - button.addTarget(self, action: #selector(didTapOnWatch), for: .touchUpInside) - return button - }() - - // Private Views - - private let scrollView: UIScrollView = { - UIScrollView() - }() - - private let posterShadowView: UIView = { - let view = UIView() - view.backgroundColor = .black - view.alpha = 0.75 - return view - }() - - private lazy var contentStackView: UIStackView = { - let subviews = [previewImageView, titleLabel, overviewLabel, - releaseDateLabel, watchedAtLabel, watchButton] - let stackView = UIStackView(arrangedSubviews: subviews) - - let spacing: CGFloat = 20 - - stackView.axis = .vertical - stackView.alignment = .fill - stackView.spacing = spacing - stackView.distribution = .equalSpacing - stackView.layoutMargins = UIEdgeInsets(top: spacing, left: spacing, bottom: spacing, right: spacing) - stackView.isLayoutMarginsRelativeArrangement = true - - return stackView - }() - - // Setup - - public override func initialize() { - super.initialize() - - backgroundColor = Colors.View.background - - addSubview(posterImageView) - addSubview(posterShadowView) - - scrollView.addSubview(contentStackView) - - addSubview(scrollView) - } - - public override func installConstraints() { - super.installConstraints() - - constrain(scrollView, - contentStackView, - posterImageView, - previewImageView, - posterShadowView) { scroll, content, poster, preview, shadow in - scroll.size == scroll.superview!.size - - poster.size == poster.superview!.size - shadow.size == shadow.superview!.size - - preview.height == scroll.superview!.height * 0.27 - - content.width == content.superview!.width - content.top == content.superview!.top + 20 - content.leading == content.superview!.leading - content.bottom == content.superview!.bottom - content.trailing == content.superview!.trailing - } - } - - @objc private func didTapOnPreview() { - didTouchOnPreview?() - } - - @objc private func didTapOnWatch() { - didTouchOnWatch?() - } -} - -final class ViewController: UIViewController { - private var episodeView: ShowEpisodeView { - guard let episodeView = self.view as? ShowEpisodeView else { - preconditionFailure("self.view should be of type ShowEpisodeView") - } - return episodeView - } - - override func loadView() { - view = ShowEpisodeView() - } - - override func viewDidLoad() { - super.viewDidLoad() - - episodeView.didTouchOnPreview = { - print("Preview!!") - } - - episodeView.didTouchOnWatch = { - print("Watch!") - } - - yourFuneral() - } - - private func yourFuneral() { - let episode = decodeJSON(named: "Your Funeral", of: WatchedEpisodeEntity.self) - - let previewLink = "https://image.tmdb.org/t/p/w500/57haJfMOxlkBSFQSK2sjGu7UcA9.jpg" - let posterLink = "https://image.tmdb.org/t/p/w780/pnUh2RawzYaSU8IjG61MT0AMRyf.jpg" - - let formatter = DateFormatter() - formatter.dateStyle = .long - - let releaseDate: String - - if let firstAired = episode.episode.firstAired { - releaseDate = formatter.string(from: firstAired) - } else { - releaseDate = "Not released" - } - - episodeView.posterImageView.kf.setImage(with: posterLink.toURL) - episodeView.previewImageView.kf.setImage(with: previewLink.toURL) - episodeView.titleLabel.text = episode.episode.title - episodeView.overviewLabel.text = episode.episode.overview ?? "No overview" - episodeView.releaseDateLabel.text = releaseDate - episodeView.watchedAtLabel.text = episode.lastWatched?.shortString() ?? "Unwatched" - - let buttonTitle = episode.lastWatched == nil ? "Adicionar ao histórico" : "Remover do histórico" - - episodeView.watchButton.setTitle(buttonTitle, for: .normal) - } -} - -func decodeJSON(named: String, of type: T.Type) -> T { - let path = Bundle.main.path(forResource: named, ofType: "json")! - let data = try! Data(contentsOf: URL(fileURLWithPath: path)) - return try! JSONDecoder().decode(type, from: data) +enum MyCoolState { + case empty + case done + case error(message: String) } +//let subject = BehaviorSubject(value: .empty) +// +//func changeSubject() { +// subject.onNext(.error(message: "Eita!")) +//} -let vc = ViewController() -//let vc = ColorsViewController() +let vc = ColorsViewController() PlaygroundPage.current.liveView = vc diff --git a/CouchTrackerPlayground.playground/timeline.xctimeline b/CouchTrackerPlayground.playground/timeline.xctimeline deleted file mode 100644 index bf468afe..00000000 --- a/CouchTrackerPlayground.playground/timeline.xctimeline +++ /dev/null @@ -1,6 +0,0 @@ - - - - - From 019ad93941ccf9334eaaab8436697feaaf0018f7 Mon Sep 17 00:00:00 2001 From: Pietro Caselani Date: Tue, 1 Jan 2019 19:26:10 -0200 Subject: [PATCH 2/4] Remove Search storyboard - but need to fix the SearchBar positioning --- CouchTracker.xcodeproj/project.pbxproj | 52 +++---- .../Extensions/UICollectionViewLayout.swift | 11 ++ CouchTrackerApp/Search/Search.storyboard | 72 ---------- CouchTrackerApp/Search/SearchBarView.swift | 36 ----- CouchTrackerApp/Search/SearchBarView.xib | 22 --- .../SearchCollectionViewDataSource.swift | 32 +++++ CouchTrackerApp/Search/SearchModule.swift | 17 +-- CouchTrackerApp/Search/SearchView.swift | 69 +++++++++ .../Search/SearchViewController.swift | 136 +++++++++--------- CouchTrackerApp/Search/SearchViewModule.swift | 26 ---- CouchTrackerApp/Search/Storyboard.storyboard | 35 +++++ CouchTrackerApp/Trending/TrendingView.swift | 12 +- .../Search/Result/SearchResultContract.swift | 21 --- .../Result/SearchResultDefaultPresenter.swift | 66 --------- 14 files changed, 242 insertions(+), 365 deletions(-) create mode 100644 CouchTrackerApp/Extensions/UICollectionViewLayout.swift delete mode 100644 CouchTrackerApp/Search/Search.storyboard delete mode 100644 CouchTrackerApp/Search/SearchBarView.swift delete mode 100644 CouchTrackerApp/Search/SearchBarView.xib create mode 100644 CouchTrackerApp/Search/SearchCollectionViewDataSource.swift create mode 100644 CouchTrackerApp/Search/SearchView.swift delete mode 100644 CouchTrackerApp/Search/SearchViewModule.swift create mode 100644 CouchTrackerApp/Search/Storyboard.storyboard delete mode 100644 CouchTrackerCore/Search/Result/SearchResultContract.swift delete mode 100644 CouchTrackerCore/Search/Result/SearchResultDefaultPresenter.swift diff --git a/CouchTracker.xcodeproj/project.pbxproj b/CouchTracker.xcodeproj/project.pbxproj index 9a2a2fd5..c45862c4 100644 --- a/CouchTracker.xcodeproj/project.pbxproj +++ b/CouchTracker.xcodeproj/project.pbxproj @@ -394,10 +394,6 @@ 4FE4BA3A2059E8FA00969AAF /* PosterViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE4BA382059E8FA00969AAF /* PosterViewModelType.swift */; }; 4FE4BA3C2059EA2B00969AAF /* PosterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE4BA3B2059EA2B00969AAF /* PosterViewModel.swift */; }; 4FE4BA3D2059EA2B00969AAF /* PosterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE4BA3B2059EA2B00969AAF /* PosterViewModel.swift */; }; - 4FE6C60A205FCA8800444B12 /* SearchResultContract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE6C609205FCA8800444B12 /* SearchResultContract.swift */; }; - 4FE6C60B205FCA8800444B12 /* SearchResultContract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE6C609205FCA8800444B12 /* SearchResultContract.swift */; }; - 4FE6C60D205FCB8800444B12 /* SearchResultDefaultPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE6C60C205FCB8800444B12 /* SearchResultDefaultPresenter.swift */; }; - 4FE6C60E205FCB8800444B12 /* SearchResultDefaultPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE6C60C205FCB8800444B12 /* SearchResultDefaultPresenter.swift */; }; 4FEF6AD621259E9900B7CF63 /* WatchedShowsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FEF6AD521259E9900B7CF63 /* WatchedShowsRepository.swift */; }; 4FEF6AD721259E9900B7CF63 /* WatchedShowsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FEF6AD521259E9900B7CF63 /* WatchedShowsRepository.swift */; }; 4FEF6AD921259F1900B7CF63 /* WatchedShowsAPIRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FEF6AD821259F1900B7CF63 /* WatchedShowsAPIRepository.swift */; }; @@ -462,12 +458,8 @@ 8204070A21D0043600127F05 /* MoviesManageriOSModuleSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6DC4102015460700AA0DC8 /* MoviesManageriOSModuleSetup.swift */; }; 8204070B21D0043600127F05 /* MoviesManagerModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6DC41C201549C100AA0DC8 /* MoviesManagerModule.swift */; }; 8204070C21D0043600127F05 /* MoviesManagerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6DC4162015477F00AA0DC8 /* MoviesManagerViewController.swift */; }; - 8204070D21D0046400127F05 /* Search.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4FE4BA332059E32A00969AAF /* Search.storyboard */; }; - 8204070E21D0046400127F05 /* SearchBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648391221F507F9500911BA0 /* SearchBarView.swift */; }; - 8204070F21D0046400127F05 /* SearchBarView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4F9CE2C1205A85400000CCD4 /* SearchBarView.xib */; }; 8204071021D0046400127F05 /* SearchModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64DA353F1F50914A005BF4E3 /* SearchModule.swift */; }; 8204071121D0046400127F05 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE4BA312059E31800969AAF /* SearchViewController.swift */; }; - 8204071221D0046400127F05 /* SearchViewModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F9CE2BF205A84A80000CCD4 /* SearchViewModule.swift */; }; 8204071421D0046D00127F05 /* ShowOverviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FFA6B2120EB1983008E0B78 /* ShowOverviewViewController.swift */; }; 8204071521D0046D00127F05 /* ShowOverviewModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FFA6B2220EB1983008E0B78 /* ShowOverviewModule.swift */; }; 8204071721D0047400127F05 /* ShowEpisodeModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 641B9D391F7D7AB300C26D10 /* ShowEpisodeModule.swift */; }; @@ -532,6 +524,10 @@ 827EB7A121DAEDD800B02001 /* AppConfigurationsViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 827EB79F21DAEDD800B02001 /* AppConfigurationsViewState.swift */; }; 827EB7A321DBE2C700B02001 /* SearchStates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 827EB7A221DBE2C700B02001 /* SearchStates.swift */; }; 827EB7A421DBE2C900B02001 /* SearchStates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 827EB7A221DBE2C700B02001 /* SearchStates.swift */; }; + 827EB7A721DBF84400B02001 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 827EB7A621DBF84400B02001 /* SearchView.swift */; }; + 827EB7A921DBF8AF00B02001 /* UICollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 827EB7A821DBF8AF00B02001 /* UICollectionViewLayout.swift */; }; + 827EB7AB21DBFB9800B02001 /* SearchCollectionViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 827EB7AA21DBFB9800B02001 /* SearchCollectionViewDataSource.swift */; }; + 827EB7AF21DC055D00B02001 /* Storyboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 827EB7AE21DC055D00B02001 /* Storyboard.storyboard */; }; 82836E98218EAAFE0037A798 /* ShowsSynchronizerContract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82836E97218EAAFE0037A798 /* ShowsSynchronizerContract.swift */; }; 82836E99218EAAFE0037A798 /* ShowsSynchronizerContract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82836E97218EAAFE0037A798 /* ShowsSynchronizerContract.swift */; }; 82836E9B218EAB580037A798 /* DefaultWatchedShowsSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82836E9A218EAB580037A798 /* DefaultWatchedShowsSynchronizer.swift */; }; @@ -748,8 +744,6 @@ 4F931540203CAADF00631D4A /* ShowEpisodeAPIRepositoryTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowEpisodeAPIRepositoryTest.swift; sourceTree = ""; }; 4F931543203CB34800631D4A /* trakt_sync_addtohistory.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = trakt_sync_addtohistory.json; sourceTree = ""; }; 4F9ABC102052982B00EA6456 /* MoviesManagerDefaultDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesManagerDefaultDataSource.swift; sourceTree = ""; }; - 4F9CE2BF205A84A80000CCD4 /* SearchViewModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModule.swift; sourceTree = ""; }; - 4F9CE2C1205A85400000CCD4 /* SearchBarView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SearchBarView.xib; sourceTree = ""; }; 4FA0C76120326DBA008656B7 /* AppConfigurationsStoreTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigurationsStoreTest.swift; sourceTree = ""; }; 4FA9B5F0209E749800E99D59 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; 4FAA2542205693BE0006735A /* NoCacheMoyaPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoCacheMoyaPlugin.swift; sourceTree = ""; }; @@ -770,11 +764,8 @@ 4FE02E442105E98000196B8E /* GenreRealmTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenreRealmTests.swift; sourceTree = ""; }; 4FE42FB4203B94B80001FF1F /* ShowProgressSortTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowProgressSortTest.swift; sourceTree = ""; }; 4FE4BA312059E31800969AAF /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; - 4FE4BA332059E32A00969AAF /* Search.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Search.storyboard; sourceTree = ""; }; 4FE4BA382059E8FA00969AAF /* PosterViewModelType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterViewModelType.swift; sourceTree = ""; }; 4FE4BA3B2059EA2B00969AAF /* PosterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterViewModel.swift; sourceTree = ""; }; - 4FE6C609205FCA8800444B12 /* SearchResultContract.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultContract.swift; sourceTree = ""; }; - 4FE6C60C205FCB8800444B12 /* SearchResultDefaultPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultDefaultPresenter.swift; sourceTree = ""; }; 4FEF6AD521259E9900B7CF63 /* WatchedShowsRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchedShowsRepository.swift; sourceTree = ""; }; 4FEF6AD821259F1900B7CF63 /* WatchedShowsAPIRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchedShowsAPIRepository.swift; sourceTree = ""; }; 4FEF6ADB21259F6900B7CF63 /* ShowWatchedProgressRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowWatchedProgressRepository.swift; sourceTree = ""; }; @@ -909,7 +900,6 @@ 648390E41F504EE500911BA0 /* SearchContract.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchContract.swift; sourceTree = ""; }; 6483911A1F505F4100911BA0 /* PosterMovieViewModelMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PosterMovieViewModelMapper.swift; sourceTree = ""; }; 6483911F1F50606100911BA0 /* SearchPresenterTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchPresenterTest.swift; sourceTree = ""; }; - 648391221F507F9500911BA0 /* SearchBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBarView.swift; sourceTree = ""; }; 6486879D1F4A3B9C005A0BA8 /* TrendingError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrendingError.swift; sourceTree = ""; }; 6488301F1FB2250C00BB217D /* trakt_sync_watched_shows.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = trakt_sync_watched_shows.json; sourceTree = ""; }; 6496D9A91F56F1CF00B76E06 /* TraktProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraktProviderMock.swift; sourceTree = ""; }; @@ -1028,6 +1018,10 @@ 827BCD3F20EFF5C30080B242 /* ShowOverviewImagesState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowOverviewImagesState.swift; sourceTree = ""; }; 827EB79F21DAEDD800B02001 /* AppConfigurationsViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigurationsViewState.swift; sourceTree = ""; }; 827EB7A221DBE2C700B02001 /* SearchStates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchStates.swift; sourceTree = ""; }; + 827EB7A621DBF84400B02001 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; + 827EB7A821DBF8AF00B02001 /* UICollectionViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UICollectionViewLayout.swift; sourceTree = ""; }; + 827EB7AA21DBFB9800B02001 /* SearchCollectionViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchCollectionViewDataSource.swift; sourceTree = ""; }; + 827EB7AE21DC055D00B02001 /* Storyboard.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Storyboard.storyboard; sourceTree = ""; }; 82836E97218EAAFE0037A798 /* ShowsSynchronizerContract.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowsSynchronizerContract.swift; sourceTree = ""; }; 82836E9A218EAB580037A798 /* DefaultWatchedShowsSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultWatchedShowsSynchronizer.swift; sourceTree = ""; }; 82836E9D218EABE20037A798 /* SyncOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncOptions.swift; sourceTree = ""; }; @@ -1367,7 +1361,6 @@ 4F0F5926203D94C200B86CB8 /* Search */ = { isa = PBXGroup; children = ( - 4FE6C608205FCA7300444B12 /* Result */, 648390E41F504EE500911BA0 /* SearchContract.swift */, 64D29C3B1F55A968008E344E /* SearchDefaultPresenter.swift */, 64D29C3C1F55A968008E344E /* SearchService.swift */, @@ -1676,15 +1669,6 @@ path = PosterCell; sourceTree = ""; }; - 4FE6C608205FCA7300444B12 /* Result */ = { - isa = PBXGroup; - children = ( - 4FE6C609205FCA8800444B12 /* SearchResultContract.swift */, - 4FE6C60C205FCB8800444B12 /* SearchResultDefaultPresenter.swift */, - ); - path = Result; - sourceTree = ""; - }; 4FEF6AFF2128C98B00B7CF63 /* Builders */ = { isa = PBXGroup; children = ( @@ -1720,6 +1704,7 @@ children = ( 64E7EECB201509D70008F488 /* Tabman+CouchTracker.swift */, 64BE63201F4F6893002E55EA /* UIAlertController+Error.swift */, + 827EB7A821DBF8AF00B02001 /* UICollectionViewLayout.swift */, 6406BEDB1F4DC353004661C4 /* UIColor+CouchTracker.swift */, 64BE633B1F4F7ED7002E55EA /* UIViewController+BaseView.swift */, 82F85FDA21D5AE1F006A66B9 /* UIViewControllerExtensions.swift */, @@ -2086,12 +2071,11 @@ 648390E31F504EE500911BA0 /* Search */ = { isa = PBXGroup; children = ( - 4FE4BA332059E32A00969AAF /* Search.storyboard */, - 648391221F507F9500911BA0 /* SearchBarView.swift */, - 4F9CE2C1205A85400000CCD4 /* SearchBarView.xib */, + 827EB7AA21DBFB9800B02001 /* SearchCollectionViewDataSource.swift */, 64DA353F1F50914A005BF4E3 /* SearchModule.swift */, + 827EB7A621DBF84400B02001 /* SearchView.swift */, 4FE4BA312059E31800969AAF /* SearchViewController.swift */, - 4F9CE2BF205A84A80000CCD4 /* SearchViewModule.swift */, + 827EB7AE21DC055D00B02001 /* Storyboard.storyboard */, ); path = Search; sourceTree = ""; @@ -2707,10 +2691,9 @@ buildActionMask = 2147483647; files = ( 8204074021D01A9D00127F05 /* Localizable.strings in Resources */, - 8204070F21D0046400127F05 /* SearchBarView.xib in Resources */, + 827EB7AF21DC055D00B02001 /* Storyboard.storyboard in Resources */, 8204073F21D0187800127F05 /* Assets.xcassets in Resources */, 8204072721D0049800127F05 /* ShowsProgress.storyboard in Resources */, - 8204070D21D0046400127F05 /* Search.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3089,7 +3072,6 @@ 82F85FD821D5ACCF006A66B9 /* StringExtensions.swift in Sources */, 4FE4BA3C2059EA2B00969AAF /* PosterViewModel.swift in Sources */, 4F0F571C203D5FA000B86CB8 /* AppConfigurationsState.swift in Sources */, - 4FE6C60D205FCB8800444B12 /* SearchResultDefaultPresenter.swift in Sources */, 4F458B1C20DD86E3008595D8 /* SyncMovieResult.swift in Sources */, 4F0F571E203D5FA000B86CB8 /* AppConfigurationsUserDefaultsDataSource.swift in Sources */, 4FEF6ADC21259F6900B7CF63 /* ShowWatchedProgressRepository.swift in Sources */, @@ -3130,7 +3112,6 @@ 827EB7A021DAEDD800B02001 /* AppConfigurationsViewState.swift in Sources */, 4F0F5815203D6AC500B86CB8 /* TrendingShowEntity.swift in Sources */, 4FEF6AE82125A19500B7CF63 /* WatchedShowsDataSource.swift in Sources */, - 4FE6C60A205FCA8800444B12 /* SearchResultContract.swift in Sources */, 4F0F573E203D5FA000B86CB8 /* String+Localizable.swift in Sources */, 4F0F5744203D5FA000B86CB8 /* GenreRepository.swift in Sources */, 4F3A594C204EAD1F0061ABB7 /* AppFlowContract.swift in Sources */, @@ -3381,7 +3362,6 @@ 4F0F591B203D7E9900B86CB8 /* MovieDetailsService.swift in Sources */, 82F85FD921D5ACCF006A66B9 /* StringExtensions.swift in Sources */, 4FE4BA3D2059EA2B00969AAF /* PosterViewModel.swift in Sources */, - 4FE6C60E205FCB8800444B12 /* SearchResultDefaultPresenter.swift in Sources */, 82930A1C216C1C8500A824F6 /* DefaultWatchedShowEntityDownloader.swift in Sources */, 4F458B1D20DD86E3008595D8 /* SyncMovieResult.swift in Sources */, 4F0F5866203D7B0A00B86CB8 /* AppConfigurationsContract.swift in Sources */, @@ -3421,7 +3401,6 @@ 827EB7A121DAEDD800B02001 /* AppConfigurationsViewState.swift in Sources */, 82930A1D216C1C8500A824F6 /* DefaultWatchedShowEntitiesDownloader.swift in Sources */, 4FEF6AE92125A19500B7CF63 /* WatchedShowsDataSource.swift in Sources */, - 4FE6C60B205FCA8800444B12 /* SearchResultContract.swift in Sources */, 4F0F5887203D7B0A00B86CB8 /* ImagesViewModelMapper.swift in Sources */, 4F0F5888203D7B0A00B86CB8 /* MovieEntityMapper.swift in Sources */, 4F3A594D204EAD1F0061ABB7 /* AppFlowContract.swift in Sources */, @@ -3617,7 +3596,9 @@ 8204072221D0048900127F05 /* ShowsManagerViewController.swift in Sources */, 8204070A21D0043600127F05 /* MoviesManageriOSModuleSetup.swift in Sources */, 82609F0F21D870C90038EB29 /* ViewCalculations.swift in Sources */, + 827EB7AB21DBFB9800B02001 /* SearchCollectionViewDataSource.swift in Sources */, 8204070C21D0043600127F05 /* MoviesManagerViewController.swift in Sources */, + 827EB7A721DBF84400B02001 /* SearchView.swift in Sources */, 8204072F21D004A500127F05 /* TrendingiOSRouter.swift in Sources */, 8226D9E621D704EC007BB979 /* ShowOverviewView.swift in Sources */, 826F9A0C21D9B02F001A4E05 /* ShowEpisodeView.swift in Sources */, @@ -3630,7 +3611,6 @@ 8204072C21D0049F00127F05 /* TraktLoginModule.swift in Sources */, 8226D9E921D71719007BB979 /* Colors.swift in Sources */, 8204072B21D0049800127F05 /* ShowsProgressViewController.swift in Sources */, - 8204071221D0046400127F05 /* SearchViewModule.swift in Sources */, 8204073E21D0180C00127F05 /* R.generated.swift in Sources */, 8204073221D004A500127F05 /* TrendingCollectionViewDataSource.swift in Sources */, 8204070521D003F200127F05 /* UIViewController+BaseView.swift in Sources */, @@ -3638,10 +3618,10 @@ 8204071521D0046D00127F05 /* ShowOverviewModule.swift in Sources */, 822A02FF21D2354A00440AF8 /* MovieDetailsView.swift in Sources */, 8204071721D0047400127F05 /* ShowEpisodeModule.swift in Sources */, - 8204070E21D0046400127F05 /* SearchBarView.swift in Sources */, 8204070721D0041300127F05 /* MovieDetailsModule.swift in Sources */, 8204073021D004A500127F05 /* TrendingModule.swift in Sources */, 8204073421D004AF00127F05 /* NoCacheMoyaPlugin.swift in Sources */, + 827EB7A921DBF8AF00B02001 /* UICollectionViewLayout.swift in Sources */, 8204073321D004AF00127F05 /* Secrets.swift in Sources */, 8204072921D0049800127F05 /* ShowsProgressModule.swift in Sources */, 8204072D21D0049F00127F05 /* TraktLoginViewController.swift in Sources */, diff --git a/CouchTrackerApp/Extensions/UICollectionViewLayout.swift b/CouchTrackerApp/Extensions/UICollectionViewLayout.swift new file mode 100644 index 00000000..05fd20da --- /dev/null +++ b/CouchTrackerApp/Extensions/UICollectionViewLayout.swift @@ -0,0 +1,11 @@ +extension UICollectionViewLayout { + static var ctDefault: UICollectionViewLayout { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .vertical + layout.itemSize = CGSize(width: 100, height: 180) + layout.sectionInset = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5) + layout.minimumInteritemSpacing = 5 + layout.minimumLineSpacing = 5 + return layout + } +} diff --git a/CouchTrackerApp/Search/Search.storyboard b/CouchTrackerApp/Search/Search.storyboard deleted file mode 100644 index fe32d945..00000000 --- a/CouchTrackerApp/Search/Search.storyboard +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/CouchTrackerApp/Search/SearchBarView.swift b/CouchTrackerApp/Search/SearchBarView.swift deleted file mode 100644 index 2bd3b244..00000000 --- a/CouchTrackerApp/Search/SearchBarView.swift +++ /dev/null @@ -1,36 +0,0 @@ -import CouchTrackerCore - -final class SearchBarView: UISearchBar, SearchView { - var presenter: SearchPresenter! - - func showHint(message: String) { - placeholder = message - } - - override func didMoveToSuperview() { - super.didMoveToSuperview() - - delegate = self - - presenter.viewDidLoad() - } -} - -extension SearchBarView: UISearchBarDelegate { - func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { - searchBar.setShowsCancelButton(true, animated: true) - } - - func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { - guard let query = searchBar.text else { return } - - presenter.search(query: query) - } - - func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { - searchBar.text = nil - searchBar.setShowsCancelButton(false, animated: true) - searchBar.resignFirstResponder() - presenter.cancelSearch() - } -} diff --git a/CouchTrackerApp/Search/SearchBarView.xib b/CouchTrackerApp/Search/SearchBarView.xib deleted file mode 100644 index b1ff2b00..00000000 --- a/CouchTrackerApp/Search/SearchBarView.xib +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/CouchTrackerApp/Search/SearchCollectionViewDataSource.swift b/CouchTrackerApp/Search/SearchCollectionViewDataSource.swift new file mode 100644 index 00000000..01377c91 --- /dev/null +++ b/CouchTrackerApp/Search/SearchCollectionViewDataSource.swift @@ -0,0 +1,32 @@ +import CouchTrackerCore + +final class SearchCollectionViewDataSource: NSObject, UICollectionViewDataSource { + private let interactor: PosterCellInteractor + + var viewModels = [PosterViewModel]() + + init(interactor: PosterCellInteractor) { + self.interactor = interactor + } + + func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -> Int { + return viewModels.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let identifier = PosterAndTitleCell.identifier + + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) + + guard let posterCell = cell as? PosterAndTitleCell else { + Swift.fatalError("cell should be an instance of PosterAndTitleCell") + } + + let viewModel = viewModels[indexPath.row] + let presenter = PosterCellDefaultPresenter(view: posterCell, interactor: interactor, viewModel: viewModel) + + posterCell.presenter = presenter + + return posterCell + } +} diff --git a/CouchTrackerApp/Search/SearchModule.swift b/CouchTrackerApp/Search/SearchModule.swift index 688d72df..bc2ef32d 100644 --- a/CouchTrackerApp/Search/SearchModule.swift +++ b/CouchTrackerApp/Search/SearchModule.swift @@ -5,14 +5,11 @@ final class SearchModule { private init() {} static func setupModule(searchTypes: [SearchType]) -> BaseView { - guard let viewController = R.storyboard.search.searchViewController() else { - Swift.fatalError("Could not instantiate view controller from storyboard") - } - let environment = Environment.instance let tmdb = environment.tmdb let tvdb = environment.tvdb let schedulers = environment.schedulers + let trakt = environment.trakt let configurationRepository = ConfigurationCachedRepository(tmdbProvider: tmdb) @@ -21,16 +18,14 @@ final class SearchModule { cofigurationRepository: configurationRepository, schedulers: schedulers) - viewController.imageRepository = imageRepository + let posterCellInteractor = PosterCellService(imageRepository: imageRepository) - let searchBaseView = SearchViewModule.setupModule(searchTypes: searchTypes, resultOutput: viewController) + let dataSource = SearchCollectionViewDataSource(interactor: posterCellInteractor) - guard let searchView = searchBaseView as? SearchView else { - Swift.fatalError("searchBaseView should be an instance of SearchView") - } + let interactor = SearchService(traktProvider: trakt) - viewController.searchView = searchView + let presenter = SearchDefaultPresenter(interactor: interactor, types: searchTypes) - return viewController + return SearchViewController(presenter: presenter, dataSource: dataSource) } } diff --git a/CouchTrackerApp/Search/SearchView.swift b/CouchTrackerApp/Search/SearchView.swift new file mode 100644 index 00000000..6034bdd2 --- /dev/null +++ b/CouchTrackerApp/Search/SearchView.swift @@ -0,0 +1,69 @@ +import Cartography + +public final class SearchView: View { + public let searchBar = UISearchBar(frame: CGRect.zero) + public let collectionView: UICollectionView + public let emptyView = DefaultEmptyView() + + private lazy var stackView: UIStackView = { + let subviews = [searchBar, collectionView] + let stackView = UIStackView(arrangedSubviews: subviews) + + stackView.axis = .vertical + stackView.alignment = .fill + stackView.distribution = .equalSpacing + // stackView.spacing = spacing + // stackView.layoutMargins = UIEdgeInsets(top: spacing, left: spacing, bottom: spacing, right: spacing) + // stackView.isLayoutMarginsRelativeArrangement = true + // stackView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapOnPoster))) + + return stackView + }() + + init(collectionViewLayout: UICollectionViewLayout = UICollectionViewLayout.ctDefault) { + collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: collectionViewLayout) + super.init() + } + + public required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public required convenience init() { + self.init(collectionViewLayout: UICollectionViewLayout.ctDefault) + } + + public override func initialize() { + addSubview(stackView) + addSubview(emptyView) + +// searchBar.isHidden = true +// searchBar.alpha = 0 +// emptyView.backgroundColor = .red + } + + public override func installConstraints() { + constrain(stackView, emptyView, searchBar) { stack, empty, search in + search.width == search.superview!.width + search.topMargin == search.superview!.topMargin + search.height == 56 + +// collectionView.top == search.bottom +// collectionView.bottom == collectionView.superview!.bottom +// collectionView.left == collectionView.superview!.left +// collectionView.right == collectionView.superview!.right + + stack.size == stack.superview!.size + stack.top == stack.superview!.top + stack.bottom == stack.superview!.bottom + stack.left == stack.superview!.left + stack.right == stack.superview!.right + + empty.top == empty.superview!.top + empty.width == empty.superview!.width + empty.height == empty.superview!.height * 0.5 +// empty.centerX == empty.superview!.centerX +// empty.center == empty.superview!.center + } + } +} diff --git a/CouchTrackerApp/Search/SearchViewController.swift b/CouchTrackerApp/Search/SearchViewController.swift index f37a00b7..9ce12c89 100644 --- a/CouchTrackerApp/Search/SearchViewController.swift +++ b/CouchTrackerApp/Search/SearchViewController.swift @@ -1,92 +1,100 @@ import CouchTrackerCore +import RxSwift import TraktSwift -final class SearchViewController: UIViewController, SearchResultOutput { - @IBOutlet var searchViewContainer: UIView! - @IBOutlet var collectionView: UICollectionView! - @IBOutlet var infoLabel: UILabel! +final class SearchViewController: UIViewController { + private let disposeBag = DisposeBag() + private let presenter: SearchPresenter + private let schedulers: Schedulers + private let dataSource: UICollectionViewDataSource - var imageRepository: ImageRepository! - var searchView: SearchView! + private var searchView: SearchView { + guard let searchView = self.view as? SearchView else { + preconditionFailure("self.view should be an instance of SearchView") + } + + return searchView + } + + init(presenter: SearchPresenter, + dataSource: UICollectionViewDataSource, + schedulers: Schedulers = DefaultSchedulers.instance) { + self.presenter = presenter + self.schedulers = schedulers + self.dataSource = dataSource + super.init(nibName: nil, bundle: nil) + } + + required init?(coder _: NSCoder) { + Swift.fatalError("init(coder:) has not been implemented") + } - private var results = [SearchResult]() + override func loadView() { + view = SearchView() + } override func viewDidLoad() { super.viewDidLoad() - guard imageRepository != nil else { - Swift.fatalError("view loaded without imageRepository") - } - - guard let searchView = searchView as? UIView else { - Swift.fatalError("searchView should be an instance of UIView") - } + adjustForNavigationBar() + extendedLayoutIncludesOpaqueBars = true + configureCollectionView() view.backgroundColor = Colors.View.background - collectionView.backgroundColor = Colors.View.background - - collectionView.register(PosterAndTitleCell.self, forCellWithReuseIdentifier: PosterAndTitleCell.identifier) - searchViewContainer.addSubview(searchView) - collectionView.dataSource = self - } + searchView.emptyView.label.text = "HeyyY!!" - func searchChangedTo(state _: SearchState) {} + presenter.observeSearchResults() + .observeOn(schedulers.mainScheduler) + .subscribe(onNext: { [weak self] resultState in + self?.handleSearchResultState(resultState) + }).disposed(by: disposeBag) - func handleEmptySearchResult() { - infoLabel.text = "No results" - collectionView.isHidden = true - infoLabel.isHidden = false + presenter.observeSearchState() + .observeOn(schedulers.mainScheduler) + .subscribe(onNext: { [weak self] searchState in + self?.handleSearchState(searchState) + }).disposed(by: disposeBag) } - func handleSearch(results: [SearchResult]) { - self.results = results - collectionView.reloadData() - collectionView.isHidden = false - infoLabel.isHidden = true - } + private func configureCollectionView() { + searchView.collectionView.backgroundColor = Colors.View.background - func handleError(message: String) { - infoLabel.text = message - collectionView.isHidden = true - infoLabel.isHidden = false - } -} + searchView.collectionView.register(PosterAndTitleCell.self, + forCellWithReuseIdentifier: PosterAndTitleCell.identifier) -extension SearchViewController: UICollectionViewDataSource { - func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -> Int { - return results.count + searchView.collectionView.dataSource = dataSource + searchView.collectionView.delegate = self } - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let identifier = PosterAndTitleCell.identifier - - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) - - guard let posterCell = cell as? PosterAndTitleCell else { - Swift.fatalError("cell should be an instance of PosterAndTitleCell") - } + private func handleSearchResultState(_: SearchResultState) {} - let result = results[indexPath.row] + private func handleSearchState(_: SearchState) {} - let viewModel: PosterViewModel + private func searchChangedTo(state _: SearchState) {} - switch result.type { - case .movie: - guard let movie = result.movie else { Swift.fatalError("Result type is movie, but there is no movie!") } - viewModel = PosterMovieViewModelMapper.viewModel(for: movie) - case .show: - guard let show = result.show else { Swift.fatalError("Result type is show, but there is no show!") } - viewModel = PosterShowViewModelMapper.viewModel(for: show) - default: - Swift.fatalError("Result type not implemented yet") - } + private func handleEmptySearchResult() { +// infoLabel.text = "No results" +// collectionView.isHidden = true +// infoLabel.isHidden = false + } - let interactor = PosterCellService(imageRepository: imageRepository) - let presenter = PosterCellDefaultPresenter(view: posterCell, interactor: interactor, viewModel: viewModel) + private func handleSearch(results _: [SearchResult]) { +// self.results = results +// collectionView.reloadData() +// collectionView.isHidden = false +// infoLabel.isHidden = true + } - posterCell.presenter = presenter + private func handleError(message _: String) { +// infoLabel.text = message +// collectionView.isHidden = true +// infoLabel.isHidden = false + } +} - return cell +extension SearchViewController: UICollectionViewDelegate { + func collectionView(_: UICollectionView, didSelectItemAt indexPath: IndexPath) { + print("Selected item at \(indexPath.row)") } } diff --git a/CouchTrackerApp/Search/SearchViewModule.swift b/CouchTrackerApp/Search/SearchViewModule.swift deleted file mode 100644 index 893366a0..00000000 --- a/CouchTrackerApp/Search/SearchViewModule.swift +++ /dev/null @@ -1,26 +0,0 @@ -import CouchTrackerCore -import TraktSwift - -final class SearchViewModule { - private init() {} - - static func setupModule(searchTypes: [SearchType], resultOutput: SearchResultOutput) -> BaseView { - guard let searchView = R.nib.searchBarView.firstView(owner: nil) else { - Swift.fatalError("searchView can't be nil") - } - - let trakt = Environment.instance.trakt - let schedulers = Environment.instance.schedulers - - let reopsitory = SearchAPIRepository(traktProvider: trakt, schedulers: schedulers) - let interactor = SearchService(repository: reopsitory) - let presenter = SearchDefaultPresenter(view: searchView, - interactor: interactor, - resultOutput: resultOutput, - types: searchTypes) - - searchView.presenter = presenter - - return searchView - } -} diff --git a/CouchTrackerApp/Search/Storyboard.storyboard b/CouchTrackerApp/Search/Storyboard.storyboard new file mode 100644 index 00000000..c90636b7 --- /dev/null +++ b/CouchTrackerApp/Search/Storyboard.storyboard @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CouchTrackerApp/Trending/TrendingView.swift b/CouchTrackerApp/Trending/TrendingView.swift index ac2e797a..5f776fb5 100644 --- a/CouchTrackerApp/Trending/TrendingView.swift +++ b/CouchTrackerApp/Trending/TrendingView.swift @@ -4,17 +4,7 @@ public final class TrendingView: View { public let collectionView: UICollectionView public let emptyView = DefaultEmptyView() - private static let defaultCollectionViewLayout: UICollectionViewLayout = { - let layout = UICollectionViewFlowLayout() - layout.scrollDirection = .vertical - layout.itemSize = CGSize(width: 100, height: 180) - layout.sectionInset = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5) - layout.minimumInteritemSpacing = 5 - layout.minimumLineSpacing = 5 - return layout - }() - - init(collectionViewLayout: UICollectionViewLayout = TrendingView.defaultCollectionViewLayout) { + init(collectionViewLayout: UICollectionViewLayout = UICollectionViewLayout.ctDefault) { collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: collectionViewLayout) super.init() } diff --git a/CouchTrackerCore/Search/Result/SearchResultContract.swift b/CouchTrackerCore/Search/Result/SearchResultContract.swift deleted file mode 100644 index 50253e42..00000000 --- a/CouchTrackerCore/Search/Result/SearchResultContract.swift +++ /dev/null @@ -1,21 +0,0 @@ -import TraktSwift - -public protocol SearchResultView: class { - var presenter: SearchResultPresenter! { get set } - - func show(viewModels: [PosterViewModel]) - func showSearching() - func showNotSearching() - func showEmptyResults() - func showError(message: String) -} - -public protocol SearchResultRouter: class { - func showDetails(of result: SearchResult) -} - -public protocol SearchResultPresenter: class { - init(view: SearchResultView, router: SearchResultRouter) - - func selectResult(at index: Int) -} diff --git a/CouchTrackerCore/Search/Result/SearchResultDefaultPresenter.swift b/CouchTrackerCore/Search/Result/SearchResultDefaultPresenter.swift deleted file mode 100644 index e77713a0..00000000 --- a/CouchTrackerCore/Search/Result/SearchResultDefaultPresenter.swift +++ /dev/null @@ -1,66 +0,0 @@ -import RxSwift -import TraktSwift - -public final class SearchResultDefaultPresenter: SearchResultPresenter { - private weak var view: SearchResultView? - private let router: SearchResultRouter - private var results = [SearchResult]() - - public init(view: SearchResultView, router: SearchResultRouter) { - self.view = view - self.router = router - } - - public func selectResult(at index: Int) { - let result = results[index] - router.showDetails(of: result) - } - - public func searchChangedTo(state: SearchState) { - guard let view = self.view else { return } - - if state == .searching { - view.showSearching() - } else { - view.showNotSearching() - } - } - - public func handleEmptySearchResult() { - view?.showEmptyResults() - } - - public func handleSearch(results: [SearchResult]) { - self.results = results - - guard let view = self.view else { return } - - let viewModels = results.map(mapToViewModel) - view.show(viewModels: viewModels) - } - - public func handleError(message: String) { - view?.showError(message: message) - } - - private func mapToViewModel(_ result: SearchResult) -> PosterViewModel { - let viewModel: PosterViewModel - - switch result.type { - case .show: - guard let show = result.show else { - Swift.fatalError("Search result is show, but there is no show associated") - } - viewModel = PosterShowViewModelMapper.viewModel(for: show) - case .movie: - guard let movie = result.movie else { - Swift.fatalError("Search result is movie, but there is no movie associated") - } - viewModel = PosterMovieViewModelMapper.viewModel(for: movie) - default: - Swift.fatalError("Search result mapper not implemented yet") - } - - return viewModel - } -} From 139f1b03fff885d4f4d3e3f872df6537074083eb Mon Sep 17 00:00:00 2001 From: Pietro Caselani Date: Wed, 2 Jan 2019 19:48:49 -0200 Subject: [PATCH 3/4] Rataria (workaround) to show SearchBar at correct position --- CouchTrackerApp/Search/SearchView.swift | 30 ++++++++----------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/CouchTrackerApp/Search/SearchView.swift b/CouchTrackerApp/Search/SearchView.swift index 6034bdd2..b0e93735 100644 --- a/CouchTrackerApp/Search/SearchView.swift +++ b/CouchTrackerApp/Search/SearchView.swift @@ -1,10 +1,16 @@ import Cartography public final class SearchView: View { - public let searchBar = UISearchBar(frame: CGRect.zero) public let collectionView: UICollectionView public let emptyView = DefaultEmptyView() + public let searchBar: UISearchBar = { + let searchBar = UISearchBar(frame: CGRect.zero) + searchBar.isUserInteractionEnabled = true + searchBar.barTintColor = Colors.NavigationBar.barTintColor + return searchBar + }() + private lazy var stackView: UIStackView = { let subviews = [searchBar, collectionView] let stackView = UIStackView(arrangedSubviews: subviews) @@ -12,10 +18,6 @@ public final class SearchView: View { stackView.axis = .vertical stackView.alignment = .fill stackView.distribution = .equalSpacing - // stackView.spacing = spacing - // stackView.layoutMargins = UIEdgeInsets(top: spacing, left: spacing, bottom: spacing, right: spacing) - // stackView.isLayoutMarginsRelativeArrangement = true - // stackView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapOnPoster))) return stackView }() @@ -36,22 +38,12 @@ public final class SearchView: View { public override func initialize() { addSubview(stackView) addSubview(emptyView) - -// searchBar.isHidden = true -// searchBar.alpha = 0 -// emptyView.backgroundColor = .red } public override func installConstraints() { constrain(stackView, emptyView, searchBar) { stack, empty, search in search.width == search.superview!.width - search.topMargin == search.superview!.topMargin - search.height == 56 - -// collectionView.top == search.bottom -// collectionView.bottom == collectionView.superview!.bottom -// collectionView.left == collectionView.superview!.left -// collectionView.right == collectionView.superview!.right + search.top == search.superview!.top + 43 stack.size == stack.superview!.size stack.top == stack.superview!.top @@ -59,11 +51,7 @@ public final class SearchView: View { stack.left == stack.superview!.left stack.right == stack.superview!.right - empty.top == empty.superview!.top - empty.width == empty.superview!.width - empty.height == empty.superview!.height * 0.5 -// empty.centerX == empty.superview!.centerX -// empty.center == empty.superview!.center + empty.center == empty.superview!.center } } } From 0f7b7af3fdfbf03a1af39b395bdb4738c251019e Mon Sep 17 00:00:00 2001 From: Pietro Caselani Date: Sat, 5 Jan 2019 03:09:48 -0200 Subject: [PATCH 4/4] Finish implementing Search module and tests --- CouchTracker.xcodeproj/project.pbxproj | 46 ++--- .../SearchCollectionViewDataSource.swift | 21 ++- CouchTrackerApp/Search/SearchModule.swift | 15 +- CouchTrackerApp/Search/SearchView.swift | 6 +- .../Search/SearchViewController.swift | 84 ++++++--- CouchTrackerApp/Search/SearchiOSRouter.swift | 35 ++++ CouchTrackerApp/Search/Storyboard.storyboard | 35 ---- .../PosterShowViewModelMapper.swift | 6 +- .../EntityMappers/ShowEntityMapper.swift | 6 +- .../PosterCell/PosterViewModel.swift | 11 +- CouchTrackerCore/Search/SearchContract.swift | 6 +- .../Search/SearchDefaultPresenter.swift | 50 +++-- .../Search/SearchResultEntity.swift | 16 ++ .../{SearchStates.swift => SearchState.swift} | 11 +- .../SearchResultDefaultPresenterTests.swift | 124 ------------- .../Search/Result/SearchResultMock.swift | 81 --------- .../Search/Result/SearchResultMocks.swift | 91 --------- .../Search/SearchMocks.swift | 24 ++- .../Search/SearchPresenterTest.swift | 172 ++++++++++++------ CouchTrackerCoreTests/TestSchedulers.swift | 10 +- CouchTrackerCoreTests/TraktEntitiesMock.swift | 19 ++ .../TraktLogin/TraktLoginPresenterTest.swift | 5 +- .../TraktTokenPolicyDeciderTest.swift | 4 +- .../Trending/TrendingPresenterTest.swift | 4 +- 24 files changed, 381 insertions(+), 501 deletions(-) create mode 100644 CouchTrackerApp/Search/SearchiOSRouter.swift delete mode 100644 CouchTrackerApp/Search/Storyboard.storyboard create mode 100644 CouchTrackerCore/Search/SearchResultEntity.swift rename CouchTrackerCore/Search/{SearchStates.swift => SearchState.swift} (71%) delete mode 100644 CouchTrackerCoreTests/Search/Result/SearchResultDefaultPresenterTests.swift delete mode 100644 CouchTrackerCoreTests/Search/Result/SearchResultMock.swift delete mode 100644 CouchTrackerCoreTests/Search/Result/SearchResultMocks.swift diff --git a/CouchTracker.xcodeproj/project.pbxproj b/CouchTracker.xcodeproj/project.pbxproj index c45862c4..77cec7ac 100644 --- a/CouchTracker.xcodeproj/project.pbxproj +++ b/CouchTracker.xcodeproj/project.pbxproj @@ -12,7 +12,6 @@ 392D91732CA7FDA4E71A3230 /* trakt_users_settings_invalid.json in Resources */ = {isa = PBXBuildFile; fileRef = 392D9C154DB74E65A233F9C3 /* trakt_users_settings_invalid.json */; }; 4F02F1AC2128D63300869BEF /* WatchedEpisodeEntityBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F02F1AB2128D63300869BEF /* WatchedEpisodeEntityBuilder.swift */; }; 4F02F1AD2128D63300869BEF /* WatchedEpisodeEntityBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F02F1AB2128D63300869BEF /* WatchedEpisodeEntityBuilder.swift */; }; - 4F09A6FF207FD9DB00E0A64C /* SearchResultMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F09A6FE207FD9DB00E0A64C /* SearchResultMock.swift */; }; 4F0F5705203D5F4C00B86CB8 /* CouchTrackerCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F0F56FC203D5F4C00B86CB8 /* CouchTrackerCore.framework */; }; 4F0F570C203D5F4D00B86CB8 /* CouchTrackerCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 4F0F56FE203D5F4C00B86CB8 /* CouchTrackerCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4F0F5714203D5FA000B86CB8 /* AppConfigurationsContract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644BF5071F6B07AF0010CE35 /* AppConfigurationsContract.swift */; }; @@ -335,8 +334,6 @@ 4F2F336E20C893E700061C13 /* MovieDetailsViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F2F336C20C893E700061C13 /* MovieDetailsViewState.swift */; }; 4F2F337020C8943D00061C13 /* MovieDetailsImagesState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F2F336F20C8943D00061C13 /* MovieDetailsImagesState.swift */; }; 4F2F337120C8943D00061C13 /* MovieDetailsImagesState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F2F336F20C8943D00061C13 /* MovieDetailsImagesState.swift */; }; - 4F36342A2063C3510045CDCA /* SearchResultDefaultPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F3634292063C3510045CDCA /* SearchResultDefaultPresenterTests.swift */; }; - 4F3A2A1120707F5100E3FA76 /* SearchResultMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F3A2A1020707F5100E3FA76 /* SearchResultMocks.swift */; }; 4F3A5949204EACD20061ABB7 /* AppFlowMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F3A5948204EACD20061ABB7 /* AppFlowMocks.swift */; }; 4F3A594C204EAD1F0061ABB7 /* AppFlowContract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F3A594B204EAD1F0061ABB7 /* AppFlowContract.swift */; }; 4F3A594D204EAD1F0061ABB7 /* AppFlowContract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F3A594B204EAD1F0061ABB7 /* AppFlowContract.swift */; }; @@ -522,12 +519,11 @@ 827EB79E21DAED9300B02001 /* AppConfigurationsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F202F6E202F208F0040185F /* AppConfigurationsStore.swift */; }; 827EB7A021DAEDD800B02001 /* AppConfigurationsViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 827EB79F21DAEDD800B02001 /* AppConfigurationsViewState.swift */; }; 827EB7A121DAEDD800B02001 /* AppConfigurationsViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 827EB79F21DAEDD800B02001 /* AppConfigurationsViewState.swift */; }; - 827EB7A321DBE2C700B02001 /* SearchStates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 827EB7A221DBE2C700B02001 /* SearchStates.swift */; }; - 827EB7A421DBE2C900B02001 /* SearchStates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 827EB7A221DBE2C700B02001 /* SearchStates.swift */; }; + 827EB7A321DBE2C700B02001 /* SearchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 827EB7A221DBE2C700B02001 /* SearchState.swift */; }; + 827EB7A421DBE2C900B02001 /* SearchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 827EB7A221DBE2C700B02001 /* SearchState.swift */; }; 827EB7A721DBF84400B02001 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 827EB7A621DBF84400B02001 /* SearchView.swift */; }; 827EB7A921DBF8AF00B02001 /* UICollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 827EB7A821DBF8AF00B02001 /* UICollectionViewLayout.swift */; }; 827EB7AB21DBFB9800B02001 /* SearchCollectionViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 827EB7AA21DBFB9800B02001 /* SearchCollectionViewDataSource.swift */; }; - 827EB7AF21DC055D00B02001 /* Storyboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 827EB7AE21DC055D00B02001 /* Storyboard.storyboard */; }; 82836E98218EAAFE0037A798 /* ShowsSynchronizerContract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82836E97218EAAFE0037A798 /* ShowsSynchronizerContract.swift */; }; 82836E99218EAAFE0037A798 /* ShowsSynchronizerContract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82836E97218EAAFE0037A798 /* ShowsSynchronizerContract.swift */; }; 82836E9B218EAB580037A798 /* DefaultWatchedShowsSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82836E9A218EAB580037A798 /* DefaultWatchedShowsSynchronizer.swift */; }; @@ -544,6 +540,9 @@ 82836EAE218EC2C10037A798 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82836EAC218EC2C10037A798 /* Defaults.swift */; }; 82836EB5219034010037A798 /* CouchTrackerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82836EB4219034010037A798 /* CouchTrackerError.swift */; }; 82836EB6219034010037A798 /* CouchTrackerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82836EB4219034010037A798 /* CouchTrackerError.swift */; }; + 82882F5721DED60E00B1A732 /* SearchResultEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82882F5621DED60E00B1A732 /* SearchResultEntity.swift */; }; + 82882F5821DED60E00B1A732 /* SearchResultEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82882F5621DED60E00B1A732 /* SearchResultEntity.swift */; }; + 82882F5A21DED8BA00B1A732 /* SearchiOSRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82882F5921DED8BA00B1A732 /* SearchiOSRouter.swift */; }; 82930A19216C1C7B00A824F6 /* DefaultWatchedShowEntityDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82930A18216C1C7B00A824F6 /* DefaultWatchedShowEntityDownloader.swift */; }; 82930A1C216C1C8500A824F6 /* DefaultWatchedShowEntityDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82930A18216C1C7B00A824F6 /* DefaultWatchedShowEntityDownloader.swift */; }; 82930A1D216C1C8500A824F6 /* DefaultWatchedShowEntitiesDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8239F812216C159500A29F01 /* DefaultWatchedShowEntitiesDownloader.swift */; }; @@ -665,7 +664,6 @@ 3C835172215EB21E106EE41B /* Pods-CouchTrackerCore.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CouchTrackerCore.release.xcconfig"; path = "Pods/Target Support Files/Pods-CouchTrackerCore/Pods-CouchTrackerCore.release.xcconfig"; sourceTree = ""; }; 4C81CF20C8EA291D750ADACE /* Pods_CouchTrackerCoreTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CouchTrackerCoreTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4F02F1AB2128D63300869BEF /* WatchedEpisodeEntityBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchedEpisodeEntityBuilder.swift; sourceTree = ""; }; - 4F09A6FE207FD9DB00E0A64C /* SearchResultMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultMock.swift; sourceTree = ""; }; 4F0F56FC203D5F4C00B86CB8 /* CouchTrackerCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CouchTrackerCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4F0F56FE203D5F4C00B86CB8 /* CouchTrackerCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CouchTrackerCore.h; sourceTree = ""; }; 4F0F56FF203D5F4C00B86CB8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -696,8 +694,6 @@ 4F2F282E212924F20000CC17 /* SeasonIdsRealm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeasonIdsRealm.swift; sourceTree = ""; }; 4F2F336C20C893E700061C13 /* MovieDetailsViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieDetailsViewState.swift; sourceTree = ""; }; 4F2F336F20C8943D00061C13 /* MovieDetailsImagesState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieDetailsImagesState.swift; sourceTree = ""; }; - 4F3634292063C3510045CDCA /* SearchResultDefaultPresenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultDefaultPresenterTests.swift; sourceTree = ""; }; - 4F3A2A1020707F5100E3FA76 /* SearchResultMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultMocks.swift; sourceTree = ""; }; 4F3A5948204EACD20061ABB7 /* AppFlowMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppFlowMocks.swift; sourceTree = ""; }; 4F3A594B204EAD1F0061ABB7 /* AppFlowContract.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppFlowContract.swift; sourceTree = ""; }; 4F3A5951204EB75B0061ABB7 /* AppFlowPresenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppFlowPresenterTests.swift; sourceTree = ""; }; @@ -1017,11 +1013,10 @@ 827BCD3C20EFF3F60080B242 /* ShowOverviewViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowOverviewViewState.swift; sourceTree = ""; }; 827BCD3F20EFF5C30080B242 /* ShowOverviewImagesState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowOverviewImagesState.swift; sourceTree = ""; }; 827EB79F21DAEDD800B02001 /* AppConfigurationsViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigurationsViewState.swift; sourceTree = ""; }; - 827EB7A221DBE2C700B02001 /* SearchStates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchStates.swift; sourceTree = ""; }; + 827EB7A221DBE2C700B02001 /* SearchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchState.swift; sourceTree = ""; }; 827EB7A621DBF84400B02001 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; 827EB7A821DBF8AF00B02001 /* UICollectionViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UICollectionViewLayout.swift; sourceTree = ""; }; 827EB7AA21DBFB9800B02001 /* SearchCollectionViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchCollectionViewDataSource.swift; sourceTree = ""; }; - 827EB7AE21DC055D00B02001 /* Storyboard.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Storyboard.storyboard; sourceTree = ""; }; 82836E97218EAAFE0037A798 /* ShowsSynchronizerContract.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowsSynchronizerContract.swift; sourceTree = ""; }; 82836E9A218EAB580037A798 /* DefaultWatchedShowsSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultWatchedShowsSynchronizer.swift; sourceTree = ""; }; 82836E9D218EABE20037A798 /* SyncOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncOptions.swift; sourceTree = ""; }; @@ -1031,6 +1026,8 @@ 82836EAC218EC2C10037A798 /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = ""; }; 82836EB4219034010037A798 /* CouchTrackerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouchTrackerError.swift; sourceTree = ""; }; 82836EB7219039DA0037A798 /* ShowSeasonsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowSeasonsViewController.swift; sourceTree = ""; }; + 82882F5621DED60E00B1A732 /* SearchResultEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultEntity.swift; sourceTree = ""; }; + 82882F5921DED8BA00B1A732 /* SearchiOSRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchiOSRouter.swift; sourceTree = ""; }; 82930A18216C1C7B00A824F6 /* DefaultWatchedShowEntityDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultWatchedShowEntityDownloader.swift; sourceTree = ""; }; 829A590021B2404700B67753 /* Container.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Container.swift; sourceTree = ""; }; 82C6477B21D72F3500175B24 /* TrendingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingView.swift; sourceTree = ""; }; @@ -1364,7 +1361,8 @@ 648390E41F504EE500911BA0 /* SearchContract.swift */, 64D29C3B1F55A968008E344E /* SearchDefaultPresenter.swift */, 64D29C3C1F55A968008E344E /* SearchService.swift */, - 827EB7A221DBE2C700B02001 /* SearchStates.swift */, + 827EB7A221DBE2C700B02001 /* SearchState.swift */, + 82882F5621DED60E00B1A732 /* SearchResultEntity.swift */, ); path = Search; sourceTree = ""; @@ -2063,7 +2061,6 @@ 648390CF1F504E7500911BA0 /* SearchInteractorTest.swift */, 648390D01F504E7500911BA0 /* SearchMocks.swift */, 6483911F1F50606100911BA0 /* SearchPresenterTest.swift */, - DF3BF079D00223397D6BEAA9 /* Result */, ); path = Search; sourceTree = ""; @@ -2072,10 +2069,10 @@ isa = PBXGroup; children = ( 827EB7AA21DBFB9800B02001 /* SearchCollectionViewDataSource.swift */, + 82882F5921DED8BA00B1A732 /* SearchiOSRouter.swift */, 64DA353F1F50914A005BF4E3 /* SearchModule.swift */, 827EB7A621DBF84400B02001 /* SearchView.swift */, 4FE4BA312059E31800969AAF /* SearchViewController.swift */, - 827EB7AE21DC055D00B02001 /* Storyboard.storyboard */, ); path = Search; sourceTree = ""; @@ -2321,16 +2318,6 @@ path = Manager; sourceTree = ""; }; - DF3BF079D00223397D6BEAA9 /* Result */ = { - isa = PBXGroup; - children = ( - 4F3634292063C3510045CDCA /* SearchResultDefaultPresenterTests.swift */, - 4F3A2A1020707F5100E3FA76 /* SearchResultMocks.swift */, - 4F09A6FE207FD9DB00E0A64C /* SearchResultMock.swift */, - ); - path = Result; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -2691,7 +2678,6 @@ buildActionMask = 2147483647; files = ( 8204074021D01A9D00127F05 /* Localizable.strings in Resources */, - 827EB7AF21DC055D00B02001 /* Storyboard.storyboard in Resources */, 8204073F21D0187800127F05 /* Assets.xcassets in Resources */, 8204072721D0049800127F05 /* ShowsProgress.storyboard in Resources */, ); @@ -3079,7 +3065,7 @@ 4F0F5721203D5FA000B86CB8 /* AppConfigurationsMoyaNetwork.swift in Sources */, 4F0F5725203D5FA000B86CB8 /* BaseView.swift in Sources */, 4F0F5726203D5FA000B86CB8 /* BuildConfig.swift in Sources */, - 827EB7A321DBE2C700B02001 /* SearchStates.swift in Sources */, + 827EB7A321DBE2C700B02001 /* SearchState.swift in Sources */, 4F0F5727203D5FA000B86CB8 /* ConfigurationRepository.swift in Sources */, 4F0F5728203D5FA000B86CB8 /* ConfigurationCachedRepository.swift in Sources */, 4F0F572B203D5FA000B86CB8 /* EpisodeEntity.swift in Sources */, @@ -3218,6 +3204,7 @@ 4F0F57B3203D5FA100B86CB8 /* TraktLoginState.swift in Sources */, 4F0F57B4203D5FA100B86CB8 /* TraktLoginStore.swift in Sources */, 4F0F57B6203D5FA100B86CB8 /* TraktTokenPolicyDecider.swift in Sources */, + 82882F5721DED60E00B1A732 /* SearchResultEntity.swift in Sources */, 4F0F57B8203D5FA100B86CB8 /* PosterCellContract.swift in Sources */, 4F0F57B9203D5FA100B86CB8 /* PosterCellDefaultPresenter.swift in Sources */, 4F0F57BA203D5FA100B86CB8 /* PosterCellService.swift in Sources */, @@ -3261,13 +3248,11 @@ 4F0F57DB203D61B300B86CB8 /* MovieDetailsInteractorTest.swift in Sources */, 4F0F57DC203D61B300B86CB8 /* MovieDetailsMocks.swift in Sources */, 4F6102232052791D0079EDBA /* ShowManagerMocks.swift in Sources */, - 4F36342A2063C3510045CDCA /* SearchResultDefaultPresenterTests.swift in Sources */, 4F0F57DD203D61B300B86CB8 /* MovieDetailsPresenterTest.swift in Sources */, 4F0F57DE203D61B300B86CB8 /* MoyaProviderMock.swift in Sources */, 4F0F57DF203D61B300B86CB8 /* TMDBErrorProviderMock.swift in Sources */, 4F0F57E0203D61B300B86CB8 /* TMDBProviderMock.swift in Sources */, 4F0F57E1203D61B300B86CB8 /* TraktErrorProviderMock.swift in Sources */, - 4F3A2A1120707F5100E3FA76 /* SearchResultMocks.swift in Sources */, 4FE02E452105E98000196B8E /* GenreRealmTests.swift in Sources */, 4F0F57E2203D61B300B86CB8 /* TraktProviderMock.swift in Sources */, 4F0F57E3203D61B300B86CB8 /* TVDBProviderMock.swift in Sources */, @@ -3331,7 +3316,6 @@ 4F3A595B204EF31A0061ABB7 /* AppFlowUserDefaultsRepositoryTests.swift in Sources */, 4F0F57D5203D619E00B86CB8 /* ConfigurationRepositoryMock.swift in Sources */, 4F0F57D7203D619E00B86CB8 /* ImageRepositoryMock.swift in Sources */, - 4F09A6FF207FD9DB00E0A64C /* SearchResultMock.swift in Sources */, 4F3A5952204EB75B0061ABB7 /* AppFlowPresenterTests.swift in Sources */, 4F0F57D8203D619E00B86CB8 /* ImageMocks.swift in Sources */, 4F0F57D9203D619E00B86CB8 /* ImageCachedRepositoryTest.swift in Sources */, @@ -3368,7 +3352,7 @@ 4F0F5867203D7B0A00B86CB8 /* AppConfigurationsDefaultRepository.swift in Sources */, 4FEF6ADD21259F6900B7CF63 /* ShowWatchedProgressRepository.swift in Sources */, 4F0F5868203D7B0A00B86CB8 /* AppConfigurationsDefaultPresenter.swift in Sources */, - 827EB7A421DBE2C900B02001 /* SearchStates.swift in Sources */, + 827EB7A421DBE2C900B02001 /* SearchState.swift in Sources */, 4F0F586D203D7B0A00B86CB8 /* AppConfigurationsService.swift in Sources */, 4F0F586E203D7B0A00B86CB8 /* AppConfigurationsState.swift in Sources */, 4F0F5870203D7B0A00B86CB8 /* AppConfigurationsUserDefaultsDataSource.swift in Sources */, @@ -3507,6 +3491,7 @@ 82930A1F216C1C8500A824F6 /* RealmShowsDataSource.swift in Sources */, 4F9ABC122052982B00EA6456 /* MoviesManagerDefaultDataSource.swift in Sources */, 4F0F58FA203D7B0A00B86CB8 /* TMDB+APIProvider.swift in Sources */, + 82882F5821DED60E00B1A732 /* SearchResultEntity.swift in Sources */, 4F0F58FB203D7B0A00B86CB8 /* Date+TraktDateFormatter.swift in Sources */, 4F0F58FC203D7B0A00B86CB8 /* Trakt+APIProvider.swift in Sources */, 4F0F5908203D7B0A00B86CB8 /* PosterCellContract.swift in Sources */, @@ -3593,6 +3578,7 @@ 822A02FD21D2352400440AF8 /* View.swift in Sources */, 820406FC21D0036400127F05 /* AppConfigurationsViewController.swift in Sources */, 8204072821D0049800127F05 /* ShowsProgressiOSRouter.swift in Sources */, + 82882F5A21DED8BA00B1A732 /* SearchiOSRouter.swift in Sources */, 8204072221D0048900127F05 /* ShowsManagerViewController.swift in Sources */, 8204070A21D0043600127F05 /* MoviesManageriOSModuleSetup.swift in Sources */, 82609F0F21D870C90038EB29 /* ViewCalculations.swift in Sources */, diff --git a/CouchTrackerApp/Search/SearchCollectionViewDataSource.swift b/CouchTrackerApp/Search/SearchCollectionViewDataSource.swift index 01377c91..1524f83b 100644 --- a/CouchTrackerApp/Search/SearchCollectionViewDataSource.swift +++ b/CouchTrackerApp/Search/SearchCollectionViewDataSource.swift @@ -1,16 +1,16 @@ import CouchTrackerCore -final class SearchCollectionViewDataSource: NSObject, UICollectionViewDataSource { +final class SearchCollectionViewDataSource: NSObject, SearchDataSource { private let interactor: PosterCellInteractor - var viewModels = [PosterViewModel]() + var entities = [SearchResultEntity]() init(interactor: PosterCellInteractor) { self.interactor = interactor } func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -> Int { - return viewModels.count + return entities.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { @@ -22,7 +22,9 @@ final class SearchCollectionViewDataSource: NSObject, UICollectionViewDataSource Swift.fatalError("cell should be an instance of PosterAndTitleCell") } - let viewModel = viewModels[indexPath.row] + let entity = entities[indexPath.row] + let viewModel = mapEntityToViewModel(entity) + let presenter = PosterCellDefaultPresenter(view: posterCell, interactor: interactor, viewModel: viewModel) posterCell.presenter = presenter @@ -30,3 +32,14 @@ final class SearchCollectionViewDataSource: NSObject, UICollectionViewDataSource return posterCell } } + +private func mapEntityToViewModel(_ entity: SearchResultEntity) -> PosterViewModel { + switch entity.type { + case let .movie(movie): + let type = movie.ids.tmdb.map { PosterViewModelType.movie(tmdbMovieId: $0) } + return PosterViewModel(title: movie.title ?? "", type: type) + case let .show(show): + let type = show.ids.tmdb.map { PosterViewModelType.show(tmdbShowId: $0) } + return PosterViewModel(title: show.title ?? "", type: type) + } +} diff --git a/CouchTrackerApp/Search/SearchModule.swift b/CouchTrackerApp/Search/SearchModule.swift index bc2ef32d..d5b9123d 100644 --- a/CouchTrackerApp/Search/SearchModule.swift +++ b/CouchTrackerApp/Search/SearchModule.swift @@ -1,9 +1,11 @@ import CouchTrackerCore import TraktSwift -final class SearchModule { - private init() {} +protocol SearchDataSource: UICollectionViewDataSource { + var entities: [SearchResultEntity] { get set } +} +enum SearchModule { static func setupModule(searchTypes: [SearchType]) -> BaseView { let environment = Environment.instance let tmdb = environment.tmdb @@ -23,9 +25,14 @@ final class SearchModule { let dataSource = SearchCollectionViewDataSource(interactor: posterCellInteractor) let interactor = SearchService(traktProvider: trakt) + let router = SearchiOSRouter() + + let presenter = SearchDefaultPresenter(interactor: interactor, types: searchTypes, router: router) + + let viewController = SearchViewController(presenter: presenter, dataSource: dataSource) - let presenter = SearchDefaultPresenter(interactor: interactor, types: searchTypes) + router.viewController = viewController - return SearchViewController(presenter: presenter, dataSource: dataSource) + return viewController } } diff --git a/CouchTrackerApp/Search/SearchView.swift b/CouchTrackerApp/Search/SearchView.swift index b0e93735..7c57e4da 100644 --- a/CouchTrackerApp/Search/SearchView.swift +++ b/CouchTrackerApp/Search/SearchView.swift @@ -42,8 +42,12 @@ public final class SearchView: View { public override func installConstraints() { constrain(stackView, emptyView, searchBar) { stack, empty, search in - search.width == search.superview!.width + /* + CT-TODO Fix this + 43 + Without this constant, the search bar appears behind the top bar + */ search.top == search.superview!.top + 43 + search.width == search.superview!.width stack.size == stack.superview!.size stack.top == stack.superview!.top diff --git a/CouchTrackerApp/Search/SearchViewController.swift b/CouchTrackerApp/Search/SearchViewController.swift index 9ce12c89..1911a3d1 100644 --- a/CouchTrackerApp/Search/SearchViewController.swift +++ b/CouchTrackerApp/Search/SearchViewController.swift @@ -6,7 +6,7 @@ final class SearchViewController: UIViewController { private let disposeBag = DisposeBag() private let presenter: SearchPresenter private let schedulers: Schedulers - private let dataSource: UICollectionViewDataSource + private let dataSource: SearchDataSource private var searchView: SearchView { guard let searchView = self.view as? SearchView else { @@ -17,7 +17,7 @@ final class SearchViewController: UIViewController { } init(presenter: SearchPresenter, - dataSource: UICollectionViewDataSource, + dataSource: SearchDataSource, schedulers: Schedulers = DefaultSchedulers.instance) { self.presenter = presenter self.schedulers = schedulers @@ -36,20 +36,10 @@ final class SearchViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - adjustForNavigationBar() - extendedLayoutIncludesOpaqueBars = true configureCollectionView() - + searchView.searchBar.delegate = self view.backgroundColor = Colors.View.background - searchView.emptyView.label.text = "HeyyY!!" - - presenter.observeSearchResults() - .observeOn(schedulers.mainScheduler) - .subscribe(onNext: { [weak self] resultState in - self?.handleSearchResultState(resultState) - }).disposed(by: disposeBag) - presenter.observeSearchState() .observeOn(schedulers.mainScheduler) .subscribe(onNext: { [weak self] searchState in @@ -67,34 +57,68 @@ final class SearchViewController: UIViewController { searchView.collectionView.delegate = self } - private func handleSearchResultState(_: SearchResultState) {} - - private func handleSearchState(_: SearchState) {} + private func handleSearchState(_ state: SearchState) { + switch state { + case .notSearching: + searchView.searchBar.resignFirstResponder() + case .emptyResults: + handleEmptySearchResult() + case let .results(entities): + handleSearch(entities: entities) + case let .error(error): + handleError(message: error.localizedDescription) + case .searching: + handleSearching() + } + } - private func searchChangedTo(state _: SearchState) {} + private func handleSearching() { + searchView.emptyView.label.text = "Searching" + searchView.emptyView.isHidden = false + } private func handleEmptySearchResult() { -// infoLabel.text = "No results" -// collectionView.isHidden = true -// infoLabel.isHidden = false + searchView.emptyView.label.text = "Nothing found" + searchView.emptyView.isHidden = false } - private func handleSearch(results _: [SearchResult]) { -// self.results = results -// collectionView.reloadData() -// collectionView.isHidden = false -// infoLabel.isHidden = true + private func handleSearch(entities: [SearchResultEntity]) { + dataSource.entities = entities + searchView.collectionView.reloadData() + searchView.emptyView.isHidden = true } - private func handleError(message _: String) { -// infoLabel.text = message -// collectionView.isHidden = true -// infoLabel.isHidden = false + private func handleError(message: String) { + searchView.emptyView.isHidden = false + searchView.emptyView.label.text = message } } extension SearchViewController: UICollectionViewDelegate { func collectionView(_: UICollectionView, didSelectItemAt indexPath: IndexPath) { - print("Selected item at \(indexPath.row)") + let entity = dataSource.entities[indexPath.row] + presenter.select(entity: entity) + } +} + +extension SearchViewController: UISearchBarDelegate { + func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { + searchBar.setShowsCancelButton(true, animated: true) + } + + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + searchBar.text = nil + searchBar.setShowsCancelButton(false, animated: true) + searchBar.resignFirstResponder() + + presenter.cancelSearch() + } + + func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { + searchBar.resignFirstResponder() + + guard let query = searchBar.text else { return } + + presenter.search(query: query) } } diff --git a/CouchTrackerApp/Search/SearchiOSRouter.swift b/CouchTrackerApp/Search/SearchiOSRouter.swift new file mode 100644 index 00000000..f6ebf633 --- /dev/null +++ b/CouchTrackerApp/Search/SearchiOSRouter.swift @@ -0,0 +1,35 @@ +import CouchTrackerCore +import TraktSwift + +final class SearchiOSRouter: SearchRouter { + weak var viewController: UIViewController? + + func showViewFor(entity: SearchResultEntity) { + let view: BaseView + switch entity.type { + case let .movie(movie): + view = MovieDetailsModule.setupModule(movieIds: movie.ids) + case let .show(show): + let showEntity = ShowEntityMapper.entity(for: show) + let watchedShow = WatchedShowEntity(show: showEntity, + aired: nil, + completed: nil, + nextEpisode: nil, + lastWatched: nil, + seasons: [WatchedSeasonEntity]()) + view = ShowManagerModule.setupModule(for: watchedShow) + } + + present(view: view) + } + + private func present(view: BaseView) { + guard let navigationController = viewController?.navigationController else { return } + + guard let nextViewController = view as? UIViewController else { + Swift.fatalError("view should be an instance of UIViewController") + } + + navigationController.pushViewController(nextViewController, animated: true) + } +} diff --git a/CouchTrackerApp/Search/Storyboard.storyboard b/CouchTrackerApp/Search/Storyboard.storyboard deleted file mode 100644 index c90636b7..00000000 --- a/CouchTrackerApp/Search/Storyboard.storyboard +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/CouchTrackerCore/EntityMappers/PosterShowViewModelMapper.swift b/CouchTrackerCore/EntityMappers/PosterShowViewModelMapper.swift index edc0ed18..81e8b3b7 100644 --- a/CouchTrackerCore/EntityMappers/PosterShowViewModelMapper.swift +++ b/CouchTrackerCore/EntityMappers/PosterShowViewModelMapper.swift @@ -1,10 +1,6 @@ import TraktSwift -public final class PosterShowViewModelMapper { - private init() { - Swift.fatalError("No instances for you!") - } - +public enum PosterShowViewModelMapper { public static func viewModel(for show: ShowEntity, defaultTitle: String = "TBA".localized) -> PosterViewModel { return PosterViewModel(title: show.title ?? defaultTitle, type: show.ids.tmdbModelType()) } diff --git a/CouchTrackerCore/EntityMappers/ShowEntityMapper.swift b/CouchTrackerCore/EntityMappers/ShowEntityMapper.swift index 0838e17c..623dac15 100644 --- a/CouchTrackerCore/EntityMappers/ShowEntityMapper.swift +++ b/CouchTrackerCore/EntityMappers/ShowEntityMapper.swift @@ -1,9 +1,7 @@ import TraktSwift -public final class ShowEntityMapper { - private init() {} - - public static func entity(for show: Show, with genres: [Genre]) -> ShowEntity { +public enum ShowEntityMapper { + public static func entity(for show: Show, with genres: [Genre] = [Genre]()) -> ShowEntity { return ShowEntity(ids: show.ids, title: show.title, overview: show.overview, network: show.network, genres: genres, status: show.status, firstAired: show.firstAired) diff --git a/CouchTrackerCore/PosterCell/PosterViewModel.swift b/CouchTrackerCore/PosterCell/PosterViewModel.swift index 30d15d7e..fe0170f6 100644 --- a/CouchTrackerCore/PosterCell/PosterViewModel.swift +++ b/CouchTrackerCore/PosterCell/PosterViewModel.swift @@ -2,17 +2,18 @@ public struct PosterViewModel: Hashable { public let title: String public let type: PosterViewModelType? + public init(title: String, type: PosterViewModelType?) { + self.title = title + self.type = type + } + public static func == (lhs: PosterViewModel, rhs: PosterViewModel) -> Bool { return lhs.hashValue == rhs.hashValue } public var hashValue: Int { var hash = title.hashValue - - if let typeHash = type?.hashValue { - hash = hash ^ typeHash - } - + type.run { hash ^= $0.hashValue } return hash } } diff --git a/CouchTrackerCore/Search/SearchContract.swift b/CouchTrackerCore/Search/SearchContract.swift index 246452b8..6865cb6e 100644 --- a/CouchTrackerCore/Search/SearchContract.swift +++ b/CouchTrackerCore/Search/SearchContract.swift @@ -4,10 +4,14 @@ import TraktSwift public protocol SearchPresenter: class { func search(query: String) func cancelSearch() + func select(entity: SearchResultEntity) func observeSearchState() -> Observable - func observeSearchResults() -> Observable } public protocol SearchInteractor: class { func search(query: String, types: [SearchType], page: Int, limit: Int) -> Single<[SearchResult]> } + +public protocol SearchRouter: class { + func showViewFor(entity: SearchResultEntity) +} diff --git a/CouchTrackerCore/Search/SearchDefaultPresenter.swift b/CouchTrackerCore/Search/SearchDefaultPresenter.swift index 9aaa9523..28a37238 100644 --- a/CouchTrackerCore/Search/SearchDefaultPresenter.swift +++ b/CouchTrackerCore/Search/SearchDefaultPresenter.swift @@ -4,15 +4,19 @@ import TraktSwift public final class SearchDefaultPresenter: SearchPresenter { private let disposeBag = DisposeBag() private let searchStateSubject = BehaviorSubject(value: .notSearching) - private let searchResultsSubject = BehaviorSubject(value: .emptyResults) private let interactor: SearchInteractor private let schedulers: Schedulers + private let router: SearchRouter private let searchTypes: [SearchType] private var currentPage = 0 - public init(interactor: SearchInteractor, types: [SearchType], schedulers: Schedulers = DefaultSchedulers.instance) { + public init(interactor: SearchInteractor, + types: [SearchType], + router: SearchRouter, + schedulers: Schedulers = DefaultSchedulers.instance) { self.interactor = interactor self.schedulers = schedulers + self.router = router searchTypes = types } @@ -20,31 +24,41 @@ public final class SearchDefaultPresenter: SearchPresenter { return searchStateSubject.distinctUntilChanged() } - public func observeSearchResults() -> Observable { - return searchResultsSubject.distinctUntilChanged() - } - public func search(query: String) { searchStateSubject.onNext(.searching) interactor.search(query: query, types: searchTypes, page: currentPage, limit: Defaults.itemsPerPage) + .map { mapResultsToSearchState($0) } + .catchError { Single.just(SearchState.error(error: $0)) } .observeOn(schedulers.mainScheduler) - .subscribe(onSuccess: { [weak self] results in - self?.searchStateSubject.onNext(.notSearching) - - guard results.count > 0 else { - self?.searchResultsSubject.onNext(.emptyResults) - return - } - - self?.searchResultsSubject.onNext(.results(results: results)) - - }, onError: { [weak self] error in - self?.searchStateSubject.onNext(.error(error: error)) + .subscribe(onSuccess: { [weak self] state in + self?.searchStateSubject.onNext(state) }).disposed(by: disposeBag) } public func cancelSearch() { searchStateSubject.onNext(.notSearching) } + + public func select(entity: SearchResultEntity) { + router.showViewFor(entity: entity) + } +} + +private func mapResultsToSearchState(_ results: [SearchResult]) -> SearchState { + let entities = results.compactMap(mapResultToEntity(result:)) + guard entities.count > 0 else { return SearchState.emptyResults } + return SearchState.results(entities: entities) +} + +private func mapResultToEntity(result: SearchResult) -> SearchResultEntity? { + switch result.type { + case .show: + guard let show = result.show else { return nil } + return SearchResultEntity(score: result.score, type: .show(show: show)) + case .movie: + guard let movie = result.movie else { return nil } + return SearchResultEntity(score: result.score, type: .movie(movie: movie)) + default: return nil + } } diff --git a/CouchTrackerCore/Search/SearchResultEntity.swift b/CouchTrackerCore/Search/SearchResultEntity.swift new file mode 100644 index 00000000..c0e7e637 --- /dev/null +++ b/CouchTrackerCore/Search/SearchResultEntity.swift @@ -0,0 +1,16 @@ +import TraktSwift + +public enum SearchResultType: Hashable { + case movie(movie: Movie) + case show(show: Show) +} + +public struct SearchResultEntity: Hashable { + public let score: Double? + public let type: SearchResultType + + public init(score: Double?, type: SearchResultType) { + self.score = score + self.type = type + } +} diff --git a/CouchTrackerCore/Search/SearchStates.swift b/CouchTrackerCore/Search/SearchState.swift similarity index 71% rename from CouchTrackerCore/Search/SearchStates.swift rename to CouchTrackerCore/Search/SearchState.swift index 1d1886e7..3aff7172 100644 --- a/CouchTrackerCore/Search/SearchStates.swift +++ b/CouchTrackerCore/Search/SearchState.swift @@ -1,19 +1,20 @@ import TraktSwift -public enum SearchResultState: Hashable { - case emptyResults - case results(results: [SearchResult]) -} - public enum SearchState: Hashable { case searching case notSearching + case emptyResults + case results(entities: [SearchResultEntity]) case error(error: Error) public func hash(into hasher: inout Hasher) { switch self { case .searching: hasher.combine("SearchState.searching") case .notSearching: hasher.combine("SearchState.notSearching") + case .emptyResults: hasher.combine("SearchState.emptyResults") + case let .results(viewModels): + hasher.combine("SearchState.results") + hasher.combine(viewModels) case let .error(error): hasher.combine("SearchState.error-\(error.localizedDescription)") } } diff --git a/CouchTrackerCoreTests/Search/Result/SearchResultDefaultPresenterTests.swift b/CouchTrackerCoreTests/Search/Result/SearchResultDefaultPresenterTests.swift deleted file mode 100644 index 4ed96f5b..00000000 --- a/CouchTrackerCoreTests/Search/Result/SearchResultDefaultPresenterTests.swift +++ /dev/null @@ -1,124 +0,0 @@ -@testable import CouchTrackerCore -import TraktSwift -import XCTest - -final class SearchResultDefaultPresenterTests: XCTestCase { - private var view: SearchResultMocks.View! - private var router: SearchResultMocks.Router! - private var presenter: SearchResultDefaultPresenter! - - override func setUp() { - super.setUp() - view = SearchResultMocks.View() - router = SearchResultMocks.Router() - presenter = SearchResultDefaultPresenter(view: view, router: router) - } - - override func tearDown() { - view = nil - router = nil - presenter = nil - super.tearDown() - } - - func testSearchResultDefaultPresenter_whenSearchStateChangesToSearching_notifyView() { - // When - presenter.searchChangedTo(state: .searching) - - // Then - XCTAssertTrue(view.invokedShowSearching) - XCTAssertEqual(view.invokedShowSearchingCount, 1) - } - - func testSearchResultDefaultPresenter_whenSearchStateChangesToNotSearching_notifyView() { - // When - presenter.searchChangedTo(state: .notSearching) - - // Then - XCTAssertTrue(view.invokedShowNotSearching) - XCTAssertEqual(view.invokedShowNotSearchingCount, 1) - } - - func testSearchResultDefaultPresenter_whenThereIsNoResult_notifyView() { - // When - presenter.handleEmptySearchResult() - - // Then - XCTAssertTrue(view.invokedShowEmptyResults) - XCTAssertEqual(view.invokedShowEmptyResultsCount, 1) - } - - func testSearchResultDefaultPresenter_whenThereIsAnError_notifyView() { - // Given - let errorMessage = "There is no internet connection" - - // When - presenter.handleError(message: errorMessage) - - // Then - XCTAssertTrue(view.invokedShowError) - XCTAssertEqual(view.invokedShowErrorCount, 1) - XCTAssertEqual(view.invokedShowErrorParameters?.message, errorMessage) - } - - func testSearchResultDefaultPresenter_whenThereIsShowResults_notifyView() { - // Given - let searchResults = [SearchResult.mock(type: .show, movie: nil)] - - // When - presenter.handleSearch(results: searchResults) - - // Then - let expectedViewModels = [PosterViewModel(title: "Game of Thrones", type: PosterViewModelType.show(tmdbShowId: 1399))] - - guard let viewModels = view.invokedShowParameters?.viewModels else { - XCTFail("Parameters can't be nil") - return - } - - XCTAssertTrue(view.invokedShow) - XCTAssertEqual(viewModels, expectedViewModels) - } - - func testSearchResultsDefaultPresenter_whenThereIsMovieResults_notifyView() { - // Given - let searchResults = [SearchResult.mock(type: .movie, show: nil)] - - // When - presenter.handleSearch(results: searchResults) - - // Then - let expectedViewModels = [PosterViewModel(title: "TRON: Legacy", type: PosterViewModelType.movie(tmdbMovieId: 20526))] - - guard let viewModels = view.invokedShowParameters?.viewModels else { - XCTFail("Parameters can't be nil") - return - } - - XCTAssertTrue(view.invokedShow) - XCTAssertEqual(viewModels, expectedViewModels) - } - - func testSearchResultsDefaultPresenter_whenThereIsMovieAndShowResults_notifyView() { - // Given - let showResult = SearchResult.mock(type: .show, movie: nil) - let movieResult = SearchResult.mock(type: .movie, show: nil) - let searchResults = [showResult, movieResult] - - // When - presenter.handleSearch(results: searchResults) - - // Then - let showViewModel = PosterViewModel(title: "Game of Thrones", type: PosterViewModelType.show(tmdbShowId: 1399)) - let movieViewModel = PosterViewModel(title: "TRON: Legacy", type: PosterViewModelType.movie(tmdbMovieId: 20526)) - let expectedViewModels = [showViewModel, movieViewModel] - - guard let viewModels = view.invokedShowParameters?.viewModels else { - XCTFail("Parameters can't be nil") - return - } - - XCTAssertTrue(view.invokedShow) - XCTAssertEqual(viewModels, expectedViewModels) - } -} diff --git a/CouchTrackerCoreTests/Search/Result/SearchResultMock.swift b/CouchTrackerCoreTests/Search/Result/SearchResultMock.swift deleted file mode 100644 index 8f012ab8..00000000 --- a/CouchTrackerCoreTests/Search/Result/SearchResultMock.swift +++ /dev/null @@ -1,81 +0,0 @@ -import TraktSwift - -extension SearchResult { - static func mock(type: SearchType = .show, - score: Double? = 2.3, - movie: Movie? = TraktEntitiesMock.createMovieDetailsMock(), - show: Show? = TraktEntitiesMock.createTraktShowDetails()) -> SearchResult { - let jsonScore: Any = score.map { "\($0)" } ?? "null" - - let typeJSON: String - - if let showJSON = self.showJSON(show) { - typeJSON = showJSON - } else if let movieJSON = self.movieJSON(movie) { - typeJSON = movieJSON - } else { - Swift.fatalError("Can't create JSON") - } - - let json = """ - { - "type": "\(type.rawValue)", - "score": \(jsonScore), - \(typeJSON) - } - """ - - guard let data = json.data(using: .utf8) else { - Swift.fatalError("Can't create data from JSON string") - } - - return try! JSONDecoder().decode(SearchResult.self, from: data) - } - - private static func movieJSON(_ movie: Movie?) -> String? { - guard let movie = movie else { return nil } - - let jsonTitle = movie.title.map { "\"\($0)\"" } ?? "null" - let jsonYear: Any = movie.year ?? "null" - let jsonImdb = movie.ids.imdb.map { "\"\($0)\"" } ?? "null" - let jsonTmdb: Any = movie.ids.tmdb ?? "null" - - return """ - "movie": { - "title": \(jsonTitle), - "year": \(jsonYear), - "ids": { - "trakt": \(movie.ids.trakt), - "slug": "\(movie.ids.slug)", - "imdb": \(jsonImdb), - "tmdb": \(jsonTmdb), - } - } - """ - } - - private static func showJSON(_ show: Show?) -> String? { - guard let show = show else { return nil } - - let jsonTitle = show.title.map { "\"\($0)\"" } ?? "null" - let jsonYear: Any = show.year ?? "null" - let jsonImdb = show.ids.imdb.map { "\"\($0)\"" } ?? "null" - let jsonTvrage: Any = show.ids.tvrage ?? "null" - let jsonTmdb: Any = show.ids.tmdb ?? "null" - - return """ - "show": { - "title": \(jsonTitle), - "year": \(jsonYear), - "ids": { - "trakt": \(show.ids.trakt), - "slug": "\(show.ids.slug)", - "tvdb": \(show.ids.tvdb), - "imdb": \(jsonImdb), - "tvrage": \(jsonTvrage), - "tmdb": \(jsonTmdb) - } - } - """ - } -} diff --git a/CouchTrackerCoreTests/Search/Result/SearchResultMocks.swift b/CouchTrackerCoreTests/Search/Result/SearchResultMocks.swift deleted file mode 100644 index d573a374..00000000 --- a/CouchTrackerCoreTests/Search/Result/SearchResultMocks.swift +++ /dev/null @@ -1,91 +0,0 @@ -import CouchTrackerCore -import TraktSwift - -final class SearchResultMocks { - private init() {} - - final class View: SearchResultView { - var invokedPresenterSetter = false - var invokedPresenterSetterCount = 0 - var invokedPresenter: SearchResultPresenter? - var invokedPresenterList = [SearchResultPresenter]() - var invokedPresenterGetter = false - var invokedPresenterGetterCount = 0 - var stubbedPresenter: SearchResultPresenter! - var presenter: SearchResultPresenter! { - set { - invokedPresenterSetter = true - invokedPresenterSetterCount += 1 - invokedPresenter = newValue - invokedPresenterList.append(newValue) - } - get { - invokedPresenterGetter = true - invokedPresenterGetterCount += 1 - return stubbedPresenter - } - } - - var invokedShow = false - var invokedShowCount = 0 - var invokedShowParameters: (viewModels: [PosterViewModel], Void)? - var invokedShowParametersList = [(viewModels: [PosterViewModel], Void)]() - - func show(viewModels: [PosterViewModel]) { - invokedShow = true - invokedShowCount += 1 - invokedShowParameters = (viewModels, ()) - invokedShowParametersList.append((viewModels, ())) - } - - var invokedShowSearching = false - var invokedShowSearchingCount = 0 - - func showSearching() { - invokedShowSearching = true - invokedShowSearchingCount += 1 - } - - var invokedShowNotSearching = false - var invokedShowNotSearchingCount = 0 - - func showNotSearching() { - invokedShowNotSearching = true - invokedShowNotSearchingCount += 1 - } - - var invokedShowEmptyResults = false - var invokedShowEmptyResultsCount = 0 - - func showEmptyResults() { - invokedShowEmptyResults = true - invokedShowEmptyResultsCount += 1 - } - - var invokedShowError = false - var invokedShowErrorCount = 0 - var invokedShowErrorParameters: (message: String, Void)? - var invokedShowErrorParametersList = [(message: String, Void)]() - - func showError(message: String) { - invokedShowError = true - invokedShowErrorCount += 1 - invokedShowErrorParameters = (message, ()) - invokedShowErrorParametersList.append((message, ())) - } - } - - final class Router: SearchResultRouter { - var invokedShowDetails = false - var invokedShowDetailsCount = 0 - var invokedShowDetailsParameters: (result: SearchResult, Void)? - var invokedShowDetailsParametersList = [(result: SearchResult, Void)]() - - func showDetails(of result: SearchResult) { - invokedShowDetails = true - invokedShowDetailsCount += 1 - invokedShowDetailsParameters = (result, ()) - invokedShowDetailsParametersList.append((result, ())) - } - } -} diff --git a/CouchTrackerCoreTests/Search/SearchMocks.swift b/CouchTrackerCoreTests/Search/SearchMocks.swift index 1459e4e1..43168801 100644 --- a/CouchTrackerCoreTests/Search/SearchMocks.swift +++ b/CouchTrackerCoreTests/Search/SearchMocks.swift @@ -8,8 +8,30 @@ enum SearchMocks { var searchInvoked = false var searchParameters: (query: String, types: [SearchType])? + private var results: [SearchResult] + private var error: Error? + + init(results: [SearchResult] = [SearchResult](), error: Error? = nil) { + self.results = results + self.error = error + } + func search(query _: String, types _: [SearchType], page _: Int, limit _: Int) -> Single<[SearchResult]> { - return Single.just([SearchResult]()) + guard let realError = error else { + return Single.just(results) + } + + return Single.error(realError) + } + } + + final class Router: SearchRouter { + var showViewInvokedCount = 0 + var showViewLastParameter: SearchResultEntity? + + func showViewFor(entity: SearchResultEntity) { + showViewInvokedCount += 1 + showViewLastParameter = entity } } } diff --git a/CouchTrackerCoreTests/Search/SearchPresenterTest.swift b/CouchTrackerCoreTests/Search/SearchPresenterTest.swift index eac7d8e3..94c836dd 100644 --- a/CouchTrackerCoreTests/Search/SearchPresenterTest.swift +++ b/CouchTrackerCoreTests/Search/SearchPresenterTest.swift @@ -1,81 +1,141 @@ @testable import CouchTrackerCore +import RxTest import TraktSwift import XCTest final class SearchPresenterTest: XCTestCase { + private var schedulers: TestSchedulers! + private var observer: TestableObserver! + override func setUp() { super.setUp() -// output = SearchMocks.ResultOutput() -// view = SearchMocks.View() + schedulers = TestSchedulers(initialClock: 0) + observer = schedulers.createObserver(SearchState.self) } override func tearDown() { -// output = nil -// view = nil + schedulers = nil + observer = nil super.tearDown() } - func testSearchPresenter_viewDidLoad_updateViewHint() { -// let store = SearchMocks.Repository() -// let interactor = SearchService(repository: store) -// let presenter = SearchDefaultPresenter(view: view, interactor: interactor, resultOutput: output, types: [SearchType.movie]) + func testSearchPresenter_performSearchSuccess_emitsResultsAndComplete() { + // Given + let results = TraktEntitiesMock.createSearchResultsMock() + let interactor = SearchMocks.Interactor(results: results) + let router = SearchMocks.Router() + let presenter = SearchDefaultPresenter(interactor: interactor, + types: [SearchType.movie], + router: router, + schedulers: schedulers) + + _ = presenter.observeSearchState().subscribe(observer) + + // When + presenter.search(query: "Tron") + + // Then + let expectedEntities = TraktEntitiesMock.createSearchResultEntitiesMock() + + let expectedEvents = [Recorded.next(0, SearchState.notSearching), + Recorded.next(0, SearchState.searching), + Recorded.next(0, SearchState.results(entities: expectedEntities))] + + XCTAssertEqual(observer.events, expectedEvents) + } + + func testSearchPresenter_performSearchReceivesNoData_emitsEmptyResultState() { + // Given + let interactor = SearchMocks.Interactor() + let router = SearchMocks.Router() + let presenter = SearchDefaultPresenter(interactor: interactor, + types: [SearchType.movie], + router: router, + schedulers: schedulers) + + _ = presenter.observeSearchState().subscribe(observer) -// presenter.viewDidLoad() + // When + presenter.search(query: "Tron") -// XCTAssertTrue(view.invokedShowHint) + // Then + let expectedEvents = [Recorded.next(0, SearchState.notSearching), + Recorded.next(0, SearchState.searching), + Recorded.next(0, SearchState.emptyResults)] + + XCTAssertEqual(observer.events, expectedEvents) } - func testSearchPresenter_performSearchSuccess_outputsTheResults() { -// let searchResultEntities = TraktEntitiesMock.createSearchResultsMock() -// let store = SearchMocks.Repository(results: searchResultEntities) -// let interactor = SearchService(repository: store) -// let presenter = SearchDefaultPresenter(view: view, interactor: interactor, resultOutput: output, types: [SearchType.movie]) -// -// presenter.search(query: "Tron") -// -// XCTAssertTrue(output.invokedHandleSearch) -// -// if output.invokedHandleSearchParameters?.results == nil { -// XCTFail("Parameters can't be nil") -// } else { -// XCTAssertEqual(output.invokedHandleSearchParameters!.results, searchResultEntities) -// } + func testSearchPresenter_performSearchFailure_emitsErrorState() { + // Given + let userInfo = [NSLocalizedDescriptionKey: "There is no active connection"] + let error = NSError(domain: "io.github.pietrocaselani.CouchTracker", code: 10, userInfo: userInfo) + let interactor = SearchMocks.Interactor(error: error) + let router = SearchMocks.Router() + let presenter = SearchDefaultPresenter(interactor: interactor, types: [.movie], router: router, schedulers: schedulers) + + _ = presenter.observeSearchState().subscribe(observer) + + // When + presenter.search(query: "Tron") + + // Then + let expectedEvents = [Recorded.next(0, SearchState.notSearching), + Recorded.next(0, SearchState.searching), + Recorded.next(0, SearchState.error(error: error))] + + XCTAssertEqual(observer.events, expectedEvents) } - func testSearchPresenter_performSearchReceivesNoData_notifyOutput() { -// let store = SearchMocks.Repository() -// let interactor = SearchService(repository: store) -// let presenter = SearchDefaultPresenter(view: view, interactor: interactor, resultOutput: output, types: [SearchType.movie]) -// -// presenter.search(query: "Tron") -// -// XCTAssertTrue(output.invokedHandleEmptySearchResult) -// } -// -// func testSearchPresenter_performSearchFailure_outputsErrorMessage() { -// let userInfo = [NSLocalizedDescriptionKey: "There is no active connection"] -// let error = NSError(domain: "io.github.pietrocaselani.CouchTracker", code: 10, userInfo: userInfo) -// let store = SearchMocks.ErrorRepository(error: error) -// let interactor = SearchService(repository: store) -// let presenter = SearchDefaultPresenter(view: view, interactor: interactor, resultOutput: output, types: [SearchType.movie]) -// -// presenter.search(query: "Tron") -// -// let expectedMessage = error.localizedDescription -// -// XCTAssertTrue(output.invokedHandleError) -// XCTAssertEqual(output.invokedHandleErrorParameters?.message, expectedMessage) + func testSearchPresenter_performCancel_emitsNotSearching() { + // Given + let interactor = SearchMocks.Interactor() + let router = SearchMocks.Router() + let presenter = SearchDefaultPresenter(interactor: interactor, + types: [SearchType.movie], + router: router, + schedulers: schedulers) + + _ = presenter.observeSearchState().subscribe(observer) + + // When + presenter.search(query: "Tron") + presenter.cancelSearch() + + // Then + let expectedEvents = [Recorded.next(0, SearchState.notSearching), + Recorded.next(0, SearchState.searching), + Recorded.next(0, SearchState.emptyResults), + Recorded.next(0, SearchState.notSearching)] + + XCTAssertEqual(observer.events, expectedEvents) } - func testSearchPresenter_performCancel_notifyOutput() { -// let store = SearchMocks.Repository() -// let interactor = SearchService(repository: store) -// let presenter = SearchDefaultPresenter(view: view, interactor: interactor, resultOutput: output, types: [SearchType.movie]) -// -// presenter.cancelSearch() -// -// XCTAssertEqual(output.searchState, SearchState.notSearching) + func testSearchPresenter_selectItem_notifyRouter() { + // Given + let results = TraktEntitiesMock.createSearchResultsMock() + + let expectedEntities = TraktEntitiesMock.createSearchResultEntitiesMock() + + let interactor = SearchMocks.Interactor(results: results) + let router = SearchMocks.Router() + let presenter = SearchDefaultPresenter(interactor: interactor, + types: [SearchType.movie], + router: router, + schedulers: schedulers) + + _ = presenter.observeSearchState().subscribe(observer) + + presenter.search(query: "Tron") + + // When + let selectedEntity = expectedEntities[1] + presenter.select(entity: selectedEntity) + + // Then + XCTAssertEqual(router.showViewInvokedCount, 1) + XCTAssertEqual(router.showViewLastParameter, selectedEntity) } } diff --git a/CouchTrackerCoreTests/TestSchedulers.swift b/CouchTrackerCoreTests/TestSchedulers.swift index fbc96209..45acd400 100644 --- a/CouchTrackerCoreTests/TestSchedulers.swift +++ b/CouchTrackerCoreTests/TestSchedulers.swift @@ -13,8 +13,12 @@ final class TestSchedulers: Schedulers { var mainQueue: DispatchQueue let testScheduler: TestScheduler - init(initialClock _: TestTime = 0) { - let scheduler = TestScheduler(initialClock: 0) + convenience init(initialClock: TestTime = 0) { + self.init(mainScheduler: MainScheduler.instance, + scheduler: TestScheduler(initialClock: initialClock)) + } + + init(mainScheduler: ImmediateSchedulerType, scheduler: TestScheduler) { testScheduler = scheduler networkQueue = DispatchQueue.main networkScheduler = scheduler @@ -23,7 +27,7 @@ final class TestSchedulers: Schedulers { ioQueue = DispatchQueue.main ioScheduler = scheduler mainQueue = DispatchQueue.main - mainScheduler = scheduler + self.mainScheduler = mainScheduler } func start() { diff --git a/CouchTrackerCoreTests/TraktEntitiesMock.swift b/CouchTrackerCoreTests/TraktEntitiesMock.swift index cdb0bdcb..c395dbac 100644 --- a/CouchTrackerCoreTests/TraktEntitiesMock.swift +++ b/CouchTrackerCoreTests/TraktEntitiesMock.swift @@ -1,3 +1,4 @@ +import CouchTrackerCore import TraktSwift final class TraktEntitiesMock { @@ -35,6 +36,24 @@ final class TraktEntitiesMock { return try! jsonDecoder.decode([SearchResult].self, from: data) } + static func createSearchResultEntitiesMock() -> [SearchResultEntity] { + let data = Search.textQuery(types: [.movie], query: "Tron", page: 0, limit: 100).sampleData + + return try! jsonDecoder.decode([SearchResult].self, from: data).compactMap { result in + let type: SearchResultType + switch result.type { + case .movie: + type = result.movie.map(SearchResultType.movie(movie:))! + case .show: + type = result.show.map(SearchResultType.show(show:))! + default: + return nil + } + + return SearchResultEntity(score: result.score, type: type) + } + } + static func createMoviesGenresMock() -> [Genre] { return try! jsonDecoder.decode([Genre].self, from: Genres.list(.movies).sampleData) } diff --git a/CouchTrackerCoreTests/TraktLogin/TraktLoginPresenterTest.swift b/CouchTrackerCoreTests/TraktLogin/TraktLoginPresenterTest.swift index b772492a..89339baa 100644 --- a/CouchTrackerCoreTests/TraktLogin/TraktLoginPresenterTest.swift +++ b/CouchTrackerCoreTests/TraktLogin/TraktLoginPresenterTest.swift @@ -1,5 +1,6 @@ @testable import CouchTrackerCore import RxSwift +import RxTest import XCTest final class TraktLoginPresenterTest: XCTestCase { @@ -10,7 +11,9 @@ final class TraktLoginPresenterTest: XCTestCase { override func setUp() { super.setUp() - schedulers = TestSchedulers(initialClock: 0) + let testScheduler = TestScheduler(initialClock: 0) + + schedulers = TestSchedulers(mainScheduler: testScheduler, scheduler: testScheduler) view = TraktLoginViewMock() output = TraktLoginOutputMock() } diff --git a/CouchTrackerCoreTests/TraktLogin/TraktTokenPolicyDeciderTest.swift b/CouchTrackerCoreTests/TraktLogin/TraktTokenPolicyDeciderTest.swift index cdf413da..017eb3b6 100644 --- a/CouchTrackerCoreTests/TraktLogin/TraktTokenPolicyDeciderTest.swift +++ b/CouchTrackerCoreTests/TraktLogin/TraktTokenPolicyDeciderTest.swift @@ -14,7 +14,9 @@ final class TraktTokenPolicyDeciderTest: XCTestCase { super.setUp() output = TraktLoginOutputMock() - schedulers = TestSchedulers(initialClock: 0) + let testScheduler = TestScheduler(initialClock: 0) + + schedulers = TestSchedulers(mainScheduler: testScheduler, scheduler: testScheduler) } override func tearDown() { diff --git a/CouchTrackerCoreTests/Trending/TrendingPresenterTest.swift b/CouchTrackerCoreTests/Trending/TrendingPresenterTest.swift index 3772d68f..02c4abfd 100644 --- a/CouchTrackerCoreTests/Trending/TrendingPresenterTest.swift +++ b/CouchTrackerCoreTests/Trending/TrendingPresenterTest.swift @@ -13,7 +13,9 @@ final class TrendingPresenterTest: XCTestCase { override func setUp() { super.setUp() - schedulers = TestSchedulers(initialClock: 0) + let testScheduler = TestScheduler(initialClock: 0) + + schedulers = TestSchedulers(mainScheduler: testScheduler, scheduler: testScheduler) view = TrendingViewMock() router = TrendingRouterMock() dataSource = TrendingDataSourceMock()