├── .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
--------------------------------------------------------------------------------