├── .gitignore ├── .swiftpm └── xcode │ └── xcshareddata │ └── xcschemes │ ├── AccountFeature.xcscheme │ ├── AiringTodayFeature.xcscheme │ ├── AppFeature.xcscheme │ ├── Networking.xcscheme │ ├── PopularsFeature.xcscheme │ ├── SearchShowsFeature.xcscheme │ ├── ShowDetailsFeature.xcscheme │ ├── ShowDetailsFeatureTests.xcscheme │ └── ShowListFeature.xcscheme ├── App ├── Config │ └── Config.xcconfig ├── Demos │ ├── AccountDemo │ │ ├── AccountFeatureDemoCoordinator.swift │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ └── Info.plist │ ├── AiringTodayDemo │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Info.plist │ │ └── TodayDemoCoordinator.swift │ ├── PopularDemo │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Info.plist │ │ └── PopularDemoCoordinator.swift │ ├── SearchShowsDemo │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Info.plist │ │ └── SearchShowsDemoCoordinator.swift │ ├── ShowDetailsDemo │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Info.plist │ │ └── ShowDetailsDemoCoordinator.swift │ └── ShowListDemo │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ │ ├── Info.plist │ │ └── ShowListDemoCoordinator.swift ├── Package.swift ├── TVToday.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ ├── AccountDemo.xcscheme │ │ ├── AiringTodayDemo.xcscheme │ │ └── TVToday.xcscheme └── iOS │ ├── AppConfigurations.swift │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon-ios-1024@1x.png │ │ ├── icon-ios-20@2x.png │ │ ├── icon-ios-20@3x.png │ │ ├── icon-ios-29@2x.png │ │ ├── icon-ios-29@3x.png │ │ ├── icon-ios-40@2x.png │ │ ├── icon-ios-40@3x.png │ │ ├── icon-ios-60@2x.png │ │ ├── icon-ios-60@3x.png │ │ ├── icon-ios-76@2x.png │ │ └── icon-ios-83.5@2x.png │ └── Contents.json │ ├── LaunchScreen.storyboard │ └── info.plist ├── LICENSE ├── Package.swift ├── README.md ├── Screenshots ├── Package.swift ├── dark │ ├── 01.png │ ├── 02.png │ ├── 03.png │ ├── 04.png │ ├── 05.png │ ├── 06.png │ ├── 07.png │ └── 08.png ├── dynamic-type-1.png ├── dynamic-type-2.png └── light │ ├── 01.png │ ├── 02.png │ ├── 03.png │ ├── 04.png │ ├── 05.png │ ├── 06.png │ ├── 07.png │ └── 08.png ├── Sources ├── AccountFeature │ ├── DIContainer │ │ ├── AccountCoordinator.swift │ │ ├── AccountCoordinatorProtocol.swift │ │ ├── DIContainer.swift │ │ └── Module.swift │ ├── Data │ │ ├── Network │ │ │ ├── DataMapping │ │ │ │ ├── AccountDTO.swift │ │ │ │ ├── NewRequestTokenDTO.swift │ │ │ │ └── NewSessionDTO.swift │ │ │ └── RequestTokenMapper.swift │ │ └── Repositories │ │ │ ├── DefaultAccountRemoteDataSource.swift │ │ │ ├── DefaultAccountRepository.swift │ │ │ ├── DefaultAuthRemoteDataSource.swift │ │ │ └── DefaultAuthRepository.swift │ ├── Domain │ │ ├── Entities │ │ │ ├── Account.swift │ │ │ ├── NewRequestToken.swift │ │ │ └── NewSession.swift │ │ ├── Interfaces │ │ │ └── Repositories │ │ │ │ ├── AccountRemoteDataSource.swift │ │ │ │ ├── AccountRepository.swift │ │ │ │ ├── AuthRemoteDataSource.swift │ │ │ │ └── AuthRepository.swift │ │ └── UseCases │ │ │ ├── CreateSession.swift │ │ │ ├── CreateTokenUseCase.swift │ │ │ ├── DeleteLoggedUserUseCase.swift │ │ │ └── FetchAccountDetailsUseCase.swift │ └── Presentation │ │ ├── Account │ │ ├── View │ │ │ └── AccountViewController.swift │ │ └── ViewModel │ │ │ └── AccountViewModel.swift │ │ ├── AuthPermission │ │ ├── View │ │ │ ├── AuthPermissionRootView.swift │ │ │ └── AuthPermissionViewController.swift │ │ └── ViewModel │ │ │ ├── AuthPermissionViewModel.swift │ │ │ └── AuthPermissionViewModelProtocol.swift │ │ ├── Profile │ │ ├── View │ │ │ ├── Cells │ │ │ │ ├── LogoutTableViewCell.swift │ │ │ │ └── ProfileTableViewCell.swift │ │ │ ├── ProfileRootView.swift │ │ │ └── ProfileViewController.swift │ │ └── ViewModel │ │ │ ├── ProfileSectionModel.swift │ │ │ ├── ProfileViewModel.swift │ │ │ └── ProfileViewModelProtocol.swift │ │ └── SignIn │ │ ├── View │ │ ├── SignInRootView.swift │ │ └── SignInViewController.swift │ │ └── ViewModel │ │ ├── SignInViewModel.swift │ │ └── SignInViewModelProtocol.swift ├── AiringTodayFeature │ ├── DIContainer │ │ ├── AiringTodayCoordinator.swift │ │ ├── AiringTodayCoordinatorProtocol.swift │ │ ├── DIContainer.swift │ │ └── Module.swift │ ├── Domain │ │ └── UseCases │ │ │ └── DefaultFetchAiringTodayTVShowsUseCase.swift │ └── Presentation │ │ ├── View │ │ ├── AiringTodayRootView.swift │ │ ├── AiringTodayRootViewCompositional.swift │ │ ├── AiringTodayViewController.swift │ │ ├── Cells │ │ │ └── AiringTodayCollectionViewCell.swift │ │ ├── CustomFlowLayout.swift │ │ └── Customs │ │ │ └── FooterReusableView.swift │ │ └── ViewModel │ │ ├── AiringTodayCollectionViewModel.swift │ │ ├── AiringTodayViewModel.swift │ │ └── AiringTodayViewModelProtocol.swift ├── AppFeature │ ├── AppConfigurationProtocol.swift │ ├── AppCoordinator.swift │ ├── AppDIContainer.swift │ └── SignedCoordinator.swift ├── KeyChainStorage │ ├── DefaultKeychainStorage.swift │ └── KeychainItemStorage.swift ├── Networking │ └── ApiClient │ │ ├── ApiClient+Live.swift │ │ ├── NetworkLogger+Live.swift │ │ ├── NetworkLogger.swift │ │ └── URLSessionManager.swift ├── NetworkingInterface │ ├── ApiClient │ │ ├── ApiClient+Test.swift │ │ ├── ApiClient.swift │ │ ├── ApiError.swift │ │ ├── Endpoint.swift │ │ ├── JSONResponseDecoder.swift │ │ ├── NetworkConfig.swift │ │ └── URLRequestable.swift │ └── NetworkError.swift ├── Persistence │ ├── Entities │ │ ├── Search.swift │ │ ├── SearchDLO.swift │ │ ├── ShowVisited.swift │ │ └── ShowVisitedDLO.swift │ ├── Interfaces │ │ ├── DataSources │ │ │ ├── SearchLocalDataSource.swift │ │ │ └── ShowsVisitedLocalDataSource.swift │ │ └── Repositories │ │ │ ├── SearchLocalRepository.swift │ │ │ ├── SearchLocalRepositoryProtocol.swift │ │ │ ├── ShowsVisitedLocalRepository.swift │ │ │ ├── ShowsVisitedLocalRepositoryProtocol+Mock.swift │ │ │ └── ShowsVisitedLocalRepositoryProtocol.swift │ └── UseCases │ │ ├── FetchSearchsUseCase.swift │ │ ├── FetchVisitedShowsUseCase.swift │ │ └── RecentVisitedShowDidChangeUseCase.swift ├── PersistenceLive │ ├── Internal │ │ ├── CoreDataStorage.xcdatamodeld │ │ │ └── Model.xcdatamodel │ │ │ │ └── contents │ │ ├── Entities │ │ │ ├── CDRecentSearch+PersistenceStore.swift │ │ │ ├── CDRecentSearch.swift │ │ │ ├── CDShowVisited+PersistenceStore.swift │ │ │ └── CDShowVisited.swift │ │ ├── Helpers │ │ │ ├── Managed.swift │ │ │ ├── NSManagedObjectContext+Extensions.swift │ │ │ └── PersistenceStore.swift │ │ └── Repositories │ │ │ ├── CoreDataSearchQueriesStorage.swift │ │ │ └── CoreDataShowVisitedStorage.swift │ └── Public │ │ ├── CoreDataStorage.swift │ │ └── LocalDataSources.swift ├── PopularsFeature │ ├── DIContainer │ │ ├── DIContainer.swift │ │ ├── Module.swift │ │ ├── PopularCoordinator.swift │ │ └── PopularCoordinatorProtocol.swift │ ├── Domain │ │ └── UseCases │ │ │ └── DefaultFetchPopularTVShowsUseCase.swift │ └── Presentation │ │ ├── View │ │ ├── PopularsRootView.swift │ │ ├── PopularsViewController.swift │ │ └── SectionPopularView.swift │ │ └── ViewModel │ │ └── PopularViewModel.swift ├── SearchShowsFeature │ ├── DIContainer │ │ ├── DIContainer.swift │ │ ├── Module.swift │ │ ├── SearchCoordinator.swift │ │ └── SearchCoordinatorProtocol.swift │ ├── Data │ │ ├── Network │ │ │ └── DataMapping │ │ │ │ └── GenreListDTO.swift │ │ └── Repositories │ │ │ ├── DefaultGenreRemoteDataSource.swift │ │ │ └── DefaultGenresRepository.swift │ ├── Domain │ │ ├── Entities │ │ │ └── GenreList.swift │ │ ├── Interfaces │ │ │ └── Repositories │ │ │ │ ├── GenreRemoteDataSource.swift │ │ │ │ └── GenresRepository.swift │ │ └── UseCases │ │ │ ├── FetchGenresUseCase.swift │ │ │ └── SearchTVShowsUseCase.swift │ └── Presentation │ │ ├── Search │ │ ├── View │ │ │ └── SearchViewController.swift │ │ └── ViewModel │ │ │ └── SearchViewModel.swift │ │ ├── SearchOptions │ │ ├── Cells │ │ │ ├── GenreTableViewCell │ │ │ │ ├── GenreTableViewCell.swift │ │ │ │ └── GenreViewModel.swift │ │ │ ├── VisitedShowCollectionViewCell │ │ │ │ └── VisitedShowCollectionViewCell.swift │ │ │ └── VisitedShowTableViewCell │ │ │ │ ├── VisitedShowSectionModel.swift │ │ │ │ ├── VisitedShowTableViewCell.swift │ │ │ │ └── VisitedShowViewModel.swift │ │ ├── View │ │ │ ├── SearchOptionRootView.swift │ │ │ ├── SearchOptionsSectionModel.swift │ │ │ ├── SearchOptionsViewController.swift │ │ │ └── SearchSectionTableViewDiffableDataSource.swift │ │ └── ViewModel │ │ │ ├── SearchOptionsViewModel.swift │ │ │ ├── SearchOptionsViewModelProtocol.swift │ │ │ └── SearchViewState.swift │ │ └── SearchResults │ │ ├── Cells │ │ └── RecentSearchTableViewCell.swift │ │ ├── View │ │ ├── CustomSectionTableViewDiffableDataSource.swift │ │ ├── ResultListView.swift │ │ ├── ResultSearchSectionModel.swift │ │ └── ResultsSearchViewController.swift │ │ └── ViewModel │ │ ├── ResultsSearchViewModel.swift │ │ └── ResultsSearchViewModelProtocol.swift ├── Shared │ └── Sources │ │ ├── Coordinator.swift │ │ ├── Data │ │ ├── DataSources │ │ │ ├── Interfaces │ │ │ │ ├── AccessTokenLocalDataSource.swift │ │ │ │ ├── AccountTVShowsDetailsRemoteDataSourceProtocol.swift │ │ │ │ ├── AccountTVShowsRemoteDataSourceProtocol.swift │ │ │ │ ├── LoggedUserLocalDataSource.swift │ │ │ │ ├── RequestTokenLocalDataSource.swift │ │ │ │ ├── TVShowsDetailsRemoteDataSourceProtocol.swift │ │ │ │ └── TVShowsRemoteDataSourceProtocol.swift │ │ │ └── RemoteDataSources │ │ │ │ ├── AccountTVShowsDetailsRemoteDataSource.swift │ │ │ │ ├── AccountTVShowsRemoteDataSource.swift │ │ │ │ ├── DefaultTVShowsRemoteDataSource.swift │ │ │ │ └── TVShowsDetailsRemoteDataSource.swift │ │ ├── Network │ │ │ └── DataMapping │ │ │ │ ├── DTOs │ │ │ │ ├── GenreDTO.swift │ │ │ │ ├── TVShowAccountStatusDTO.swift │ │ │ │ ├── TVShowActionStatusDTO.swift │ │ │ │ ├── TVShowDetailDTO.swift │ │ │ │ └── TVShowPageDTO.swift │ │ │ │ └── Mappers │ │ │ │ ├── DefaultAccountTVShowDetailsMapper.swift │ │ │ │ ├── DefaultTVShowDetailsMapper.swift │ │ │ │ ├── DefaultTVShowPageMapper.swift │ │ │ │ └── MappersInterfaces.swift │ │ └── Repositories │ │ │ ├── AccessTokenRepository.swift │ │ │ ├── DefaultAccountTVShowsDetailsRepository.swift │ │ │ ├── DefaultAccountTVShowsRepository.swift │ │ │ ├── DefaultTVShowsDetailRepository.swift │ │ │ ├── DefaultTVShowsPageRepository.swift │ │ │ ├── LoggedUserRepository.swift │ │ │ └── RequestTokenRepository.swift │ │ ├── Domain │ │ ├── Entities │ │ │ ├── Account.swift │ │ │ ├── Genre.swift │ │ │ ├── TVShowAccountStatus.swift │ │ │ ├── TVShowActionStatus.swift │ │ │ ├── TVShowDetail.swift │ │ │ └── TVShowPage.swift │ │ ├── ErrorEnvelope.swift │ │ ├── Interfaces │ │ │ └── Repositories │ │ │ │ ├── AccessTokenRepositoryProtocol.swift │ │ │ │ ├── AccountTVShowsDetailsRepository.swift │ │ │ │ ├── AccountTVShowsRepository.swift │ │ │ │ ├── LoggedUserRepositoryProtocol.swift │ │ │ │ ├── RequestTokenRepositoryProtocol.swift │ │ │ │ ├── TVShowsDetailsRepository.swift │ │ │ │ └── TVShowsPageRepository.swift │ │ └── UseCases │ │ │ ├── FetchLoggedUser.swift │ │ │ └── FetchShowsUseCase.swift │ │ ├── Language.swift │ │ └── SimpleViewState.swift ├── ShowDetailsFeature │ ├── DIContainer │ │ ├── DIContainer.swift │ │ ├── Module.swift │ │ ├── TVShowDetailCoordinator.swift │ │ └── TVShowDetailCoordinatorProtocol.swift │ ├── Data │ │ ├── Network │ │ │ └── DataMapping │ │ │ │ ├── TVEpisodesMapper.swift │ │ │ │ ├── TVShowEpisodeDTO.swift │ │ │ │ └── TVShowSeasonDTO.swift │ │ └── Repositories │ │ │ ├── DefaultTVEpisodesRemoteDataSource.swift │ │ │ └── DefaultTVEpisodesRepository.swift │ ├── Domain │ │ ├── Entities │ │ │ ├── TVShowEpisode.swift │ │ │ └── TVShowSeason.swift │ │ ├── Interfaces │ │ │ └── Repositories │ │ │ │ └── TVEpisodesRepository.swift │ │ └── UseCases │ │ │ ├── FetchEpisodesUseCase.swift │ │ │ ├── FetchTVAccountStates.swift │ │ │ ├── FetchTVShowDetails.swift │ │ │ ├── MarkAsFavoriteUseCase.swift │ │ │ └── SaveToWatchListUseCase.swift │ └── Presentation │ │ ├── SeasonScene │ │ ├── SectionModel │ │ │ ├── Episode+SectionModelType.swift │ │ │ └── SeasonsSectionModel.swift │ │ ├── View │ │ │ ├── Cells │ │ │ │ ├── EpisodesList │ │ │ │ │ └── EpisodeItemTableViewCell.swift │ │ │ │ ├── Header │ │ │ │ │ └── HeaderSeasonsTableViewCell.swift │ │ │ │ └── SeasonEpisodeList │ │ │ │ │ ├── SeasonEpisodeCollectionViewCell.swift │ │ │ │ │ └── SeasonListTableViewCell.swift │ │ │ ├── EpisodesListRootView.swift │ │ │ └── EpisodesListViewController.swift │ │ └── ViewModel │ │ │ ├── EpisodeItemViewModel.swift │ │ │ ├── EpisodesListViewModel.swift │ │ │ ├── SeasonEpisodeViewModel.swift │ │ │ ├── SeasonHeaderViewModel.swift │ │ │ └── SeasonListViewModel.swift │ │ └── ShowDetailsScene │ │ ├── View │ │ ├── TVShowDetailRootView.swift │ │ └── TVShowDetailViewController.swift │ │ └── ViewModel │ │ ├── TVShowDetailInfo.swift │ │ └── TVShowDetailViewModel.swift ├── ShowDetailsFeatureInterface │ └── ShowDetailsModule.swift ├── ShowListFeature │ ├── DIContainer │ │ ├── DIContainer.swift │ │ ├── Module.swift │ │ ├── TVShowListCoordinator.swift │ │ └── TVShowListCoordinatorProtocol.swift │ ├── Domain │ │ └── UseCases │ │ │ ├── DefaultUserFavoritesShowsUseCase.swift │ │ │ ├── DefaultUserWatchListShowsUseCase.swift │ │ │ └── FetchShowsByGenreTVShowsUseCase.swift │ └── Presentation │ │ ├── View │ │ ├── SectionTVShowListView.swift │ │ ├── TVShowListRootView.swift │ │ └── TVShowListViewController.swift │ │ └── ViewModel │ │ └── TVShowListViewModel.swift ├── ShowListFeatureInterface │ └── ModuleInterface.swift └── UI │ ├── Components │ ├── Cells │ │ ├── GenericViewCell.swift │ │ ├── TVShowCellViewModel.swift │ │ └── TVShowViewCell.swift │ ├── DefaultRefreshControl.swift │ ├── EmptyView.swift │ ├── ErrorView.swift │ ├── LoadableButton.swift │ ├── LoadingView.swift │ ├── MessageImageView.swift │ └── MessageView.swift │ ├── Extensions │ ├── Dequeuable.swift │ ├── Fonts.swift │ ├── UICollectionView+Extensions.swift │ ├── UIImage+Loader.swift │ ├── UIImageView+Kingfisher.swift │ ├── UINavigationController+Create.swift │ ├── UIRefreshControl+Extensions.swift │ ├── UITableView+Extensions.swift │ ├── UIView+Extensions.swift │ └── UIViewController+Extensions.swift │ ├── Generated │ └── Strings+Generated.swift │ ├── Localized │ └── Strings+localized.swift │ ├── Protocols │ ├── Emptiable.swift │ ├── Loadable.swift │ ├── NiblessCollectionViewCell.swift │ ├── NiblessTableViewCell.swift │ ├── NiblessView.swift │ ├── NiblessViewController.swift │ └── Retryable.swift │ └── Resources │ ├── Assets.xcassets │ ├── Contents.json │ ├── account.tv.imageset │ │ ├── Contents.json │ │ └── kisspng-television-free-content-free-to-air-clip-art-examples-of-grow-foods-clipart-5a886cb5abb616.3415104615188901657033.png │ ├── empty.placeholder.imageset │ │ ├── Contents.json │ │ └── Error009.png │ ├── error.list.placeholder.imageset │ │ ├── Contents.json │ │ └── error5.png │ ├── error.placeholder.imageset │ │ ├── Contents.json │ │ └── Error010.png │ ├── loginbackground.imageset │ │ ├── Contents.json │ │ ├── ic_boton_primario_1.png │ │ ├── ic_boton_primario_1@2x.png │ │ └── ic_boton_primario_1@3x.png │ ├── placeholder.imageset │ │ ├── Contents.json │ │ └── placeholder2.png │ └── tvshowEmpty.imageset │ │ ├── Contents.json │ │ └── tvshowEmpty.png │ ├── en.lproj │ └── Localizable.strings │ └── es.lproj │ └── Localizable.strings ├── Tests ├── AccountFeatureTests │ ├── Account │ │ ├── Mocks │ │ │ ├── Entities │ │ │ │ └── AccountResult+stub.swift │ │ │ ├── UsesCases │ │ │ │ ├── CreateSessionUseCaseMock.swift │ │ │ │ ├── CreateTokenUseCaseMock.swift │ │ │ │ ├── DeleteLoguedUserUseCaseMock.swift │ │ │ │ ├── FetchAccountDetailsUseCaseMock.swift │ │ │ │ └── FetchLoggedUserMock.swift │ │ │ └── ViewModels │ │ │ │ ├── AccountViewModelMock.swift │ │ │ │ └── AuthPermissionViewModelMock.swift │ │ └── Presentation │ │ │ ├── AccountViewControllerFactoryMock.swift │ │ │ ├── AccountViewModelTests.swift │ │ │ ├── AccountViewTests.swift │ │ │ └── __Snapshots__ │ │ │ └── AccountViewTests │ │ │ ├── test_WhenViewIsLogged_thenShowProfileScreen.1.png │ │ │ ├── test_WhenViewIsLogged_thenShowProfileScreen.2.png │ │ │ ├── test_WhenViewIsLogin_thenShowLoginScreen.1.png │ │ │ └── test_WhenViewIsLogin_thenShowLoginScreen.2.png │ ├── Profile │ │ ├── Mocks │ │ │ └── ProfileViewModelMock.swift │ │ └── Presentation │ │ │ └── ProfileViewModelTests.swift │ └── SignIn │ │ ├── Mocks │ │ └── ViewModels │ │ │ ├── SignInViewModelDelegateMock.swift │ │ │ └── SignInViewModelMock.swift │ │ └── Presentation │ │ ├── SignInViewModelTests.swift │ │ ├── SignInViewTests.swift │ │ └── __Snapshots__ │ │ └── SignInViewTests │ │ ├── test_WhenViewIsError_thenShowErrorScreen.1.png │ │ ├── test_WhenViewIsError_thenShowErrorScreen.2.png │ │ ├── test_WhenViewIsInitial_thenShowInitialScreen.1.png │ │ ├── test_WhenViewIsInitial_thenShowInitialScreen.2.png │ │ ├── test_WhenViewIsLoading_thenShowLoadingScreen.1.png │ │ └── test_WhenViewIsLoading_thenShowLoadingScreen.2.png ├── AiringTodayFeatureTests │ ├── Mocks │ │ ├── AiringTodayViewModelMock.swift │ │ └── BuildPages.swift │ └── Presentation │ │ ├── AiringTodayViewModelTests.swift │ │ └── SnapshotTests │ │ ├── AiringTodayViewTests.swift │ │ └── __Snapshots__ │ │ └── AiringTodayViewTests │ │ ├── test_WhenViewIsEmpty_thenShowEmptyScreen.1.png │ │ ├── test_WhenViewIsEmpty_thenShowEmptyScreen.2.png │ │ ├── test_WhenViewIsError_thenShowErrorScreen.1.png │ │ ├── test_WhenViewIsError_thenShowErrorScreen.2.png │ │ ├── test_WhenViewIsLoading_thenShowLoadingScreen.1.png │ │ ├── test_WhenViewIsLoading_thenShowLoadingScreen.2.png │ │ ├── test_WhenViewPaging_thenShowPagingScreen.1.png │ │ ├── test_WhenViewPaging_thenShowPagingScreen.2.png │ │ ├── test_WhenViewPopulated_thenShowPopulatedScreen.1.png │ │ └── test_WhenViewPopulated_thenShowPopulatedScreen.2.png ├── AppFeatureTests │ └── AppFeature.xctestplan ├── CommonMocks │ ├── FetchShowsUseCaseMock.swift │ ├── MappingHelpers.swift │ ├── TVShow+Stub.swift │ └── TVShowPage+Stub.swift ├── NetworkingTests │ └── ApiClientTests.swift ├── PopularsFeatureTests │ ├── Mocks │ │ ├── PopularViewModel+Mock.swift │ │ └── TVShowResult+Build.swift │ └── Presentation │ │ ├── PopularViewModelTests.swift │ │ └── SnapshotTests │ │ ├── PopularViewTests.swift │ │ └── __Snapshots__ │ │ └── PopularViewTests │ │ ├── test_WhenViewIsEmpty_thenShowEmptyScreen.1.png │ │ ├── test_WhenViewIsEmpty_thenShowEmptyScreen.2.png │ │ ├── test_WhenViewIsError_thenShowErrorScreen.1.png │ │ ├── test_WhenViewIsError_thenShowErrorScreen.2.png │ │ ├── test_WhenViewIsLoading_thenShowLoadingScreen.1.png │ │ ├── test_WhenViewIsLoading_thenShowLoadingScreen.2.png │ │ ├── test_WhenViewPaging_thenShowPagingScreen.1.png │ │ ├── test_WhenViewPaging_thenShowPagingScreen.2.png │ │ ├── test_WhenViewPopulated_thenShowPopulatedScreen.1.png │ │ └── test_WhenViewPopulated_thenShowPopulatedScreen.2.png ├── SearchShowsFeatureTests │ ├── SearchOptions │ │ ├── Mocks │ │ │ ├── Entities │ │ │ │ ├── Genre+Stub.swift │ │ │ │ ├── ShowVisited+Build.swift │ │ │ │ └── ShowVisited+Stub.swift │ │ │ ├── UsesCases │ │ │ │ ├── FetchGenresUseCase+Mock.swift │ │ │ │ ├── FetchVisitedShowsUseCase+Mock.swift │ │ │ │ └── RecentVisitedShowDidChangeUseCase+Mock.swift │ │ │ └── ViewModels │ │ │ │ ├── GenreViewModel+Mock.swift │ │ │ │ └── SearchOptionsViewModel+Mock.swift │ │ └── Presentation │ │ │ ├── SearchOptionsViewModelTests.swift │ │ │ └── SnapshotTests │ │ │ ├── SearchShowsOptionsHelper.swift │ │ │ ├── SearchShowsOptionsViewTests.swift │ │ │ └── __Snapshots__ │ │ │ └── SearchShowsOptionsViewTests │ │ │ ├── test_WhenViewIsEmpty_thenShowEmptyScreen.1.png │ │ │ ├── test_WhenViewIsEmpty_thenShowEmptyScreen.2.png │ │ │ ├── test_WhenViewIsError_thenShowErrorScreen.1.png │ │ │ ├── test_WhenViewIsError_thenShowErrorScreen.2.png │ │ │ ├── test_WhenViewIsLoading_thenShowLoadingScreen.1.png │ │ │ ├── test_WhenViewIsLoading_thenShowLoadingScreen.2.png │ │ │ ├── test_WhenViewPopulated_thenShowPopulatedScreen.1.png │ │ │ └── test_WhenViewPopulated_thenShowPopulatedScreen.2.png │ └── SearchResults │ │ ├── Mocks │ │ ├── ResultsSearchViewModelMock.swift │ │ └── UsesCases │ │ │ ├── FetchSearchsUseCase+Mock.swift │ │ │ └── SearchTVShowsUseCase+Mock.swift │ │ └── Presentation │ │ ├── ResultsSearchViewModelTests.swift │ │ └── SnapshotTests │ │ ├── ResultsSearchViewHelper.swift │ │ ├── ResultsSearchViewTests.swift │ │ └── __Snapshots__ │ │ └── ResultsSearchViewTests │ │ ├── test_WhenViewDidError_thenShowErrorScreen.1.png │ │ ├── test_WhenViewDidError_thenShowErrorScreen.2.png │ │ ├── test_WhenViewInitial_thenShowInitialScreen.1.png │ │ ├── test_WhenViewInitial_thenShowInitialScreen.2.png │ │ ├── test_WhenViewIsEmpty_thenShowEmptyScreen.1.png │ │ ├── test_WhenViewIsEmpty_thenShowEmptyScreen.2.png │ │ ├── test_WhenViewIsLoading_thenShowLoadingScreen.1.png │ │ ├── test_WhenViewIsLoading_thenShowLoadingScreen.2.png │ │ ├── test_WhenViewIsPopulated_thenShowPopulatedScreen.1.png │ │ └── test_WhenViewIsPopulated_thenShowPopulatedScreen.2.png ├── SharedTests │ └── TestLocalizable.swift ├── ShowDetailsFeatureTests │ ├── DetailsScene │ │ ├── Mocks │ │ │ ├── Entities │ │ │ │ ├── Account+Stub.swift │ │ │ │ ├── TVShowAccountStateResult+Stub.swift │ │ │ │ ├── TVShowDetailInfo+Stub.swift │ │ │ │ └── TVShowDetailResult+Stub.swift │ │ │ ├── UsesCases │ │ │ │ ├── FetchLoggedUserMock.swift │ │ │ │ ├── FetchTVAccountStateMock.swift │ │ │ │ ├── FetchTVShowDetailsUseCaseMock.swift │ │ │ │ ├── MarkAsFavoriteUseCaseMock.swift │ │ │ │ └── SaveToWatchListUseCaseMock.swift │ │ │ └── ViewModel │ │ │ │ └── TVShowDetailViewModel+Mock.swift │ │ └── Presentation │ │ │ ├── View │ │ │ ├── TVShowDetailViewTests.swift │ │ │ └── __Snapshots__ │ │ │ │ └── TVShowDetailViewTests │ │ │ │ ├── test_WhenViewIsError_thenShowErrorScreen.1.png │ │ │ │ ├── test_WhenViewIsError_thenShowErrorScreen.2.png │ │ │ │ ├── test_WhenViewIsLoading_thenShowLoadingScreen.1.png │ │ │ │ ├── test_WhenViewIsLoading_thenShowLoadingScreen.2.png │ │ │ │ ├── test_WhenViewPopulated_thenShowPopulatedScreen.1.png │ │ │ │ ├── test_WhenViewPopulated_thenShowPopulatedScreen.2.png │ │ │ │ ├── test_WhenViewPopulated_thenShowPopulatedScreen.3.png │ │ │ │ └── test_WhenViewPopulated_thenShowPopulatedScreen.4.png │ │ │ └── ViewModel │ │ │ ├── FavoriteTapsTests.swift │ │ │ ├── TVShowDetailViewModelGuestUsersTests.swift │ │ │ ├── TVShowDetailViewModelLoggedUsersTests.swift │ │ │ └── WatchListTapsTests.swift │ └── SeasonsScene │ │ ├── Mocks │ │ ├── Entities │ │ │ └── Episode+Stub.swift │ │ ├── UseCases │ │ │ └── FetchEpisodesUseCase+Mock.swift │ │ └── ViewModel │ │ │ ├── EpisodesListViewModel+Mock.swift │ │ │ └── SeasonListViewModelMock.swift │ │ └── Presentation │ │ ├── View │ │ ├── EpisodesListViewTests.swift │ │ └── __Snapshots__ │ │ │ └── EpisodesListViewTests │ │ │ ├── test_WhenViewIsLoading_thenShow_LoadingScreen.1.png │ │ │ ├── test_WhenViewIsLoading_thenShow_LoadingScreen.2.png │ │ │ ├── test_WhenViewModelDidPopulated_thenShow_PopulatedScreen.1.png │ │ │ ├── test_WhenViewModelDidPopulated_thenShow_PopulatedScreen.2.png │ │ │ ├── test_WhenViewModelLoadSeason_thenShow_LoadingSeasonScreen.1.png │ │ │ ├── test_WhenViewModelLoadSeason_thenShow_LoadingSeasonScreen.2.png │ │ │ ├── test_WhenViewModelReturnsEmpty_thenShow_EmptyScreen.1.png │ │ │ ├── test_WhenViewModelReturnsEmpty_thenShow_EmptyScreen.2.png │ │ │ ├── test_WhenViewModelReturnsErrorSeason_thenShow_ErrorSeasonScreen.1.png │ │ │ ├── test_WhenViewModelReturnsErrorSeason_thenShow_ErrorSeasonScreen.2.png │ │ │ ├── test_WhenViewModelReturnsError_thenShow_ErrorScreen.1.png │ │ │ └── test_WhenViewModelReturnsError_thenShow_ErrorScreen.2.png │ │ └── ViewModel │ │ └── EpisodesListViewModelTests.swift └── ShowListFeatureTests │ ├── Mocks │ ├── TVShowListViewModelMock.swift │ └── TVShowResult+Build.swift │ └── Presentation │ ├── ShowListModelTests.swift │ └── SnapshotTests │ ├── TVShowListViewTests.swift │ └── __Snapshots__ │ └── TVShowListViewTests │ ├── test_WhenViewIsEmpty_thenShowEmptyScreen.1.png │ ├── test_WhenViewIsEmpty_thenShowEmptyScreen.2.png │ ├── test_WhenViewIsError_thenShowErrorScreen.1.png │ ├── test_WhenViewIsError_thenShowErrorScreen.2.png │ ├── test_WhenViewIsLoading_thenShowLoadingScreen.1.png │ ├── test_WhenViewIsLoading_thenShowLoadingScreen.2.png │ ├── test_WhenViewPaging_thenShowPagingScreen.1.png │ ├── test_WhenViewPaging_thenShowPagingScreen.2.png │ ├── test_WhenViewPopulated_thenShowPopulatedScreen.1.png │ └── test_WhenViewPopulated_thenShowPopulatedScreen.2.png └── bin ├── Package.swift ├── structured-swift5-custom.stencil └── swiftgen.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /Package.resolved 5 | Package.resolved 6 | .package.resolved 7 | 8 | /*.xcodeproj 9 | xcuserdata/ 10 | *.build 11 | 12 | App/.swiftpm 13 | Screenshots/.swiftpm 14 | -------------------------------------------------------------------------------- /App/Config/Config.xcconfig: -------------------------------------------------------------------------------- 1 | SLASH = / 2 | 3 | API_BASE_URL=https:$(SLASH)/api.themoviedb.org 4 | 5 | API_KEY=06e1a8c1f39b7a033e2efb972625fee2 6 | 7 | IMAGE_BASE_URL=https:$(SLASH)/image.tmdb.org 8 | 9 | AUTHENTICATE_BASE_URL=https:$(SLASH)/themoviedb.org/authenticate 10 | 11 | GRAVATAR_BASE_URL=https:$(SLASH)/gravatar.com/avatar 12 | 13 | // Default 14 | ASSETCATALOG_COMPILER_APPICON_NAME=AppIcon 15 | 16 | CODE_SIGN_STYLE=Automatic 17 | 18 | PRODUCT_BUNDLE_IDENTIFIER=home.rcaos.tvtoday 19 | 20 | SWIFT_VERSION=5.0 21 | 22 | TARGETED_DEVICE_FAMILY=1,2 23 | -------------------------------------------------------------------------------- /App/Demos/AccountDemo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /App/Demos/AccountDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /App/Demos/AccountDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | API_BASE_URL 6 | $(API_BASE_URL) 7 | API_KEY 8 | $(API_KEY) 9 | IMAGE_BASE_URL 10 | $(IMAGE_BASE_URL) 11 | AUTHENTICATE_BASE_URL 12 | $(AUTHENTICATE_BASE_URL) 13 | GRAVATAR_BASE_URL 14 | $(GRAVATAR_BASE_URL) 15 | UIApplicationSceneManifest 16 | 17 | UIApplicationSupportsMultipleScenes 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /App/Demos/AiringTodayDemo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /App/Demos/AiringTodayDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /App/Demos/AiringTodayDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | API_BASE_URL 6 | $(API_BASE_URL) 7 | API_KEY 8 | $(API_KEY) 9 | IMAGE_BASE_URL 10 | $(IMAGE_BASE_URL) 11 | UIApplicationSceneManifest 12 | 13 | UIApplicationSupportsMultipleScenes 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /App/Demos/PopularDemo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /App/Demos/PopularDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /App/Demos/PopularDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | API_BASE_URL 6 | $(API_BASE_URL) 7 | API_KEY 8 | $(API_KEY) 9 | IMAGE_BASE_URL 10 | $(IMAGE_BASE_URL) 11 | 12 | 13 | -------------------------------------------------------------------------------- /App/Demos/SearchShowsDemo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /App/Demos/SearchShowsDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /App/Demos/SearchShowsDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | API_BASE_URL 6 | $(API_BASE_URL) 7 | API_KEY 8 | $(API_KEY) 9 | IMAGE_BASE_URL 10 | $(IMAGE_BASE_URL) 11 | UIApplicationSceneManifest 12 | 13 | UIApplicationSupportsMultipleScenes 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /App/Demos/ShowDetailsDemo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /App/Demos/ShowDetailsDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /App/Demos/ShowDetailsDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | API_BASE_URL 6 | $(API_BASE_URL) 7 | API_KEY 8 | $(API_KEY) 9 | IMAGE_BASE_URL 10 | $(IMAGE_BASE_URL) 11 | UIApplicationSceneManifest 12 | 13 | UIApplicationSupportsMultipleScenes 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /App/Demos/ShowListDemo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /App/Demos/ShowListDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /App/Demos/ShowListDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | API_BASE_URL 6 | $(API_BASE_URL) 7 | API_KEY 8 | $(API_KEY) 9 | IMAGE_BASE_URL 10 | $(IMAGE_BASE_URL) 11 | UIApplicationSceneManifest 12 | 13 | UIApplicationSupportsMultipleScenes 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /App/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | 3 | // Leave blank. This is only here so that Xcode doesn't display it. 4 | 5 | import PackageDescription 6 | 7 | let package = Package( 8 | name: "App", 9 | products: [], 10 | targets: [] 11 | ) 12 | -------------------------------------------------------------------------------- /App/TVToday.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /App/TVToday.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /App/iOS/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MyMovies 4 | // 5 | // Created by Jeans on 8/20/19. 6 | // Copyright © 2019 Jeans. All rights reserved. 7 | // 8 | 9 | import AppFeature 10 | import UIKit 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | var window: UIWindow? 16 | let appDIContainer = AppDIContainer(appConfigurations: AppConfigurations()) 17 | var appCoordinator: AppCoordinator? 18 | 19 | func application(_ application: UIApplication, 20 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 21 | UINavigationController.replaceAppearance() 22 | 23 | window = UIWindow(frame: UIScreen.main.bounds) 24 | appCoordinator = AppCoordinator(window: window!, appDIContainer: appDIContainer) 25 | appCoordinator?.start() 26 | 27 | return true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /App/iOS/Assets.xcassets/AppIcon.appiconset/icon-ios-1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/App/iOS/Assets.xcassets/AppIcon.appiconset/icon-ios-1024@1x.png -------------------------------------------------------------------------------- /App/iOS/Assets.xcassets/AppIcon.appiconset/icon-ios-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/App/iOS/Assets.xcassets/AppIcon.appiconset/icon-ios-20@2x.png -------------------------------------------------------------------------------- /App/iOS/Assets.xcassets/AppIcon.appiconset/icon-ios-20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/App/iOS/Assets.xcassets/AppIcon.appiconset/icon-ios-20@3x.png -------------------------------------------------------------------------------- /App/iOS/Assets.xcassets/AppIcon.appiconset/icon-ios-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/App/iOS/Assets.xcassets/AppIcon.appiconset/icon-ios-29@2x.png -------------------------------------------------------------------------------- /App/iOS/Assets.xcassets/AppIcon.appiconset/icon-ios-29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/App/iOS/Assets.xcassets/AppIcon.appiconset/icon-ios-29@3x.png -------------------------------------------------------------------------------- /App/iOS/Assets.xcassets/AppIcon.appiconset/icon-ios-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/App/iOS/Assets.xcassets/AppIcon.appiconset/icon-ios-40@2x.png -------------------------------------------------------------------------------- /App/iOS/Assets.xcassets/AppIcon.appiconset/icon-ios-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/App/iOS/Assets.xcassets/AppIcon.appiconset/icon-ios-40@3x.png -------------------------------------------------------------------------------- /App/iOS/Assets.xcassets/AppIcon.appiconset/icon-ios-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/App/iOS/Assets.xcassets/AppIcon.appiconset/icon-ios-60@2x.png -------------------------------------------------------------------------------- /App/iOS/Assets.xcassets/AppIcon.appiconset/icon-ios-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/App/iOS/Assets.xcassets/AppIcon.appiconset/icon-ios-60@3x.png -------------------------------------------------------------------------------- /App/iOS/Assets.xcassets/AppIcon.appiconset/icon-ios-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/App/iOS/Assets.xcassets/AppIcon.appiconset/icon-ios-76@2x.png -------------------------------------------------------------------------------- /App/iOS/Assets.xcassets/AppIcon.appiconset/icon-ios-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/App/iOS/Assets.xcassets/AppIcon.appiconset/icon-ios-83.5@2x.png -------------------------------------------------------------------------------- /App/iOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jeans Ruiz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Screenshots/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | 3 | // Leave blank. This is only here so that Xcode doesn't display it. 4 | 5 | import PackageDescription 6 | 7 | let package = Package( 8 | name: "Screenshots", 9 | products: [], 10 | targets: [] 11 | ) 12 | -------------------------------------------------------------------------------- /Screenshots/dark/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Screenshots/dark/01.png -------------------------------------------------------------------------------- /Screenshots/dark/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Screenshots/dark/02.png -------------------------------------------------------------------------------- /Screenshots/dark/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Screenshots/dark/03.png -------------------------------------------------------------------------------- /Screenshots/dark/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Screenshots/dark/04.png -------------------------------------------------------------------------------- /Screenshots/dark/05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Screenshots/dark/05.png -------------------------------------------------------------------------------- /Screenshots/dark/06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Screenshots/dark/06.png -------------------------------------------------------------------------------- /Screenshots/dark/07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Screenshots/dark/07.png -------------------------------------------------------------------------------- /Screenshots/dark/08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Screenshots/dark/08.png -------------------------------------------------------------------------------- /Screenshots/dynamic-type-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Screenshots/dynamic-type-1.png -------------------------------------------------------------------------------- /Screenshots/dynamic-type-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Screenshots/dynamic-type-2.png -------------------------------------------------------------------------------- /Screenshots/light/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Screenshots/light/01.png -------------------------------------------------------------------------------- /Screenshots/light/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Screenshots/light/02.png -------------------------------------------------------------------------------- /Screenshots/light/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Screenshots/light/03.png -------------------------------------------------------------------------------- /Screenshots/light/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Screenshots/light/04.png -------------------------------------------------------------------------------- /Screenshots/light/05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Screenshots/light/05.png -------------------------------------------------------------------------------- /Screenshots/light/06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Screenshots/light/06.png -------------------------------------------------------------------------------- /Screenshots/light/07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Screenshots/light/07.png -------------------------------------------------------------------------------- /Screenshots/light/08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Screenshots/light/08.png -------------------------------------------------------------------------------- /Sources/AccountFeature/DIContainer/AccountCoordinatorProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 8/12/20. 3 | // 4 | 5 | import UIKit 6 | import Shared 7 | import ShowListFeatureInterface 8 | 9 | protocol AccountCoordinatorProtocol: AnyObject { 10 | func navigate(to step: AccountStep) 11 | } 12 | 13 | // MARK: - Coordinator Dependencies 14 | protocol AccountCoordinatorDependencies { 15 | func buildAccountViewController(coordinator: AccountCoordinatorProtocol?) -> UIViewController 16 | 17 | func buildAuthPermissionViewController(url: URL, delegate: AuthPermissionViewModelDelegate?) async -> AuthPermissionViewController 18 | 19 | func buildTVShowListCoordinator(navigationController: UINavigationController, 20 | delegate: TVShowListCoordinatorDelegate?) -> TVShowListCoordinatorProtocol 21 | } 22 | 23 | // MARK: - Steps 24 | public enum AccountStep: Step { 25 | case accountFeatureInit 26 | case signInIsPicked(url: URL, delegate: AuthPermissionViewModelDelegate?) 27 | case authorizationIsComplete 28 | case favoritesIsPicked 29 | case watchListIsPicked 30 | } 31 | 32 | // MARK: - Child Coordinators 33 | public enum AccountChildCoordinator { 34 | case tvShowList 35 | } 36 | -------------------------------------------------------------------------------- /Sources/AccountFeature/Data/Network/DataMapping/AccountDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccountDTO.swift 3 | // TVToday 4 | // 5 | // Created by Jeans Ruiz on 6/21/20. 6 | // Copyright © 2020 Jeans. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct AccountDTO: Decodable { 12 | let id: Int 13 | let userName: String 14 | let avatar: AvatarDTO? 15 | 16 | enum CodingKeys: String, CodingKey { 17 | case avatar 18 | case id 19 | case userName = "username" 20 | } 21 | } 22 | 23 | // MARK: - Avatar 24 | public struct AvatarDTO: Decodable { 25 | let gravatar: GravatarDTO? 26 | 27 | enum CodingKeys: String, CodingKey { 28 | case gravatar 29 | } 30 | } 31 | 32 | // MARK: - Gravatar 33 | public struct GravatarDTO: Decodable { 34 | let hash: String? 35 | 36 | enum CodingKeys: String, CodingKey { 37 | case hash 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/AccountFeature/Data/Network/DataMapping/NewRequestTokenDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewRequestTokenDTO.swift 3 | // 4 | // Created by Jeans Ruiz on 6/19/20. 5 | // Copyright © 2020 Jeans. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | struct NewRequestTokenDTO: Decodable { 11 | let success: Bool 12 | let token: String 13 | 14 | enum CodingKeys: String, CodingKey { 15 | case success 16 | case token = "request_token" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/AccountFeature/Data/Network/DataMapping/NewSessionDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewSessionDTO.swift 3 | // 4 | // Created by Jeans Ruiz on 6/19/20. 5 | // Copyright © 2020 Jeans. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | struct NewSessionDTO: Decodable { 11 | let success: Bool 12 | let sessionId: String 13 | 14 | enum CodingKeys: String, CodingKey { 15 | case success 16 | case sessionId = "session_id" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/AccountFeature/Data/Network/RequestTokenMapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 13/07/22. 3 | // 4 | 5 | import Foundation 6 | import NetworkingInterface 7 | 8 | protocol RequestTokenMapperProtocol { 9 | func mapRequestToken(model: NewRequestTokenDTO) throws -> NewRequestToken 10 | } 11 | 12 | struct RequestTokenMapper: RequestTokenMapperProtocol { 13 | private let authenticateBaseURL: String 14 | 15 | init(authenticateBaseURL: String) { 16 | self.authenticateBaseURL = authenticateBaseURL 17 | } 18 | 19 | func mapRequestToken(model: NewRequestTokenDTO) throws -> NewRequestToken { 20 | if model.success == true, 21 | let url = URL(string: "\(authenticateBaseURL)/\(model.token)") { 22 | return NewRequestToken(token: model.token, url: url) 23 | } else { 24 | print("cannot Convert request token= \(model), basePath=\(authenticateBaseURL)") 25 | throw ApiError(error: NSError(domain: "RequestTokenMapper", code: 0, userInfo: nil)) // MARk: - TODO, change error 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/AccountFeature/Data/Repositories/DefaultAccountRemoteDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 7/05/22. 3 | // 4 | 5 | import NetworkingInterface 6 | 7 | final class DefaultAccountRemoteDataSource: AccountRemoteDataSource { 8 | private let apiClient: ApiClient 9 | 10 | init(apiClient: ApiClient) { 11 | self.apiClient = apiClient 12 | } 13 | 14 | func getAccountDetails(session: String) async throws -> AccountDTO { 15 | let endpoint = Endpoint( 16 | path: "3/account", 17 | method: .get, 18 | queryParameters: [ 19 | "session_id": session 20 | ] 21 | ) 22 | return try await apiClient.apiRequest(endpoint: endpoint, as: AccountDTO.self) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/AccountFeature/Data/Repositories/DefaultAuthRemoteDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 7/05/22. 3 | // 4 | 5 | import NetworkingInterface 6 | 7 | final class DefaultAuthRemoteDataSource: AuthRemoteDataSource { 8 | private let apiClient: ApiClient 9 | 10 | init(apiClient: ApiClient) { 11 | self.apiClient = apiClient 12 | } 13 | 14 | func requestToken() async throws -> NewRequestTokenDTO { 15 | let endpoint = Endpoint( 16 | path: "3/authentication/token/new", 17 | method: .get 18 | ) 19 | return try await apiClient.apiRequest(endpoint: endpoint, as: NewRequestTokenDTO.self) 20 | } 21 | 22 | func createSession(requestToken: String) async throws -> NewSessionDTO { 23 | let endpoint = Endpoint( 24 | path: "3/authentication/session/new", 25 | method: .post, 26 | queryParameters: [ 27 | "request_token": requestToken 28 | ] 29 | ) 30 | return try await apiClient.apiRequest(endpoint: endpoint, as: NewSessionDTO.self) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/AccountFeature/Domain/Entities/Account.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Account.swift 3 | // TVToday 4 | // 5 | // Created by Jeans Ruiz on 6/21/20. 6 | // Copyright © 2020 Jeans. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Account: Hashable { 12 | let id: Int 13 | let userName: String 14 | let avatarURL: URL? 15 | } 16 | -------------------------------------------------------------------------------- /Sources/AccountFeature/Domain/Entities/NewRequestToken.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewRequestToken.swift 3 | // TVToday 4 | // 5 | // Created by Jeans Ruiz on 6/19/20. 6 | // Copyright © 2020 Jeans. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct NewRequestToken { 12 | let token: String 13 | let url: URL 14 | } 15 | -------------------------------------------------------------------------------- /Sources/AccountFeature/Domain/Entities/NewSession.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreateSessionResult.swift 3 | // TVToday 4 | // 5 | // Created by Jeans Ruiz on 6/19/20. 6 | // Copyright © 2020 Jeans. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct NewSession { 12 | let success: Bool 13 | let sessionId: String 14 | } 15 | -------------------------------------------------------------------------------- /Sources/AccountFeature/Domain/Interfaces/Repositories/AccountRemoteDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 7/05/22. 3 | // 4 | 5 | public protocol AccountRemoteDataSource { 6 | func getAccountDetails(session: String) async throws -> AccountDTO 7 | } 8 | -------------------------------------------------------------------------------- /Sources/AccountFeature/Domain/Interfaces/Repositories/AccountRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 6/21/20. 3 | // 4 | 5 | import Foundation 6 | 7 | public protocol AccountRepository { 8 | func getAccountDetails() async -> Account? 9 | } 10 | -------------------------------------------------------------------------------- /Sources/AccountFeature/Domain/Interfaces/Repositories/AuthRemoteDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 7/05/22. 3 | // 4 | 5 | protocol AuthRemoteDataSource { 6 | func requestToken() async throws -> NewRequestTokenDTO 7 | func createSession(requestToken: String) async throws -> NewSessionDTO 8 | } 9 | -------------------------------------------------------------------------------- /Sources/AccountFeature/Domain/Interfaces/Repositories/AuthRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 6/19/20. 3 | // 4 | 5 | import Foundation 6 | 7 | protocol AuthRepository { 8 | func requestToken() async -> NewRequestToken? 9 | func createSession() async -> NewSession? 10 | } 11 | -------------------------------------------------------------------------------- /Sources/AccountFeature/Domain/UseCases/CreateSession.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 6/21/20. 3 | // 4 | 5 | import NetworkingInterface 6 | 7 | protocol CreateSessionUseCase { 8 | func execute() async -> Bool 9 | } 10 | 11 | final class DefaultCreateSessionUseCase: CreateSessionUseCase { 12 | private let authRepository: AuthRepository 13 | 14 | init(authRepository: AuthRepository) { 15 | self.authRepository = authRepository 16 | } 17 | 18 | func execute() async -> Bool { 19 | return await authRepository.createSession()?.success == true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/AccountFeature/Domain/UseCases/CreateTokenUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 6/19/20. 3 | // 4 | 5 | import Foundation 6 | 7 | protocol CreateTokenUseCase { 8 | func execute() async -> URL? 9 | } 10 | 11 | final class DefaultCreateTokenUseCase: CreateTokenUseCase { 12 | private let authRepository: AuthRepository 13 | 14 | init(authRepository: AuthRepository) { 15 | self.authRepository = authRepository 16 | } 17 | 18 | func execute() async -> URL? { 19 | return await authRepository.requestToken()?.url 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/AccountFeature/Domain/UseCases/DeleteLoggedUserUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeleteLoggedUserUseCase.swift 3 | // AccountFeature 4 | // 5 | // Created by Jeans Ruiz on 6/21/20. 6 | // Copyright © 2020 Jeans. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Shared 11 | 12 | protocol DeleteLoggedUserUseCase { 13 | func execute() 14 | } 15 | 16 | final class DefaultDeleteLoggedUserUseCase: DeleteLoggedUserUseCase { 17 | private let loggedRepository: LoggedUserRepositoryProtocol 18 | 19 | init(loggedRepository: LoggedUserRepositoryProtocol) { 20 | self.loggedRepository = loggedRepository 21 | } 22 | 23 | func execute() { 24 | loggedRepository.deleteUser() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/AccountFeature/Domain/UseCases/FetchAccountDetailsUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 6/21/20. 3 | // 4 | 5 | import Foundation 6 | 7 | protocol FetchAccountDetailsUseCase { 8 | func execute() async -> Account? 9 | } 10 | 11 | final class DefaultFetchAccountDetailsUseCase: FetchAccountDetailsUseCase { 12 | private let accountRepository: AccountRepository 13 | 14 | init(accountRepository: AccountRepository) { 15 | self.accountRepository = accountRepository 16 | } 17 | 18 | func execute() async -> Account? { 19 | return await accountRepository.getAccountDetails() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/AccountFeature/Presentation/AuthPermission/ViewModel/AuthPermissionViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 6/19/20. 3 | // 4 | 5 | import Foundation 6 | 7 | final class AuthPermissionViewModel: AuthPermissionViewModelProtocol { 8 | weak var delegate: AuthPermissionViewModelDelegate? 9 | 10 | let authPermissionURL: URL 11 | 12 | // MARK: - Initializer 13 | init(url: URL) { 14 | authPermissionURL = url 15 | } 16 | 17 | func signIn() async { 18 | await delegate?.authPermissionViewModel(didSignedIn: true) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/AccountFeature/Presentation/AuthPermission/ViewModel/AuthPermissionViewModelProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 8/8/20. 3 | // 4 | 5 | import Foundation 6 | 7 | public protocol AuthPermissionViewModelDelegate: AnyObject { 8 | func authPermissionViewModel(didSignedIn signedIn: Bool) async 9 | } 10 | 11 | protocol AuthPermissionViewModelProtocol { 12 | func signIn() async 13 | var authPermissionURL: URL { get } 14 | var delegate: AuthPermissionViewModelDelegate? { get set } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/AccountFeature/Presentation/Profile/View/Cells/LogoutTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogoutTableViewCell.swift 3 | // AccountFeature 4 | // 5 | // Created by Jeans Ruiz on 6/22/20. 6 | // Copyright © 2020 Jeans. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import UI 11 | 12 | class LogoutTableViewCell: NiblessTableViewCell { 13 | 14 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 15 | super.init(style: style, reuseIdentifier: reuseIdentifier) 16 | setupUI() 17 | } 18 | 19 | private func setupUI() { 20 | backgroundColor = .secondarySystemBackground 21 | textLabel?.text = Strings.accountLogout.localized() 22 | textLabel?.textAlignment = .center 23 | textLabel?.textColor = .red 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/AccountFeature/Presentation/Profile/ViewModel/ProfileViewModelProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 8/8/20. 3 | // 4 | 5 | import Foundation 6 | 7 | protocol ProfileViewModelDelegate: AnyObject { 8 | func profileViewModel(didTapLogoutButton tapped: Bool) 9 | func profileViewModel(didUserList tapped: UserListType) 10 | } 11 | 12 | protocol ProfileViewModelProtocol { 13 | // MARK: - Input 14 | func didTapLogoutButton() 15 | func didCellTap(model: ProfilesSectionItem) 16 | var delegate: ProfileViewModelDelegate? { get set } 17 | 18 | // MARK: - Output 19 | var dataSource: Published<[ProfileSectionModel]>.Publisher { get } 20 | var presentSignOutAlert: Published.Publisher { get } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/AccountFeature/Presentation/SignIn/ViewModel/SignInViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 6/19/20. 3 | // 4 | 5 | import Foundation 6 | 7 | @MainActor 8 | class SignInViewModel: SignInViewModelProtocol { 9 | private let createTokenUseCase: CreateTokenUseCase 10 | 11 | @Published private var viewStateInternal: SignInViewState = .initial 12 | var viewState: Published.Publisher { $viewStateInternal } 13 | 14 | weak var delegate: SignInViewModelDelegate? 15 | 16 | init(createTokenUseCase: CreateTokenUseCase) { 17 | self.createTokenUseCase = createTokenUseCase 18 | } 19 | 20 | // MARK: - Public 21 | func signInDidTapped() async { 22 | viewStateInternal = .loading 23 | if let url = await createTokenUseCase.execute() { 24 | delegate?.signInViewModel(self, didTapSignInButton: url) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/AccountFeature/Presentation/SignIn/ViewModel/SignInViewModelProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 8/8/20. 3 | // 4 | 5 | import Foundation 6 | 7 | protocol SignInViewModelDelegate: AnyObject { 8 | func signInViewModel(_ signInViewModel: SignInViewModel, didTapSignInButton url: URL) 9 | } 10 | 11 | protocol SignInViewModelProtocol { 12 | // MARK: - Input 13 | func signInDidTapped() async 14 | 15 | // MARK: - Output 16 | var viewState: Published.Publisher { get } 17 | var delegate: SignInViewModelDelegate? { get set } 18 | } 19 | 20 | // MARK: - View State 21 | enum SignInViewState: Equatable { 22 | case initial 23 | case loading 24 | case error 25 | } 26 | -------------------------------------------------------------------------------- /Sources/AiringTodayFeature/DIContainer/AiringTodayCoordinatorProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AiringTodayCoordinatorProtocol.swift 3 | // AiringToday 4 | // 5 | // Created by Jeans Ruiz on 8/12/20. 6 | // 7 | 8 | import UIKit 9 | import Shared 10 | import ShowDetailsFeatureInterface 11 | 12 | protocol AiringTodayCoordinatorProtocol: AnyObject { 13 | func navigate(to step: AiringTodayStep) 14 | } 15 | 16 | // MARK: - Coordinator Dependencies 17 | protocol AiringTodayCoordinatorDependencies { 18 | func buildAiringTodayViewController(coordinator: AiringTodayCoordinatorProtocol?) -> UIViewController 19 | 20 | func buildTVShowDetailCoordinator(navigationController: UINavigationController, 21 | delegate: TVShowDetailCoordinatorDelegate?) -> TVShowDetailCoordinatorProtocol 22 | } 23 | 24 | // MARK: - Steps 25 | public enum AiringTodayStep: Step { 26 | case todayFeatureInit 27 | case showIsPicked(Int) 28 | } 29 | 30 | // MARK: - ChildCoordinators 31 | public enum AiringTodayChildCoordinator { 32 | case detailShow 33 | } 34 | -------------------------------------------------------------------------------- /Sources/AiringTodayFeature/DIContainer/Module.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 6/28/20. 3 | // 4 | 5 | import Foundation 6 | import UIKit 7 | import NetworkingInterface 8 | import Persistence 9 | import Shared 10 | import ShowDetailsFeatureInterface 11 | 12 | public struct ModuleDependencies { 13 | let apiClient: ApiClient 14 | let imagesBaseURL: String 15 | let showDetailsBuilder: ModuleShowDetailsBuilder 16 | 17 | public init( 18 | apiClient: ApiClient, 19 | imagesBaseURL: String, 20 | showDetailsBuilder: ModuleShowDetailsBuilder 21 | ) { 22 | self.apiClient = apiClient 23 | self.imagesBaseURL = imagesBaseURL 24 | self.showDetailsBuilder = showDetailsBuilder 25 | } 26 | } 27 | 28 | // MARK: - Entry to Module 29 | public struct Module { 30 | private let diContainer: DIContainer 31 | 32 | public init(dependencies: ModuleDependencies) { 33 | self.diContainer = DIContainer(dependencies: dependencies) 34 | } 35 | 36 | public func buildAiringTodayCoordinator(in navigationController: UINavigationController) -> Coordinator { 37 | return diContainer.buildModuleCoordinator(navigationController: navigationController) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/AiringTodayFeature/Domain/UseCases/DefaultFetchAiringTodayTVShowsUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 6/28/20. 3 | // 4 | 5 | import Shared 6 | import NetworkingInterface 7 | 8 | final class DefaultFetchAiringTodayTVShowsUseCase: FetchTVShowsUseCase { 9 | private let tvShowsPageRepository: TVShowsPageRepository 10 | 11 | init(tvShowsPageRepository: TVShowsPageRepository) { 12 | self.tvShowsPageRepository = tvShowsPageRepository 13 | } 14 | 15 | func execute(request: FetchTVShowsUseCaseRequestValue) async -> TVShowPage? { 16 | return await tvShowsPageRepository.fetchAiringTodayShows(page: request.page) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/AiringTodayFeature/Presentation/View/Customs/FooterReusableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FooterReusableView.swift 3 | // TVToday 4 | // 5 | // Created by Jeans on 10/17/19. 6 | // Copyright © 2019 Jeans. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import UI 12 | 13 | class FooterReusableView: UICollectionReusableView, Loadable { 14 | 15 | required init?(coder aDecoder: NSCoder) { 16 | fatalError() 17 | } 18 | 19 | override init(frame: CGRect) { 20 | super.init(frame: frame) 21 | setupUI() 22 | } 23 | 24 | // MARK: - Private 25 | private func setupUI() { 26 | (self as Loadable).showLoadingView() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/AiringTodayFeature/Presentation/ViewModel/AiringTodayCollectionViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AiringTodayCollectionViewModel.swift 3 | // MyTvShows 4 | // 5 | // Created by Jeans on 10/2/19. 6 | // Copyright © 2019 Jeans. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Shared 11 | 12 | struct AiringTodayCollectionViewModel: Hashable { 13 | private let showId: Int 14 | let showName: String? 15 | let average: String? 16 | let posterURL: URL? 17 | 18 | public init(show: TVShowPage.TVShow) { 19 | showId = show.id 20 | showName = show.name 21 | if show.voteAverage == 0 { 22 | average = "0.0" 23 | } else { 24 | average = String(show.voteAverage) 25 | } 26 | posterURL = show.backDropPath 27 | } 28 | } 29 | 30 | enum SectionAiringTodayFeed: Hashable { 31 | case shows 32 | } 33 | -------------------------------------------------------------------------------- /Sources/AiringTodayFeature/Presentation/ViewModel/AiringTodayViewModelProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 19/03/22. 3 | // 4 | 5 | import Shared 6 | import Combine 7 | 8 | protocol AiringTodayViewModelProtocol { 9 | // MARK: - Input 10 | func viewDidLoad() async 11 | func showIsPicked(index: Int) 12 | func refreshView() async 13 | func willDisplayRow(_ row: Int, outOf totalRows: Int) async 14 | 15 | // MARK: - Output 16 | var viewStateObservableSubject: CurrentValueSubject, Never> { get } 17 | 18 | func getCurrentViewState() -> SimpleViewState 19 | } 20 | -------------------------------------------------------------------------------- /Sources/AppFeature/AppConfigurationProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 19/04/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol AppConfigurationProtocol { 11 | var apiKey: String { get set } 12 | var apiBaseURL: URL { get set } 13 | var imagesBaseURL: String { get set } 14 | var authenticateBaseURL: String { get set } 15 | var gravatarBaseURL: String { get set } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/KeyChainStorage/KeychainItemStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeychainItemStorage.swift 3 | // TVToday 4 | // 5 | // Created by Jeans Ruiz on 6/21/20. 6 | // Copyright © 2020 Jeans. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import KeychainSwift 11 | 12 | @propertyWrapper 13 | struct KeychainItemStorage { 14 | 15 | private let key: String 16 | private lazy var keychain = KeychainSwift() 17 | 18 | init(key: String) { 19 | self.key = key 20 | } 21 | 22 | var wrappedValue: String? { 23 | mutating get { 24 | return keychain.get(key) 25 | } 26 | set { 27 | if let newValue = newValue { 28 | keychain.set(newValue, forKey: key) 29 | } else { 30 | keychain.delete(key) 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Networking/ApiClient/ApiClient+Live.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 11/08/23. 3 | // 4 | 5 | import Foundation 6 | import NetworkingInterface 7 | 8 | extension ApiClient { 9 | public static func live( 10 | networkConfig: NetworkConfig, 11 | urlSession: URLSessionManager = .live, 12 | logger: NetworkLogger = .live 13 | ) -> ApiClient { 14 | return ApiClient( 15 | apiRequest: { try await request(endpoint: $0, networkConfig: networkConfig, urlSession: urlSession, logger: logger) }, 16 | logError: { logger.logError($0) } 17 | ) 18 | } 19 | } 20 | 21 | private func request( 22 | endpoint: URLRequestable, 23 | networkConfig: NetworkConfig, 24 | urlSession: URLSessionManager, 25 | logger: NetworkLogger 26 | ) async throws -> (Data, URLResponse) { 27 | guard let request = try? endpoint.urlRequest(with: networkConfig) else { 28 | throw URLError(.badURL) 29 | } 30 | logger.logRequest(request) 31 | do { 32 | let (data, response) = try await urlSession.request(request) 33 | logger.logResponse(data, response) 34 | return (data, response) 35 | } catch { 36 | logger.logError(error) 37 | throw error 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Networking/ApiClient/NetworkLogger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 11/08/23. 3 | // 4 | 5 | import Foundation 6 | 7 | public struct NetworkLogger { 8 | let logRequest: (URLRequest) -> Void 9 | let logResponse: (Data, URLResponse) -> Void 10 | let logError: (Error) -> Void 11 | 12 | public init( 13 | logRequest: @escaping (URLRequest) -> Void, 14 | logResponse: @escaping (Data, URLResponse) -> Void, 15 | logError: @escaping (Error) -> Void 16 | ) { 17 | self.logRequest = logRequest 18 | self.logResponse = logResponse 19 | self.logError = logError 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Networking/ApiClient/URLSessionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 13/08/23. 3 | // 4 | 5 | import Foundation 6 | 7 | public struct URLSessionManager { 8 | let request: (URLRequest) async throws -> (Data, URLResponse) 9 | 10 | public init(request: @escaping (URLRequest) async throws -> (Data, URLResponse)) { 11 | self.request = request 12 | } 13 | } 14 | 15 | extension URLSessionManager { 16 | public static var live: URLSessionManager = { 17 | return URLSessionManager(request: { 18 | return try await URLSession.shared.data(for: $0) 19 | }) 20 | }() 21 | } 22 | -------------------------------------------------------------------------------- /Sources/NetworkingInterface/ApiClient/ApiClient+Test.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 11/09/23. 3 | // 4 | 5 | import Foundation 6 | 7 | extension ApiClient { 8 | /// For Preview Version 9 | public static var noop: ApiClient = { 10 | return ApiClient( 11 | apiRequest: { _ in 12 | return (Data(), .init()) 13 | }, 14 | logError: { 15 | debugPrint("debugPrint: \($0)") 16 | } 17 | ) 18 | }() 19 | 20 | /// For testing purposes 21 | public static var testMock: ApiClient = { 22 | return ApiClient( 23 | apiRequest: { 24 | throw UnimplementedFailure(description: "Not implemented for: \($0)") 25 | }, 26 | logError: { 27 | fatalError("Unimplemented for: \($0)") 28 | } 29 | ) 30 | }() 31 | 32 | public struct UnimplementedFailure: Error { 33 | public let description: String 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/NetworkingInterface/ApiClient/ApiError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 13/08/23. 3 | // 4 | 5 | import Foundation 6 | 7 | public struct ApiError: Error, LocalizedError { 8 | public let errorDump: String 9 | public let file: String 10 | public let line: UInt 11 | public let message: String 12 | public let rawError: Error 13 | 14 | public init( 15 | error: Error, 16 | file: StaticString = #fileID, 17 | line: UInt = #line 18 | ) { 19 | var string = "" 20 | dump(error, to: &string) 21 | self.errorDump = string 22 | self.file = String(describing: file) 23 | self.line = line 24 | self.message = error.localizedDescription 25 | self.rawError = error 26 | } 27 | 28 | public var errorDescription: String? { 29 | self.message 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/NetworkingInterface/ApiClient/JSONResponseDecoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 13/08/23. 3 | // 4 | 5 | import Foundation 6 | 7 | public struct JSONResponseDecoder: ResponseDecoder { 8 | private let jsonDecoder = JSONDecoder() 9 | 10 | public init() {} 11 | 12 | public func decode(_ type: A.Type, from data: Data) throws -> A where A : Decodable { 13 | return try jsonDecoder.decode(A.self, from: data) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/NetworkingInterface/ApiClient/NetworkConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 13/08/23. 3 | // 4 | 5 | import Foundation 6 | 7 | public struct NetworkConfig { 8 | let baseURL: URL 9 | let headers: [String: String] 10 | let queryParameters: [String: String] 11 | 12 | public init(baseURL: URL, headers: [String : String] = [:], queryParameters: [String : String] = [:]) { 13 | self.baseURL = baseURL 14 | self.headers = headers 15 | self.queryParameters = queryParameters 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/NetworkingInterface/ApiClient/URLRequestable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 11/08/23. 3 | // 4 | 5 | import Foundation 6 | 7 | public protocol URLRequestable { 8 | func urlRequest(with: NetworkConfig) throws -> URLRequest 9 | var responseDecoder: ResponseDecoder { get } 10 | } 11 | 12 | public protocol ResponseDecoder { 13 | func decode(_ type: A.Type, from data: Data) throws -> A 14 | } 15 | -------------------------------------------------------------------------------- /Sources/NetworkingInterface/NetworkError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 19/03/22. 3 | // 4 | 5 | import Foundation 6 | 7 | #warning("TODO: Unify with ApiError") 8 | public enum NetworkError: Error { 9 | case error(statusCode: Int, data: Data) 10 | case notConnected 11 | case cancelled 12 | case generic(Error) 13 | case urlGeneration 14 | } 15 | 16 | // MARK: - NetworkError extension 17 | extension NetworkError { 18 | public var isNotFoundError: Bool { 19 | return hasStatusCode(404) 20 | } 21 | 22 | public func hasStatusCode(_ codeError: Int) -> Bool { 23 | switch self { 24 | case let .error(code, _): 25 | return code == codeError 26 | default: return false 27 | } 28 | } 29 | } 30 | 31 | public func printIfDebug(_ string: String) { 32 | #if DEBUG 33 | print(string) 34 | #endif 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Persistence/Entities/Search.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Search.swift 3 | // TVToday 4 | // 5 | // Created by Jeans Ruiz on 7/2/20. 6 | // Copyright © 2020 Jeans. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Search { 12 | 13 | public let query: String 14 | 15 | public init(query: String) { 16 | self.query = query 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Persistence/Entities/SearchDLO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchDLO.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 11/05/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct SearchDLO { 11 | 12 | public let query: String 13 | 14 | public init(query: String) { 15 | self.query = query 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Persistence/Entities/ShowVisited.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShowVisited.swift 3 | // TVToday 4 | // 5 | // Created by Jeans Ruiz on 7/2/20. 6 | // Copyright © 2020 Jeans. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct ShowVisited: Hashable { 12 | public let id: Int 13 | public let pathImage: String // MARK: - TODO, consider this could contain the URL already 14 | 15 | public init(id: Int, pathImage: String) { 16 | self.id = id 17 | self.pathImage = pathImage 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Persistence/Entities/ShowVisitedDLO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShowVisitedDLO.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 11/05/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct ShowVisitedDLO: Hashable { 11 | public let id: Int 12 | public let pathImage: String 13 | 14 | public init(id: Int, pathImage: String) { 15 | self.id = id 16 | self.pathImage = pathImage 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Persistence/Interfaces/DataSources/SearchLocalDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 11/05/22. 3 | // 4 | 5 | import Shared 6 | 7 | // it will throws ErrorEnvelope 8 | public protocol SearchLocalDataSource { 9 | func saveSearch(query: String, userId: Int) async throws 10 | func fetchRecentSearches(userId: Int) async throws -> [SearchDLO] 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Persistence/Interfaces/DataSources/ShowsVisitedLocalDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 11/05/22. 3 | // 4 | 5 | import Shared 6 | 7 | public protocol ShowsVisitedLocalDataSource { 8 | func saveShow(id: Int, pathImage: String, userId: Int) 9 | func fetchVisitedShows(userId: Int) -> [ShowVisitedDLO] 10 | func recentVisitedShowsDidChange() -> AsyncStream 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Persistence/Interfaces/Repositories/SearchLocalRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 11/05/22. 3 | // 4 | 5 | import Shared 6 | 7 | public final class SearchLocalRepository { 8 | private let dataSource: SearchLocalDataSource 9 | private let loggedUserRepository: LoggedUserRepositoryProtocol 10 | 11 | public init(dataSource: SearchLocalDataSource, loggedUserRepository: LoggedUserRepositoryProtocol) { 12 | self.dataSource = dataSource 13 | self.loggedUserRepository = loggedUserRepository 14 | } 15 | } 16 | 17 | extension SearchLocalRepository: SearchLocalRepositoryProtocol { 18 | public func saveSearch(query: String) async throws { 19 | let userId = loggedUserRepository.getUser()?.id ?? 0 20 | try await dataSource.saveSearch(query: query, userId: userId) 21 | } 22 | 23 | public func fetchRecentSearches() async throws -> [Search] { 24 | let userId = loggedUserRepository.getUser()?.id ?? 0 25 | let localSearchs = try await dataSource.fetchRecentSearches(userId: userId) 26 | return localSearchs.map { Search(query: $0.query) } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Persistence/Interfaces/Repositories/SearchLocalRepositoryProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 7/2/20. 3 | // 4 | 5 | import Combine 6 | import Shared 7 | 8 | // todo, throws ErrorEnvelope 9 | public protocol SearchLocalRepositoryProtocol { 10 | func saveSearch(query: String) async throws 11 | func fetchRecentSearches() async throws -> [Search] 12 | } 13 | -------------------------------------------------------------------------------- /Sources/Persistence/Interfaces/Repositories/ShowsVisitedLocalRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 11/05/22. 3 | // 4 | 5 | import Shared 6 | 7 | public final class ShowsVisitedLocalRepository { 8 | private let dataSource: ShowsVisitedLocalDataSource 9 | private let loggedUserRepository: LoggedUserRepositoryProtocol 10 | 11 | public init(dataSource: ShowsVisitedLocalDataSource, loggedUserRepository: LoggedUserRepositoryProtocol) { 12 | self.dataSource = dataSource 13 | self.loggedUserRepository = loggedUserRepository 14 | } 15 | } 16 | 17 | extension ShowsVisitedLocalRepository: ShowsVisitedLocalRepositoryProtocol { 18 | public func saveShow(id: Int, pathImage: String) { 19 | let userId = loggedUserRepository.getUser()?.id ?? 0 20 | return dataSource.saveShow(id: id, pathImage: pathImage, userId: userId) 21 | } 22 | 23 | public func fetchVisitedShows() -> [ShowVisited] { 24 | let userId = loggedUserRepository.getUser()?.id ?? 0 25 | return dataSource.fetchVisitedShows(userId: userId).map { ShowVisited(id: $0.id, pathImage: $0.pathImage) } 26 | } 27 | 28 | public func recentVisitedShowsDidChange() -> AsyncStream { 29 | return dataSource.recentVisitedShowsDidChange() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Persistence/Interfaces/Repositories/ShowsVisitedLocalRepositoryProtocol+Mock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 1/11/24. 3 | // 4 | 5 | import Foundation 6 | 7 | #if DEBUG 8 | public final class FakeShowsVisitedLocalRepository: ShowsVisitedLocalRepositoryProtocol { 9 | 10 | public init() {} 11 | 12 | public func saveShow(id: Int, pathImage: String){ 13 | 14 | } 15 | 16 | public func fetchVisitedShows() -> [ShowVisited] { 17 | return [ 18 | .init(id: 01, pathImage: "https://image.tmdb.org/t/p/w500/4EYPN5mVIhKLfxGruy7Dy41dTVn.jpg"), 19 | .init(id: 02, pathImage: "https://image.tmdb.org/t/p/w200/Ap86RyRhP7ikeRCpysnfC9PO2H0.jpg"), 20 | .init(id: 03, pathImage: "https://image.tmdb.org/t/p/w200/4EYPN5mVIhKLfxGruy7Dy41dTVn.jpg"), 21 | .init(id: 04, pathImage: "https://image.tmdb.org/t/p/w200/Ap86RyRhP7ikeRCpysnfC9PO2H0.jpg") 22 | ] 23 | } 24 | 25 | public func recentVisitedShowsDidChange() -> AsyncStream { 26 | return AsyncStream(unfolding: { false }) 27 | } 28 | } 29 | #endif 30 | -------------------------------------------------------------------------------- /Sources/Persistence/Interfaces/Repositories/ShowsVisitedLocalRepositoryProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 7/2/20. 3 | // 4 | 5 | import Shared 6 | 7 | public protocol ShowsVisitedLocalRepositoryProtocol { 8 | func saveShow(id: Int, pathImage: String) 9 | func fetchVisitedShows() -> [ShowVisited] 10 | func recentVisitedShowsDidChange() -> AsyncStream 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Persistence/UseCases/FetchSearchsUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 7/7/20. 3 | // 4 | 5 | import Combine 6 | import Shared 7 | 8 | public protocol FetchSearchesUseCase { 9 | func execute() async -> [Search] 10 | } 11 | 12 | public final class DefaultFetchSearchesUseCase: FetchSearchesUseCase { 13 | private let searchLocalRepository: SearchLocalRepositoryProtocol 14 | 15 | public init(searchLocalRepository: SearchLocalRepositoryProtocol) { 16 | self.searchLocalRepository = searchLocalRepository 17 | } 18 | 19 | public func execute() async -> [Search] { 20 | do { 21 | return try await searchLocalRepository.fetchRecentSearches() 22 | } catch { 23 | return [] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Persistence/UseCases/FetchVisitedShowsUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 7/3/20. 3 | // 4 | 5 | import Shared 6 | 7 | public protocol FetchVisitedShowsUseCase { 8 | func execute() -> [ShowVisited] 9 | } 10 | 11 | public final class DefaultFetchVisitedShowsUseCase: FetchVisitedShowsUseCase { 12 | 13 | private let showsVisitedLocalRepository: ShowsVisitedLocalRepositoryProtocol 14 | 15 | public init(showsVisitedLocalRepository: ShowsVisitedLocalRepositoryProtocol) { 16 | self.showsVisitedLocalRepository = showsVisitedLocalRepository 17 | } 18 | 19 | public func execute() -> [ShowVisited] { 20 | return showsVisitedLocalRepository.fetchVisitedShows() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Persistence/UseCases/RecentVisitedShowDidChangeUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 7/7/20. 3 | // 4 | 5 | public protocol RecentVisitedShowDidChangeUseCase { 6 | func execute() -> AsyncStream 7 | } 8 | 9 | public final class DefaultRecentVisitedShowDidChangeUseCase: RecentVisitedShowDidChangeUseCase { 10 | private let showsVisitedLocalRepository: ShowsVisitedLocalRepositoryProtocol 11 | 12 | public init(showsVisitedLocalRepository: ShowsVisitedLocalRepositoryProtocol) { 13 | self.showsVisitedLocalRepository = showsVisitedLocalRepository 14 | } 15 | 16 | public func execute() -> AsyncStream { 17 | return showsVisitedLocalRepository.recentVisitedShowsDidChange() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/PersistenceLive/Internal/Entities/CDRecentSearch+PersistenceStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CDRecentSearch+PersistenceStore.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 26/04/22. 6 | // 7 | 8 | import CoreData 9 | 10 | extension PersistenceStore where Entity == CDRecentSearch { 11 | 12 | func delete(query: String) { 13 | do { 14 | let fetchRequest: NSFetchRequest = CDRecentSearch.fetchRequest() 15 | fetchRequest.predicate = NSPredicate(format: "%K = %@", #keyPath(CDRecentSearch.query), query) 16 | let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) 17 | try self.managedObjectContext.execute(deleteRequest) 18 | } catch { } 19 | } 20 | 21 | func insert(query: String, userId: Int) { 22 | managedObjectContext.performChanges { [managedObjectContext] in 23 | _ = CDRecentSearch.insert(into: managedObjectContext, query: query, userId: userId) 24 | } 25 | } 26 | 27 | func findAll(userId: Int) -> [CDRecentSearch] { 28 | return CDRecentSearch.fetch(in: managedObjectContext, configurationBlock: { request in 29 | request.predicate = NSPredicate(format: "%K = %d", #keyPath(CDRecentSearch.userId), userId) 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/PersistenceLive/Internal/Entities/CDRecentSearch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CDRecentSearch.swift 3 | // PersistenceLive 4 | // 5 | // Created by Jeans Ruiz on 7/6/20. 6 | // 7 | 8 | import CoreData 9 | import Persistence 10 | 11 | final class CDRecentSearch: NSManagedObject { 12 | 13 | @NSManaged private(set) var id: String 14 | @NSManaged private(set) var query: String 15 | @NSManaged private(set) var createdAt: Date 16 | @NSManaged private(set) var userId: Int 17 | 18 | static func insert(into context: NSManagedObjectContext, query: String, userId: Int) -> CDRecentSearch { 19 | let recentSearch: CDRecentSearch = context.insertObject() 20 | recentSearch.id = UUID().uuidString 21 | recentSearch.query = query 22 | recentSearch.userId = userId 23 | recentSearch.createdAt = Date() 24 | return recentSearch 25 | } 26 | } 27 | 28 | extension CDRecentSearch { 29 | func toDomain() -> SearchDLO { 30 | return SearchDLO(query: query) 31 | } 32 | } 33 | 34 | extension CDRecentSearch: Managed { 35 | static var defaultSortDescriptors: [NSSortDescriptor] { 36 | return [NSSortDescriptor(key: #keyPath(createdAt), ascending: false)] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/PersistenceLive/Internal/Entities/CDShowVisited.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CDShowVisited.swift 3 | // PersistenceLive 4 | // 5 | // Created by Jeans Ruiz on 7/2/20. 6 | // 7 | 8 | import CoreData 9 | import Persistence 10 | 11 | final class CDShowVisited: NSManagedObject { 12 | 13 | @NSManaged private(set) var id: Int 14 | @NSManaged private(set) var createdAt: Date 15 | @NSManaged private(set) var pathImage: String 16 | @NSManaged private(set) var userId: Int 17 | 18 | static func insert(into context: NSManagedObjectContext, showId: Int, pathImage: String, userId: Int) -> CDShowVisited { 19 | let showVisited: CDShowVisited = context.insertObject() 20 | showVisited.id = showId 21 | showVisited.createdAt = Date() 22 | showVisited.pathImage = pathImage 23 | showVisited.userId = userId 24 | return showVisited 25 | } 26 | } 27 | 28 | extension CDShowVisited { 29 | func toDomain() -> ShowVisitedDLO { 30 | return ShowVisitedDLO(id: id, pathImage: pathImage) 31 | } 32 | } 33 | 34 | extension CDShowVisited: Managed { 35 | static var defaultSortDescriptors: [NSSortDescriptor] { 36 | return [NSSortDescriptor(key: #keyPath(createdAt), ascending: false)] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/PersistenceLive/Internal/Helpers/NSManagedObjectContext+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSManagedObjectContext+Extensions.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 26/04/22. 6 | // 7 | 8 | import CoreData 9 | 10 | extension NSManagedObjectContext { 11 | 12 | func insertObject() -> A where A: Managed { 13 | guard let obj = NSEntityDescription.insertNewObject(forEntityName: A.entityName, into: self) as? A else { 14 | fatalError("Wrong object type") 15 | } 16 | return obj 17 | } 18 | 19 | func performChanges(block: @escaping () -> Void) { 20 | perform { 21 | block() 22 | self.saveOrRollback() 23 | } 24 | } 25 | 26 | func performChangesAndWait(block: () -> Void) { 27 | performAndWait { 28 | block() 29 | self.saveOrRollback() 30 | } 31 | } 32 | 33 | @discardableResult 34 | private func saveOrRollback() -> Bool { 35 | do { 36 | try save() 37 | return true 38 | } catch { 39 | rollback() 40 | return false 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/PersistenceLive/Internal/Repositories/CoreDataSearchQueriesStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 7/2/20. 3 | // 4 | 5 | import Combine 6 | import CoreData 7 | import Persistence 8 | import Shared 9 | 10 | #warning("Use SwiftData instead") 11 | final class CoreDataSearchQueriesStorage { 12 | private let store: PersistenceStore 13 | 14 | public init(store: PersistenceStore) { 15 | self.store = store 16 | } 17 | } 18 | 19 | extension CoreDataSearchQueriesStorage: SearchLocalDataSource { 20 | 21 | public func saveSearch(query: String, userId: Int) { 22 | store.delete(query: query) 23 | store.insert(query: query, userId: userId) 24 | } 25 | 26 | public func fetchRecentSearches(userId: Int) -> [SearchDLO] { 27 | let recentSearchs = store.findAll(userId: userId).map { $0.toDomain() } 28 | return recentSearchs 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/PersistenceLive/Public/CoreDataStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataStorage.swift 3 | // PersistenceLive 4 | // 5 | // Created by Jeans Ruiz on 7/2/20. 6 | // Copyright © 2020 Jeans. All rights reserved. 7 | // 8 | 9 | import CoreData 10 | 11 | public final class CoreDataStorage { 12 | 13 | public static let shared = CoreDataStorage() 14 | 15 | private init() { } 16 | 17 | lazy var persistentContainer: NSPersistentContainer = { 18 | guard let modelURL = Bundle.module.url(forResource: "CoreDataStorage", withExtension: "momd"), 19 | let objectModel = NSManagedObjectModel(contentsOf: modelURL) else { 20 | fatalError("CoreDataStorage cannot found resource") 21 | } 22 | 23 | let container = NSPersistentContainer(name: "CoreDataStorage", managedObjectModel: objectModel) 24 | 25 | container.loadPersistentStores { _, error in 26 | if let error = error as NSError? { 27 | assertionFailure("CoreDataStorage Unresolved error \(error), \(error.userInfo)") 28 | } 29 | } 30 | return container 31 | }() 32 | } 33 | -------------------------------------------------------------------------------- /Sources/PersistenceLive/Public/LocalDataSources.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalStorage.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 26/04/22. 6 | // 7 | 8 | import Persistence 9 | 10 | public protocol LocalDataSourceProtocol { 11 | func getShowVisitedDataSource(limitStorage: Int) -> ShowsVisitedLocalDataSource 12 | func getRecentSearchesDataSource() -> SearchLocalDataSource 13 | } 14 | 15 | final public class LocalStorage: LocalDataSourceProtocol { 16 | private let coreDataStorage: CoreDataStorage 17 | 18 | public init(coreDataStorage: CoreDataStorage) { 19 | self.coreDataStorage = coreDataStorage 20 | } 21 | 22 | public func getShowVisitedDataSource(limitStorage: Int) -> ShowsVisitedLocalDataSource { 23 | let store: PersistenceStore = PersistenceStore(coreDataStorage.persistentContainer) 24 | return CoreDataShowVisitedStorage(limitStorage: limitStorage, store: store) 25 | } 26 | 27 | public func getRecentSearchesDataSource() -> SearchLocalDataSource { 28 | let store: PersistenceStore = PersistenceStore(coreDataStorage.persistentContainer) 29 | return CoreDataSearchQueriesStorage(store: store) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/PopularsFeature/DIContainer/Module.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 6/28/20. 3 | // 4 | 5 | import Foundation 6 | import UIKit 7 | import NetworkingInterface 8 | import Shared 9 | import ShowDetailsFeatureInterface 10 | 11 | public struct ModuleDependencies { 12 | let apiClient: ApiClient 13 | let imagesBaseURL: String 14 | let showDetailsBuilder: ModuleShowDetailsBuilder 15 | 16 | public init( 17 | apiClient: ApiClient, 18 | imagesBaseURL: String, 19 | showDetailsBuilder: ModuleShowDetailsBuilder 20 | ) { 21 | self.apiClient = apiClient 22 | self.imagesBaseURL = imagesBaseURL 23 | self.showDetailsBuilder = showDetailsBuilder 24 | } 25 | } 26 | 27 | // MARK: - Entry to Module 28 | public struct Module { 29 | 30 | private let diContainer: DIContainer 31 | 32 | public init(dependencies: ModuleDependencies) { 33 | self.diContainer = DIContainer(dependencies: dependencies) 34 | } 35 | 36 | public func buildPopularCoordinator(in navigationController: UINavigationController) -> Coordinator { 37 | return diContainer.buildPopularCoordinator(navigationController: navigationController) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/PopularsFeature/DIContainer/PopularCoordinatorProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PopularCoordinatorProtocol.swift 3 | // PopularShows 4 | // 5 | // Created by Jeans Ruiz on 8/12/20. 6 | // 7 | 8 | import UIKit 9 | import Shared 10 | import ShowDetailsFeatureInterface 11 | 12 | protocol PopularCoordinatorProtocol: AnyObject { 13 | func navigate(to step: PopularStep) 14 | } 15 | 16 | // MARK: - Coordinator Dependencies 17 | protocol PopularCoordinatorDependencies { 18 | func buildPopularViewController(coordinator: PopularCoordinatorProtocol?) -> UIViewController 19 | 20 | func buildTVShowDetailCoordinator(navigationController: UINavigationController, 21 | delegate: TVShowDetailCoordinatorDelegate?) -> TVShowDetailCoordinatorProtocol 22 | } 23 | 24 | // MARK: - Steps 25 | public enum PopularStep: Step { 26 | case popularFeatureInit 27 | case showIsPicked(Int) 28 | } 29 | 30 | // MARK: - ChildCoordinators 31 | public enum PopularChildCoordinator { 32 | case detailShow 33 | } 34 | -------------------------------------------------------------------------------- /Sources/PopularsFeature/Domain/UseCases/DefaultFetchPopularTVShowsUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 6/28/20. 3 | // 4 | 5 | import Shared 6 | import NetworkingInterface 7 | 8 | final class DefaultFetchPopularTVShowsUseCase: FetchTVShowsUseCase { 9 | private let tvShowsPageRepository: TVShowsPageRepository 10 | 11 | init(tvShowsPageRepository: TVShowsPageRepository) { 12 | self.tvShowsPageRepository = tvShowsPageRepository 13 | } 14 | 15 | func execute(request: FetchTVShowsUseCaseRequestValue) async -> TVShowPage? { 16 | return await tvShowsPageRepository.fetchPopularShows(page: request.page) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/PopularsFeature/Presentation/View/SectionPopularView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionPopularView.swift 3 | // PopularShows 4 | // 5 | // Created by Jeans Ruiz on 8/3/20. 6 | // 7 | 8 | enum SectionPopularView: Hashable { 9 | case list 10 | } 11 | -------------------------------------------------------------------------------- /Sources/SearchShowsFeature/Data/Network/DataMapping/GenreListDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GenreListDTO.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 6/05/22. 6 | // 7 | 8 | import Shared 9 | 10 | public struct GenreListDTO: Decodable { 11 | public let genres: [GenreDTO] 12 | } 13 | -------------------------------------------------------------------------------- /Sources/SearchShowsFeature/Data/Repositories/DefaultGenreRemoteDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 6/05/22. 3 | // 4 | 5 | import Networking 6 | import NetworkingInterface 7 | 8 | final class DefaultGenreRemoteDataSource: GenreRemoteDataSource { 9 | private let apiClient: ApiClient 10 | 11 | init(apiClient: ApiClient) { 12 | self.apiClient = apiClient 13 | } 14 | 15 | func fetchGenres() async throws -> GenreListDTO { 16 | let endpoint = Endpoint( 17 | path: "3/genre/tv/list", 18 | method: .get 19 | ) 20 | return try await apiClient.apiRequest(endpoint: endpoint, as: GenreListDTO.self) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/SearchShowsFeature/Data/Repositories/DefaultGenresRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 1/16/20. 3 | // 4 | 5 | import Combine 6 | import NetworkingInterface 7 | import Networking 8 | import Shared 9 | 10 | final class DefaultGenreRepository: GenresRepository { 11 | private let remoteDataSource: GenreRemoteDataSource 12 | 13 | init(remoteDataSource: GenreRemoteDataSource) { 14 | self.remoteDataSource = remoteDataSource 15 | } 16 | 17 | func genresList() async throws -> GenreList { 18 | let dto = try await remoteDataSource.fetchGenres() 19 | let genres = dto.genres.map { Genre(id: $0.id, name: $0.name) } 20 | return GenreList(genres: genres) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/SearchShowsFeature/Domain/Entities/GenreList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GenreTVShowListResult.swift 3 | // MyMovies 4 | // 5 | // Created by Jeans on 8/21/19. 6 | // Copyright © 2019 Jeans. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Shared 11 | 12 | struct GenreList { 13 | let genres: [Genre] 14 | } 15 | -------------------------------------------------------------------------------- /Sources/SearchShowsFeature/Domain/Interfaces/Repositories/GenreRemoteDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 6/05/22. 3 | // 4 | 5 | public protocol GenreRemoteDataSource { 6 | func fetchGenres() async throws -> GenreListDTO 7 | } 8 | -------------------------------------------------------------------------------- /Sources/SearchShowsFeature/Domain/Interfaces/Repositories/GenresRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 1/16/20. 3 | // 4 | 5 | import NetworkingInterface 6 | 7 | protocol GenresRepository { 8 | func genresList() async throws -> GenreList 9 | } 10 | -------------------------------------------------------------------------------- /Sources/SearchShowsFeature/Domain/UseCases/FetchGenresUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans on 1/14/20. 3 | // 4 | 5 | import NetworkingInterface 6 | 7 | protocol FetchGenresUseCase { 8 | func execute() async throws -> GenreList 9 | } 10 | 11 | final class DefaultFetchGenresUseCase: FetchGenresUseCase { 12 | 13 | private let genresRepository: GenresRepository 14 | 15 | init(genresRepository: GenresRepository) { 16 | self.genresRepository = genresRepository 17 | } 18 | 19 | //DataTransferError 20 | func execute() async throws -> GenreList { 21 | return try await genresRepository.genresList() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/SearchShowsFeature/Presentation/SearchOptions/Cells/GenreTableViewCell/GenreViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GenreViewModel.swift 3 | // SearchShows 4 | // 5 | // Created by Jeans Ruiz on 7/28/20. 6 | // 7 | 8 | import Shared 9 | 10 | protocol GenreViewModelProtocol { 11 | var id: Int { get } 12 | var name: String { get } 13 | } 14 | 15 | final class GenreViewModel: GenreViewModelProtocol, Hashable { 16 | let id: Int 17 | let name: String 18 | private let genre: Genre 19 | 20 | public init(genre: Genre) { 21 | self.genre = genre 22 | id = genre.id 23 | name = genre.name 24 | } 25 | 26 | func hash(into hasher: inout Hasher) { 27 | hasher.combine(id) 28 | } 29 | 30 | static func == (lhs: GenreViewModel, rhs: GenreViewModel) -> Bool { 31 | return lhs.id == rhs.id 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/SearchShowsFeature/Presentation/SearchOptions/Cells/VisitedShowTableViewCell/VisitedShowSectionModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VisitedShowSectionModel.swift 3 | // SearchShows 4 | // 5 | // Created by Jeans Ruiz on 7/8/20. 6 | // 7 | 8 | import Persistence 9 | 10 | enum HeaderModel: Hashable { 11 | case header 12 | } 13 | -------------------------------------------------------------------------------- /Sources/SearchShowsFeature/Presentation/SearchOptions/View/SearchOptionsSectionModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 7/8/20. 3 | // 4 | 5 | import Shared 6 | import UI 7 | 8 | enum SearchOptionsSectionModel { 9 | case showsVisited(items: [SearchSectionItem]) 10 | case genres(items: [SearchSectionItem]) 11 | 12 | var sectionView: SearchOptionsSectionView { 13 | switch self { 14 | case .showsVisited: 15 | return .showsVisited 16 | case .genres: 17 | return .genres 18 | } 19 | } 20 | 21 | var items: [SearchSectionItem] { 22 | switch self { 23 | case let .showsVisited(items): 24 | return items 25 | case let .genres(items): 26 | return items 27 | } 28 | } 29 | } 30 | 31 | enum SearchOptionsSectionView: Hashable { 32 | case showsVisited 33 | case genres 34 | 35 | var header: String? { 36 | switch self { 37 | case .showsVisited: 38 | return Strings.searchSectionRecentTitle.localized() 39 | case .genres: 40 | return Strings.searchSectionGenresTitle.localized() 41 | } 42 | } 43 | } 44 | 45 | enum SearchSectionItem: Hashable { 46 | case showsVisited(items: VisitedShowViewModel) 47 | case genres(items: GenreViewModel) 48 | } 49 | -------------------------------------------------------------------------------- /Sources/SearchShowsFeature/Presentation/SearchOptions/View/SearchSectionTableViewDiffableDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchSectionTableViewDiffableDataSource.swift 3 | // SearchShows 4 | // 5 | // Created by Jeans Ruiz on 17/03/22. 6 | // 7 | 8 | import UIKit 9 | 10 | class SearchSectionTableViewDiffableDataSource: UITableViewDiffableDataSource { 11 | 12 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 13 | let index = IndexPath(row: 0, section: section) 14 | 15 | if let model = itemIdentifier(for: index), let section = snapshot().sectionIdentifier(containingItem: model) { 16 | return section.header 17 | } 18 | return nil 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/SearchShowsFeature/Presentation/SearchOptions/ViewModel/SearchOptionsViewModelProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 8/7/20. 3 | // 4 | 5 | import Combine 6 | 7 | protocol SearchOptionsViewModelDelegate: AnyObject { 8 | func searchOptionsViewModel(_ searchOptionsViewModel: SearchOptionsViewModel, 9 | didGenrePicked idGenre: Int, 10 | title: String?) 11 | 12 | func searchOptionsViewModel(_ searchOptionsViewModel: SearchOptionsViewModel, 13 | didRecentShowPicked idShow: Int) 14 | } 15 | 16 | protocol SearchOptionsViewModelProtocol: VisitedShowViewModelDelegate { 17 | // MARK: - Input 18 | func viewDidLoad() async 19 | func modelIsPicked(with item: SearchSectionItem) 20 | 21 | // MARK: - Output 22 | var viewState: CurrentValueSubject { get } 23 | var dataSource: CurrentValueSubject<[SearchOptionsSectionModel], Never> { get } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/SearchShowsFeature/Presentation/SearchOptions/ViewModel/SearchViewState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchViewState.swift 3 | // SearchShows 4 | // 5 | // Created by Jeans Ruiz on 30/03/22. 6 | // 7 | 8 | enum SearchViewState { 9 | case loading 10 | case populated 11 | case empty 12 | case error(String) 13 | } 14 | 15 | extension SearchViewState: Equatable { 16 | 17 | static public func == (lhs: SearchViewState, rhs: SearchViewState) -> Bool { 18 | switch (lhs, rhs) { 19 | case (.loading, .loading): 20 | return true 21 | case (.populated, .populated): 22 | return true 23 | case (.empty, .empty): 24 | return true 25 | case (.error, .error): 26 | return true 27 | default: 28 | return false 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/SearchShowsFeature/Presentation/SearchResults/View/CustomSectionTableViewDiffableDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomSectionTableViewDiffableDataSource.swift 3 | // SearchShows 4 | // 5 | // Created by Jeans Ruiz on 17/03/22. 6 | // 7 | 8 | import UIKit 9 | 10 | class CustomSectionTableViewDiffableDataSource: UITableViewDiffableDataSource { 11 | 12 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 13 | let index = IndexPath(row: 0, section: section) 14 | 15 | if let model = itemIdentifier(for: index), let section = snapshot().sectionIdentifier(containingItem: model) { 16 | return section.header 17 | } 18 | return nil 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Coordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Coordinator.swift 3 | // Shared 4 | // 5 | // Created by Jeans Ruiz on 7/17/20. 6 | // 7 | 8 | import UIKit 9 | 10 | public protocol Coordinator: AnyObject { 11 | func start(with step: Step) 12 | func start() 13 | } 14 | 15 | public extension Coordinator { 16 | func start(with step: Step = DefaultStep() ) { } 17 | func start() { } 18 | } 19 | 20 | public protocol NavigationCoordinator: Coordinator { 21 | var navigationController: UINavigationController { get } 22 | } 23 | 24 | // MARK: - Step Protocol 25 | /// Describe un posible estado de navegación dentro de un Coordinator 26 | public protocol Step { } 27 | 28 | public struct DefaultStep: Step { 29 | public init() { } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Data/DataSources/Interfaces/AccessTokenLocalDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccessTokenLocalDataSource.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 12/05/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol AccessTokenLocalDataSource { 11 | func saveAccessToken(_ token: String) 12 | func getAccessToken() -> String 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Data/DataSources/Interfaces/AccountTVShowsDetailsRemoteDataSourceProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 13/05/22. 3 | // 4 | 5 | import Combine 6 | import NetworkingInterface 7 | 8 | public protocol AccountTVShowsDetailsRemoteDataSourceProtocol { 9 | func markAsFavorite(tvShowId: Int, userId: String,session: String, favorite: Bool) async throws-> TVShowActionStatusDTO 10 | 11 | func saveToWatchList(tvShowId: Int, userId: String, session: String, watchedList: Bool) async throws -> TVShowActionStatusDTO 12 | 13 | func fetchTVShowStatus(tvShowId: Int, sessionId: String) async throws -> TVShowAccountStatusDTO 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Data/DataSources/Interfaces/AccountTVShowsRemoteDataSourceProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 13/05/22. 3 | // 4 | 5 | public protocol AccountTVShowsRemoteDataSourceProtocol { 6 | func fetchFavoritesShows(page: Int, userId: Int, sessionId: String) async throws -> TVShowPageDTO 7 | func fetchWatchListShows(page: Int, userId: Int, sessionId: String) async throws -> TVShowPageDTO 8 | } 9 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Data/DataSources/Interfaces/LoggedUserLocalDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoggedUserLocalDataSource.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 12/05/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol LoggedUserLocalDataSource { 11 | func saveUser(userId: Int, sessionId: String) 12 | func getUser() -> AccountDomain? // MARK: - TODO, change by DTO 13 | func deleteUser() 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Data/DataSources/Interfaces/RequestTokenLocalDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestTokenLocalDataSource.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 12/05/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol RequestTokenLocalDataSource { 11 | func saveRequestToken(_ token: String) 12 | func getRequestToken() -> String? 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Data/DataSources/Interfaces/TVShowsDetailsRemoteDataSourceProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 13/05/22. 3 | // 4 | 5 | import Combine 6 | import NetworkingInterface 7 | 8 | public protocol TVShowsDetailsRemoteDataSourceProtocol { 9 | func fetchTVShowDetails(with showId: Int) async throws -> TVShowDetailDTO 10 | } 11 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Data/DataSources/Interfaces/TVShowsRemoteDataSourceProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 13/05/22. 3 | // 4 | 5 | public protocol TVShowsRemoteDataSourceProtocol { 6 | func fetchAiringTodayShows(page: Int) async throws -> TVShowPageDTO 7 | func fetchPopularShows(page: Int) async throws -> TVShowPageDTO 8 | func fetchShowsByGenre(genreId: Int, page: Int) async throws -> TVShowPageDTO 9 | func searchShowsFor(query: String, page: Int) async throws -> TVShowPageDTO 10 | } 11 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Data/DataSources/RemoteDataSources/TVShowsDetailsRemoteDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 13/05/22. 3 | // 4 | 5 | import Networking 6 | import NetworkingInterface 7 | 8 | public final class TVShowsDetailsRemoteDataSource: TVShowsDetailsRemoteDataSourceProtocol { 9 | private let apiClient: ApiClient 10 | 11 | public init(apiClient: ApiClient) { 12 | self.apiClient = apiClient 13 | } 14 | 15 | public func fetchTVShowDetails(with showId: Int) async throws ->TVShowDetailDTO { 16 | let endpoint = Endpoint( 17 | path: "3/tv/\(showId)", 18 | method: .get 19 | ) 20 | return try await apiClient.apiRequest(endpoint: endpoint, as: TVShowDetailDTO.self) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Data/Network/DataMapping/DTOs/GenreDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GenreDTO.swift 3 | // 4 | // Created by Jeans Ruiz on 1/16/20. 5 | // Copyright © 2020 Jeans. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct GenreDTO: Decodable { 11 | public let id: Int 12 | public let name: String 13 | 14 | enum CodingKeys: String, CodingKey { 15 | case id 16 | case name 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Data/Network/DataMapping/DTOs/TVShowAccountStatusDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVShowAccountStatusDTO.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 5/05/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct TVShowAccountStatusDTO: Decodable { 11 | public let showId: Int 12 | public let isFavorite: Bool 13 | public let isWatchList: Bool 14 | 15 | enum CodingKeys: String, CodingKey { 16 | case showId = "id" 17 | case isFavorite = "favorite" 18 | case isWatchList = "watchlist" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Data/Network/DataMapping/DTOs/TVShowActionStatusDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVShowActionStatusDTO.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 5/05/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct TVShowActionStatusDTO: Decodable { 11 | let code: Int 12 | let message: String? 13 | 14 | enum CodingKeys: String, CodingKey { 15 | case code = "status_code" 16 | case message = "status_message" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Data/Network/DataMapping/Mappers/DefaultAccountTVShowDetailsMapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultAccountTVShowDetailsMapper.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 5/05/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public final class DefaultAccountTVShowDetailsMapper: AccountTVShowsDetailsMapperProtocol { 11 | 12 | public init() { } 13 | 14 | public func mapActionResult(result: TVShowActionStatusDTO) -> TVShowActionStatus { 15 | return TVShowActionStatus(statusCode: result.code, statusMessage: result.message ?? "") 16 | } 17 | 18 | public func mapTVShowStatusResult(result: TVShowAccountStatusDTO) -> TVShowAccountStatus { 19 | return TVShowAccountStatus(showId: result.showId, isFavorite: result.isFavorite, isWatchList: result.isWatchList) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Data/Network/DataMapping/Mappers/MappersInterfaces.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MappersInterfaces.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 13/05/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol TVShowPageMapperProtocol { 11 | func mapTVShowPage(_ page: TVShowPageDTO, imageBasePath: String, imageSize: ImageSize) -> TVShowPage 12 | } 13 | 14 | public protocol TVShowDetailsMapperProtocol { 15 | func mapTVShow(_ show: TVShowDetailDTO, imageBasePath: String, imageSize: ImageSize) -> TVShowDetail 16 | } 17 | 18 | public protocol AccountTVShowsDetailsMapperProtocol { 19 | func mapActionResult(result: TVShowActionStatusDTO) -> TVShowActionStatus 20 | func mapTVShowStatusResult(result: TVShowAccountStatusDTO) -> TVShowAccountStatus 21 | } 22 | 23 | public enum ImageSize: String { 24 | case small = "w342" 25 | case medium = "w780" 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Data/Repositories/AccessTokenRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccessTokenRepository.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 12/05/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public final class AccessTokenRepository { 11 | private let dataSource: AccessTokenLocalDataSource 12 | 13 | public init(dataSource: AccessTokenLocalDataSource) { 14 | self.dataSource = dataSource 15 | } 16 | } 17 | 18 | extension AccessTokenRepository: AccessTokenRepositoryProtocol { 19 | public func saveAccessToken(_ token: String) { 20 | dataSource.saveAccessToken(token) 21 | } 22 | 23 | public func getAccessToken() -> String { 24 | dataSource.getAccessToken() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Data/Repositories/DefaultTVShowsDetailRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 4/05/22. 3 | // 4 | 5 | import Networking 6 | import NetworkingInterface 7 | 8 | public final class DefaultTVShowsDetailRepository { 9 | private let showsPageRemoteDataSource: TVShowsDetailsRemoteDataSourceProtocol 10 | private let mapper: TVShowDetailsMapperProtocol 11 | private let imageBasePath: String 12 | 13 | public init( 14 | showsPageRemoteDataSource: TVShowsDetailsRemoteDataSourceProtocol, 15 | mapper: TVShowDetailsMapperProtocol, 16 | imageBasePath: String 17 | ) { 18 | self.showsPageRemoteDataSource = showsPageRemoteDataSource 19 | self.mapper = mapper 20 | self.imageBasePath = imageBasePath 21 | } 22 | } 23 | 24 | extension DefaultTVShowsDetailRepository: TVShowsDetailsRepository { 25 | public func fetchTVShowDetails(with showId: Int) async throws -> TVShowDetail { 26 | let dto = try await showsPageRemoteDataSource.fetchTVShowDetails(with: showId) 27 | return mapper.mapTVShow(dto, imageBasePath: imageBasePath, imageSize: .medium) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Data/Repositories/LoggedUserRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoggedUserRepository.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 12/05/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public final class LoggedUserRepository { 11 | let dataSource: LoggedUserLocalDataSource 12 | 13 | public init(dataSource: LoggedUserLocalDataSource) { 14 | self.dataSource = dataSource 15 | } 16 | } 17 | 18 | extension LoggedUserRepository: LoggedUserRepositoryProtocol { 19 | 20 | public func saveUser(userId: Int, sessionId: String) {// MARK: - TODO, use userId as String 21 | dataSource.saveUser(userId: userId, sessionId: sessionId) 22 | } 23 | 24 | public func getUser() -> AccountDomain? { 25 | return dataSource.getUser() 26 | } 27 | 28 | public func deleteUser() { 29 | dataSource.deleteUser() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Data/Repositories/RequestTokenRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestTokenRepository.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 12/05/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public final class RequestTokenRepository { 11 | private let dataSource: RequestTokenLocalDataSource 12 | 13 | public init(dataSource: RequestTokenLocalDataSource) { 14 | self.dataSource = dataSource 15 | } 16 | } 17 | 18 | extension RequestTokenRepository: RequestTokenRepositoryProtocol { 19 | public func saveRequestToken(_ token: String) { 20 | dataSource.saveRequestToken(token) 21 | } 22 | 23 | public func getRequestToken() -> String? { 24 | dataSource.getRequestToken() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Domain/Entities/Account.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Account.swift 3 | // TVToday 4 | // 5 | // Created by Jeans Ruiz on 6/21/20. 6 | // Copyright © 2020 Jeans. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct AccountDomain { 12 | public let id: Int 13 | public let sessionId: String 14 | 15 | public init(id: Int, sessionId: String) { 16 | self.id = id 17 | self.sessionId = sessionId 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Domain/Entities/Genre.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Genre.swift 3 | // MyMovies 4 | // 5 | // Created by Jeans on 8/21/19. 6 | // Copyright © 2019 Jeans. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Genre: Hashable { 12 | 13 | public init(id: Int, name: String) { 14 | self.id = id 15 | self.name = name 16 | } 17 | 18 | public let id: Int 19 | public let name: String 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Domain/Entities/TVShowAccountStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVShowAccountStatus.swift 3 | // 4 | // Created by Jeans Ruiz on 6/23/20. 5 | // Copyright © 2020 Jeans. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct TVShowAccountStatus { 11 | public let showId: Int 12 | public let isFavorite: Bool 13 | public let isWatchList: Bool 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Domain/Entities/TVShowActionStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVShowActionStatus.swift 3 | // 4 | // Created by Jeans Ruiz on 6/23/20. 5 | // Copyright © 2020 Jeans. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct TVShowActionStatus { 11 | let statusCode: Int 12 | let statusMessage: String 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Domain/ErrorEnvelope.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 6/26/20. 3 | // 4 | 5 | import Foundation 6 | import NetworkingInterface 7 | 8 | #warning("Use ApiError instead") 9 | public struct ErrorEnvelope { 10 | public let errorMessages: [String] 11 | public let apiCode: TodayCode? 12 | 13 | public init( 14 | errorMessages: [String] = [], 15 | apiCode: TodayCode? = nil 16 | ) { 17 | self.errorMessages = errorMessages 18 | self.apiCode = apiCode 19 | } 20 | 21 | public enum TodayCode: String { 22 | // Codes defined by the client 23 | case MappingFailed = "mapping_failed" 24 | case TransferError = "transfer_error" 25 | } 26 | } 27 | 28 | extension ErrorEnvelope: Error { } 29 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Domain/Interfaces/Repositories/AccessTokenRepositoryProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccessTokenRepositoryProtocol.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 12/05/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol AccessTokenRepositoryProtocol { 11 | func saveAccessToken(_ token: String) 12 | func getAccessToken() -> String 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Domain/Interfaces/Repositories/AccountTVShowsDetailsRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 7/05/22. 3 | // 4 | 5 | import Combine 6 | import NetworkingInterface 7 | 8 | public protocol AccountTVShowsDetailsRepository { 9 | func markAsFavorite(tvShowId: Int, favorite: Bool) async throws -> TVShowActionStatus 10 | func saveToWatchList(tvShowId: Int, watchedList: Bool) async throws -> TVShowActionStatus 11 | func fetchTVShowStatus(tvShowId: Int) async throws -> TVShowAccountStatus 12 | } 13 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Domain/Interfaces/Repositories/AccountTVShowsRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 6/27/20. 3 | // 4 | 5 | public protocol AccountTVShowsRepository { 6 | func fetchFavoritesShows(page: Int) async throws -> TVShowPage 7 | func fetchWatchListShows(page: Int) async throws -> TVShowPage 8 | } 9 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Domain/Interfaces/Repositories/LoggedUserRepositoryProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoggedUserRepositoryProtocol.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 12/05/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol LoggedUserRepositoryProtocol { 11 | func saveUser(userId: Int, sessionId: String) // MARK: - TODO, use userId as String 12 | func getUser() -> AccountDomain? 13 | func deleteUser() 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Domain/Interfaces/Repositories/RequestTokenRepositoryProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestTokenRepositoryProtocol.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 12/05/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol RequestTokenRepositoryProtocol { 11 | func saveRequestToken(_ token: String) 12 | func getRequestToken() -> String? 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Domain/Interfaces/Repositories/TVShowsDetailsRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 13/05/22. 3 | // 4 | 5 | public protocol TVShowsDetailsRepository { 6 | func fetchTVShowDetails(with showId: Int) async throws -> TVShowDetail 7 | } 8 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Domain/Interfaces/Repositories/TVShowsPageRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans on 1/14/20. 3 | // 4 | 5 | public protocol TVShowsPageRepository { 6 | func fetchAiringTodayShows(page: Int) async -> TVShowPage? // todo, return nil?? nahhh 7 | func fetchPopularShows(page: Int) async -> TVShowPage? 8 | func fetchShowsByGenre(genreId: Int, page: Int) async -> TVShowPage? 9 | func searchShowsFor(query: String, page: Int) async -> TVShowPage? 10 | } 11 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Domain/UseCases/FetchLoggedUser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FetchLoggedUser.swift 3 | // TVToday 4 | // 5 | // Created by Jeans Ruiz on 6/21/20. 6 | // Copyright © 2020 Jeans. All rights reserved. 7 | // 8 | 9 | public protocol FetchLoggedUser { 10 | func execute() -> AccountDomain? 11 | } 12 | 13 | public final class DefaultFetchLoggedUser: FetchLoggedUser { 14 | private let loggedRepository: LoggedUserRepositoryProtocol 15 | 16 | public init(loggedRepository: LoggedUserRepositoryProtocol) { 17 | self.loggedRepository = loggedRepository 18 | } 19 | 20 | public func execute() -> AccountDomain? { 21 | return loggedRepository.getUser() // MARK: - TODO, Show Details access 3 times? 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Domain/UseCases/FetchShowsUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans on 1/14/20. 3 | // 4 | 5 | import Combine 6 | import NetworkingInterface 7 | 8 | public protocol FetchTVShowsUseCase { 9 | func execute(request: FetchTVShowsUseCaseRequestValue) async -> TVShowPage? 10 | } 11 | 12 | public struct FetchTVShowsUseCaseRequestValue { 13 | public let page: Int 14 | 15 | public init(page: Int) { 16 | self.page = page 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/Shared/Sources/Language.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Language.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 15/06/22. 6 | // 7 | 8 | /// The Languages the App supported 9 | public enum Language: String, CaseIterable { 10 | case en 11 | case es 12 | 13 | public init?(languageStrings languages: [String]) { 14 | guard let preferedLanguage = languages.first, 15 | let language = Language.init( 16 | rawValue: String(preferedLanguage.prefix(2).lowercased())) else { 17 | return nil 18 | } 19 | 20 | self = language 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/ShowDetailsFeature/DIContainer/Module.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShowDetailsDependencies.swift 3 | // ShowDetails 4 | // 5 | // Created by Jeans Ruiz on 6/28/20. 6 | // 7 | 8 | import UIKit 9 | import Shared 10 | import ShowDetailsFeatureInterface 11 | 12 | public struct Module: ModuleShowDetailsBuilder { 13 | 14 | private let diContainer: DIContainer 15 | 16 | public init(dependencies: ModuleDependencies) { 17 | self.diContainer = DIContainer(dependencies: dependencies) 18 | } 19 | 20 | public func buildModuleCoordinator(in navigationController: UINavigationController, 21 | delegate: TVShowDetailCoordinatorDelegate?) -> TVShowDetailCoordinatorProtocol { 22 | return diContainer.buildModuleCoordinator(navigationController: navigationController, delegate: delegate) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/ShowDetailsFeature/DIContainer/TVShowDetailCoordinatorProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVShowDetailCoordinatorProtocol.swift 3 | // ShowDetails 4 | // 5 | // Created by Jeans Ruiz on 8/12/20. 6 | // 7 | 8 | import UIKit 9 | import Shared 10 | import ShowDetailsFeatureInterface 11 | 12 | protocol TVShowDetailCoordinatorDependencies { 13 | func buildShowDetailsViewController(with showId: Int, 14 | coordinator: TVShowDetailCoordinatorProtocol?, 15 | closures: TVShowDetailViewModelClosures?) -> UIViewController 16 | 17 | func buildEpisodesViewController(with showId: Int) -> UIViewController 18 | } 19 | -------------------------------------------------------------------------------- /Sources/ShowDetailsFeature/Data/Network/DataMapping/TVEpisodesMapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVEpisodesMapper.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 5/05/22. 6 | // 7 | 8 | import Foundation 9 | import Shared 10 | 11 | public class TVEpisodesMapper: TVEpisodesMapperProtocol { 12 | public init() { } 13 | 14 | public func mapSeasonDTO(_ season: TVShowSeasonDTO, imageBasePath: String, imageSize: ImageSize) -> TVShowSeason { 15 | let episodes: [TVShowEpisode] = season.episodes.map { 16 | let posterPath = $0.posterPath ?? "" 17 | let posterPathURL = URL(string: "\(imageBasePath)/t/p/\(imageSize.rawValue)\(posterPath)") 18 | return TVShowEpisode( 19 | id: $0.id, 20 | episodeNumber: $0.episodeNumber, 21 | name: $0.name, 22 | airDate: $0.airDate, 23 | voteAverage: $0.voteAverage, 24 | posterPathURL: posterPathURL 25 | ) 26 | } 27 | return TVShowSeason(id: season.id, episodes: episodes, seasonNumber: season.seasonNumber) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/ShowDetailsFeature/Data/Network/DataMapping/TVShowEpisodeDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVShowEpisodeDTO.swift 3 | // TVToday 4 | // 5 | // Created by Jeans Ruiz on 1/16/20. 6 | // Copyright © 2020 Jeans. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct TVShowEpisodeDTO: Decodable { 12 | let id: Int 13 | let episodeNumber: Int 14 | let name: String? 15 | let airDate: String? 16 | let voteAverage: Double? 17 | let posterPath: String? 18 | 19 | enum CodingKeys: String, CodingKey { 20 | case id = "id" 21 | case episodeNumber = "episode_number" 22 | case name 23 | case airDate = "air_date" 24 | case voteAverage = "vote_average" 25 | case posterPath = "still_path" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/ShowDetailsFeature/Data/Network/DataMapping/TVShowSeasonDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVShowSeasonDTO.swift 3 | // TVToday 4 | // 5 | // Created by Jeans Ruiz on 1/16/20. 6 | // Copyright © 2020 Jeans. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct TVShowSeasonDTO: Decodable { 12 | let id: String 13 | let episodes: [TVShowEpisodeDTO] 14 | let seasonNumber: Int 15 | 16 | enum CodingKeys: String, CodingKey { 17 | case id = "_id" 18 | case episodes 19 | case seasonNumber = "season_number" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/ShowDetailsFeature/Data/Repositories/DefaultTVEpisodesRemoteDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 5/05/22. 3 | // 4 | 5 | import Networking 6 | import NetworkingInterface 7 | 8 | public final class DefaultTVEpisodesRemoteDataSource { 9 | private let apiClient: ApiClient 10 | 11 | public init(apiClient: ApiClient) { 12 | self.apiClient = apiClient 13 | } 14 | } 15 | 16 | extension DefaultTVEpisodesRemoteDataSource: TVEpisodesRemoteDataSource { 17 | public func fetchEpisodes(for showId: Int, season: Int) async throws -> TVShowSeasonDTO { 18 | let endpoint = Endpoint( 19 | path: "3/tv/\(showId)/season/\(season)", 20 | method: .get 21 | ) 22 | return try await apiClient.apiRequest(endpoint: endpoint, as: TVShowSeasonDTO.self) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/ShowDetailsFeature/Data/Repositories/DefaultTVEpisodesRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 1/20/20. 3 | // 4 | 5 | import NetworkingInterface 6 | import Networking 7 | import Shared 8 | 9 | public final class DefaultTVEpisodesRepository { 10 | private let remoteDataSource: TVEpisodesRemoteDataSource 11 | private let mapper: TVEpisodesMapperProtocol 12 | private let imageBasePath: String 13 | 14 | public init(remoteDataSource: TVEpisodesRemoteDataSource, mapper: TVEpisodesMapperProtocol, imageBasePath: String) { 15 | self.remoteDataSource = remoteDataSource 16 | self.mapper = mapper 17 | self.imageBasePath = imageBasePath 18 | } 19 | } 20 | 21 | extension DefaultTVEpisodesRepository: TVEpisodesRepository { 22 | func fetchEpisodesList(for show: Int, season: Int) async throws -> TVShowSeason { 23 | let dto = try await remoteDataSource.fetchEpisodes(for: show, season: season) 24 | return mapper.mapSeasonDTO(dto, imageBasePath: imageBasePath, imageSize: .small) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/ShowDetailsFeature/Domain/Entities/TVShowEpisode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVShowEpisode.swift 3 | // MyTvShows 4 | // 5 | // Created by Jeans on 9/20/19. 6 | // Copyright © 2019 Jeans. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct TVShowEpisode { 12 | let id: Int 13 | let episodeNumber: Int 14 | let name: String? 15 | let airDate: String? 16 | let voteAverage: Double? 17 | let posterPathURL: URL? 18 | } 19 | 20 | extension TVShowEpisode { 21 | public var average: String { 22 | if let voteAverage = self.voteAverage { 23 | return String(format: "%.1f", voteAverage) 24 | } 25 | return "" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/ShowDetailsFeature/Domain/Entities/TVShowSeason.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SeasonResult.swift 3 | // MyTvShows 4 | // 5 | // Created by Jeans on 9/20/19. 6 | // Copyright © 2019 Jeans. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct TVShowSeason { 12 | let id: String 13 | let episodes: [TVShowEpisode] 14 | let seasonNumber: Int 15 | } 16 | -------------------------------------------------------------------------------- /Sources/ShowDetailsFeature/Domain/Interfaces/Repositories/TVEpisodesRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 1/20/20. 3 | // 4 | 5 | import Foundation 6 | import NetworkingInterface 7 | import Shared 8 | 9 | protocol TVEpisodesRepository { 10 | func fetchEpisodesList(for show: Int, season: Int) async throws -> TVShowSeason // MARK: - TODO, Change Name 11 | } 12 | 13 | public protocol TVEpisodesRemoteDataSource { 14 | func fetchEpisodes(for showId: Int, season: Int) async throws -> TVShowSeasonDTO 15 | } 16 | 17 | public protocol TVEpisodesMapperProtocol { 18 | func mapSeasonDTO(_ season: TVShowSeasonDTO, imageBasePath: String, imageSize: ImageSize) -> TVShowSeason 19 | } 20 | -------------------------------------------------------------------------------- /Sources/ShowDetailsFeature/Domain/UseCases/FetchEpisodesUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 1/20/20. 3 | // 4 | 5 | protocol FetchEpisodesUseCase { 6 | func execute(request: FetchEpisodesUseCaseRequestValue) async throws -> TVShowSeason 7 | } 8 | 9 | struct FetchEpisodesUseCaseRequestValue { 10 | let showIdentifier: Int 11 | let seasonNumber: Int 12 | } 13 | 14 | // MARK: - DefaultFetchEpisodesUseCase 15 | final class DefaultFetchEpisodesUseCase: FetchEpisodesUseCase { 16 | 17 | private let episodesRepository: TVEpisodesRepository 18 | 19 | init(episodesRepository: TVEpisodesRepository) { 20 | self.episodesRepository = episodesRepository 21 | } 22 | 23 | func execute(request: FetchEpisodesUseCaseRequestValue) async throws -> TVShowSeason { 24 | return try await episodesRepository.fetchEpisodesList( 25 | for: request.showIdentifier, 26 | season: request.seasonNumber 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/ShowDetailsFeature/Domain/UseCases/FetchTVAccountStates.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 6/23/20. 3 | // 4 | 5 | import Shared 6 | 7 | protocol FetchTVAccountStates { 8 | func execute(request: FetchTVAccountStatesRequestValue) async throws -> TVShowAccountStatus 9 | } 10 | 11 | struct FetchTVAccountStatesRequestValue { 12 | let showId: Int 13 | } 14 | 15 | final class DefaultFetchTVAccountStates: FetchTVAccountStates { 16 | private let accountShowsRepository: AccountTVShowsDetailsRepository 17 | 18 | init(accountShowsRepository: AccountTVShowsDetailsRepository) { 19 | self.accountShowsRepository = accountShowsRepository 20 | } 21 | 22 | func execute(request: FetchTVAccountStatesRequestValue) async throws -> TVShowAccountStatus { 23 | return try await accountShowsRepository.fetchTVShowStatus(tvShowId: request.showId) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/ShowDetailsFeature/Domain/UseCases/MarkAsFavoriteUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 6/23/20. 3 | // 4 | 5 | import Shared 6 | import NetworkingInterface 7 | 8 | public protocol MarkAsFavoriteUseCase { 9 | func execute(request: MarkAsFavoriteUseCaseRequestValue) async throws -> Bool 10 | } 11 | 12 | public struct MarkAsFavoriteUseCaseRequestValue { 13 | let showId: Int 14 | let favorite: Bool 15 | } 16 | 17 | public final class DefaultMarkAsFavoriteUseCase: MarkAsFavoriteUseCase { 18 | private let accountShowsRepository: AccountTVShowsDetailsRepository 19 | 20 | public init(accountShowsRepository: AccountTVShowsDetailsRepository) { 21 | self.accountShowsRepository = accountShowsRepository 22 | } 23 | 24 | public func execute(request: MarkAsFavoriteUseCaseRequestValue) async throws -> Bool { 25 | _ = try await accountShowsRepository.markAsFavorite( 26 | tvShowId: request.showId, 27 | favorite: request.favorite 28 | ) 29 | return request.favorite 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/ShowDetailsFeature/Domain/UseCases/SaveToWatchListUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 6/23/20. 3 | // 4 | 5 | import Shared 6 | import NetworkingInterface 7 | 8 | public protocol SaveToWatchListUseCase { 9 | func execute(request: SaveToWatchListUseCaseRequestValue) async throws -> Bool 10 | } 11 | 12 | public struct SaveToWatchListUseCaseRequestValue { 13 | let showId: Int 14 | let watchList: Bool 15 | } 16 | 17 | final class DefaultSaveToWatchListUseCase: SaveToWatchListUseCase { 18 | private let accountShowsRepository: AccountTVShowsDetailsRepository 19 | 20 | init(accountShowsRepository: AccountTVShowsDetailsRepository) { 21 | self.accountShowsRepository = accountShowsRepository 22 | } 23 | 24 | public func execute(request: SaveToWatchListUseCaseRequestValue) async throws -> Bool { 25 | _ = try await accountShowsRepository.saveToWatchList( 26 | tvShowId: request.showId, 27 | watchedList: request.watchList 28 | ) 29 | return request.watchList 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/ShowDetailsFeature/Presentation/SeasonScene/ViewModel/EpisodeItemViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SeasonListTableViewModel.swift 3 | // MyTvShows 4 | // 5 | // Created by Jeans on 9/23/19. 6 | // Copyright © 2019 Jeans. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class EpisodeItemViewModel { 12 | var episodeNumber: String? 13 | var episodeName: String? 14 | var releaseDate: String? 15 | var average: String? 16 | var posterURL: URL? 17 | 18 | private let episode: TVShowEpisode 19 | 20 | init(episode: TVShowEpisode) { 21 | self.episode = episode 22 | setupData(with: episode) 23 | } 24 | 25 | private func setupData(with episode: TVShowEpisode) { 26 | episodeNumber = String(episode.episodeNumber) + "." 27 | episodeName = episode.name 28 | releaseDate = episode.airDate 29 | average = episode.average 30 | posterURL = episode.posterPathURL 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/ShowDetailsFeature/Presentation/SeasonScene/ViewModel/SeasonEpisodeViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SeasonEpisodeViewModel.swift 3 | // MyTvShows 4 | // 5 | // Created by Jeans on 9/24/19. 6 | // Copyright © 2019 Jeans. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class SeasonEpisodeViewModel { 12 | var seasonNumber: String 13 | 14 | init( seasonNumber: Int) { 15 | self.seasonNumber = String(seasonNumber) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/ShowDetailsFeature/Presentation/SeasonScene/ViewModel/SeasonHeaderViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SeasonHeaderViewModel.swift 3 | // MyTvShows 4 | // 5 | // Created by Jeans on 9/25/19. 6 | // Copyright © 2019 Jeans. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Shared 11 | 12 | public struct SeasonHeaderViewModel: Hashable { 13 | let showName: String 14 | 15 | public init(showDetail: TVShowDetail) { 16 | if let years = showDetail.releaseYears { 17 | showName = showDetail.name + " (" + years + ")" 18 | } else { 19 | showName = showDetail.name 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/ShowDetailsFeature/Presentation/ShowDetailsScene/ViewModel/TVShowDetailInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVShowDetailInfo.swift 3 | // ShowDetails 4 | // 5 | // Created by Jeans Ruiz on 8/4/20. 6 | // 7 | 8 | import Foundation 9 | import Shared 10 | 11 | public struct TVShowDetailInfo { 12 | var id: Int 13 | var backDropPath: URL? 14 | var nameShow: String? 15 | var yearsRelease: String? 16 | var duration: String? 17 | var genre: String? 18 | var numberOfEpisodes: String? 19 | var posterPath: URL? 20 | var overView: String? 21 | var score: String? 22 | var maxScore: String = "/10" 23 | var countVote: String? 24 | 25 | public init(show: TVShowDetail) { 26 | id = show.id 27 | backDropPath = show.backDropPathURL 28 | nameShow = show.name 29 | yearsRelease = show.releaseYears 30 | duration = show.episodeDuration 31 | genre = show.genreIds.first?.name 32 | numberOfEpisodes = String(show.numberOfEpisodes) 33 | posterPath = show.posterPathURL 34 | overView = show.overview 35 | score = String(show.voteAverage) 36 | countVote = String(show.voteCount) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/ShowListFeature/DIContainer/Module.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShowListDependencies.swift 3 | // TVShowsList 4 | // 5 | // Created by Jeans Ruiz on 6/27/20. 6 | // 7 | 8 | import UIKit 9 | import Shared 10 | import ShowListFeatureInterface 11 | 12 | public struct Module: ModuleShowListDetailsBuilder { 13 | 14 | private let diContainer: DIContainer 15 | 16 | public init(dependencies: ModuleDependencies) { 17 | self.diContainer = DIContainer(dependencies: dependencies) 18 | } 19 | 20 | public func buildModuleCoordinator(in navigationController: UINavigationController, 21 | delegate: TVShowListCoordinatorDelegate?) -> TVShowListCoordinatorProtocol { 22 | return diContainer.buildModuleCoordinator(navigationController: navigationController, delegate: delegate) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/ShowListFeature/Domain/UseCases/DefaultUserFavoritesShowsUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 6/27/20. 3 | // 4 | 5 | import Shared 6 | import NetworkingInterface 7 | 8 | public final class DefaultUserFavoritesShowsUseCase: FetchTVShowsUseCase { 9 | private let accountShowsRepository: AccountTVShowsRepository 10 | 11 | public init(accountShowsRepository: AccountTVShowsRepository) { 12 | self.accountShowsRepository = accountShowsRepository 13 | } 14 | 15 | public func execute(request: FetchTVShowsUseCaseRequestValue) async -> TVShowPage? { 16 | do { 17 | return try await accountShowsRepository.fetchFavoritesShows(page: request.page) 18 | } catch { 19 | return nil 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/ShowListFeature/Domain/UseCases/DefaultUserWatchListShowsUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 6/27/20. 3 | // 4 | 5 | import Shared 6 | import NetworkingInterface 7 | 8 | public final class DefaultUserWatchListShowsUseCase: FetchTVShowsUseCase { 9 | private let accountShowsRepository: AccountTVShowsRepository 10 | 11 | public init(accountShowsRepository: AccountTVShowsRepository) { 12 | self.accountShowsRepository = accountShowsRepository 13 | } 14 | 15 | public func execute(request: FetchTVShowsUseCaseRequestValue) async -> TVShowPage? { 16 | do { 17 | return try await accountShowsRepository.fetchWatchListShows(page: request.page) 18 | } catch { 19 | return nil 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/ShowListFeature/Domain/UseCases/FetchShowsByGenreTVShowsUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FetchShowsByGenreTVShowsUseCase.swift 3 | // SearchShows 4 | // 5 | // Created by Jeans Ruiz on 6/28/20. 6 | // 7 | 8 | import Combine 9 | import Shared 10 | import NetworkingInterface 11 | 12 | final class DefaultFetchShowsByGenreTVShowsUseCase: FetchTVShowsUseCase { 13 | private let genreId: Int 14 | private let tvShowsPageRepository: TVShowsPageRepository 15 | 16 | init(genreId: Int, tvShowsPageRepository: TVShowsPageRepository) { 17 | self.genreId = genreId 18 | self.tvShowsPageRepository = tvShowsPageRepository 19 | } 20 | 21 | func execute(request: FetchTVShowsUseCaseRequestValue) async -> TVShowPage? { 22 | return await tvShowsPageRepository.fetchShowsByGenre(genreId: genreId, page: request.page) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/ShowListFeature/Presentation/View/SectionTVShowListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionTVShowListView.swift 3 | // TVShowsList 4 | // 5 | // Created by Jeans Ruiz on 8/3/20. 6 | // 7 | 8 | enum SectionListShowsView: Hashable { 9 | case list 10 | } 11 | -------------------------------------------------------------------------------- /Sources/UI/Components/Cells/TVShowCellViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVShowCellViewModel.swift 3 | // Shared 4 | // 5 | // Created by Jeans on 9/14/19. 6 | // Copyright © 2019 Jeans. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Shared 11 | 12 | public struct TVShowCellViewModel: Hashable { 13 | private let showId: Int 14 | let name: String 15 | let average: String 16 | let firstAirDate: String 17 | let posterPathURL: URL? 18 | 19 | public init(show: TVShowPage.TVShow) { 20 | showId = show.id 21 | name = show.name 22 | average = (show.voteAverage == 0) ? "0.0": String(show.voteAverage) 23 | firstAirDate = show.firstAirDate 24 | posterPathURL = show.posterPath 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/UI/Components/DefaultRefreshControl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultRefreshControl.swift 3 | // Shared 4 | // 5 | // Created by Jeans Ruiz on 8/20/20. 6 | // 7 | 8 | import UIKit 9 | 10 | public class DefaultRefreshControl: UIRefreshControl { 11 | 12 | var refreshHandler: () -> Void 13 | 14 | // MARK: - Initializer 15 | public init(tintColor: UIColor = .systemBlue, 16 | attributedTitle: String = "", 17 | backgroundColor: UIColor? = .clear, 18 | refreshHandler: @escaping () -> Void) { 19 | self.refreshHandler = refreshHandler 20 | super.init() 21 | self.tintColor = tintColor 22 | self.backgroundColor = backgroundColor 23 | self.attributedTitle = NSAttributedString( 24 | string: attributedTitle, 25 | attributes: [ 26 | NSAttributedString.Key.font: UIFont.app_caption1(), 27 | NSAttributedString.Key.foregroundColor: tintColor 28 | ] 29 | ) 30 | addTarget(self, action: #selector(refreshControlAction), for: .valueChanged) 31 | } 32 | 33 | required init?(coder aDecoder: NSCoder) { 34 | fatalError() 35 | } 36 | 37 | @objc func refreshControlAction() { 38 | refreshHandler() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/UI/Components/LoadableButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadableButton.swift 3 | // TVToday 4 | // 5 | // Created by Jeans Ruiz on 6/22/20. 6 | // Copyright © 2020 Jeans. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class LoadableButton: UIButton, Loadable { 12 | 13 | public var defaultTitle: String? = "" 14 | 15 | public func defaultShowLoadingView() { 16 | (self as Loadable).showLoadingView() 17 | setTitle("", for: .normal) 18 | } 19 | 20 | public func defaultHideLoadingView() { 21 | (self as Loadable).hideLoadingView() 22 | setTitle(defaultTitle, for: .normal) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/UI/Extensions/Dequeuable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dequeuable.swift 3 | // Shared 4 | // 5 | // Created by Jeans Ruiz on 6/26/20. 6 | // 7 | 8 | import UIKit 9 | 10 | public protocol Dequeuable { 11 | static var dequeuIdentifier: String { get } 12 | } 13 | 14 | extension Dequeuable where Self: UIView { 15 | public static var dequeuIdentifier: String { 16 | return String(describing: self) 17 | } 18 | 19 | } 20 | 21 | extension UITableViewCell: Dequeuable { } 22 | 23 | extension UICollectionViewCell: Dequeuable { } 24 | -------------------------------------------------------------------------------- /Sources/UI/Extensions/UICollectionView+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionView+Extensions.swift 3 | // Shared 4 | // 5 | // Created by Jeans Ruiz on 6/26/20. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UICollectionView { 11 | 12 | // MARK: - Register Cell 13 | public func registerCell(cellType: T.Type) { 14 | let identifier = cellType.dequeuIdentifier 15 | register(cellType, forCellWithReuseIdentifier: identifier) 16 | } 17 | 18 | // MARK: - Dequeuing 19 | public func dequeueReusableCell(with type: T.Type, for indexPath: IndexPath) -> T { 20 | return self.dequeueReusableCell(withReuseIdentifier: type.dequeuIdentifier, for: indexPath) as! T 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/UI/Extensions/UIImage+Loader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Loader.swift 3 | // Account 4 | // 5 | // Created by Jeans Ruiz on 6/26/20. 6 | // 7 | 8 | import UIKit 9 | 10 | public extension UIImage { 11 | 12 | convenience init?(name: String) { 13 | self.init(named: name, in: Bundle.module, compatibleWith: .none) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/UI/Extensions/UIImageView+Kingfisher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImageView+Kingfisher.swift 3 | // TVToday 4 | // 5 | // Created by Jeans Ruiz on 3/25/20. 6 | // Copyright © 2020 Jeans. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Kingfisher 11 | 12 | extension UIImageView { 13 | 14 | public func setImage(with url: URL?, placeholder: UIImage? = nil) { 15 | kf.indicatorType = .activity 16 | kf.setImage(with: url, placeholder: placeholder) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/UI/Extensions/UINavigationController+Create.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UINavigationController+Create.swift 3 | // UI 4 | // 5 | // Created by Jeans Ruiz on 7/13/20. 6 | // 7 | 8 | import UIKit 9 | 10 | public extension UINavigationController { 11 | 12 | class func replaceAppearance() { 13 | let standard = UINavigationBarAppearance() 14 | standard.configureWithDefaultBackground() 15 | 16 | standard.titleTextAttributes = [ 17 | .foregroundColor: UIColor.systemBlue, 18 | .font: UIFont.app_title3().bolded.designed(.rounded) 19 | ] 20 | 21 | UINavigationBar.appearance().standardAppearance = standard 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/UI/Extensions/UIRefreshControl+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIRefreshControl+Extensions.swift 3 | // Shared 4 | // 5 | // Created by Jeans Ruiz on 8/20/20. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIRefreshControl { 11 | 12 | public func endRefreshing(with delay: TimeInterval = 0.5) { 13 | if isRefreshing { 14 | perform(#selector(UIRefreshControl.endRefreshing), with: nil, afterDelay: delay) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/UI/Extensions/UITableView+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITableView+Extensions.swift 3 | // Shared 4 | // 5 | // Created by Jeans Ruiz on 6/26/20. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UITableView { 11 | 12 | // MARK: - Register Cell 13 | public func registerCell(cellType: T.Type) { 14 | let identifier = cellType.dequeuIdentifier 15 | register(cellType, forCellReuseIdentifier: identifier) 16 | } 17 | 18 | // MARK: - Dequeing 19 | public func dequeueReusableCell(with type: T.Type, for indexPath: IndexPath) -> T { 20 | return self.dequeueReusableCell(withIdentifier: type.dequeuIdentifier, for: indexPath) as! T 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/UI/Extensions/UIView+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Extensions.swift 3 | // Shared 4 | // 5 | // Created by Jeans Ruiz on 8/22/20. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIView { 11 | 12 | public func pin(to view: UIView, insets: UIEdgeInsets = .zero) { 13 | NSLayoutConstraint.activate([ 14 | topAnchor.constraint(equalTo: view.topAnchor, constant: insets.top), 15 | bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -insets.bottom), 16 | leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: insets.left), 17 | trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -insets.right) 18 | ]) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/UI/Localized/Strings+localized.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Strings+localized.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 13/06/22. 6 | // 7 | 8 | import Foundation 9 | import Shared 10 | 11 | /// Used by Strings+Generated.swift 12 | func localizeKey(_ key: String, _ locale: Locale) -> String { 13 | guard let bundle = buildBundleForLocalization(locale) else { 14 | return "" 15 | } 16 | return bundle.localizedString(forKey: key, value: nil, table: nil) 17 | } 18 | 19 | private func buildBundleForLocalization(_ locale: Locale) -> Bundle? { 20 | guard let pathBundle = Bundle.module.path(forResource: lprojFileNameForLanguageCode(locale), ofType: "lproj") else { 21 | return nil 22 | } 23 | return Bundle(path: pathBundle) 24 | } 25 | 26 | private func lprojFileNameForLanguageCode(_ locale: Locale) -> String { 27 | return Language(rawValue: locale.languageCode ?? "en")?.rawValue ?? "en" 28 | } 29 | -------------------------------------------------------------------------------- /Sources/UI/Protocols/NiblessCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NiblessCollectionViewCell.swift 3 | // Shared 4 | // 5 | // Created by Jeans Ruiz on 4/10/21. 6 | // 7 | 8 | import UIKit 9 | 10 | open class NiblessCollectionViewCell: UICollectionViewCell { 11 | 12 | public override init(frame: CGRect) { 13 | super.init(frame: frame) 14 | } 15 | 16 | @available(*, unavailable, 17 | message: "Loading this view Cell from a nib is unsupported in favor of initializer dependency injection.") 18 | public required init?(coder aDecoder: NSCoder) { 19 | fatalError("Loading this view Cell from a nib is unsupported in favor of initializer dependency injection.") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/UI/Protocols/NiblessTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NiblessTableViewCell.swift 3 | // Shared 4 | // 5 | // Created by Jeans Ruiz on 4/10/21. 6 | // 7 | 8 | import UIKit 9 | 10 | open class NiblessTableViewCell: UITableViewCell { 11 | 12 | public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 13 | super.init(style: style, reuseIdentifier: reuseIdentifier) 14 | } 15 | 16 | // MARK: - Restricted Init 17 | @available(*, unavailable, 18 | message: "Loading this view Cell from a nib is unsupported in favor of initializer dependency injection.") 19 | public required init?(coder: NSCoder) { 20 | fatalError("Loading this view Cell from a nib is unsupported in favor of initializer dependency injection.") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/UI/Protocols/NiblessView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NiblessView.swift 3 | // Shared 4 | // 5 | // Created by Jeans Ruiz on 8/21/20. 6 | // 7 | 8 | import UIKit 9 | 10 | open class NiblessView: UIView { 11 | 12 | public override init(frame: CGRect) { 13 | super.init(frame: frame) 14 | } 15 | 16 | @available(*, unavailable, 17 | message: "Loading this view from a nib is unsupported in favor of initializer dependency injection." 18 | ) 19 | 20 | public required init?(coder aDecoder: NSCoder) { 21 | fatalError("Loading this view from a nib is unsupported in favor of initializer dependency injection.") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/UI/Protocols/NiblessViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NiblessViewController.swift 3 | // Shared 4 | // 5 | // Created by Jeans Ruiz on 8/21/20. 6 | // 7 | 8 | import UIKit 9 | 10 | open class NiblessViewController: UIViewController { 11 | 12 | // MARK: - Methods 13 | public init() { 14 | super.init(nibName: nil, bundle: nil) 15 | } 16 | 17 | @available(*, unavailable, 18 | message: "Loading this view controller from a nib is unsupported in favor of initializer dependency injection." 19 | ) 20 | public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 21 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 22 | } 23 | 24 | @available(*, unavailable, 25 | message: "Loading this view controller from a nib is unsupported in favor of initializer dependency injection." 26 | ) 27 | public required init?(coder aDecoder: NSCoder) { 28 | fatalError("Loading this view controller from a nib is unsupported in favor of initializer dependency injection.") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/UI/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/UI/Resources/Assets.xcassets/account.tv.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "kisspng-television-free-content-free-to-air-clip-art-examples-of-grow-foods-clipart-5a886cb5abb616.3415104615188901657033.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/UI/Resources/Assets.xcassets/account.tv.imageset/kisspng-television-free-content-free-to-air-clip-art-examples-of-grow-foods-clipart-5a886cb5abb616.3415104615188901657033.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Sources/UI/Resources/Assets.xcassets/account.tv.imageset/kisspng-television-free-content-free-to-air-clip-art-examples-of-grow-foods-clipart-5a886cb5abb616.3415104615188901657033.png -------------------------------------------------------------------------------- /Sources/UI/Resources/Assets.xcassets/empty.placeholder.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Error009.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/UI/Resources/Assets.xcassets/empty.placeholder.imageset/Error009.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Sources/UI/Resources/Assets.xcassets/empty.placeholder.imageset/Error009.png -------------------------------------------------------------------------------- /Sources/UI/Resources/Assets.xcassets/error.list.placeholder.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "error5.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/UI/Resources/Assets.xcassets/error.list.placeholder.imageset/error5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Sources/UI/Resources/Assets.xcassets/error.list.placeholder.imageset/error5.png -------------------------------------------------------------------------------- /Sources/UI/Resources/Assets.xcassets/error.placeholder.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Error010.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/UI/Resources/Assets.xcassets/error.placeholder.imageset/Error010.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Sources/UI/Resources/Assets.xcassets/error.placeholder.imageset/Error010.png -------------------------------------------------------------------------------- /Sources/UI/Resources/Assets.xcassets/loginbackground.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ic_boton_primario_1.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "ic_boton_primario_1@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "ic_boton_primario_1@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/UI/Resources/Assets.xcassets/loginbackground.imageset/ic_boton_primario_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Sources/UI/Resources/Assets.xcassets/loginbackground.imageset/ic_boton_primario_1.png -------------------------------------------------------------------------------- /Sources/UI/Resources/Assets.xcassets/loginbackground.imageset/ic_boton_primario_1@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Sources/UI/Resources/Assets.xcassets/loginbackground.imageset/ic_boton_primario_1@2x.png -------------------------------------------------------------------------------- /Sources/UI/Resources/Assets.xcassets/loginbackground.imageset/ic_boton_primario_1@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Sources/UI/Resources/Assets.xcassets/loginbackground.imageset/ic_boton_primario_1@3x.png -------------------------------------------------------------------------------- /Sources/UI/Resources/Assets.xcassets/placeholder.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "placeholder2.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Sources/UI/Resources/Assets.xcassets/placeholder.imageset/placeholder2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Sources/UI/Resources/Assets.xcassets/placeholder.imageset/placeholder2.png -------------------------------------------------------------------------------- /Sources/UI/Resources/Assets.xcassets/tvshowEmpty.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "tvshowEmpty.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Sources/UI/Resources/Assets.xcassets/tvshowEmpty.imageset/tvshowEmpty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Sources/UI/Resources/Assets.xcassets/tvshowEmpty.imageset/tvshowEmpty.png -------------------------------------------------------------------------------- /Tests/AccountFeatureTests/Account/Mocks/Entities/AccountResult+stub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccountResult+stub.swift 3 | // AccountTV-Unit-Tests 4 | // 5 | // Created by Jeans Ruiz on 8/8/20. 6 | // 7 | 8 | import Foundation 9 | @testable import AccountFeature 10 | 11 | extension Account { 12 | 13 | static func stub( 14 | accountId: Int = 1, 15 | userName: String = "userName", 16 | avatarURL: URL? = nil 17 | ) -> Self { 18 | Account( 19 | id: accountId, 20 | userName: userName, 21 | avatarURL: avatarURL 22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Tests/AccountFeatureTests/Account/Mocks/UsesCases/CreateSessionUseCaseMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 8/8/20. 3 | // 4 | 5 | import Combine 6 | import NetworkingInterface 7 | @testable import AccountFeature 8 | 9 | final class CreateSessionUseCaseMock: CreateSessionUseCase { 10 | 11 | var result: Bool 12 | var error: ApiError? 13 | 14 | init(result: Bool = false, error: ApiError? = nil) { 15 | self.result = result 16 | self.error = error 17 | } 18 | 19 | func execute() async -> Bool { 20 | if error != nil { 21 | return false 22 | } 23 | 24 | return result 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/AccountFeatureTests/Account/Mocks/UsesCases/CreateTokenUseCaseMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 8/8/20. 3 | // 4 | 5 | import Foundation 6 | import Combine 7 | import NetworkingInterface 8 | @testable import AccountFeature 9 | 10 | final class CreateTokenUseCaseMock: CreateTokenUseCase { 11 | 12 | var result: URL? 13 | var error: ApiError? 14 | 15 | func execute() async -> URL? { 16 | if let error = error { 17 | return nil 18 | } 19 | return result 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/AccountFeatureTests/Account/Mocks/UsesCases/DeleteLoguedUserUseCaseMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeleteLoguedUserUseCaseMock.swift 3 | // AccountTV-Unit-Tests 4 | // 5 | // Created by Jeans Ruiz on 8/8/20. 6 | // 7 | 8 | @testable import AccountFeature 9 | 10 | final class DeleteLoguedUserUseCaseMock: DeleteLoggedUserUseCase { 11 | func execute() { } 12 | } 13 | -------------------------------------------------------------------------------- /Tests/AccountFeatureTests/Account/Mocks/UsesCases/FetchAccountDetailsUseCaseMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 8/8/20. 3 | // 4 | 5 | import Combine 6 | import NetworkingInterface 7 | @testable import AccountFeature 8 | 9 | final class FetchAccountDetailsUseCaseMock: FetchAccountDetailsUseCase { 10 | 11 | var result: Account? 12 | var error: ApiError? 13 | 14 | func execute() async -> Account? { 15 | if error != nil { 16 | return nil 17 | } 18 | return result 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/AccountFeatureTests/Account/Mocks/UsesCases/FetchLoggedUserMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FetchLoggedUserMock.swift 3 | // AccountTV-Unit-Tests 4 | // 5 | // Created by Jeans Ruiz on 8/8/20. 6 | // 7 | 8 | @testable import Shared 9 | 10 | class FetchLoggedUserMock: FetchLoggedUser { 11 | var account: AccountDomain? 12 | 13 | func execute() -> AccountDomain? { 14 | return account 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Tests/AccountFeatureTests/Account/Mocks/ViewModels/AccountViewModelMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 8/8/20. 3 | // 4 | 5 | import Combine 6 | @testable import AccountFeature 7 | 8 | final class AccountViewModelMock: AccountViewModelProtocol { 9 | 10 | func viewDidLoad() async { } 11 | 12 | @Published private var viewStateInternal = AccountViewState.login 13 | var viewState: Published.Publisher { $viewStateInternal } 14 | 15 | init(state: AccountViewState) { 16 | viewStateInternal = state 17 | } 18 | 19 | func authPermissionViewModel(didSignedIn signedIn: Bool) { } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/AccountFeatureTests/Account/Mocks/ViewModels/AuthPermissionViewModelMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthPermissionViewModelMock.swift 3 | // AccountTV-Unit-Tests 4 | // 5 | // Created by Jeans Ruiz on 8/8/20. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | @testable import AccountFeature 11 | 12 | class AuthPermissionViewModelMock: AuthPermissionViewModelProtocol { 13 | func signIn() async { 14 | await delegate?.authPermissionViewModel(didSignedIn: true) 15 | } 16 | 17 | var authPermissionURL = URL(string: "http://www.123.com")! 18 | 19 | weak var delegate: AuthPermissionViewModelDelegate? 20 | } 21 | -------------------------------------------------------------------------------- /Tests/AccountFeatureTests/Account/Presentation/AccountViewControllerFactoryMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccountViewControllerFactoryMock.swift 3 | // AccountTests 4 | // 5 | // Created by Jeans Ruiz on 20/12/21. 6 | // 7 | 8 | import UIKit 9 | @testable import AccountFeature 10 | 11 | class AccountViewControllerFactoryMock: AccountViewControllerFactory { 12 | func makeSignInViewController() -> UIViewController { 13 | let viewModel = SignInViewModelMock(state: .initial) 14 | return SignInViewController(viewModel: viewModel) 15 | } 16 | 17 | func makeProfileViewController(with account: Account) -> UIViewController { 18 | let viewModel = ProfileViewModelMock(account: account) 19 | return ProfileViewController(viewModel: viewModel) 20 | } 21 | } 22 | 23 | func configureWith(_ viewController: UIViewController, style: UIUserInterfaceStyle) { 24 | viewController.overrideUserInterfaceStyle = style 25 | _ = viewController.view 26 | } 27 | -------------------------------------------------------------------------------- /Tests/AccountFeatureTests/Account/Presentation/__Snapshots__/AccountViewTests/test_WhenViewIsLogged_thenShowProfileScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/AccountFeatureTests/Account/Presentation/__Snapshots__/AccountViewTests/test_WhenViewIsLogged_thenShowProfileScreen.1.png -------------------------------------------------------------------------------- /Tests/AccountFeatureTests/Account/Presentation/__Snapshots__/AccountViewTests/test_WhenViewIsLogged_thenShowProfileScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/AccountFeatureTests/Account/Presentation/__Snapshots__/AccountViewTests/test_WhenViewIsLogged_thenShowProfileScreen.2.png -------------------------------------------------------------------------------- /Tests/AccountFeatureTests/Account/Presentation/__Snapshots__/AccountViewTests/test_WhenViewIsLogin_thenShowLoginScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/AccountFeatureTests/Account/Presentation/__Snapshots__/AccountViewTests/test_WhenViewIsLogin_thenShowLoginScreen.1.png -------------------------------------------------------------------------------- /Tests/AccountFeatureTests/Account/Presentation/__Snapshots__/AccountViewTests/test_WhenViewIsLogin_thenShowLoginScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/AccountFeatureTests/Account/Presentation/__Snapshots__/AccountViewTests/test_WhenViewIsLogin_thenShowLoginScreen.2.png -------------------------------------------------------------------------------- /Tests/AccountFeatureTests/SignIn/Mocks/ViewModels/SignInViewModelDelegateMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignInViewModelDelegateMock.swift 3 | // AccountTV-Unit-Tests 4 | // 5 | // Created by Jeans Ruiz on 8/8/20. 6 | // 7 | 8 | import Foundation 9 | @testable import AccountFeature 10 | 11 | final class SignInViewModelDelegateMock: SignInViewModelDelegate { 12 | 13 | var url: URL? 14 | 15 | func signInViewModel(_ signInViewModel: SignInViewModel, 16 | didTapSignInButton url: URL) { 17 | self.url = url 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/AccountFeatureTests/SignIn/Mocks/ViewModels/SignInViewModelMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 8/8/20. 3 | // 4 | 5 | import Foundation 6 | @testable import AccountFeature 7 | 8 | final class SignInViewModelMock: SignInViewModelProtocol { 9 | func signInDidTapped() { } 10 | func changeState(with state: SignInViewState) { } 11 | 12 | @Published private var viewStateInternal: SignInViewState = .initial 13 | public var viewState: Published.Publisher { $viewStateInternal } 14 | 15 | weak var delegate: SignInViewModelDelegate? 16 | 17 | init(state: SignInViewState) { 18 | viewStateInternal = state 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/AccountFeatureTests/SignIn/Presentation/__Snapshots__/SignInViewTests/test_WhenViewIsError_thenShowErrorScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/AccountFeatureTests/SignIn/Presentation/__Snapshots__/SignInViewTests/test_WhenViewIsError_thenShowErrorScreen.1.png -------------------------------------------------------------------------------- /Tests/AccountFeatureTests/SignIn/Presentation/__Snapshots__/SignInViewTests/test_WhenViewIsError_thenShowErrorScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/AccountFeatureTests/SignIn/Presentation/__Snapshots__/SignInViewTests/test_WhenViewIsError_thenShowErrorScreen.2.png -------------------------------------------------------------------------------- /Tests/AccountFeatureTests/SignIn/Presentation/__Snapshots__/SignInViewTests/test_WhenViewIsInitial_thenShowInitialScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/AccountFeatureTests/SignIn/Presentation/__Snapshots__/SignInViewTests/test_WhenViewIsInitial_thenShowInitialScreen.1.png -------------------------------------------------------------------------------- /Tests/AccountFeatureTests/SignIn/Presentation/__Snapshots__/SignInViewTests/test_WhenViewIsInitial_thenShowInitialScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/AccountFeatureTests/SignIn/Presentation/__Snapshots__/SignInViewTests/test_WhenViewIsInitial_thenShowInitialScreen.2.png -------------------------------------------------------------------------------- /Tests/AccountFeatureTests/SignIn/Presentation/__Snapshots__/SignInViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/AccountFeatureTests/SignIn/Presentation/__Snapshots__/SignInViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.1.png -------------------------------------------------------------------------------- /Tests/AccountFeatureTests/SignIn/Presentation/__Snapshots__/SignInViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/AccountFeatureTests/SignIn/Presentation/__Snapshots__/SignInViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.2.png -------------------------------------------------------------------------------- /Tests/AiringTodayFeatureTests/Mocks/AiringTodayViewModelMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AiringTodayViewModelMock.swift 3 | // AiringTodayTests 4 | // 5 | // Created by Jeans Ruiz on 19/12/21. 6 | // 7 | 8 | import Shared 9 | import Combine 10 | @testable import AiringTodayFeature 11 | 12 | class AiringTodayViewModelMock: AiringTodayViewModelProtocol { 13 | 14 | func viewDidLoad() { } 15 | 16 | func willDisplayRow(_ row: Int, outOf totalRows: Int) { } 17 | 18 | func showIsPicked(index id: Int) { } 19 | 20 | func refreshView() { } 21 | 22 | func getCurrentViewState() -> SimpleViewState { 23 | return viewStateObservableSubject.value 24 | } 25 | 26 | let viewStateObservableSubject: CurrentValueSubject, Never> 27 | 28 | init(state: SimpleViewState) { 29 | viewStateObservableSubject = CurrentValueSubject(state) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/AiringTodayFeatureTests/Presentation/SnapshotTests/__Snapshots__/AiringTodayViewTests/test_WhenViewIsEmpty_thenShowEmptyScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/AiringTodayFeatureTests/Presentation/SnapshotTests/__Snapshots__/AiringTodayViewTests/test_WhenViewIsEmpty_thenShowEmptyScreen.1.png -------------------------------------------------------------------------------- /Tests/AiringTodayFeatureTests/Presentation/SnapshotTests/__Snapshots__/AiringTodayViewTests/test_WhenViewIsEmpty_thenShowEmptyScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/AiringTodayFeatureTests/Presentation/SnapshotTests/__Snapshots__/AiringTodayViewTests/test_WhenViewIsEmpty_thenShowEmptyScreen.2.png -------------------------------------------------------------------------------- /Tests/AiringTodayFeatureTests/Presentation/SnapshotTests/__Snapshots__/AiringTodayViewTests/test_WhenViewIsError_thenShowErrorScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/AiringTodayFeatureTests/Presentation/SnapshotTests/__Snapshots__/AiringTodayViewTests/test_WhenViewIsError_thenShowErrorScreen.1.png -------------------------------------------------------------------------------- /Tests/AiringTodayFeatureTests/Presentation/SnapshotTests/__Snapshots__/AiringTodayViewTests/test_WhenViewIsError_thenShowErrorScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/AiringTodayFeatureTests/Presentation/SnapshotTests/__Snapshots__/AiringTodayViewTests/test_WhenViewIsError_thenShowErrorScreen.2.png -------------------------------------------------------------------------------- /Tests/AiringTodayFeatureTests/Presentation/SnapshotTests/__Snapshots__/AiringTodayViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/AiringTodayFeatureTests/Presentation/SnapshotTests/__Snapshots__/AiringTodayViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.1.png -------------------------------------------------------------------------------- /Tests/AiringTodayFeatureTests/Presentation/SnapshotTests/__Snapshots__/AiringTodayViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/AiringTodayFeatureTests/Presentation/SnapshotTests/__Snapshots__/AiringTodayViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.2.png -------------------------------------------------------------------------------- /Tests/AiringTodayFeatureTests/Presentation/SnapshotTests/__Snapshots__/AiringTodayViewTests/test_WhenViewPaging_thenShowPagingScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/AiringTodayFeatureTests/Presentation/SnapshotTests/__Snapshots__/AiringTodayViewTests/test_WhenViewPaging_thenShowPagingScreen.1.png -------------------------------------------------------------------------------- /Tests/AiringTodayFeatureTests/Presentation/SnapshotTests/__Snapshots__/AiringTodayViewTests/test_WhenViewPaging_thenShowPagingScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/AiringTodayFeatureTests/Presentation/SnapshotTests/__Snapshots__/AiringTodayViewTests/test_WhenViewPaging_thenShowPagingScreen.2.png -------------------------------------------------------------------------------- /Tests/AiringTodayFeatureTests/Presentation/SnapshotTests/__Snapshots__/AiringTodayViewTests/test_WhenViewPopulated_thenShowPopulatedScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/AiringTodayFeatureTests/Presentation/SnapshotTests/__Snapshots__/AiringTodayViewTests/test_WhenViewPopulated_thenShowPopulatedScreen.1.png -------------------------------------------------------------------------------- /Tests/AiringTodayFeatureTests/Presentation/SnapshotTests/__Snapshots__/AiringTodayViewTests/test_WhenViewPopulated_thenShowPopulatedScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/AiringTodayFeatureTests/Presentation/SnapshotTests/__Snapshots__/AiringTodayViewTests/test_WhenViewPopulated_thenShowPopulatedScreen.2.png -------------------------------------------------------------------------------- /Tests/CommonMocks/FetchShowsUseCaseMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 14/05/22. 3 | // 4 | 5 | import Foundation 6 | import Combine 7 | import NetworkingInterface 8 | @testable import Shared 9 | 10 | import ConcurrencyExtras 11 | 12 | public class FetchShowsUseCaseMock: FetchTVShowsUseCase { 13 | 14 | public var error: ApiError? 15 | public var result: TVShowPage? 16 | 17 | public init() { } 18 | 19 | public func execute(request: FetchTVShowsUseCaseRequestValue) async -> TVShowPage? { 20 | await Task.yield() 21 | if error != nil { 22 | return nil 23 | } 24 | 25 | return result 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/CommonMocks/MappingHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MappingHelpers.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 14/05/22. 6 | // 7 | 8 | import Foundation 9 | @testable import Shared 10 | 11 | public func buildFirstPage() -> TVShowPage { 12 | let firstShow = TVShowPage.TVShow.stub( 13 | id: 1, 14 | name: "title1 🐶", 15 | overview: "overview" 16 | ) 17 | let secondShow = TVShowPage.TVShow.stub( 18 | id: 2, 19 | name: "title2 🔫", 20 | overview: "overview2" 21 | ) 22 | return TVShowPage.stub(page: 1, 23 | showsList: [firstShow, secondShow], 24 | totalPages: 2, 25 | totalShows: 3) 26 | } 27 | 28 | public func buildSecondPage() -> TVShowPage { 29 | let thirdShow = TVShowPage.TVShow.stub( 30 | id: 3, 31 | name: "title3 🚨", 32 | overview: "overview3" 33 | ) 34 | return TVShowPage.stub(page: 2, 35 | showsList: [thirdShow], 36 | totalPages: 2, 37 | totalShows: 3) 38 | } 39 | -------------------------------------------------------------------------------- /Tests/CommonMocks/TVShow+Stub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVShow+Stub.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 14/05/22. 6 | // 7 | 8 | import Foundation 9 | 10 | @testable import Shared 11 | 12 | extension TVShowPage.TVShow { 13 | public static func stub( 14 | id: Int = 1, 15 | name: String = "title1", 16 | voteAverage: Double = 1.0, 17 | posterPath: URL? = nil, 18 | backDropPath: URL? = nil, 19 | overview: String = "overview1" 20 | ) -> Self { 21 | 22 | TVShowPage.TVShow( 23 | id: id, 24 | name: name, 25 | overview: overview, 26 | firstAirDate: "", 27 | posterPath: posterPath, 28 | backDropPath: backDropPath, 29 | genreIds: [], 30 | voteAverage: voteAverage, 31 | voteCount: 0 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/CommonMocks/TVShowPage+Stub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVShowPage+Stub.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 14/05/22. 6 | // 7 | 8 | @testable import Shared 9 | 10 | extension TVShowPage { 11 | public static func stub(page: Int = 1, 12 | showsList: [TVShow], 13 | totalPages: Int, 14 | totalShows: Int) -> Self { 15 | TVShowPage( 16 | page: page, 17 | showsList: showsList, 18 | totalPages: totalPages, 19 | totalShows: totalShows 20 | ) 21 | } 22 | 23 | // MARK: - Empty 24 | public static var empty: TVShowPage { 25 | return TVShowPage.stub( 26 | page: 1, 27 | showsList: [], 28 | totalPages: 0, 29 | totalShows: 1 30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/PopularsFeatureTests/Mocks/PopularViewModel+Mock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PopularViewModel+Mock.swift 3 | // PopularShows-Unit-Tests 4 | // 5 | // Created by Jeans Ruiz on 8/7/20. 6 | // 7 | 8 | import Combine 9 | import Shared 10 | import UI 11 | @testable import PopularsFeature 12 | 13 | class PopularViewModelMock: PopularViewModelProtocol { 14 | 15 | func viewDidLoad() { } 16 | 17 | func willDisplayRow(_ row: Int, outOf totalRows: Int) { } 18 | 19 | func showIsPicked(index: Int) { } 20 | 21 | func refreshView() { } 22 | 23 | var viewStateObservableSubject: CurrentValueSubject, Never> 24 | 25 | init(state: SimpleViewState) { 26 | viewStateObservableSubject = CurrentValueSubject(state) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/PopularsFeatureTests/Presentation/SnapshotTests/__Snapshots__/PopularViewTests/test_WhenViewIsEmpty_thenShowEmptyScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/PopularsFeatureTests/Presentation/SnapshotTests/__Snapshots__/PopularViewTests/test_WhenViewIsEmpty_thenShowEmptyScreen.1.png -------------------------------------------------------------------------------- /Tests/PopularsFeatureTests/Presentation/SnapshotTests/__Snapshots__/PopularViewTests/test_WhenViewIsEmpty_thenShowEmptyScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/PopularsFeatureTests/Presentation/SnapshotTests/__Snapshots__/PopularViewTests/test_WhenViewIsEmpty_thenShowEmptyScreen.2.png -------------------------------------------------------------------------------- /Tests/PopularsFeatureTests/Presentation/SnapshotTests/__Snapshots__/PopularViewTests/test_WhenViewIsError_thenShowErrorScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/PopularsFeatureTests/Presentation/SnapshotTests/__Snapshots__/PopularViewTests/test_WhenViewIsError_thenShowErrorScreen.1.png -------------------------------------------------------------------------------- /Tests/PopularsFeatureTests/Presentation/SnapshotTests/__Snapshots__/PopularViewTests/test_WhenViewIsError_thenShowErrorScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/PopularsFeatureTests/Presentation/SnapshotTests/__Snapshots__/PopularViewTests/test_WhenViewIsError_thenShowErrorScreen.2.png -------------------------------------------------------------------------------- /Tests/PopularsFeatureTests/Presentation/SnapshotTests/__Snapshots__/PopularViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/PopularsFeatureTests/Presentation/SnapshotTests/__Snapshots__/PopularViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.1.png -------------------------------------------------------------------------------- /Tests/PopularsFeatureTests/Presentation/SnapshotTests/__Snapshots__/PopularViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/PopularsFeatureTests/Presentation/SnapshotTests/__Snapshots__/PopularViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.2.png -------------------------------------------------------------------------------- /Tests/PopularsFeatureTests/Presentation/SnapshotTests/__Snapshots__/PopularViewTests/test_WhenViewPaging_thenShowPagingScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/PopularsFeatureTests/Presentation/SnapshotTests/__Snapshots__/PopularViewTests/test_WhenViewPaging_thenShowPagingScreen.1.png -------------------------------------------------------------------------------- /Tests/PopularsFeatureTests/Presentation/SnapshotTests/__Snapshots__/PopularViewTests/test_WhenViewPaging_thenShowPagingScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/PopularsFeatureTests/Presentation/SnapshotTests/__Snapshots__/PopularViewTests/test_WhenViewPaging_thenShowPagingScreen.2.png -------------------------------------------------------------------------------- /Tests/PopularsFeatureTests/Presentation/SnapshotTests/__Snapshots__/PopularViewTests/test_WhenViewPopulated_thenShowPopulatedScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/PopularsFeatureTests/Presentation/SnapshotTests/__Snapshots__/PopularViewTests/test_WhenViewPopulated_thenShowPopulatedScreen.1.png -------------------------------------------------------------------------------- /Tests/PopularsFeatureTests/Presentation/SnapshotTests/__Snapshots__/PopularViewTests/test_WhenViewPopulated_thenShowPopulatedScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/PopularsFeatureTests/Presentation/SnapshotTests/__Snapshots__/PopularViewTests/test_WhenViewPopulated_thenShowPopulatedScreen.2.png -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchOptions/Mocks/Entities/Genre+Stub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVShowResult+Stub.swift 3 | // AiringToday-Unit-Tests 4 | // 5 | // Created by Jeans Ruiz on 7/28/20. 6 | // 7 | 8 | @testable import Shared 9 | 10 | extension Genre { 11 | static func stub(id: Int = 1, 12 | name: String = "") -> Self { 13 | Genre(id: id, name: name) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchOptions/Mocks/Entities/ShowVisited+Build.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShowVisited+Build.swift 3 | // SearchShowsTests 4 | // 5 | // Created by Jeans Ruiz on 30/03/22. 6 | // 7 | 8 | import Persistence 9 | import Shared 10 | 11 | func buildShowVisited() -> [ShowVisited] { 12 | return [ 13 | ShowVisited.stub(id: 1, pathImage: ""), 14 | ShowVisited.stub(id: 2, pathImage: ""), 15 | ShowVisited.stub(id: 3, pathImage: "") 16 | ] 17 | } 18 | 19 | func buildGenres() -> [Genre] { 20 | return [ 21 | Genre.stub(id: 1, name: "Genre 1"), 22 | Genre.stub(id: 2, name: "Genre with a long name to show how the cell could increase its height") 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchOptions/Mocks/Entities/ShowVisited+Stub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShowVisited+Stub.swift 3 | // SearchShows-Unit-Tests 4 | // 5 | // Created by Jeans Ruiz on 8/7/20. 6 | // 7 | 8 | @testable import Persistence 9 | 10 | extension ShowVisited { 11 | static func stub(id: Int = 1, 12 | pathImage: String = "") -> Self { 13 | ShowVisited(id: id, pathImage: pathImage) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchOptions/Mocks/UsesCases/FetchGenresUseCase+Mock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 8/7/20. 3 | // 4 | 5 | import Foundation 6 | import Combine 7 | import NetworkingInterface 8 | @testable import SearchShowsFeature 9 | 10 | final class FetchGenresUseCaseMock: FetchGenresUseCase { 11 | var error: ApiError? 12 | var result: GenreList? 13 | 14 | func execute() async throws -> GenreList { 15 | if let error = error { 16 | throw error 17 | } 18 | 19 | if let genreList = result { 20 | return genreList 21 | } else { 22 | throw ApiError(error: NSError(domain: "Mock", code: 404, userInfo: nil)) 23 | } 24 | 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchOptions/Mocks/UsesCases/FetchVisitedShowsUseCase+Mock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 8/7/20. 3 | // 4 | 5 | import Combine 6 | import Persistence 7 | import Shared 8 | import NetworkingInterface 9 | 10 | final class FetchVisitedShowsUseCaseMock: FetchVisitedShowsUseCase { 11 | var error: ApiError? 12 | var result: [ShowVisited]? 13 | 14 | public func execute() -> [ShowVisited] { 15 | if error != nil { 16 | return [] 17 | } 18 | 19 | if let result = result { 20 | return result 21 | } else { 22 | return [] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchOptions/Mocks/UsesCases/RecentVisitedShowDidChangeUseCase+Mock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 8/7/20. 3 | // 4 | 5 | import Combine 6 | import Persistence 7 | 8 | final class RecentVisitedShowDidChangeUseCaseMock: RecentVisitedShowDidChangeUseCase { 9 | var result: Bool? 10 | 11 | public func execute() -> AsyncStream { 12 | return AsyncStream { continuation in 13 | if let result = self.result { 14 | continuation.yield(result) 15 | } 16 | continuation.finish() 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchOptions/Mocks/ViewModels/GenreViewModel+Mock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GenreViewModel+Mock.swift 3 | // SearchShows-Unit-Tests 4 | // 5 | // Created by Jeans Ruiz on 8/7/20. 6 | // 7 | 8 | @testable import SearchShowsFeature 9 | @testable import Shared 10 | @testable import Persistence 11 | 12 | extension GenreViewModel { 13 | static var mock: (Genre) -> GenreViewModel = { genre in 14 | return GenreViewModel(genre: genre) 15 | } 16 | } 17 | 18 | extension VisitedShowViewModel { 19 | static var mock: ([ShowVisited]) -> VisitedShowViewModel = { shows in 20 | return VisitedShowViewModel(shows: shows) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchOptions/Mocks/ViewModels/SearchOptionsViewModel+Mock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchOptionsViewModel+Mock.swift 3 | // SearchShows-Unit-Tests 4 | // 5 | // Created by Jeans Ruiz on 8/7/20. 6 | // 7 | 8 | import Combine 9 | @testable import SearchShowsFeature 10 | @testable import Shared 11 | 12 | final class SearchOptionsViewModelMock: SearchOptionsViewModelProtocol { 13 | func viewDidLoad() { } 14 | func modelIsPicked(with item: SearchSectionItem) { } 15 | 16 | var viewState: CurrentValueSubject 17 | var dataSource: CurrentValueSubject<[SearchOptionsSectionModel], Never> 18 | 19 | init(state: SearchViewState, sections: [SearchOptionsSectionModel] = []) { 20 | viewState = CurrentValueSubject(state) 21 | dataSource = CurrentValueSubject(sections) 22 | } 23 | 24 | func visitedShowViewModel(_ visitedShowViewModel: VisitedShowViewModelProtocol, 25 | didSelectRecentlyVisitedShow id: Int) { 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchOptions/Presentation/SnapshotTests/__Snapshots__/SearchShowsOptionsViewTests/test_WhenViewIsEmpty_thenShowEmptyScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/SearchShowsFeatureTests/SearchOptions/Presentation/SnapshotTests/__Snapshots__/SearchShowsOptionsViewTests/test_WhenViewIsEmpty_thenShowEmptyScreen.1.png -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchOptions/Presentation/SnapshotTests/__Snapshots__/SearchShowsOptionsViewTests/test_WhenViewIsEmpty_thenShowEmptyScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/SearchShowsFeatureTests/SearchOptions/Presentation/SnapshotTests/__Snapshots__/SearchShowsOptionsViewTests/test_WhenViewIsEmpty_thenShowEmptyScreen.2.png -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchOptions/Presentation/SnapshotTests/__Snapshots__/SearchShowsOptionsViewTests/test_WhenViewIsError_thenShowErrorScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/SearchShowsFeatureTests/SearchOptions/Presentation/SnapshotTests/__Snapshots__/SearchShowsOptionsViewTests/test_WhenViewIsError_thenShowErrorScreen.1.png -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchOptions/Presentation/SnapshotTests/__Snapshots__/SearchShowsOptionsViewTests/test_WhenViewIsError_thenShowErrorScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/SearchShowsFeatureTests/SearchOptions/Presentation/SnapshotTests/__Snapshots__/SearchShowsOptionsViewTests/test_WhenViewIsError_thenShowErrorScreen.2.png -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchOptions/Presentation/SnapshotTests/__Snapshots__/SearchShowsOptionsViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/SearchShowsFeatureTests/SearchOptions/Presentation/SnapshotTests/__Snapshots__/SearchShowsOptionsViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.1.png -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchOptions/Presentation/SnapshotTests/__Snapshots__/SearchShowsOptionsViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/SearchShowsFeatureTests/SearchOptions/Presentation/SnapshotTests/__Snapshots__/SearchShowsOptionsViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.2.png -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchOptions/Presentation/SnapshotTests/__Snapshots__/SearchShowsOptionsViewTests/test_WhenViewPopulated_thenShowPopulatedScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/SearchShowsFeatureTests/SearchOptions/Presentation/SnapshotTests/__Snapshots__/SearchShowsOptionsViewTests/test_WhenViewPopulated_thenShowPopulatedScreen.1.png -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchOptions/Presentation/SnapshotTests/__Snapshots__/SearchShowsOptionsViewTests/test_WhenViewPopulated_thenShowPopulatedScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/SearchShowsFeatureTests/SearchOptions/Presentation/SnapshotTests/__Snapshots__/SearchShowsOptionsViewTests/test_WhenViewPopulated_thenShowPopulatedScreen.2.png -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchResults/Mocks/ResultsSearchViewModelMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResultsSearchViewModelMock.swift 3 | // SearchShowsTests 4 | // 5 | // Created by Jeans Ruiz on 20/12/21. 6 | // 7 | 8 | import Combine 9 | @testable import SearchShowsFeature 10 | 11 | final class ResultsSearchViewModelMock: ResultsSearchViewModelProtocol { 12 | func recentSearchIsPicked(query: String) { } 13 | func showIsPicked(index: Int) { } 14 | func searchShows(with query: String) { } 15 | func resetSearch() { } 16 | 17 | var viewState: CurrentValueSubject 18 | var dataSource: CurrentValueSubject<[ResultSearchSectionModel], Never> 19 | weak var delegate: ResultsSearchViewModelDelegate? 20 | 21 | func getViewState() -> ResultViewState { 22 | return state 23 | } 24 | 25 | private let state: ResultViewState 26 | 27 | init(state: ResultViewState, source: [ResultSearchSectionModel] = []) { 28 | self.state = state 29 | viewState = CurrentValueSubject(state) 30 | 31 | dataSource = CurrentValueSubject(source) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchResults/Mocks/UsesCases/FetchSearchsUseCase+Mock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 8/7/20. 3 | // 4 | 5 | import Foundation 6 | import Combine 7 | @testable import SearchShowsFeature 8 | @testable import Shared 9 | @testable import Persistence 10 | 11 | final class FetchSearchsUseCaseMock: FetchSearchesUseCase { 12 | var error: ErrorEnvelope? 13 | var result: [Search]? 14 | 15 | public func execute() async -> [Search] { 16 | if error != nil { 17 | return [] 18 | } 19 | 20 | if let result = result { 21 | return result 22 | } else { 23 | return [] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchResults/Mocks/UsesCases/SearchTVShowsUseCase+Mock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 8/7/20. 3 | // 4 | 5 | import Foundation 6 | import CommonMocks 7 | import Combine 8 | import SearchShowsFeature 9 | import Shared 10 | import NetworkingInterface 11 | 12 | final class SearchTVShowsUseCaseMock: SearchTVShowsUseCase { 13 | var error: ApiError? 14 | var result: TVShowPage? 15 | 16 | func execute(request: SearchTVShowsUseCaseRequestValue) async throws -> TVShowPage { 17 | if let error = error { 18 | throw error 19 | } 20 | 21 | if let result = result { 22 | return result 23 | } else { 24 | throw ApiError(error: NSError(domain: "", code: 0, userInfo: nil)) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchResults/Presentation/SnapshotTests/ResultsSearchViewHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResultsSearchViewHelper.swift 3 | // SearchShowsTests 4 | // 5 | // Created by Jeans Ruiz on 20/12/21. 6 | // 7 | 8 | import UIKit 9 | @testable import SearchShowsFeature 10 | import Shared 11 | import UI 12 | 13 | public func createSectionModel(recentSearchs: [String], resultShows: [TVShowPage.TVShow]) -> [ResultSearchSectionModel] { 14 | var dataSource: [ResultSearchSectionModel] = [] 15 | 16 | let recentSearchsItem = recentSearchs.map { ResultSearchSectionItem.recentSearchs(items: $0) } 17 | 18 | let resultsShowsItem = resultShows 19 | .map { TVShowCellViewModel(show: $0) } 20 | .map { ResultSearchSectionItem.results(items: $0) } 21 | 22 | if !recentSearchsItem.isEmpty { 23 | dataSource.append(.recentSearchs(items: recentSearchsItem)) 24 | } 25 | 26 | if !resultsShowsItem.isEmpty { 27 | dataSource.append(.results(items: resultsShowsItem)) 28 | } 29 | 30 | return dataSource 31 | } 32 | 33 | func configureWith(_ viewController: UIViewController, style: UIUserInterfaceStyle) { 34 | viewController.overrideUserInterfaceStyle = style 35 | _ = viewController.view 36 | } 37 | -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchResults/Presentation/SnapshotTests/__Snapshots__/ResultsSearchViewTests/test_WhenViewDidError_thenShowErrorScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/SearchShowsFeatureTests/SearchResults/Presentation/SnapshotTests/__Snapshots__/ResultsSearchViewTests/test_WhenViewDidError_thenShowErrorScreen.1.png -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchResults/Presentation/SnapshotTests/__Snapshots__/ResultsSearchViewTests/test_WhenViewDidError_thenShowErrorScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/SearchShowsFeatureTests/SearchResults/Presentation/SnapshotTests/__Snapshots__/ResultsSearchViewTests/test_WhenViewDidError_thenShowErrorScreen.2.png -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchResults/Presentation/SnapshotTests/__Snapshots__/ResultsSearchViewTests/test_WhenViewInitial_thenShowInitialScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/SearchShowsFeatureTests/SearchResults/Presentation/SnapshotTests/__Snapshots__/ResultsSearchViewTests/test_WhenViewInitial_thenShowInitialScreen.1.png -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchResults/Presentation/SnapshotTests/__Snapshots__/ResultsSearchViewTests/test_WhenViewInitial_thenShowInitialScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/SearchShowsFeatureTests/SearchResults/Presentation/SnapshotTests/__Snapshots__/ResultsSearchViewTests/test_WhenViewInitial_thenShowInitialScreen.2.png -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchResults/Presentation/SnapshotTests/__Snapshots__/ResultsSearchViewTests/test_WhenViewIsEmpty_thenShowEmptyScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/SearchShowsFeatureTests/SearchResults/Presentation/SnapshotTests/__Snapshots__/ResultsSearchViewTests/test_WhenViewIsEmpty_thenShowEmptyScreen.1.png -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchResults/Presentation/SnapshotTests/__Snapshots__/ResultsSearchViewTests/test_WhenViewIsEmpty_thenShowEmptyScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/SearchShowsFeatureTests/SearchResults/Presentation/SnapshotTests/__Snapshots__/ResultsSearchViewTests/test_WhenViewIsEmpty_thenShowEmptyScreen.2.png -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchResults/Presentation/SnapshotTests/__Snapshots__/ResultsSearchViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/SearchShowsFeatureTests/SearchResults/Presentation/SnapshotTests/__Snapshots__/ResultsSearchViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.1.png -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchResults/Presentation/SnapshotTests/__Snapshots__/ResultsSearchViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/SearchShowsFeatureTests/SearchResults/Presentation/SnapshotTests/__Snapshots__/ResultsSearchViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.2.png -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchResults/Presentation/SnapshotTests/__Snapshots__/ResultsSearchViewTests/test_WhenViewIsPopulated_thenShowPopulatedScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/SearchShowsFeatureTests/SearchResults/Presentation/SnapshotTests/__Snapshots__/ResultsSearchViewTests/test_WhenViewIsPopulated_thenShowPopulatedScreen.1.png -------------------------------------------------------------------------------- /Tests/SearchShowsFeatureTests/SearchResults/Presentation/SnapshotTests/__Snapshots__/ResultsSearchViewTests/test_WhenViewIsPopulated_thenShowPopulatedScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/SearchShowsFeatureTests/SearchResults/Presentation/SnapshotTests/__Snapshots__/ResultsSearchViewTests/test_WhenViewIsPopulated_thenShowPopulatedScreen.2.png -------------------------------------------------------------------------------- /Tests/SharedTests/TestLocalizable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestLocalizable.swift 3 | // 4 | // 5 | // Created by Jeans Ruiz on 15/06/22. 6 | // 7 | 8 | import XCTest 9 | @testable import Shared 10 | @testable import UI 11 | 12 | class TestLocalizable: XCTestCase { 13 | 14 | func test_Spanish_Keys_probably_missing() throws { 15 | Strings.currentLocale = Locale(identifier: Language.es.rawValue) 16 | 17 | for key in Strings.allCases { 18 | XCTAssertNotEqual(key.rawValue, key.localized(), "Check this key for Spanish Localizable.String Its Probably Missing. Avoid use same key for the same description") 19 | } 20 | } 21 | 22 | func test_English_Keys_probably_missing() throws { 23 | Strings.currentLocale = Locale(identifier: Language.en.rawValue) 24 | 25 | for key in Strings.allCases { 26 | XCTAssertNotEqual(key.rawValue, key.localized(), "Check this key for English Localizable.String Its Probably Missing. Avoid use same key for the same description") 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/DetailsScene/Mocks/Entities/Account+Stub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Account+Stub.swift 3 | // ShowDetails-Unit-Tests 4 | // 5 | // Created by Jeans Ruiz on 8/4/20. 6 | // 7 | 8 | @testable import Shared 9 | 10 | extension AccountDomain { 11 | static func stub(id: Int = 1, 12 | sessionId: String = "session") -> Self { 13 | return AccountDomain(id: id, sessionId: sessionId) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/DetailsScene/Mocks/Entities/TVShowAccountStateResult+Stub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVShowAccountStateResult+Stub.swift 3 | // ShowDetails-Unit-Tests 4 | // 5 | // Created by Jeans Ruiz on 8/4/20. 6 | // 7 | 8 | @testable import Shared 9 | 10 | extension TVShowAccountStatus { 11 | static func stub(showId: Int = 1, 12 | isFavorite: Bool = true, 13 | isWatchList: Bool = true) -> Self { 14 | TVShowAccountStatus( 15 | showId: showId, 16 | isFavorite: isFavorite, 17 | isWatchList: isWatchList) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/DetailsScene/Mocks/Entities/TVShowDetailInfo+Stub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVShowDetailInfo+Stub.swift 3 | // ShowDetails-Unit-Tests 4 | // 5 | // Created by Jeans Ruiz on 8/4/20. 6 | // 7 | 8 | @testable import Shared 9 | @testable import ShowDetailsFeature 10 | 11 | extension TVShowDetailInfo { 12 | static func stub() -> Self { 13 | return TVShowDetailInfo(show: TVShowDetail.stub()) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/DetailsScene/Mocks/UsesCases/FetchLoggedUserMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FetchLoggedUserMock.swift 3 | // ShowDetails-Unit-Tests 4 | // 5 | // Created by Jeans Ruiz on 8/4/20. 6 | // 7 | 8 | @testable import ShowDetailsFeature 9 | @testable import Shared 10 | 11 | class FetchLoggedUserMock: FetchLoggedUser { 12 | var account: AccountDomain? 13 | 14 | func execute() -> AccountDomain? { 15 | return account 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/DetailsScene/Mocks/UsesCases/FetchTVAccountStateMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 8/4/20. 3 | // 4 | 5 | import Foundation 6 | import Combine 7 | import NetworkingInterface 8 | import Shared 9 | @testable import ShowDetailsFeature 10 | 11 | class FetchTVAccountStateMock: FetchTVAccountStates { 12 | 13 | var result: TVShowAccountStatus? 14 | var error: ApiError? 15 | 16 | func execute(request: FetchTVAccountStatesRequestValue) async throws -> TVShowAccountStatus { 17 | if let error = error { 18 | throw error 19 | } 20 | 21 | if let result = result { 22 | return result 23 | } else { 24 | throw ApiError(error: NSError(domain: "Mock", code: 404, userInfo: nil)) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/DetailsScene/Mocks/UsesCases/FetchTVShowDetailsUseCaseMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 8/4/20. 3 | // 4 | 5 | import Foundation 6 | import Combine 7 | import NetworkingInterface 8 | import Shared 9 | @testable import ShowDetailsFeature 10 | 11 | class FetchTVShowDetailsUseCaseMock: FetchTVShowDetailsUseCase { 12 | var result: TVShowDetail? 13 | var error: ApiError? 14 | 15 | public func execute(request: FetchTVShowDetailsUseCaseRequestValue) async throws -> TVShowDetail { 16 | await Task.yield() 17 | if let error = error { 18 | throw error 19 | } 20 | 21 | if let result { 22 | return result 23 | } else { 24 | throw ApiError(error: NSError(domain: "Details not Set", code: 0, userInfo: nil)) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/DetailsScene/Mocks/UsesCases/MarkAsFavoriteUseCaseMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 8/4/20. 3 | // 4 | 5 | import Foundation 6 | import Combine 7 | import NetworkingInterface 8 | import ShowDetailsFeature 9 | import Shared 10 | 11 | class MarkAsFavoriteUseCaseMock: MarkAsFavoriteUseCase { 12 | //let subject = PassthroughSubject() 13 | var result: Bool? 14 | var error: ApiError? 15 | var calledCounter = 0 16 | 17 | public func execute(request: MarkAsFavoriteUseCaseRequestValue) async throws -> Bool { 18 | calledCounter += 1 19 | 20 | if let error = error { 21 | throw error 22 | } 23 | 24 | if let result = result { 25 | return result 26 | } else { 27 | throw ApiError(error: NSError(domain: "MockError", code: 0, userInfo: nil)) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/DetailsScene/Mocks/UsesCases/SaveToWatchListUseCaseMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 8/4/20. 3 | // 4 | 5 | import Foundation 6 | import Combine 7 | import NetworkingInterface 8 | @testable import ShowDetailsFeature 9 | @testable import Shared 10 | 11 | class SaveToWatchListUseCaseMock: SaveToWatchListUseCase { 12 | //let subject = PassthroughSubject() 13 | var result: Bool? 14 | var error: ApiError? 15 | var calledCounter = 0 16 | 17 | public func execute(request: SaveToWatchListUseCaseRequestValue) async throws -> Bool { 18 | calledCounter += 1 19 | 20 | if let error = error { 21 | throw error 22 | } 23 | 24 | if let result = result { 25 | return result 26 | } else { 27 | throw ApiError(error: NSError(domain: "Mock", code: 0, userInfo: nil)) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/DetailsScene/Mocks/ViewModel/TVShowDetailViewModel+Mock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVShowDetailViewModel+Mock.swift 3 | // ShowDetails-Unit-Tests 4 | // 5 | // Created by Jeans Ruiz on 8/6/20. 6 | // 7 | 8 | import Combine 9 | @testable import ShowDetailsFeature 10 | 11 | class TVShowDetailViewModelMock: TVShowDetailViewModelProtocol { 12 | // MARK: - Input 13 | func viewDidLoad() { } 14 | func refreshView() { } 15 | func viewDidFinish() { } 16 | 17 | func favoriteButtonDidTapped() { } 18 | func watchedButtonDidTapped() { } 19 | 20 | // MARK: - Output 21 | func isUserLogged() -> Bool { return true } 22 | func navigateToSeasons() { } 23 | 24 | var viewState: CurrentValueSubject 25 | var isFavorite = CurrentValueSubject(false) 26 | var isWatchList = CurrentValueSubject(false) 27 | 28 | init(state: TVShowDetailViewModel.ViewState) { 29 | viewState = CurrentValueSubject(state) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/DetailsScene/Presentation/View/__Snapshots__/TVShowDetailViewTests/test_WhenViewIsError_thenShowErrorScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowDetailsFeatureTests/DetailsScene/Presentation/View/__Snapshots__/TVShowDetailViewTests/test_WhenViewIsError_thenShowErrorScreen.1.png -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/DetailsScene/Presentation/View/__Snapshots__/TVShowDetailViewTests/test_WhenViewIsError_thenShowErrorScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowDetailsFeatureTests/DetailsScene/Presentation/View/__Snapshots__/TVShowDetailViewTests/test_WhenViewIsError_thenShowErrorScreen.2.png -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/DetailsScene/Presentation/View/__Snapshots__/TVShowDetailViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowDetailsFeatureTests/DetailsScene/Presentation/View/__Snapshots__/TVShowDetailViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.1.png -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/DetailsScene/Presentation/View/__Snapshots__/TVShowDetailViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowDetailsFeatureTests/DetailsScene/Presentation/View/__Snapshots__/TVShowDetailViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.2.png -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/DetailsScene/Presentation/View/__Snapshots__/TVShowDetailViewTests/test_WhenViewPopulated_thenShowPopulatedScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowDetailsFeatureTests/DetailsScene/Presentation/View/__Snapshots__/TVShowDetailViewTests/test_WhenViewPopulated_thenShowPopulatedScreen.1.png -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/DetailsScene/Presentation/View/__Snapshots__/TVShowDetailViewTests/test_WhenViewPopulated_thenShowPopulatedScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowDetailsFeatureTests/DetailsScene/Presentation/View/__Snapshots__/TVShowDetailViewTests/test_WhenViewPopulated_thenShowPopulatedScreen.2.png -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/DetailsScene/Presentation/View/__Snapshots__/TVShowDetailViewTests/test_WhenViewPopulated_thenShowPopulatedScreen.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowDetailsFeatureTests/DetailsScene/Presentation/View/__Snapshots__/TVShowDetailViewTests/test_WhenViewPopulated_thenShowPopulatedScreen.3.png -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/DetailsScene/Presentation/View/__Snapshots__/TVShowDetailViewTests/test_WhenViewPopulated_thenShowPopulatedScreen.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowDetailsFeatureTests/DetailsScene/Presentation/View/__Snapshots__/TVShowDetailViewTests/test_WhenViewPopulated_thenShowPopulatedScreen.4.png -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/SeasonsScene/Mocks/Entities/Episode+Stub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Episode+Stub.swift 3 | // ShowDetails-Unit-Tests 4 | // 5 | // Created by Jeans Ruiz on 8/6/20. 6 | // 7 | 8 | import Foundation 9 | @testable import ShowDetailsFeature 10 | 11 | extension TVShowEpisode { 12 | 13 | public static func stub(id: Int = 1, 14 | episodeNumber: Int = 1, 15 | name: String = "Name 1", 16 | airDate: String = "01/01/1990", 17 | voteAverage: Double = 1.0, 18 | posterPathURL: URL? = nil 19 | ) -> Self { 20 | TVShowEpisode(id: id, 21 | episodeNumber: episodeNumber, 22 | name: name, 23 | airDate: airDate, 24 | voteAverage: voteAverage, 25 | posterPathURL: posterPathURL) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/SeasonsScene/Mocks/UseCases/FetchEpisodesUseCase+Mock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Jeans Ruiz on 8/6/20. 3 | // 4 | 5 | import Foundation 6 | import Combine 7 | import NetworkingInterface 8 | @testable import ShowDetailsFeature 9 | 10 | final class FetchEpisodesUseCaseMock: FetchEpisodesUseCase { 11 | var result: TVShowSeason? 12 | var error: ApiError? 13 | 14 | func execute(request: FetchEpisodesUseCaseRequestValue) async throws -> TVShowSeason { 15 | await Task.yield() 16 | if let error = error { 17 | throw error 18 | } 19 | 20 | if let result { 21 | return result 22 | } else { 23 | throw ApiError(error: NSError(domain: "Season Value not set", code: 0, userInfo: nil)) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/SeasonsScene/Mocks/ViewModel/SeasonListViewModelMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SeasonListViewModelMock.swift 3 | // ShowDetails-Unit-Tests 4 | // 5 | // Created by Jeans Ruiz on 8/8/20. 6 | // 7 | 8 | import Combine 9 | @testable import ShowDetailsFeature 10 | 11 | class SeasonListViewModelMock: SeasonListViewModelProtocol { 12 | var inputSelectedSeason = CurrentValueSubject(0) 13 | 14 | func selectSeason(seasonNumber: Int) { } 15 | 16 | var seasons = CurrentValueSubject<[Int], Never>([]) 17 | 18 | var seasonSelected = CurrentValueSubject(0) 19 | 20 | func getModel(for season: Int) -> SeasonEpisodeViewModel { 21 | return SeasonEpisodeViewModel(seasonNumber: season) 22 | } 23 | 24 | weak var delegate: SeasonListViewModelDelegate? 25 | 26 | init(seasonList: [Int]) { 27 | seasons = CurrentValueSubject(seasonList) 28 | } 29 | 30 | func selectSeason(_ season: Int) { 31 | seasonSelected.send(season) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/SeasonsScene/Presentation/View/__Snapshots__/EpisodesListViewTests/test_WhenViewIsLoading_thenShow_LoadingScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowDetailsFeatureTests/SeasonsScene/Presentation/View/__Snapshots__/EpisodesListViewTests/test_WhenViewIsLoading_thenShow_LoadingScreen.1.png -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/SeasonsScene/Presentation/View/__Snapshots__/EpisodesListViewTests/test_WhenViewIsLoading_thenShow_LoadingScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowDetailsFeatureTests/SeasonsScene/Presentation/View/__Snapshots__/EpisodesListViewTests/test_WhenViewIsLoading_thenShow_LoadingScreen.2.png -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/SeasonsScene/Presentation/View/__Snapshots__/EpisodesListViewTests/test_WhenViewModelDidPopulated_thenShow_PopulatedScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowDetailsFeatureTests/SeasonsScene/Presentation/View/__Snapshots__/EpisodesListViewTests/test_WhenViewModelDidPopulated_thenShow_PopulatedScreen.1.png -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/SeasonsScene/Presentation/View/__Snapshots__/EpisodesListViewTests/test_WhenViewModelDidPopulated_thenShow_PopulatedScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowDetailsFeatureTests/SeasonsScene/Presentation/View/__Snapshots__/EpisodesListViewTests/test_WhenViewModelDidPopulated_thenShow_PopulatedScreen.2.png -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/SeasonsScene/Presentation/View/__Snapshots__/EpisodesListViewTests/test_WhenViewModelLoadSeason_thenShow_LoadingSeasonScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowDetailsFeatureTests/SeasonsScene/Presentation/View/__Snapshots__/EpisodesListViewTests/test_WhenViewModelLoadSeason_thenShow_LoadingSeasonScreen.1.png -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/SeasonsScene/Presentation/View/__Snapshots__/EpisodesListViewTests/test_WhenViewModelLoadSeason_thenShow_LoadingSeasonScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowDetailsFeatureTests/SeasonsScene/Presentation/View/__Snapshots__/EpisodesListViewTests/test_WhenViewModelLoadSeason_thenShow_LoadingSeasonScreen.2.png -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/SeasonsScene/Presentation/View/__Snapshots__/EpisodesListViewTests/test_WhenViewModelReturnsEmpty_thenShow_EmptyScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowDetailsFeatureTests/SeasonsScene/Presentation/View/__Snapshots__/EpisodesListViewTests/test_WhenViewModelReturnsEmpty_thenShow_EmptyScreen.1.png -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/SeasonsScene/Presentation/View/__Snapshots__/EpisodesListViewTests/test_WhenViewModelReturnsEmpty_thenShow_EmptyScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowDetailsFeatureTests/SeasonsScene/Presentation/View/__Snapshots__/EpisodesListViewTests/test_WhenViewModelReturnsEmpty_thenShow_EmptyScreen.2.png -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/SeasonsScene/Presentation/View/__Snapshots__/EpisodesListViewTests/test_WhenViewModelReturnsErrorSeason_thenShow_ErrorSeasonScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowDetailsFeatureTests/SeasonsScene/Presentation/View/__Snapshots__/EpisodesListViewTests/test_WhenViewModelReturnsErrorSeason_thenShow_ErrorSeasonScreen.1.png -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/SeasonsScene/Presentation/View/__Snapshots__/EpisodesListViewTests/test_WhenViewModelReturnsErrorSeason_thenShow_ErrorSeasonScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowDetailsFeatureTests/SeasonsScene/Presentation/View/__Snapshots__/EpisodesListViewTests/test_WhenViewModelReturnsErrorSeason_thenShow_ErrorSeasonScreen.2.png -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/SeasonsScene/Presentation/View/__Snapshots__/EpisodesListViewTests/test_WhenViewModelReturnsError_thenShow_ErrorScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowDetailsFeatureTests/SeasonsScene/Presentation/View/__Snapshots__/EpisodesListViewTests/test_WhenViewModelReturnsError_thenShow_ErrorScreen.1.png -------------------------------------------------------------------------------- /Tests/ShowDetailsFeatureTests/SeasonsScene/Presentation/View/__Snapshots__/EpisodesListViewTests/test_WhenViewModelReturnsError_thenShow_ErrorScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowDetailsFeatureTests/SeasonsScene/Presentation/View/__Snapshots__/EpisodesListViewTests/test_WhenViewModelReturnsError_thenShow_ErrorScreen.2.png -------------------------------------------------------------------------------- /Tests/ShowListFeatureTests/Mocks/TVShowListViewModelMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TVShowListViewModelMock.swift 3 | // TVShowsListTests 4 | // 5 | // Created by Jeans Ruiz on 19/12/21. 6 | // 7 | 8 | import XCTest 9 | import Combine 10 | import Shared 11 | import UI 12 | 13 | @testable import ShowListFeature 14 | 15 | class TVShowListViewModelMock: TVShowListViewModelProtocol { 16 | 17 | func viewDidLoad() { } 18 | 19 | func willDisplayRow(_ row: Int, outOf totalRows: Int) { } 20 | 21 | func showIsPicked(index: Int) { } 22 | 23 | func refreshView() { } 24 | 25 | func viewDidFinish() { } 26 | 27 | var viewStateObservableSubject: CurrentValueSubject, Never> 28 | 29 | init(state: SimpleViewState) { 30 | viewStateObservableSubject = CurrentValueSubject(state) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/ShowListFeatureTests/Presentation/SnapshotTests/__Snapshots__/TVShowListViewTests/test_WhenViewIsEmpty_thenShowEmptyScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowListFeatureTests/Presentation/SnapshotTests/__Snapshots__/TVShowListViewTests/test_WhenViewIsEmpty_thenShowEmptyScreen.1.png -------------------------------------------------------------------------------- /Tests/ShowListFeatureTests/Presentation/SnapshotTests/__Snapshots__/TVShowListViewTests/test_WhenViewIsEmpty_thenShowEmptyScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowListFeatureTests/Presentation/SnapshotTests/__Snapshots__/TVShowListViewTests/test_WhenViewIsEmpty_thenShowEmptyScreen.2.png -------------------------------------------------------------------------------- /Tests/ShowListFeatureTests/Presentation/SnapshotTests/__Snapshots__/TVShowListViewTests/test_WhenViewIsError_thenShowErrorScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowListFeatureTests/Presentation/SnapshotTests/__Snapshots__/TVShowListViewTests/test_WhenViewIsError_thenShowErrorScreen.1.png -------------------------------------------------------------------------------- /Tests/ShowListFeatureTests/Presentation/SnapshotTests/__Snapshots__/TVShowListViewTests/test_WhenViewIsError_thenShowErrorScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowListFeatureTests/Presentation/SnapshotTests/__Snapshots__/TVShowListViewTests/test_WhenViewIsError_thenShowErrorScreen.2.png -------------------------------------------------------------------------------- /Tests/ShowListFeatureTests/Presentation/SnapshotTests/__Snapshots__/TVShowListViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowListFeatureTests/Presentation/SnapshotTests/__Snapshots__/TVShowListViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.1.png -------------------------------------------------------------------------------- /Tests/ShowListFeatureTests/Presentation/SnapshotTests/__Snapshots__/TVShowListViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowListFeatureTests/Presentation/SnapshotTests/__Snapshots__/TVShowListViewTests/test_WhenViewIsLoading_thenShowLoadingScreen.2.png -------------------------------------------------------------------------------- /Tests/ShowListFeatureTests/Presentation/SnapshotTests/__Snapshots__/TVShowListViewTests/test_WhenViewPaging_thenShowPagingScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowListFeatureTests/Presentation/SnapshotTests/__Snapshots__/TVShowListViewTests/test_WhenViewPaging_thenShowPagingScreen.1.png -------------------------------------------------------------------------------- /Tests/ShowListFeatureTests/Presentation/SnapshotTests/__Snapshots__/TVShowListViewTests/test_WhenViewPaging_thenShowPagingScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowListFeatureTests/Presentation/SnapshotTests/__Snapshots__/TVShowListViewTests/test_WhenViewPaging_thenShowPagingScreen.2.png -------------------------------------------------------------------------------- /Tests/ShowListFeatureTests/Presentation/SnapshotTests/__Snapshots__/TVShowListViewTests/test_WhenViewPopulated_thenShowPopulatedScreen.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowListFeatureTests/Presentation/SnapshotTests/__Snapshots__/TVShowListViewTests/test_WhenViewPopulated_thenShowPopulatedScreen.1.png -------------------------------------------------------------------------------- /Tests/ShowListFeatureTests/Presentation/SnapshotTests/__Snapshots__/TVShowListViewTests/test_WhenViewPopulated_thenShowPopulatedScreen.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rcaos/TVToday/8837c21b1ec7bbce37b81c57411273bacdb6a040/Tests/ShowListFeatureTests/Presentation/SnapshotTests/__Snapshots__/TVShowListViewTests/test_WhenViewPopulated_thenShowPopulatedScreen.2.png -------------------------------------------------------------------------------- /bin/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | 3 | // Leave blank. This is only here so that Xcode doesn't display it. 4 | 5 | import PackageDescription 6 | 7 | let package = Package( 8 | name: "bin", 9 | products: [], 10 | targets: [] 11 | ) 12 | -------------------------------------------------------------------------------- /bin/swiftgen.yml: -------------------------------------------------------------------------------- 1 | ## https://github.com/SwiftGen/SwiftGen/tree/6.4.0/Documentation/ 2 | 3 | input_dir: ../Sources/UI/Resources/ 4 | output_dir: ../Sources/UI/Generated/ 5 | 6 | strings: 7 | inputs: 8 | - en.lproj 9 | outputs: 10 | - templatePath: structured-swift5-custom.stencil 11 | params: 12 | enumName: Strings 13 | publicAccess: true 14 | output: Strings+Generated.swift --------------------------------------------------------------------------------