diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..8951f0ea Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..755da9bb --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +Movie-Application/Movie-Application.xcodeproj/project.xcworkspace/xcuserdata/mohanna.xcuserdatad/UserInterfaceState.xcuserstate diff --git a/Movie-Application/.DS_Store b/Movie-Application/.DS_Store new file mode 100644 index 00000000..df5ef2e1 Binary files /dev/null and b/Movie-Application/.DS_Store differ diff --git a/Movie-Application/Movie-Application.xcodeproj/project.pbxproj b/Movie-Application/Movie-Application.xcodeproj/project.pbxproj new file mode 100644 index 00000000..8630a2c4 --- /dev/null +++ b/Movie-Application/Movie-Application.xcodeproj/project.pbxproj @@ -0,0 +1,2190 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 1B026CA528218354006D2BFE /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B026CA428218354006D2BFE /* AppCoordinator.swift */; }; + 1B026CA9282184F7006D2BFE /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B026CA8282184F7006D2BFE /* Coordinator.swift */; }; + 1B026CAB2821858A006D2BFE /* Storyboarded.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B026CAA2821858A006D2BFE /* Storyboarded.swift */; }; + 1B026CB0282188C6006D2BFE /* TabBarPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B026CAF282188C6006D2BFE /* TabBarPage.swift */; }; + 1B026CB7282193B8006D2BFE /* TopRatedMoviesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B026CB6282193B8006D2BFE /* TopRatedMoviesViewModel.swift */; }; + 1B026CBB282193EB006D2BFE /* TopRatedMoviesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B026CBA282193EB006D2BFE /* TopRatedMoviesViewController.swift */; }; + 1B026CBE2821990F006D2BFE /* MoviesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8C5EB7281802270013909D /* MoviesService.swift */; }; + 1B026CBF28219926006D2BFE /* RequestManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8C5EA72817F67A0013909D /* RequestManager.swift */; }; + 1B026CC028219929006D2BFE /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8C5EA82817F67A0013909D /* HTTPMethod.swift */; }; + 1B026CC12821992C006D2BFE /* RequestError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8C5EAE2817F67A0013909D /* RequestError.swift */; }; + 1B026CC228219932006D2BFE /* ResponseValidatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8C5EA52817F67A0013909D /* ResponseValidatorProtocol.swift */; }; + 1B026CC328219935006D2BFE /* ResponseValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8C5EA62817F67A0013909D /* ResponseValidator.swift */; }; + 1B026CC428219938006D2BFE /* ResponseLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8C5EAA2817F67A0013909D /* ResponseLog.swift */; }; + 1B026CC52821993C006D2BFE /* URLRequestLoggableProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8C5EAB2817F67A0013909D /* URLRequestLoggableProtocol.swift */; }; + 1B026CC62821993F006D2BFE /* RequestManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8C5EAD2817F67A0013909D /* RequestManagerProtocol.swift */; }; + 1B026CC728219943006D2BFE /* MoviesServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD16F3F281B4162003E87B5 /* MoviesServiceProtocol.swift */; }; + 1B026CC828219946006D2BFE /* Movies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8C5EBB281804D20013909D /* Movies.swift */; }; + 1B026CC928219949006D2BFE /* MoviesGeneres.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8C5EBD2818069D0013909D /* MoviesGeneres.swift */; }; + 1B026CCA282199B0006D2BFE /* FavoriteMovieModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 1B15287228196CD9008071CC /* FavoriteMovieModel.xcdatamodeld */; }; + 1B026CCB282199B7006D2BFE /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B152875281971BA008071CC /* CoreDataManager.swift */; }; + 1B026CCC282199BE006D2BFE /* CoreDataProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B152878281971E5008071CC /* CoreDataProtocol.swift */; }; + 1B026CCD282199CC006D2BFE /* MovieCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD16F1D281AFB7C003E87B5 /* MovieCell.swift */; }; + 1B026CD028219C6A006D2BFE /* MovieCollectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B026CCF28219C6A006D2BFE /* MovieCollectionViewDelegate.swift */; }; + 1B026CD228219EAD006D2BFE /* MovieCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B026CD128219EAD006D2BFE /* MovieCollectionViewCell.swift */; }; + 1B026CD42821B4A7006D2BFE /* TopRatedMoviesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B026CD32821B4A7006D2BFE /* TopRatedMoviesCoordinator.swift */; }; + 1B026CD62822BFF9006D2BFE /* MainTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B026CD52822BFF9006D2BFE /* MainTabBarController.swift */; }; + 1B026CDA2822CA0D006D2BFE /* PopularMoviesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B026CD92822CA0D006D2BFE /* PopularMoviesViewController.swift */; }; + 1B026CDC2822CA1D006D2BFE /* PopularMoviesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B026CDB2822CA1D006D2BFE /* PopularMoviesViewModel.swift */; }; + 1B026CDE2822CBBB006D2BFE /* PopularMoviesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B026CDD2822CBBB006D2BFE /* PopularMoviesCoordinator.swift */; }; + 1B026CE32822CEB5006D2BFE /* WatchlistMoviesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B026CE22822CEB5006D2BFE /* WatchlistMoviesViewModel.swift */; }; + 1B026CE42822DAC4006D2BFE /* MovieCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B026CD128219EAD006D2BFE /* MovieCollectionViewCell.swift */; }; + 1B026CE72823C205006D2BFE /* WatchlistMoviesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B026CE62823C205006D2BFE /* WatchlistMoviesViewController.swift */; }; + 1B026CE92823C2D5006D2BFE /* WatchlistMoviesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B026CE82823C2D5006D2BFE /* WatchlistMoviesCoordinator.swift */; }; + 1B026CEB2823C425006D2BFE /* TopRatedMovies.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1B026CEA2823C425006D2BFE /* TopRatedMovies.storyboard */; }; + 1B026CED2823C4B7006D2BFE /* PopularMovies.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1B026CEC2823C4B7006D2BFE /* PopularMovies.storyboard */; }; + 1B026CEF2823C72D006D2BFE /* WatchlistMovies.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1B026CEE2823C72C006D2BFE /* WatchlistMovies.storyboard */; }; + 1B026CF12824200E006D2BFE /* MovieDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B026CF02824200D006D2BFE /* MovieDetailsViewController.swift */; }; + 1B026CF22824206A006D2BFE /* MovieDetailsInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD032AD281C488E00139C3B /* MovieDetailsInfoViewController.swift */; }; + 1B026CF328242088006D2BFE /* BottomSheetContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD032AA281C3C6800139C3B /* BottomSheetContainerViewController.swift */; }; + 1B026CF5282420C9006D2BFE /* MovieInfoContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B026CF4282420C9006D2BFE /* MovieInfoContentViewController.swift */; }; + 1B026CF928242154006D2BFE /* MovieInfoContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B026CF828242154006D2BFE /* MovieInfoContentViewModel.swift */; }; + 1B026D0228251A60006D2BFE /* TestAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B026D0128251A60006D2BFE /* TestAppCoordinator.swift */; }; + 1B026D0428251C5A006D2BFE /* UnitTestError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B026D0328251C5A006D2BFE /* UnitTestError.swift */; }; + 1B026D0728256F39006D2BFE /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1B026D0528256F39006D2BFE /* LaunchScreen.storyboard */; }; + 1B026D09282582A6006D2BFE /* TestTopRatedMoviesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B026D08282582A6006D2BFE /* TestTopRatedMoviesCoordinator.swift */; }; + 1B026D0C2825B366006D2BFE /* TestPopularMoviesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B026D0B2825B366006D2BFE /* TestPopularMoviesCoordinator.swift */; }; + 1B026D0E2825B4FF006D2BFE /* TestWatchlistMoviesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B026D0D2825B4FF006D2BFE /* TestWatchlistMoviesCoordinator.swift */; }; + 1B15287428196CD9008071CC /* FavoriteMovieModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 1B15287228196CD9008071CC /* FavoriteMovieModel.xcdatamodeld */; }; + 1B152876281971BA008071CC /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B152875281971BA008071CC /* CoreDataManager.swift */; }; + 1B152879281971E5008071CC /* CoreDataProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B152878281971E5008071CC /* CoreDataProtocol.swift */; }; + 1B5B0D4E28202124001136BB /* TestWatchlistMoviesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B5B0D4D28202124001136BB /* TestWatchlistMoviesView.swift */; }; + 1B5B0D50282024C3001136BB /* TestWatchlistMoviesPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B5B0D4F282024C3001136BB /* TestWatchlistMoviesPresenter.swift */; }; + 1B5B0D5228202CF0001136BB /* TestWatchlistMoviesInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B5B0D5128202CF0001136BB /* TestWatchlistMoviesInteractor.swift */; }; + 1B5B0D5428202DB3001136BB /* TestWatchlistMoviesRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B5B0D5328202DB3001136BB /* TestWatchlistMoviesRouter.swift */; }; + 1B5B0D5628202F22001136BB /* TestTopRatedMoviesRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B5B0D5528202F22001136BB /* TestTopRatedMoviesRouter.swift */; }; + 1B5B0D5828202F83001136BB /* TestPopularMoviesRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B5B0D5728202F83001136BB /* TestPopularMoviesRouter.swift */; }; + 1B8C5EAF2817F67A0013909D /* ResponseValidatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8C5EA52817F67A0013909D /* ResponseValidatorProtocol.swift */; }; + 1B8C5EB02817F67A0013909D /* ResponseValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8C5EA62817F67A0013909D /* ResponseValidator.swift */; }; + 1B8C5EB12817F67A0013909D /* RequestManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8C5EA72817F67A0013909D /* RequestManager.swift */; }; + 1B8C5EB22817F67A0013909D /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8C5EA82817F67A0013909D /* HTTPMethod.swift */; }; + 1B8C5EB32817F67A0013909D /* ResponseLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8C5EAA2817F67A0013909D /* ResponseLog.swift */; }; + 1B8C5EB42817F67A0013909D /* URLRequestLoggableProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8C5EAB2817F67A0013909D /* URLRequestLoggableProtocol.swift */; }; + 1B8C5EB52817F67A0013909D /* RequestManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8C5EAD2817F67A0013909D /* RequestManagerProtocol.swift */; }; + 1B8C5EB62817F67A0013909D /* RequestError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8C5EAE2817F67A0013909D /* RequestError.swift */; }; + 1B8C5EB8281802270013909D /* MoviesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8C5EB7281802270013909D /* MoviesService.swift */; }; + 1B8C5EBC281804D20013909D /* Movies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8C5EBB281804D20013909D /* Movies.swift */; }; + 1B8C5EBE2818069D0013909D /* MoviesGeneres.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8C5EBD2818069D0013909D /* MoviesGeneres.swift */; }; + 1B8C5EE128182A980013909D /* TestTopRatedMoviesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8C5EE028182A970013909D /* TestTopRatedMoviesView.swift */; }; + 1BD0329C281C10EF00139C3B /* MovieDetailsEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD0328C281C10EF00139C3B /* MovieDetailsEntity.swift */; }; + 1BD0329D281C10EF00139C3B /* MovieDetailsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD0328E281C10EF00139C3B /* MovieDetailsPresenter.swift */; }; + 1BD0329E281C10EF00139C3B /* MovieDetailsPresenterInteractorInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD0328F281C10EF00139C3B /* MovieDetailsPresenterInteractorInterface.swift */; }; + 1BD0329F281C10EF00139C3B /* MovieDetailsPresenterRouterInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD03290281C10EF00139C3B /* MovieDetailsPresenterRouterInterface.swift */; }; + 1BD032A0281C10EF00139C3B /* MovieDetailsPresenterViewInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD03291281C10EF00139C3B /* MovieDetailsPresenterViewInterface.swift */; }; + 1BD032A1281C10EF00139C3B /* MovieDetailsViewInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD03293281C10EF00139C3B /* MovieDetailsViewInterface.swift */; }; + 1BD032A2281C10EF00139C3B /* MovieDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD03294281C10EF00139C3B /* MovieDetailsView.swift */; }; + 1BD032A3281C10EF00139C3B /* MovieDetailsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD03296281C10EF00139C3B /* MovieDetailsInteractor.swift */; }; + 1BD032A4281C10EF00139C3B /* MovieDetailsInteractorInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD03297281C10EF00139C3B /* MovieDetailsInteractorInterface.swift */; }; + 1BD032A5281C10EF00139C3B /* MovieDetailsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD03298281C10EF00139C3B /* MovieDetailsModule.swift */; }; + 1BD032A6281C10EF00139C3B /* MovieDetailsRouterInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD0329A281C10EF00139C3B /* MovieDetailsRouterInterface.swift */; }; + 1BD032A7281C10EF00139C3B /* MovieDetailsRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD0329B281C10EF00139C3B /* MovieDetailsRouter.swift */; }; + 1BD032AB281C3C6800139C3B /* BottomSheetContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD032AA281C3C6800139C3B /* BottomSheetContainerViewController.swift */; }; + 1BD032AE281C488E00139C3B /* MovieDetailsInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD032AD281C488E00139C3B /* MovieDetailsInfoViewController.swift */; }; + 1BD032C9281C5A8B00139C3B /* MovieInfoContentPresenterInteractorInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD032BA281C5A8B00139C3B /* MovieInfoContentPresenterInteractorInterface.swift */; }; + 1BD032CA281C5A8B00139C3B /* MovieInfoContentPresenterRouterInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD032BB281C5A8B00139C3B /* MovieInfoContentPresenterRouterInterface.swift */; }; + 1BD032CB281C5A8B00139C3B /* MovieInfoContentPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD032BC281C5A8B00139C3B /* MovieInfoContentPresenter.swift */; }; + 1BD032CC281C5A8B00139C3B /* MovieInfoContentPresenterViewInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD032BD281C5A8B00139C3B /* MovieInfoContentPresenterViewInterface.swift */; }; + 1BD032CD281C5A8B00139C3B /* MovieInfoContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD032BF281C5A8B00139C3B /* MovieInfoContentView.swift */; }; + 1BD032CE281C5A8B00139C3B /* MovieInfoContentViewInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD032C0281C5A8B00139C3B /* MovieInfoContentViewInterface.swift */; }; + 1BD032CF281C5A8B00139C3B /* MovieInfoContentModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD032C1281C5A8B00139C3B /* MovieInfoContentModule.swift */; }; + 1BD032D0281C5A8B00139C3B /* MovieInfoContentInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD032C3281C5A8B00139C3B /* MovieInfoContentInteractor.swift */; }; + 1BD032D1281C5A8B00139C3B /* MovieInfoContentInteractorInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD032C4281C5A8B00139C3B /* MovieInfoContentInteractorInterface.swift */; }; + 1BD032D2281C5A8B00139C3B /* MovieInfoContentRouterInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD032C6281C5A8B00139C3B /* MovieInfoContentRouterInterface.swift */; }; + 1BD032D3281C5A8B00139C3B /* MovieInfoContentRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD032C7281C5A8B00139C3B /* MovieInfoContentRouter.swift */; }; + 1BD0330E281F0AB700139C3B /* TestTopRatedMoviesPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD0330D281F0AB700139C3B /* TestTopRatedMoviesPresenter.swift */; }; + 1BD03310281F14ED00139C3B /* TestTopRatedMoviesInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD0330F281F14ED00139C3B /* TestTopRatedMoviesInteractor.swift */; }; + 1BD03313281F1CF000139C3B /* TestPopularMoviesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD03312281F1CF000139C3B /* TestPopularMoviesView.swift */; }; + 1BD03315281F1E8C00139C3B /* TestPopularMoviesPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD03314281F1E8C00139C3B /* TestPopularMoviesPresenter.swift */; }; + 1BD03317281F24A000139C3B /* TestPopularMoviesInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD03316281F24A000139C3B /* TestPopularMoviesInteractor.swift */; }; + 1BD16F17281AF731003E87B5 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD16F16281AF731003E87B5 /* Extensions.swift */; }; + 1BD16F1F281AFB7C003E87B5 /* MovieCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD16F1D281AFB7C003E87B5 /* MovieCell.swift */; }; + 1BD16F40281B4162003E87B5 /* MoviesServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD16F3F281B4162003E87B5 /* MoviesServiceProtocol.swift */; }; + 1BD16F42281B426F003E87B5 /* TopRatedMovies.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1BD16F41281B426F003E87B5 /* TopRatedMovies.storyboard */; }; + 1BD16F44281B5169003E87B5 /* PopularMovies.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1BD16F43281B5169003E87B5 /* PopularMovies.storyboard */; }; + 1BD16F77281B5D9A003E87B5 /* WatchlistMoviesModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD16F66281B5D99003E87B5 /* WatchlistMoviesModule.swift */; }; + 1BD16F79281B5D9A003E87B5 /* WatchlistMoviesPresenterRouterInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD16F6A281B5D99003E87B5 /* WatchlistMoviesPresenterRouterInterface.swift */; }; + 1BD16F7A281B5D9A003E87B5 /* WatchlistMoviesPresenterViewInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD16F6B281B5D9A003E87B5 /* WatchlistMoviesPresenterViewInterface.swift */; }; + 1BD16F7B281B5D9A003E87B5 /* WatchlistMoviesPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD16F6C281B5D9A003E87B5 /* WatchlistMoviesPresenter.swift */; }; + 1BD16F7C281B5D9A003E87B5 /* WatchlistMoviesPresenterInteractorInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD16F6D281B5D9A003E87B5 /* WatchlistMoviesPresenterInteractorInterface.swift */; }; + 1BD16F7D281B5D9A003E87B5 /* WatchlistMoviesViewInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD16F6F281B5D9A003E87B5 /* WatchlistMoviesViewInterface.swift */; }; + 1BD16F7E281B5D9A003E87B5 /* WatchlistMoviesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD16F70281B5D9A003E87B5 /* WatchlistMoviesView.swift */; }; + 1BD16F7F281B5D9A003E87B5 /* WatchlistMoviesInteractorInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD16F72281B5D9A003E87B5 /* WatchlistMoviesInteractorInterface.swift */; }; + 1BD16F80281B5D9A003E87B5 /* WatchlistMoviesInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD16F73281B5D9A003E87B5 /* WatchlistMoviesInteractor.swift */; }; + 1BD16F81281B5D9A003E87B5 /* WatchlistMoviesRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD16F75281B5D9A003E87B5 /* WatchlistMoviesRouter.swift */; }; + 1BD16F82281B5D9A003E87B5 /* WatchlistMoviesRouterInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BD16F76281B5D9A003E87B5 /* WatchlistMoviesRouterInterface.swift */; }; + 1BD16F84281B5DAC003E87B5 /* WatchlistMovies.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1BD16F83281B5DAC003E87B5 /* WatchlistMovies.storyboard */; }; + 1BE985932813F0A20001DCAB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE985922813F0A20001DCAB /* AppDelegate.swift */; }; + 1BE985952813F0A20001DCAB /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE985942813F0A20001DCAB /* SceneDelegate.swift */; }; + 1BE9859C2813F0A20001DCAB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1BE9859B2813F0A20001DCAB /* Assets.xcassets */; }; + 1BE9859F2813F0A20001DCAB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1BE9859D2813F0A20001DCAB /* LaunchScreen.storyboard */; }; + 1BE985AA2813F0A30001DCAB /* Movie_ApplicationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE985A92813F0A30001DCAB /* Movie_ApplicationTests.swift */; }; + 1BE985B62813F0A30001DCAB /* Movie_ApplicationUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE985B52813F0A30001DCAB /* Movie_ApplicationUITestsLaunchTests.swift */; }; + 1BE985D52813F2430001DCAB /* TopRatedMoviesModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE985C42813F2430001DCAB /* TopRatedMoviesModule.swift */; }; + 1BE985D72813F2430001DCAB /* TopRatedMoviesPresenterInteractorInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE985C82813F2430001DCAB /* TopRatedMoviesPresenterInteractorInterface.swift */; }; + 1BE985D82813F2430001DCAB /* TopRatedMoviesPresenterRouterInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE985C92813F2430001DCAB /* TopRatedMoviesPresenterRouterInterface.swift */; }; + 1BE985D92813F2430001DCAB /* TopRatedMoviesPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE985CA2813F2430001DCAB /* TopRatedMoviesPresenter.swift */; }; + 1BE985DA2813F2430001DCAB /* TopRatedMoviesPresenterViewInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE985CB2813F2430001DCAB /* TopRatedMoviesPresenterViewInterface.swift */; }; + 1BE985DB2813F2430001DCAB /* TopRatedMoviesViewInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE985CD2813F2430001DCAB /* TopRatedMoviesViewInterface.swift */; }; + 1BE985DC2813F2430001DCAB /* TopRatedMoviesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE985CE2813F2430001DCAB /* TopRatedMoviesView.swift */; }; + 1BE985DD2813F2430001DCAB /* TopRatedMoviesInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE985D02813F2430001DCAB /* TopRatedMoviesInteractor.swift */; }; + 1BE985DE2813F2430001DCAB /* TopRatedMoviesInteractorInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE985D12813F2430001DCAB /* TopRatedMoviesInteractorInterface.swift */; }; + 1BE985DF2813F2430001DCAB /* TopRatedMoviesRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE985D32813F2430001DCAB /* TopRatedMoviesRouter.swift */; }; + 1BE985E02813F2430001DCAB /* TopRatedMoviesRouterInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE985D42813F2430001DCAB /* TopRatedMoviesRouterInterface.swift */; }; + 1BE985E22813F2500001DCAB /* Interfaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE985E12813F2500001DCAB /* Interfaces.swift */; }; + 1BE9863A28140D150001DCAB /* PopularMoviesPresenterRouterInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE9862B28140D150001DCAB /* PopularMoviesPresenterRouterInterface.swift */; }; + 1BE9863B28140D150001DCAB /* PopularMoviesPresenterViewInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE9862C28140D150001DCAB /* PopularMoviesPresenterViewInterface.swift */; }; + 1BE9863C28140D150001DCAB /* PopularMoviesPresenterInteractorInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE9862D28140D150001DCAB /* PopularMoviesPresenterInteractorInterface.swift */; }; + 1BE9863D28140D150001DCAB /* PopularMoviesPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE9862E28140D150001DCAB /* PopularMoviesPresenter.swift */; }; + 1BE9863E28140D150001DCAB /* PopularMoviesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE9863028140D150001DCAB /* PopularMoviesView.swift */; }; + 1BE9863F28140D150001DCAB /* PopularMoviesViewInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE9863128140D150001DCAB /* PopularMoviesViewInterface.swift */; }; + 1BE9864028140D150001DCAB /* PopularMoviesModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE9863228140D150001DCAB /* PopularMoviesModule.swift */; }; + 1BE9864128140D150001DCAB /* PopularMoviesInteractorInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE9863428140D150001DCAB /* PopularMoviesInteractorInterface.swift */; }; + 1BE9864228140D150001DCAB /* PopularMoviesInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE9863528140D150001DCAB /* PopularMoviesInteractor.swift */; }; + 1BE9864328140D150001DCAB /* PopularMoviesRouterInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE9863728140D150001DCAB /* PopularMoviesRouterInterface.swift */; }; + 1BE9864428140D150001DCAB /* PopularMoviesRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE9863828140D150001DCAB /* PopularMoviesRouter.swift */; }; + 1BE9864F2815D49A0001DCAB /* TabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE9864E2815D49A0001DCAB /* TabBarViewController.swift */; }; + 1BE986522815D5CF0001DCAB /* TestTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE986512815D5CF0001DCAB /* TestTabBarViewController.swift */; }; + 1BF787CE282127A1004B6D2B /* Movie_ApplicationUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE985B32813F0A30001DCAB /* Movie_ApplicationUITests.swift */; }; + 1BF787D128213D9E004B6D2B /* Movies.json in Resources */ = {isa = PBXBuildFile; fileRef = 1BF787D028213D9E004B6D2B /* Movies.json */; }; + 1BF787D328213DDC004B6D2B /* URLSessionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BF787D228213DDC004B6D2B /* URLSessionMock.swift */; }; + 1BF787D528213E34004B6D2B /* URLSessionDataTaskMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BF787D428213E34004B6D2B /* URLSessionDataTaskMock.swift */; }; + 1BF787D728213E87004B6D2B /* RequestManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BF787D628213E87004B6D2B /* RequestManagerMock.swift */; }; + 1BF787D928213EFB004B6D2B /* MockResponseValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BF787D828213EFB004B6D2B /* MockResponseValidator.swift */; }; + 1BF787DB28213F32004B6D2B /* UnitTestError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BF787DA28213F32004B6D2B /* UnitTestError.swift */; }; + 1BF787DF28213FCB004B6D2B /* TestMoviesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BF787DE28213FCB004B6D2B /* TestMoviesService.swift */; }; + 1BF787E528216193004B6D2B /* Movie.json in Resources */ = {isa = PBXBuildFile; fileRef = 1BF787E428216193004B6D2B /* Movie.json */; }; + 1BF787ED2821691B004B6D2B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BF787EC2821691B004B6D2B /* AppDelegate.swift */; }; + 1BF787F92821691D004B6D2B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1BF787F82821691D004B6D2B /* Assets.xcassets */; }; + 1BF788072821691D004B6D2B /* MovieApplicationMVVMTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BF788062821691D004B6D2B /* MovieApplicationMVVMTests.swift */; }; + 1BF788112821691D004B6D2B /* MovieApplicationMVVMUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BF788102821691D004B6D2B /* MovieApplicationMVVMUITests.swift */; }; + 1BF788132821691D004B6D2B /* MovieApplicationMVVMUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BF788122821691D004B6D2B /* MovieApplicationMVVMUITestsLaunchTests.swift */; }; + 2AED66104B329C5D6F3C5FED /* Pods_Movie_Application_Movie_ApplicationUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 80E20878F6E954381A53AFF4 /* Pods_Movie_Application_Movie_ApplicationUITests.framework */; }; + 662B1788192ABA8C653B70A8 /* Pods_Movie_Application.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6CBAB8843523803C510244D /* Pods_Movie_Application.framework */; }; + E218ED5C6277C8ACD57255B0 /* Pods_Movie_ApplicationTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90E49B8E9B0BABB57B2DD4C9 /* Pods_Movie_ApplicationTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 1BE985A62813F0A30001DCAB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 1BE985872813F0A10001DCAB /* Project object */; + proxyType = 1; + remoteGlobalIDString = 1BE9858E2813F0A20001DCAB; + remoteInfo = "Movie-Application"; + }; + 1BE985B02813F0A30001DCAB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 1BE985872813F0A10001DCAB /* Project object */; + proxyType = 1; + remoteGlobalIDString = 1BE9858E2813F0A20001DCAB; + remoteInfo = "Movie-Application"; + }; + 1BF788032821691D004B6D2B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 1BE985872813F0A10001DCAB /* Project object */; + proxyType = 1; + remoteGlobalIDString = 1BF787E92821691B004B6D2B; + remoteInfo = MovieApplicationMVVM; + }; + 1BF7880D2821691D004B6D2B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 1BE985872813F0A10001DCAB /* Project object */; + proxyType = 1; + remoteGlobalIDString = 1BF787E92821691B004B6D2B; + remoteInfo = MovieApplicationMVVM; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 1B026CA428218354006D2BFE /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; + 1B026CA8282184F7006D2BFE /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; }; + 1B026CAA2821858A006D2BFE /* Storyboarded.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storyboarded.swift; sourceTree = ""; }; + 1B026CAF282188C6006D2BFE /* TabBarPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarPage.swift; sourceTree = ""; }; + 1B026CB6282193B8006D2BFE /* TopRatedMoviesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopRatedMoviesViewModel.swift; sourceTree = ""; }; + 1B026CBA282193EB006D2BFE /* TopRatedMoviesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopRatedMoviesViewController.swift; sourceTree = ""; }; + 1B026CCF28219C6A006D2BFE /* MovieCollectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieCollectionViewDelegate.swift; sourceTree = ""; }; + 1B026CD128219EAD006D2BFE /* MovieCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieCollectionViewCell.swift; sourceTree = ""; }; + 1B026CD32821B4A7006D2BFE /* TopRatedMoviesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopRatedMoviesCoordinator.swift; sourceTree = ""; }; + 1B026CD52822BFF9006D2BFE /* MainTabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarController.swift; sourceTree = ""; }; + 1B026CD92822CA0D006D2BFE /* PopularMoviesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopularMoviesViewController.swift; sourceTree = ""; }; + 1B026CDB2822CA1D006D2BFE /* PopularMoviesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopularMoviesViewModel.swift; sourceTree = ""; }; + 1B026CDD2822CBBB006D2BFE /* PopularMoviesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopularMoviesCoordinator.swift; sourceTree = ""; }; + 1B026CE22822CEB5006D2BFE /* WatchlistMoviesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchlistMoviesViewModel.swift; sourceTree = ""; }; + 1B026CE62823C205006D2BFE /* WatchlistMoviesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchlistMoviesViewController.swift; sourceTree = ""; }; + 1B026CE82823C2D5006D2BFE /* WatchlistMoviesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchlistMoviesCoordinator.swift; sourceTree = ""; }; + 1B026CEA2823C425006D2BFE /* TopRatedMovies.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = TopRatedMovies.storyboard; sourceTree = ""; }; + 1B026CEC2823C4B7006D2BFE /* PopularMovies.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PopularMovies.storyboard; sourceTree = ""; }; + 1B026CEE2823C72C006D2BFE /* WatchlistMovies.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = WatchlistMovies.storyboard; sourceTree = ""; }; + 1B026CF02824200D006D2BFE /* MovieDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieDetailsViewController.swift; sourceTree = ""; }; + 1B026CF4282420C9006D2BFE /* MovieInfoContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieInfoContentViewController.swift; sourceTree = ""; }; + 1B026CF828242154006D2BFE /* MovieInfoContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieInfoContentViewModel.swift; sourceTree = ""; }; + 1B026D0128251A60006D2BFE /* TestAppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAppCoordinator.swift; sourceTree = ""; }; + 1B026D0328251C5A006D2BFE /* UnitTestError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitTestError.swift; sourceTree = ""; }; + 1B026D0628256F39006D2BFE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 1B026D08282582A6006D2BFE /* TestTopRatedMoviesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTopRatedMoviesCoordinator.swift; sourceTree = ""; }; + 1B026D0B2825B366006D2BFE /* TestPopularMoviesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestPopularMoviesCoordinator.swift; sourceTree = ""; }; + 1B026D0D2825B4FF006D2BFE /* TestWatchlistMoviesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestWatchlistMoviesCoordinator.swift; sourceTree = ""; }; + 1B15287328196CD9008071CC /* MovieModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MovieModel.xcdatamodel; sourceTree = ""; }; + 1B152875281971BA008071CC /* CoreDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManager.swift; sourceTree = ""; }; + 1B152878281971E5008071CC /* CoreDataProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataProtocol.swift; sourceTree = ""; }; + 1B5B0D4D28202124001136BB /* TestWatchlistMoviesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestWatchlistMoviesView.swift; sourceTree = ""; }; + 1B5B0D4F282024C3001136BB /* TestWatchlistMoviesPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestWatchlistMoviesPresenter.swift; sourceTree = ""; }; + 1B5B0D5128202CF0001136BB /* TestWatchlistMoviesInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestWatchlistMoviesInteractor.swift; sourceTree = ""; }; + 1B5B0D5328202DB3001136BB /* TestWatchlistMoviesRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestWatchlistMoviesRouter.swift; sourceTree = ""; }; + 1B5B0D5528202F22001136BB /* TestTopRatedMoviesRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTopRatedMoviesRouter.swift; sourceTree = ""; }; + 1B5B0D5728202F83001136BB /* TestPopularMoviesRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestPopularMoviesRouter.swift; sourceTree = ""; }; + 1B8C5EA52817F67A0013909D /* ResponseValidatorProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseValidatorProtocol.swift; sourceTree = ""; }; + 1B8C5EA62817F67A0013909D /* ResponseValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseValidator.swift; sourceTree = ""; }; + 1B8C5EA72817F67A0013909D /* RequestManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestManager.swift; sourceTree = ""; }; + 1B8C5EA82817F67A0013909D /* HTTPMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; + 1B8C5EAA2817F67A0013909D /* ResponseLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseLog.swift; sourceTree = ""; }; + 1B8C5EAB2817F67A0013909D /* URLRequestLoggableProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLRequestLoggableProtocol.swift; sourceTree = ""; }; + 1B8C5EAD2817F67A0013909D /* RequestManagerProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestManagerProtocol.swift; sourceTree = ""; }; + 1B8C5EAE2817F67A0013909D /* RequestError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestError.swift; sourceTree = ""; }; + 1B8C5EB7281802270013909D /* MoviesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesService.swift; sourceTree = ""; }; + 1B8C5EBB281804D20013909D /* Movies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Movies.swift; sourceTree = ""; }; + 1B8C5EBD2818069D0013909D /* MoviesGeneres.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesGeneres.swift; sourceTree = ""; }; + 1B8C5EE028182A970013909D /* TestTopRatedMoviesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestTopRatedMoviesView.swift; sourceTree = ""; }; + 1BD0328C281C10EF00139C3B /* MovieDetailsEntity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieDetailsEntity.swift; sourceTree = ""; }; + 1BD0328E281C10EF00139C3B /* MovieDetailsPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieDetailsPresenter.swift; sourceTree = ""; }; + 1BD0328F281C10EF00139C3B /* MovieDetailsPresenterInteractorInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieDetailsPresenterInteractorInterface.swift; sourceTree = ""; }; + 1BD03290281C10EF00139C3B /* MovieDetailsPresenterRouterInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieDetailsPresenterRouterInterface.swift; sourceTree = ""; }; + 1BD03291281C10EF00139C3B /* MovieDetailsPresenterViewInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieDetailsPresenterViewInterface.swift; sourceTree = ""; }; + 1BD03293281C10EF00139C3B /* MovieDetailsViewInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieDetailsViewInterface.swift; sourceTree = ""; }; + 1BD03294281C10EF00139C3B /* MovieDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieDetailsView.swift; sourceTree = ""; }; + 1BD03296281C10EF00139C3B /* MovieDetailsInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieDetailsInteractor.swift; sourceTree = ""; }; + 1BD03297281C10EF00139C3B /* MovieDetailsInteractorInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieDetailsInteractorInterface.swift; sourceTree = ""; }; + 1BD03298281C10EF00139C3B /* MovieDetailsModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieDetailsModule.swift; sourceTree = ""; }; + 1BD0329A281C10EF00139C3B /* MovieDetailsRouterInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieDetailsRouterInterface.swift; sourceTree = ""; }; + 1BD0329B281C10EF00139C3B /* MovieDetailsRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieDetailsRouter.swift; sourceTree = ""; }; + 1BD032AA281C3C6800139C3B /* BottomSheetContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetContainerViewController.swift; sourceTree = ""; }; + 1BD032AD281C488E00139C3B /* MovieDetailsInfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieDetailsInfoViewController.swift; sourceTree = ""; }; + 1BD032BA281C5A8B00139C3B /* MovieInfoContentPresenterInteractorInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieInfoContentPresenterInteractorInterface.swift; sourceTree = ""; }; + 1BD032BB281C5A8B00139C3B /* MovieInfoContentPresenterRouterInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieInfoContentPresenterRouterInterface.swift; sourceTree = ""; }; + 1BD032BC281C5A8B00139C3B /* MovieInfoContentPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieInfoContentPresenter.swift; sourceTree = ""; }; + 1BD032BD281C5A8B00139C3B /* MovieInfoContentPresenterViewInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieInfoContentPresenterViewInterface.swift; sourceTree = ""; }; + 1BD032BF281C5A8B00139C3B /* MovieInfoContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieInfoContentView.swift; sourceTree = ""; }; + 1BD032C0281C5A8B00139C3B /* MovieInfoContentViewInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieInfoContentViewInterface.swift; sourceTree = ""; }; + 1BD032C1281C5A8B00139C3B /* MovieInfoContentModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieInfoContentModule.swift; sourceTree = ""; }; + 1BD032C3281C5A8B00139C3B /* MovieInfoContentInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieInfoContentInteractor.swift; sourceTree = ""; }; + 1BD032C4281C5A8B00139C3B /* MovieInfoContentInteractorInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieInfoContentInteractorInterface.swift; sourceTree = ""; }; + 1BD032C6281C5A8B00139C3B /* MovieInfoContentRouterInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieInfoContentRouterInterface.swift; sourceTree = ""; }; + 1BD032C7281C5A8B00139C3B /* MovieInfoContentRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieInfoContentRouter.swift; sourceTree = ""; }; + 1BD0330D281F0AB700139C3B /* TestTopRatedMoviesPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTopRatedMoviesPresenter.swift; sourceTree = ""; }; + 1BD0330F281F14ED00139C3B /* TestTopRatedMoviesInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTopRatedMoviesInteractor.swift; sourceTree = ""; }; + 1BD03312281F1CF000139C3B /* TestPopularMoviesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestPopularMoviesView.swift; sourceTree = ""; }; + 1BD03314281F1E8C00139C3B /* TestPopularMoviesPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestPopularMoviesPresenter.swift; sourceTree = ""; }; + 1BD03316281F24A000139C3B /* TestPopularMoviesInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestPopularMoviesInteractor.swift; sourceTree = ""; }; + 1BD16F16281AF731003E87B5 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; + 1BD16F1D281AFB7C003E87B5 /* MovieCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieCell.swift; sourceTree = ""; }; + 1BD16F3F281B4162003E87B5 /* MoviesServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesServiceProtocol.swift; sourceTree = ""; }; + 1BD16F41281B426F003E87B5 /* TopRatedMovies.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = TopRatedMovies.storyboard; sourceTree = ""; }; + 1BD16F43281B5169003E87B5 /* PopularMovies.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = PopularMovies.storyboard; sourceTree = ""; }; + 1BD16F66281B5D99003E87B5 /* WatchlistMoviesModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchlistMoviesModule.swift; sourceTree = ""; }; + 1BD16F6A281B5D99003E87B5 /* WatchlistMoviesPresenterRouterInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchlistMoviesPresenterRouterInterface.swift; sourceTree = ""; }; + 1BD16F6B281B5D9A003E87B5 /* WatchlistMoviesPresenterViewInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchlistMoviesPresenterViewInterface.swift; sourceTree = ""; }; + 1BD16F6C281B5D9A003E87B5 /* WatchlistMoviesPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchlistMoviesPresenter.swift; sourceTree = ""; }; + 1BD16F6D281B5D9A003E87B5 /* WatchlistMoviesPresenterInteractorInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchlistMoviesPresenterInteractorInterface.swift; sourceTree = ""; }; + 1BD16F6F281B5D9A003E87B5 /* WatchlistMoviesViewInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchlistMoviesViewInterface.swift; sourceTree = ""; }; + 1BD16F70281B5D9A003E87B5 /* WatchlistMoviesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchlistMoviesView.swift; sourceTree = ""; }; + 1BD16F72281B5D9A003E87B5 /* WatchlistMoviesInteractorInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchlistMoviesInteractorInterface.swift; sourceTree = ""; }; + 1BD16F73281B5D9A003E87B5 /* WatchlistMoviesInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchlistMoviesInteractor.swift; sourceTree = ""; }; + 1BD16F75281B5D9A003E87B5 /* WatchlistMoviesRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchlistMoviesRouter.swift; sourceTree = ""; }; + 1BD16F76281B5D9A003E87B5 /* WatchlistMoviesRouterInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchlistMoviesRouterInterface.swift; sourceTree = ""; }; + 1BD16F83281B5DAC003E87B5 /* WatchlistMovies.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = WatchlistMovies.storyboard; sourceTree = ""; }; + 1BE9858F2813F0A20001DCAB /* Movie-Application.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Movie-Application.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 1BE985922813F0A20001DCAB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 1BE985942813F0A20001DCAB /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 1BE9859B2813F0A20001DCAB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 1BE9859E2813F0A20001DCAB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 1BE985A02813F0A20001DCAB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 1BE985A52813F0A30001DCAB /* Movie-ApplicationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Movie-ApplicationTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 1BE985A92813F0A30001DCAB /* Movie_ApplicationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Movie_ApplicationTests.swift; sourceTree = ""; }; + 1BE985AF2813F0A30001DCAB /* Movie-ApplicationUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Movie-ApplicationUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 1BE985B32813F0A30001DCAB /* Movie_ApplicationUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Movie_ApplicationUITests.swift; sourceTree = ""; }; + 1BE985B52813F0A30001DCAB /* Movie_ApplicationUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Movie_ApplicationUITestsLaunchTests.swift; sourceTree = ""; }; + 1BE985C42813F2430001DCAB /* TopRatedMoviesModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopRatedMoviesModule.swift; sourceTree = ""; }; + 1BE985C82813F2430001DCAB /* TopRatedMoviesPresenterInteractorInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopRatedMoviesPresenterInteractorInterface.swift; sourceTree = ""; }; + 1BE985C92813F2430001DCAB /* TopRatedMoviesPresenterRouterInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopRatedMoviesPresenterRouterInterface.swift; sourceTree = ""; }; + 1BE985CA2813F2430001DCAB /* TopRatedMoviesPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopRatedMoviesPresenter.swift; sourceTree = ""; }; + 1BE985CB2813F2430001DCAB /* TopRatedMoviesPresenterViewInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopRatedMoviesPresenterViewInterface.swift; sourceTree = ""; }; + 1BE985CD2813F2430001DCAB /* TopRatedMoviesViewInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopRatedMoviesViewInterface.swift; sourceTree = ""; }; + 1BE985CE2813F2430001DCAB /* TopRatedMoviesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopRatedMoviesView.swift; sourceTree = ""; }; + 1BE985D02813F2430001DCAB /* TopRatedMoviesInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopRatedMoviesInteractor.swift; sourceTree = ""; }; + 1BE985D12813F2430001DCAB /* TopRatedMoviesInteractorInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopRatedMoviesInteractorInterface.swift; sourceTree = ""; }; + 1BE985D32813F2430001DCAB /* TopRatedMoviesRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopRatedMoviesRouter.swift; sourceTree = ""; }; + 1BE985D42813F2430001DCAB /* TopRatedMoviesRouterInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopRatedMoviesRouterInterface.swift; sourceTree = ""; }; + 1BE985E12813F2500001DCAB /* Interfaces.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Interfaces.swift; sourceTree = ""; }; + 1BE9862B28140D150001DCAB /* PopularMoviesPresenterRouterInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopularMoviesPresenterRouterInterface.swift; sourceTree = ""; }; + 1BE9862C28140D150001DCAB /* PopularMoviesPresenterViewInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopularMoviesPresenterViewInterface.swift; sourceTree = ""; }; + 1BE9862D28140D150001DCAB /* PopularMoviesPresenterInteractorInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopularMoviesPresenterInteractorInterface.swift; sourceTree = ""; }; + 1BE9862E28140D150001DCAB /* PopularMoviesPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopularMoviesPresenter.swift; sourceTree = ""; }; + 1BE9863028140D150001DCAB /* PopularMoviesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopularMoviesView.swift; sourceTree = ""; }; + 1BE9863128140D150001DCAB /* PopularMoviesViewInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopularMoviesViewInterface.swift; sourceTree = ""; }; + 1BE9863228140D150001DCAB /* PopularMoviesModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopularMoviesModule.swift; sourceTree = ""; }; + 1BE9863428140D150001DCAB /* PopularMoviesInteractorInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopularMoviesInteractorInterface.swift; sourceTree = ""; }; + 1BE9863528140D150001DCAB /* PopularMoviesInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopularMoviesInteractor.swift; sourceTree = ""; }; + 1BE9863728140D150001DCAB /* PopularMoviesRouterInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopularMoviesRouterInterface.swift; sourceTree = ""; }; + 1BE9863828140D150001DCAB /* PopularMoviesRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopularMoviesRouter.swift; sourceTree = ""; }; + 1BE9864E2815D49A0001DCAB /* TabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarViewController.swift; sourceTree = ""; }; + 1BE986512815D5CF0001DCAB /* TestTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTabBarViewController.swift; sourceTree = ""; }; + 1BF787D028213D9E004B6D2B /* Movies.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Movies.json; sourceTree = ""; }; + 1BF787D228213DDC004B6D2B /* URLSessionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionMock.swift; sourceTree = ""; }; + 1BF787D428213E34004B6D2B /* URLSessionDataTaskMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDataTaskMock.swift; sourceTree = ""; }; + 1BF787D628213E87004B6D2B /* RequestManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestManagerMock.swift; sourceTree = ""; }; + 1BF787D828213EFB004B6D2B /* MockResponseValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockResponseValidator.swift; sourceTree = ""; }; + 1BF787DA28213F32004B6D2B /* UnitTestError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitTestError.swift; sourceTree = ""; }; + 1BF787DE28213FCB004B6D2B /* TestMoviesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestMoviesService.swift; sourceTree = ""; }; + 1BF787E428216193004B6D2B /* Movie.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Movie.json; sourceTree = ""; }; + 1BF787EA2821691B004B6D2B /* MovieApplicationMVVM.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MovieApplicationMVVM.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 1BF787EC2821691B004B6D2B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 1BF787F82821691D004B6D2B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 1BF787FD2821691D004B6D2B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 1BF788022821691D004B6D2B /* MovieApplicationMVVMTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MovieApplicationMVVMTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 1BF788062821691D004B6D2B /* MovieApplicationMVVMTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieApplicationMVVMTests.swift; sourceTree = ""; }; + 1BF7880C2821691D004B6D2B /* MovieApplicationMVVMUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MovieApplicationMVVMUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 1BF788102821691D004B6D2B /* MovieApplicationMVVMUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieApplicationMVVMUITests.swift; sourceTree = ""; }; + 1BF788122821691D004B6D2B /* MovieApplicationMVVMUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieApplicationMVVMUITestsLaunchTests.swift; sourceTree = ""; }; + 3A782884A39CB8B6B643183D /* Pods-Movie-Application-Movie-ApplicationUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Movie-Application-Movie-ApplicationUITests.release.xcconfig"; path = "Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests.release.xcconfig"; sourceTree = ""; }; + 80E20878F6E954381A53AFF4 /* Pods_Movie_Application_Movie_ApplicationUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Movie_Application_Movie_ApplicationUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 82D2E5DD96CA7A69AA21161C /* Pods-Movie-Application.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Movie-Application.debug.xcconfig"; path = "Target Support Files/Pods-Movie-Application/Pods-Movie-Application.debug.xcconfig"; sourceTree = ""; }; + 90E49B8E9B0BABB57B2DD4C9 /* Pods_Movie_ApplicationTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Movie_ApplicationTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9C3DEA32D4C3AB8BAE6A700C /* Pods-Movie-Application.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Movie-Application.release.xcconfig"; path = "Target Support Files/Pods-Movie-Application/Pods-Movie-Application.release.xcconfig"; sourceTree = ""; }; + B6CBAB8843523803C510244D /* Pods_Movie_Application.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Movie_Application.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B8F4B8F4F346FAC3155BD932 /* Pods-Movie-ApplicationTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Movie-ApplicationTests.release.xcconfig"; path = "Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests.release.xcconfig"; sourceTree = ""; }; + BF201D811AE4556FBC0BA0B9 /* Pods-Movie-Application-Movie-ApplicationUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Movie-Application-Movie-ApplicationUITests.debug.xcconfig"; path = "Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests.debug.xcconfig"; sourceTree = ""; }; + E0AA62B85B7167696A146DEE /* Pods-Movie-ApplicationTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Movie-ApplicationTests.debug.xcconfig"; path = "Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 1BE9858C2813F0A20001DCAB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 662B1788192ABA8C653B70A8 /* Pods_Movie_Application.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1BE985A22813F0A30001DCAB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E218ED5C6277C8ACD57255B0 /* Pods_Movie_ApplicationTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1BE985AC2813F0A30001DCAB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2AED66104B329C5D6F3C5FED /* Pods_Movie_Application_Movie_ApplicationUITests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1BF787E72821691B004B6D2B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1BF787FF2821691D004B6D2B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1BF788092821691D004B6D2B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1B026CA6282184D0006D2BFE /* UIKitUtilities */ = { + isa = PBXGroup; + children = ( + 1B026CCE28219C47006D2BFE /* UICollectionViewDelegate */, + 1B026CA7282184E4006D2BFE /* Coordinator */, + ); + path = UIKitUtilities; + sourceTree = ""; + }; + 1B026CA7282184E4006D2BFE /* Coordinator */ = { + isa = PBXGroup; + children = ( + 1B026CA8282184F7006D2BFE /* Coordinator.swift */, + 1B026CAA2821858A006D2BFE /* Storyboarded.swift */, + ); + path = Coordinator; + sourceTree = ""; + }; + 1B026CAE28218880006D2BFE /* TabBar */ = { + isa = PBXGroup; + children = ( + 1B026CD52822BFF9006D2BFE /* MainTabBarController.swift */, + 1B026CAF282188C6006D2BFE /* TabBarPage.swift */, + ); + path = TabBar; + sourceTree = ""; + }; + 1B026CB128219294006D2BFE /* TopRatedMovies */ = { + isa = PBXGroup; + children = ( + 1B026CD32821B4A7006D2BFE /* TopRatedMoviesCoordinator.swift */, + 1B026CB9282193CC006D2BFE /* View */, + 1B026CB8282193C4006D2BFE /* ViewModel */, + ); + path = TopRatedMovies; + sourceTree = ""; + }; + 1B026CB3282192AF006D2BFE /* PopularMovies */ = { + isa = PBXGroup; + children = ( + 1B026CDD2822CBBB006D2BFE /* PopularMoviesCoordinator.swift */, + 1B026CD82822C9F2006D2BFE /* ViewModel */, + 1B026CD72822C9EB006D2BFE /* View */, + ); + path = PopularMovies; + sourceTree = ""; + }; + 1B026CB4282192B8006D2BFE /* WatchlistMovies */ = { + isa = PBXGroup; + children = ( + 1B026CE82823C2D5006D2BFE /* WatchlistMoviesCoordinator.swift */, + 1B026CE12822CEA3006D2BFE /* ViewModel */, + 1B026CE02822CE9C006D2BFE /* View */, + ); + path = WatchlistMovies; + sourceTree = ""; + }; + 1B026CB5282192C9006D2BFE /* MovieDetails */ = { + isa = PBXGroup; + children = ( + 1B026CDF2822CBE2006D2BFE /* View */, + ); + path = MovieDetails; + sourceTree = ""; + }; + 1B026CB8282193C4006D2BFE /* ViewModel */ = { + isa = PBXGroup; + children = ( + 1B026CB6282193B8006D2BFE /* TopRatedMoviesViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 1B026CB9282193CC006D2BFE /* View */ = { + isa = PBXGroup; + children = ( + 1B026CBA282193EB006D2BFE /* TopRatedMoviesViewController.swift */, + 1B026CEA2823C425006D2BFE /* TopRatedMovies.storyboard */, + ); + path = View; + sourceTree = ""; + }; + 1B026CCE28219C47006D2BFE /* UICollectionViewDelegate */ = { + isa = PBXGroup; + children = ( + 1B026CCF28219C6A006D2BFE /* MovieCollectionViewDelegate.swift */, + 1B026CD128219EAD006D2BFE /* MovieCollectionViewCell.swift */, + ); + path = UICollectionViewDelegate; + sourceTree = ""; + }; + 1B026CD72822C9EB006D2BFE /* View */ = { + isa = PBXGroup; + children = ( + 1B026CD92822CA0D006D2BFE /* PopularMoviesViewController.swift */, + 1B026CEC2823C4B7006D2BFE /* PopularMovies.storyboard */, + ); + path = View; + sourceTree = ""; + }; + 1B026CD82822C9F2006D2BFE /* ViewModel */ = { + isa = PBXGroup; + children = ( + 1B026CDB2822CA1D006D2BFE /* PopularMoviesViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 1B026CDF2822CBE2006D2BFE /* View */ = { + isa = PBXGroup; + children = ( + 1B026CF02824200D006D2BFE /* MovieDetailsViewController.swift */, + 1B026CF62824211C006D2BFE /* MovieInfoContent */, + ); + path = View; + sourceTree = ""; + }; + 1B026CE02822CE9C006D2BFE /* View */ = { + isa = PBXGroup; + children = ( + 1B026CEE2823C72C006D2BFE /* WatchlistMovies.storyboard */, + 1B026CE62823C205006D2BFE /* WatchlistMoviesViewController.swift */, + ); + path = View; + sourceTree = ""; + }; + 1B026CE12822CEA3006D2BFE /* ViewModel */ = { + isa = PBXGroup; + children = ( + 1B026CE22822CEB5006D2BFE /* WatchlistMoviesViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 1B026CF62824211C006D2BFE /* MovieInfoContent */ = { + isa = PBXGroup; + children = ( + 1B026CF728242139006D2BFE /* View */, + 1B026CFA2824215A006D2BFE /* ViewModel */, + ); + path = MovieInfoContent; + sourceTree = ""; + }; + 1B026CF728242139006D2BFE /* View */ = { + isa = PBXGroup; + children = ( + 1B026CF4282420C9006D2BFE /* MovieInfoContentViewController.swift */, + ); + path = View; + sourceTree = ""; + }; + 1B026CFA2824215A006D2BFE /* ViewModel */ = { + isa = PBXGroup; + children = ( + 1B026CF828242154006D2BFE /* MovieInfoContentViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 1B026D0A2825B33E006D2BFE /* CoordinatorTests */ = { + isa = PBXGroup; + children = ( + 1B026D0128251A60006D2BFE /* TestAppCoordinator.swift */, + 1B026D08282582A6006D2BFE /* TestTopRatedMoviesCoordinator.swift */, + 1B026D0B2825B366006D2BFE /* TestPopularMoviesCoordinator.swift */, + 1B026D0D2825B4FF006D2BFE /* TestWatchlistMoviesCoordinator.swift */, + ); + path = CoordinatorTests; + sourceTree = ""; + }; + 1B152877281971C1008071CC /* CoreData */ = { + isa = PBXGroup; + children = ( + 1B15287228196CD9008071CC /* FavoriteMovieModel.xcdatamodeld */, + 1B152875281971BA008071CC /* CoreDataManager.swift */, + 1B152878281971E5008071CC /* CoreDataProtocol.swift */, + ); + path = CoreData; + sourceTree = ""; + }; + 1B5B0D4C28201F6F001136BB /* WatchlistMoviesTests */ = { + isa = PBXGroup; + children = ( + 1B5B0D4D28202124001136BB /* TestWatchlistMoviesView.swift */, + 1B5B0D4F282024C3001136BB /* TestWatchlistMoviesPresenter.swift */, + 1B5B0D5128202CF0001136BB /* TestWatchlistMoviesInteractor.swift */, + 1B5B0D5328202DB3001136BB /* TestWatchlistMoviesRouter.swift */, + ); + path = WatchlistMoviesTests; + sourceTree = ""; + }; + 1B8C5EA22817F6450013909D /* Utilities */ = { + isa = PBXGroup; + children = ( + 1B152877281971C1008071CC /* CoreData */, + 1B8C5EA32817F67A0013909D /* Network */, + 1B8C5EB9281803370013909D /* Service */, + 1B8C5EBA281804BD0013909D /* Model */, + 1BD16F16281AF731003E87B5 /* Extensions.swift */, + 1BD16F1D281AFB7C003E87B5 /* MovieCell.swift */, + ); + path = Utilities; + sourceTree = ""; + }; + 1B8C5EA32817F67A0013909D /* Network */ = { + isa = PBXGroup; + children = ( + 1B8C5EA72817F67A0013909D /* RequestManager.swift */, + 1B8C5EA82817F67A0013909D /* HTTPMethod.swift */, + 1B8C5EAE2817F67A0013909D /* RequestError.swift */, + 1B8C5EA42817F67A0013909D /* ResponseValidator */, + 1B8C5EA92817F67A0013909D /* APILogger */, + 1B8C5EAC2817F67A0013909D /* Protocols */, + ); + path = Network; + sourceTree = ""; + }; + 1B8C5EA42817F67A0013909D /* ResponseValidator */ = { + isa = PBXGroup; + children = ( + 1B8C5EA52817F67A0013909D /* ResponseValidatorProtocol.swift */, + 1B8C5EA62817F67A0013909D /* ResponseValidator.swift */, + ); + path = ResponseValidator; + sourceTree = ""; + }; + 1B8C5EA92817F67A0013909D /* APILogger */ = { + isa = PBXGroup; + children = ( + 1B8C5EAA2817F67A0013909D /* ResponseLog.swift */, + 1B8C5EAB2817F67A0013909D /* URLRequestLoggableProtocol.swift */, + ); + path = APILogger; + sourceTree = ""; + }; + 1B8C5EAC2817F67A0013909D /* Protocols */ = { + isa = PBXGroup; + children = ( + 1B8C5EAD2817F67A0013909D /* RequestManagerProtocol.swift */, + ); + path = Protocols; + sourceTree = ""; + }; + 1B8C5EB9281803370013909D /* Service */ = { + isa = PBXGroup; + children = ( + 1B8C5EB7281802270013909D /* MoviesService.swift */, + 1BD16F3F281B4162003E87B5 /* MoviesServiceProtocol.swift */, + ); + path = Service; + sourceTree = ""; + }; + 1B8C5EBA281804BD0013909D /* Model */ = { + isa = PBXGroup; + children = ( + 1B8C5EBB281804D20013909D /* Movies.swift */, + 1B8C5EBD2818069D0013909D /* MoviesGeneres.swift */, + ); + path = Model; + sourceTree = ""; + }; + 1B8C5EDD281824D00013909D /* UtilitiesTest */ = { + isa = PBXGroup; + children = ( + 1BF787DE28213FCB004B6D2B /* TestMoviesService.swift */, + 1BF787CF28213D76004B6D2B /* Mocks */, + 1BF787DA28213F32004B6D2B /* UnitTestError.swift */, + ); + path = UtilitiesTest; + sourceTree = ""; + }; + 1B8C5EE228182A9E0013909D /* TopRatedMoviesTests */ = { + isa = PBXGroup; + children = ( + 1B8C5EE028182A970013909D /* TestTopRatedMoviesView.swift */, + 1BD0330D281F0AB700139C3B /* TestTopRatedMoviesPresenter.swift */, + 1BD0330F281F14ED00139C3B /* TestTopRatedMoviesInteractor.swift */, + 1B5B0D5528202F22001136BB /* TestTopRatedMoviesRouter.swift */, + ); + path = TopRatedMoviesTests; + sourceTree = ""; + }; + 1BD0328A281C10EF00139C3B /* MovieDetails */ = { + isa = PBXGroup; + children = ( + 1BD03298281C10EF00139C3B /* MovieDetailsModule.swift */, + 1BD03299281C10EF00139C3B /* Router */, + 1BD03295281C10EF00139C3B /* Interactor */, + 1BD03292281C10EF00139C3B /* View */, + 1BD0328D281C10EF00139C3B /* Presenter */, + 1BD0328B281C10EF00139C3B /* Entity */, + ); + path = MovieDetails; + sourceTree = ""; + }; + 1BD0328B281C10EF00139C3B /* Entity */ = { + isa = PBXGroup; + children = ( + 1BD0328C281C10EF00139C3B /* MovieDetailsEntity.swift */, + ); + path = Entity; + sourceTree = ""; + }; + 1BD0328D281C10EF00139C3B /* Presenter */ = { + isa = PBXGroup; + children = ( + 1BD0328E281C10EF00139C3B /* MovieDetailsPresenter.swift */, + 1BD0328F281C10EF00139C3B /* MovieDetailsPresenterInteractorInterface.swift */, + 1BD03290281C10EF00139C3B /* MovieDetailsPresenterRouterInterface.swift */, + 1BD03291281C10EF00139C3B /* MovieDetailsPresenterViewInterface.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + 1BD03292281C10EF00139C3B /* View */ = { + isa = PBXGroup; + children = ( + 1BD03293281C10EF00139C3B /* MovieDetailsViewInterface.swift */, + 1BD03294281C10EF00139C3B /* MovieDetailsView.swift */, + 1BD032AC281C3CFE00139C3B /* Components */, + ); + path = View; + sourceTree = ""; + }; + 1BD03295281C10EF00139C3B /* Interactor */ = { + isa = PBXGroup; + children = ( + 1BD03296281C10EF00139C3B /* MovieDetailsInteractor.swift */, + 1BD03297281C10EF00139C3B /* MovieDetailsInteractorInterface.swift */, + ); + path = Interactor; + sourceTree = ""; + }; + 1BD03299281C10EF00139C3B /* Router */ = { + isa = PBXGroup; + children = ( + 1BD0329A281C10EF00139C3B /* MovieDetailsRouterInterface.swift */, + 1BD0329B281C10EF00139C3B /* MovieDetailsRouter.swift */, + ); + path = Router; + sourceTree = ""; + }; + 1BD032AC281C3CFE00139C3B /* Components */ = { + isa = PBXGroup; + children = ( + 1BD032B6281C5A8B00139C3B /* MovieInfoContent */, + 1BD032AD281C488E00139C3B /* MovieDetailsInfoViewController.swift */, + 1BD032AA281C3C6800139C3B /* BottomSheetContainerViewController.swift */, + ); + path = Components; + sourceTree = ""; + }; + 1BD032B6281C5A8B00139C3B /* MovieInfoContent */ = { + isa = PBXGroup; + children = ( + 1BD032C1281C5A8B00139C3B /* MovieInfoContentModule.swift */, + 1BD032B9281C5A8B00139C3B /* Presenter */, + 1BD032BE281C5A8B00139C3B /* View */, + 1BD032C2281C5A8B00139C3B /* Interactor */, + 1BD032C5281C5A8B00139C3B /* Router */, + ); + path = MovieInfoContent; + sourceTree = ""; + }; + 1BD032B9281C5A8B00139C3B /* Presenter */ = { + isa = PBXGroup; + children = ( + 1BD032BA281C5A8B00139C3B /* MovieInfoContentPresenterInteractorInterface.swift */, + 1BD032BB281C5A8B00139C3B /* MovieInfoContentPresenterRouterInterface.swift */, + 1BD032BC281C5A8B00139C3B /* MovieInfoContentPresenter.swift */, + 1BD032BD281C5A8B00139C3B /* MovieInfoContentPresenterViewInterface.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + 1BD032BE281C5A8B00139C3B /* View */ = { + isa = PBXGroup; + children = ( + 1BD032BF281C5A8B00139C3B /* MovieInfoContentView.swift */, + 1BD032C0281C5A8B00139C3B /* MovieInfoContentViewInterface.swift */, + ); + path = View; + sourceTree = ""; + }; + 1BD032C2281C5A8B00139C3B /* Interactor */ = { + isa = PBXGroup; + children = ( + 1BD032C3281C5A8B00139C3B /* MovieInfoContentInteractor.swift */, + 1BD032C4281C5A8B00139C3B /* MovieInfoContentInteractorInterface.swift */, + ); + path = Interactor; + sourceTree = ""; + }; + 1BD032C5281C5A8B00139C3B /* Router */ = { + isa = PBXGroup; + children = ( + 1BD032C6281C5A8B00139C3B /* MovieInfoContentRouterInterface.swift */, + 1BD032C7281C5A8B00139C3B /* MovieInfoContentRouter.swift */, + ); + path = Router; + sourceTree = ""; + }; + 1BD03311281F1B1200139C3B /* PopularMoviesTests */ = { + isa = PBXGroup; + children = ( + 1BD03312281F1CF000139C3B /* TestPopularMoviesView.swift */, + 1BD03314281F1E8C00139C3B /* TestPopularMoviesPresenter.swift */, + 1BD03316281F24A000139C3B /* TestPopularMoviesInteractor.swift */, + 1B5B0D5728202F83001136BB /* TestPopularMoviesRouter.swift */, + ); + path = PopularMoviesTests; + sourceTree = ""; + }; + 1BD16F65281B5D99003E87B5 /* WatchlistMovies */ = { + isa = PBXGroup; + children = ( + 1BD16F66281B5D99003E87B5 /* WatchlistMoviesModule.swift */, + 1BD16F69281B5D99003E87B5 /* Presenter */, + 1BD16F6E281B5D9A003E87B5 /* View */, + 1BD16F71281B5D9A003E87B5 /* Interactor */, + 1BD16F74281B5D9A003E87B5 /* Router */, + ); + path = WatchlistMovies; + sourceTree = ""; + }; + 1BD16F69281B5D99003E87B5 /* Presenter */ = { + isa = PBXGroup; + children = ( + 1BD16F6A281B5D99003E87B5 /* WatchlistMoviesPresenterRouterInterface.swift */, + 1BD16F6B281B5D9A003E87B5 /* WatchlistMoviesPresenterViewInterface.swift */, + 1BD16F6C281B5D9A003E87B5 /* WatchlistMoviesPresenter.swift */, + 1BD16F6D281B5D9A003E87B5 /* WatchlistMoviesPresenterInteractorInterface.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + 1BD16F6E281B5D9A003E87B5 /* View */ = { + isa = PBXGroup; + children = ( + 1BD16F6F281B5D9A003E87B5 /* WatchlistMoviesViewInterface.swift */, + 1BD16F70281B5D9A003E87B5 /* WatchlistMoviesView.swift */, + 1BD16F83281B5DAC003E87B5 /* WatchlistMovies.storyboard */, + ); + path = View; + sourceTree = ""; + }; + 1BD16F71281B5D9A003E87B5 /* Interactor */ = { + isa = PBXGroup; + children = ( + 1BD16F72281B5D9A003E87B5 /* WatchlistMoviesInteractorInterface.swift */, + 1BD16F73281B5D9A003E87B5 /* WatchlistMoviesInteractor.swift */, + ); + path = Interactor; + sourceTree = ""; + }; + 1BD16F74281B5D9A003E87B5 /* Router */ = { + isa = PBXGroup; + children = ( + 1BD16F75281B5D9A003E87B5 /* WatchlistMoviesRouter.swift */, + 1BD16F76281B5D9A003E87B5 /* WatchlistMoviesRouterInterface.swift */, + ); + path = Router; + sourceTree = ""; + }; + 1BE985862813F0A10001DCAB = { + isa = PBXGroup; + children = ( + 1BE985912813F0A20001DCAB /* Movie-Application */, + 1BE985A82813F0A30001DCAB /* Movie-ApplicationTests */, + 1BE985B22813F0A30001DCAB /* Movie-ApplicationUITests */, + 1BF787EB2821691B004B6D2B /* MovieApplicationMVVM */, + 1BF788052821691D004B6D2B /* MovieApplicationMVVMTests */, + 1BF7880F2821691D004B6D2B /* MovieApplicationMVVMUITests */, + 1BE985902813F0A20001DCAB /* Products */, + A9B4B8167CDA3F6E56D481F9 /* Pods */, + E4169B655B537A50AEA219BE /* Frameworks */, + ); + sourceTree = ""; + }; + 1BE985902813F0A20001DCAB /* Products */ = { + isa = PBXGroup; + children = ( + 1BE9858F2813F0A20001DCAB /* Movie-Application.app */, + 1BE985A52813F0A30001DCAB /* Movie-ApplicationTests.xctest */, + 1BE985AF2813F0A30001DCAB /* Movie-ApplicationUITests.xctest */, + 1BF787EA2821691B004B6D2B /* MovieApplicationMVVM.app */, + 1BF788022821691D004B6D2B /* MovieApplicationMVVMTests.xctest */, + 1BF7880C2821691D004B6D2B /* MovieApplicationMVVMUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 1BE985912813F0A20001DCAB /* Movie-Application */ = { + isa = PBXGroup; + children = ( + 1BE98608281404A30001DCAB /* App */, + 1B8C5EA22817F6450013909D /* Utilities */, + 1BE985C22813F2350001DCAB /* Modules */, + 1BE98607281404650001DCAB /* Supproting Files */, + ); + path = "Movie-Application"; + sourceTree = ""; + }; + 1BE985A82813F0A30001DCAB /* Movie-ApplicationTests */ = { + isa = PBXGroup; + children = ( + 1B5B0D4C28201F6F001136BB /* WatchlistMoviesTests */, + 1BD03311281F1B1200139C3B /* PopularMoviesTests */, + 1B8C5EE228182A9E0013909D /* TopRatedMoviesTests */, + 1B8C5EDD281824D00013909D /* UtilitiesTest */, + 1BE986502815D5AC0001DCAB /* TabBarViewControllerTest */, + 1BE985A92813F0A30001DCAB /* Movie_ApplicationTests.swift */, + ); + path = "Movie-ApplicationTests"; + sourceTree = ""; + }; + 1BE985B22813F0A30001DCAB /* Movie-ApplicationUITests */ = { + isa = PBXGroup; + children = ( + 1BE985B32813F0A30001DCAB /* Movie_ApplicationUITests.swift */, + 1BE985B52813F0A30001DCAB /* Movie_ApplicationUITestsLaunchTests.swift */, + ); + path = "Movie-ApplicationUITests"; + sourceTree = ""; + }; + 1BE985C22813F2350001DCAB /* Modules */ = { + isa = PBXGroup; + children = ( + 1BE9862728140D150001DCAB /* PopularMovies */, + 1BE985C32813F2430001DCAB /* TopRatedMovies */, + 1BD16F65281B5D99003E87B5 /* WatchlistMovies */, + 1BD0328A281C10EF00139C3B /* MovieDetails */, + ); + path = Modules; + sourceTree = ""; + }; + 1BE985C32813F2430001DCAB /* TopRatedMovies */ = { + isa = PBXGroup; + children = ( + 1BE985C42813F2430001DCAB /* TopRatedMoviesModule.swift */, + 1BE985C72813F2430001DCAB /* Presenter */, + 1BE985CC2813F2430001DCAB /* View */, + 1BE985CF2813F2430001DCAB /* Interactor */, + 1BE985D22813F2430001DCAB /* Router */, + ); + path = TopRatedMovies; + sourceTree = ""; + }; + 1BE985C72813F2430001DCAB /* Presenter */ = { + isa = PBXGroup; + children = ( + 1BE985C82813F2430001DCAB /* TopRatedMoviesPresenterInteractorInterface.swift */, + 1BE985C92813F2430001DCAB /* TopRatedMoviesPresenterRouterInterface.swift */, + 1BE985CA2813F2430001DCAB /* TopRatedMoviesPresenter.swift */, + 1BE985CB2813F2430001DCAB /* TopRatedMoviesPresenterViewInterface.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + 1BE985CC2813F2430001DCAB /* View */ = { + isa = PBXGroup; + children = ( + 1BE985CD2813F2430001DCAB /* TopRatedMoviesViewInterface.swift */, + 1BE985CE2813F2430001DCAB /* TopRatedMoviesView.swift */, + 1BD16F41281B426F003E87B5 /* TopRatedMovies.storyboard */, + ); + path = View; + sourceTree = ""; + }; + 1BE985CF2813F2430001DCAB /* Interactor */ = { + isa = PBXGroup; + children = ( + 1BE985D02813F2430001DCAB /* TopRatedMoviesInteractor.swift */, + 1BE985D12813F2430001DCAB /* TopRatedMoviesInteractorInterface.swift */, + ); + path = Interactor; + sourceTree = ""; + }; + 1BE985D22813F2430001DCAB /* Router */ = { + isa = PBXGroup; + children = ( + 1BE985D32813F2430001DCAB /* TopRatedMoviesRouter.swift */, + 1BE985D42813F2430001DCAB /* TopRatedMoviesRouterInterface.swift */, + ); + path = Router; + sourceTree = ""; + }; + 1BE98607281404650001DCAB /* Supproting Files */ = { + isa = PBXGroup; + children = ( + 1BE9859B2813F0A20001DCAB /* Assets.xcassets */, + 1BE9859D2813F0A20001DCAB /* LaunchScreen.storyboard */, + 1BE985A02813F0A20001DCAB /* Info.plist */, + ); + path = "Supproting Files"; + sourceTree = ""; + }; + 1BE98608281404A30001DCAB /* App */ = { + isa = PBXGroup; + children = ( + 1BE985E12813F2500001DCAB /* Interfaces.swift */, + 1BE985922813F0A20001DCAB /* AppDelegate.swift */, + 1BE985942813F0A20001DCAB /* SceneDelegate.swift */, + 1BE9864E2815D49A0001DCAB /* TabBarViewController.swift */, + ); + path = App; + sourceTree = ""; + }; + 1BE9862728140D150001DCAB /* PopularMovies */ = { + isa = PBXGroup; + children = ( + 1BE9863228140D150001DCAB /* PopularMoviesModule.swift */, + 1BE9862A28140D150001DCAB /* Presenter */, + 1BE9862F28140D150001DCAB /* View */, + 1BE9863328140D150001DCAB /* Interactor */, + 1BE9863628140D150001DCAB /* Router */, + ); + path = PopularMovies; + sourceTree = ""; + }; + 1BE9862A28140D150001DCAB /* Presenter */ = { + isa = PBXGroup; + children = ( + 1BE9862B28140D150001DCAB /* PopularMoviesPresenterRouterInterface.swift */, + 1BE9862C28140D150001DCAB /* PopularMoviesPresenterViewInterface.swift */, + 1BE9862D28140D150001DCAB /* PopularMoviesPresenterInteractorInterface.swift */, + 1BE9862E28140D150001DCAB /* PopularMoviesPresenter.swift */, + ); + path = Presenter; + sourceTree = ""; + }; + 1BE9862F28140D150001DCAB /* View */ = { + isa = PBXGroup; + children = ( + 1BE9863028140D150001DCAB /* PopularMoviesView.swift */, + 1BE9863128140D150001DCAB /* PopularMoviesViewInterface.swift */, + 1BD16F43281B5169003E87B5 /* PopularMovies.storyboard */, + ); + path = View; + sourceTree = ""; + }; + 1BE9863328140D150001DCAB /* Interactor */ = { + isa = PBXGroup; + children = ( + 1BE9863428140D150001DCAB /* PopularMoviesInteractorInterface.swift */, + 1BE9863528140D150001DCAB /* PopularMoviesInteractor.swift */, + ); + path = Interactor; + sourceTree = ""; + }; + 1BE9863628140D150001DCAB /* Router */ = { + isa = PBXGroup; + children = ( + 1BE9863728140D150001DCAB /* PopularMoviesRouterInterface.swift */, + 1BE9863828140D150001DCAB /* PopularMoviesRouter.swift */, + ); + path = Router; + sourceTree = ""; + }; + 1BE986502815D5AC0001DCAB /* TabBarViewControllerTest */ = { + isa = PBXGroup; + children = ( + 1BE986512815D5CF0001DCAB /* TestTabBarViewController.swift */, + ); + path = TabBarViewControllerTest; + sourceTree = ""; + }; + 1BF787CF28213D76004B6D2B /* Mocks */ = { + isa = PBXGroup; + children = ( + 1BF787E428216193004B6D2B /* Movie.json */, + 1BF787D028213D9E004B6D2B /* Movies.json */, + 1BF787D228213DDC004B6D2B /* URLSessionMock.swift */, + 1BF787D428213E34004B6D2B /* URLSessionDataTaskMock.swift */, + 1BF787D628213E87004B6D2B /* RequestManagerMock.swift */, + 1BF787D828213EFB004B6D2B /* MockResponseValidator.swift */, + ); + path = Mocks; + sourceTree = ""; + }; + 1BF787EB2821691B004B6D2B /* MovieApplicationMVVM */ = { + isa = PBXGroup; + children = ( + 1BF7881E282182F2004B6D2B /* App */, + 1B026CA6282184D0006D2BFE /* UIKitUtilities */, + 1B026CB3282192AF006D2BFE /* PopularMovies */, + 1B026CB5282192C9006D2BFE /* MovieDetails */, + 1B026CB4282192B8006D2BFE /* WatchlistMovies */, + 1B026CB128219294006D2BFE /* TopRatedMovies */, + 1BF7881D28218251004B6D2B /* Supporting Files */, + ); + path = MovieApplicationMVVM; + sourceTree = ""; + }; + 1BF788052821691D004B6D2B /* MovieApplicationMVVMTests */ = { + isa = PBXGroup; + children = ( + 1BF788062821691D004B6D2B /* MovieApplicationMVVMTests.swift */, + 1B026D0328251C5A006D2BFE /* UnitTestError.swift */, + 1B026D0A2825B33E006D2BFE /* CoordinatorTests */, + ); + path = MovieApplicationMVVMTests; + sourceTree = ""; + }; + 1BF7880F2821691D004B6D2B /* MovieApplicationMVVMUITests */ = { + isa = PBXGroup; + children = ( + 1BF788102821691D004B6D2B /* MovieApplicationMVVMUITests.swift */, + 1BF788122821691D004B6D2B /* MovieApplicationMVVMUITestsLaunchTests.swift */, + ); + path = MovieApplicationMVVMUITests; + sourceTree = ""; + }; + 1BF7881D28218251004B6D2B /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 1B026D0528256F39006D2BFE /* LaunchScreen.storyboard */, + 1BF787F82821691D004B6D2B /* Assets.xcassets */, + 1BF787FD2821691D004B6D2B /* Info.plist */, + ); + path = "Supporting Files"; + sourceTree = ""; + }; + 1BF7881E282182F2004B6D2B /* App */ = { + isa = PBXGroup; + children = ( + 1BF787EC2821691B004B6D2B /* AppDelegate.swift */, + 1B026CA428218354006D2BFE /* AppCoordinator.swift */, + 1B026CAE28218880006D2BFE /* TabBar */, + ); + path = App; + sourceTree = ""; + }; + A9B4B8167CDA3F6E56D481F9 /* Pods */ = { + isa = PBXGroup; + children = ( + 82D2E5DD96CA7A69AA21161C /* Pods-Movie-Application.debug.xcconfig */, + 9C3DEA32D4C3AB8BAE6A700C /* Pods-Movie-Application.release.xcconfig */, + BF201D811AE4556FBC0BA0B9 /* Pods-Movie-Application-Movie-ApplicationUITests.debug.xcconfig */, + 3A782884A39CB8B6B643183D /* Pods-Movie-Application-Movie-ApplicationUITests.release.xcconfig */, + E0AA62B85B7167696A146DEE /* Pods-Movie-ApplicationTests.debug.xcconfig */, + B8F4B8F4F346FAC3155BD932 /* Pods-Movie-ApplicationTests.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + E4169B655B537A50AEA219BE /* Frameworks */ = { + isa = PBXGroup; + children = ( + B6CBAB8843523803C510244D /* Pods_Movie_Application.framework */, + 80E20878F6E954381A53AFF4 /* Pods_Movie_Application_Movie_ApplicationUITests.framework */, + 90E49B8E9B0BABB57B2DD4C9 /* Pods_Movie_ApplicationTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 1BE9858E2813F0A20001DCAB /* Movie-Application */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1BE985B92813F0A30001DCAB /* Build configuration list for PBXNativeTarget "Movie-Application" */; + buildPhases = ( + EED24A7910D6F572E118D31E /* [CP] Check Pods Manifest.lock */, + 1BE9858B2813F0A20001DCAB /* Sources */, + 1BE9858C2813F0A20001DCAB /* Frameworks */, + 1BE9858D2813F0A20001DCAB /* Resources */, + 1BF787CD28211DF5004B6D2B /* Run Script */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Movie-Application"; + productName = "Movie-Application"; + productReference = 1BE9858F2813F0A20001DCAB /* Movie-Application.app */; + productType = "com.apple.product-type.application"; + }; + 1BE985A42813F0A30001DCAB /* Movie-ApplicationTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1BE985BC2813F0A30001DCAB /* Build configuration list for PBXNativeTarget "Movie-ApplicationTests" */; + buildPhases = ( + 022D7CF8C1CA201F0AE5F710 /* [CP] Check Pods Manifest.lock */, + 1BE985A12813F0A30001DCAB /* Sources */, + 1BE985A22813F0A30001DCAB /* Frameworks */, + 1BE985A32813F0A30001DCAB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 1BE985A72813F0A30001DCAB /* PBXTargetDependency */, + ); + name = "Movie-ApplicationTests"; + productName = "Movie-ApplicationTests"; + productReference = 1BE985A52813F0A30001DCAB /* Movie-ApplicationTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 1BE985AE2813F0A30001DCAB /* Movie-ApplicationUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1BE985BF2813F0A30001DCAB /* Build configuration list for PBXNativeTarget "Movie-ApplicationUITests" */; + buildPhases = ( + 5F70C0083F8879E9BE3D8C10 /* [CP] Check Pods Manifest.lock */, + 1BE985AB2813F0A30001DCAB /* Sources */, + 1BE985AC2813F0A30001DCAB /* Frameworks */, + 1BE985AD2813F0A30001DCAB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 1BE985B12813F0A30001DCAB /* PBXTargetDependency */, + ); + name = "Movie-ApplicationUITests"; + productName = "Movie-ApplicationUITests"; + productReference = 1BE985AF2813F0A30001DCAB /* Movie-ApplicationUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; + 1BF787E92821691B004B6D2B /* MovieApplicationMVVM */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1BF788142821691D004B6D2B /* Build configuration list for PBXNativeTarget "MovieApplicationMVVM" */; + buildPhases = ( + 1BF787E62821691B004B6D2B /* Sources */, + 1BF787E72821691B004B6D2B /* Frameworks */, + 1BF787E82821691B004B6D2B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MovieApplicationMVVM; + productName = MovieApplicationMVVM; + productReference = 1BF787EA2821691B004B6D2B /* MovieApplicationMVVM.app */; + productType = "com.apple.product-type.application"; + }; + 1BF788012821691D004B6D2B /* MovieApplicationMVVMTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1BF788172821691D004B6D2B /* Build configuration list for PBXNativeTarget "MovieApplicationMVVMTests" */; + buildPhases = ( + 1BF787FE2821691D004B6D2B /* Sources */, + 1BF787FF2821691D004B6D2B /* Frameworks */, + 1BF788002821691D004B6D2B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 1BF788042821691D004B6D2B /* PBXTargetDependency */, + ); + name = MovieApplicationMVVMTests; + productName = MovieApplicationMVVMTests; + productReference = 1BF788022821691D004B6D2B /* MovieApplicationMVVMTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 1BF7880B2821691D004B6D2B /* MovieApplicationMVVMUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1BF7881A2821691D004B6D2B /* Build configuration list for PBXNativeTarget "MovieApplicationMVVMUITests" */; + buildPhases = ( + 1BF788082821691D004B6D2B /* Sources */, + 1BF788092821691D004B6D2B /* Frameworks */, + 1BF7880A2821691D004B6D2B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 1BF7880E2821691D004B6D2B /* PBXTargetDependency */, + ); + name = MovieApplicationMVVMUITests; + productName = MovieApplicationMVVMUITests; + productReference = 1BF7880C2821691D004B6D2B /* MovieApplicationMVVMUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 1BE985872813F0A10001DCAB /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1320; + LastUpgradeCheck = 1320; + TargetAttributes = { + 1BE9858E2813F0A20001DCAB = { + CreatedOnToolsVersion = 13.2.1; + }; + 1BE985A42813F0A30001DCAB = { + CreatedOnToolsVersion = 13.2.1; + TestTargetID = 1BE9858E2813F0A20001DCAB; + }; + 1BE985AE2813F0A30001DCAB = { + CreatedOnToolsVersion = 13.2.1; + TestTargetID = 1BE9858E2813F0A20001DCAB; + }; + 1BF787E92821691B004B6D2B = { + CreatedOnToolsVersion = 13.2.1; + }; + 1BF788012821691D004B6D2B = { + CreatedOnToolsVersion = 13.2.1; + TestTargetID = 1BF787E92821691B004B6D2B; + }; + 1BF7880B2821691D004B6D2B = { + CreatedOnToolsVersion = 13.2.1; + TestTargetID = 1BF787E92821691B004B6D2B; + }; + }; + }; + buildConfigurationList = 1BE9858A2813F0A10001DCAB /* Build configuration list for PBXProject "Movie-Application" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 1BE985862813F0A10001DCAB; + productRefGroup = 1BE985902813F0A20001DCAB /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 1BE9858E2813F0A20001DCAB /* Movie-Application */, + 1BE985A42813F0A30001DCAB /* Movie-ApplicationTests */, + 1BE985AE2813F0A30001DCAB /* Movie-ApplicationUITests */, + 1BF787E92821691B004B6D2B /* MovieApplicationMVVM */, + 1BF788012821691D004B6D2B /* MovieApplicationMVVMTests */, + 1BF7880B2821691D004B6D2B /* MovieApplicationMVVMUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 1BE9858D2813F0A20001DCAB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1BE9859F2813F0A20001DCAB /* LaunchScreen.storyboard in Resources */, + 1BD16F84281B5DAC003E87B5 /* WatchlistMovies.storyboard in Resources */, + 1BD16F44281B5169003E87B5 /* PopularMovies.storyboard in Resources */, + 1BD16F42281B426F003E87B5 /* TopRatedMovies.storyboard in Resources */, + 1BE9859C2813F0A20001DCAB /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1BE985A32813F0A30001DCAB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1BF787D128213D9E004B6D2B /* Movies.json in Resources */, + 1BF787E528216193004B6D2B /* Movie.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1BE985AD2813F0A30001DCAB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1BF787E82821691B004B6D2B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1B026CEB2823C425006D2BFE /* TopRatedMovies.storyboard in Resources */, + 1B026CED2823C4B7006D2BFE /* PopularMovies.storyboard in Resources */, + 1B026D0728256F39006D2BFE /* LaunchScreen.storyboard in Resources */, + 1BF787F92821691D004B6D2B /* Assets.xcassets in Resources */, + 1B026CEF2823C72D006D2BFE /* WatchlistMovies.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1BF788002821691D004B6D2B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1BF7880A2821691D004B6D2B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 022D7CF8C1CA201F0AE5F710 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Movie-ApplicationTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 1BF787CD28211DF5004B6D2B /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nif which swiftlint > /dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; + }; + 5F70C0083F8879E9BE3D8C10 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Movie-Application-Movie-ApplicationUITests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + EED24A7910D6F572E118D31E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Movie-Application-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 1BE9858B2813F0A20001DCAB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1BE9864028140D150001DCAB /* PopularMoviesModule.swift in Sources */, + 1BE9864328140D150001DCAB /* PopularMoviesRouterInterface.swift in Sources */, + 1BE9864428140D150001DCAB /* PopularMoviesRouter.swift in Sources */, + 1B8C5EB12817F67A0013909D /* RequestManager.swift in Sources */, + 1BD0329C281C10EF00139C3B /* MovieDetailsEntity.swift in Sources */, + 1BD0329F281C10EF00139C3B /* MovieDetailsPresenterRouterInterface.swift in Sources */, + 1BD032AE281C488E00139C3B /* MovieDetailsInfoViewController.swift in Sources */, + 1BE985D72813F2430001DCAB /* TopRatedMoviesPresenterInteractorInterface.swift in Sources */, + 1B152879281971E5008071CC /* CoreDataProtocol.swift in Sources */, + 1BE985DB2813F2430001DCAB /* TopRatedMoviesViewInterface.swift in Sources */, + 1BD032CF281C5A8B00139C3B /* MovieInfoContentModule.swift in Sources */, + 1BD032D1281C5A8B00139C3B /* MovieInfoContentInteractorInterface.swift in Sources */, + 1B8C5EB42817F67A0013909D /* URLRequestLoggableProtocol.swift in Sources */, + 1BE985DE2813F2430001DCAB /* TopRatedMoviesInteractorInterface.swift in Sources */, + 1BE985D52813F2430001DCAB /* TopRatedMoviesModule.swift in Sources */, + 1BD16F7E281B5D9A003E87B5 /* WatchlistMoviesView.swift in Sources */, + 1BE985DC2813F2430001DCAB /* TopRatedMoviesView.swift in Sources */, + 1BD032D3281C5A8B00139C3B /* MovieInfoContentRouter.swift in Sources */, + 1BD0329E281C10EF00139C3B /* MovieDetailsPresenterInteractorInterface.swift in Sources */, + 1BD16F80281B5D9A003E87B5 /* WatchlistMoviesInteractor.swift in Sources */, + 1BE985E02813F2430001DCAB /* TopRatedMoviesRouterInterface.swift in Sources */, + 1B8C5EB02817F67A0013909D /* ResponseValidator.swift in Sources */, + 1BE9863A28140D150001DCAB /* PopularMoviesPresenterRouterInterface.swift in Sources */, + 1BD032A3281C10EF00139C3B /* MovieDetailsInteractor.swift in Sources */, + 1BE9863F28140D150001DCAB /* PopularMoviesViewInterface.swift in Sources */, + 1BE985DD2813F2430001DCAB /* TopRatedMoviesInteractor.swift in Sources */, + 1BD032CC281C5A8B00139C3B /* MovieInfoContentPresenterViewInterface.swift in Sources */, + 1BD16F7C281B5D9A003E87B5 /* WatchlistMoviesPresenterInteractorInterface.swift in Sources */, + 1B8C5EB8281802270013909D /* MoviesService.swift in Sources */, + 1BD032D2281C5A8B00139C3B /* MovieInfoContentRouterInterface.swift in Sources */, + 1B8C5EB22817F67A0013909D /* HTTPMethod.swift in Sources */, + 1B8C5EBC281804D20013909D /* Movies.swift in Sources */, + 1BD032CD281C5A8B00139C3B /* MovieInfoContentView.swift in Sources */, + 1BD16F1F281AFB7C003E87B5 /* MovieCell.swift in Sources */, + 1BD16F77281B5D9A003E87B5 /* WatchlistMoviesModule.swift in Sources */, + 1BD16F7B281B5D9A003E87B5 /* WatchlistMoviesPresenter.swift in Sources */, + 1B026CE42822DAC4006D2BFE /* MovieCollectionViewCell.swift in Sources */, + 1BE9863D28140D150001DCAB /* PopularMoviesPresenter.swift in Sources */, + 1BD16F7A281B5D9A003E87B5 /* WatchlistMoviesPresenterViewInterface.swift in Sources */, + 1BE985932813F0A20001DCAB /* AppDelegate.swift in Sources */, + 1B8C5EBE2818069D0013909D /* MoviesGeneres.swift in Sources */, + 1BE985952813F0A20001DCAB /* SceneDelegate.swift in Sources */, + 1B8C5EB52817F67A0013909D /* RequestManagerProtocol.swift in Sources */, + 1BD032A1281C10EF00139C3B /* MovieDetailsViewInterface.swift in Sources */, + 1BE9863B28140D150001DCAB /* PopularMoviesPresenterViewInterface.swift in Sources */, + 1BD032A5281C10EF00139C3B /* MovieDetailsModule.swift in Sources */, + 1BD032A7281C10EF00139C3B /* MovieDetailsRouter.swift in Sources */, + 1BE985D92813F2430001DCAB /* TopRatedMoviesPresenter.swift in Sources */, + 1BD0329D281C10EF00139C3B /* MovieDetailsPresenter.swift in Sources */, + 1BD032D0281C5A8B00139C3B /* MovieInfoContentInteractor.swift in Sources */, + 1BE9864128140D150001DCAB /* PopularMoviesInteractorInterface.swift in Sources */, + 1BE985DA2813F2430001DCAB /* TopRatedMoviesPresenterViewInterface.swift in Sources */, + 1B8C5EB62817F67A0013909D /* RequestError.swift in Sources */, + 1BD16F79281B5D9A003E87B5 /* WatchlistMoviesPresenterRouterInterface.swift in Sources */, + 1BD16F7D281B5D9A003E87B5 /* WatchlistMoviesViewInterface.swift in Sources */, + 1BD16F40281B4162003E87B5 /* MoviesServiceProtocol.swift in Sources */, + 1BE9863E28140D150001DCAB /* PopularMoviesView.swift in Sources */, + 1BD16F81281B5D9A003E87B5 /* WatchlistMoviesRouter.swift in Sources */, + 1BD032CA281C5A8B00139C3B /* MovieInfoContentPresenterRouterInterface.swift in Sources */, + 1BD032A2281C10EF00139C3B /* MovieDetailsView.swift in Sources */, + 1BD032AB281C3C6800139C3B /* BottomSheetContainerViewController.swift in Sources */, + 1BD032C9281C5A8B00139C3B /* MovieInfoContentPresenterInteractorInterface.swift in Sources */, + 1B8C5EB32817F67A0013909D /* ResponseLog.swift in Sources */, + 1BD16F7F281B5D9A003E87B5 /* WatchlistMoviesInteractorInterface.swift in Sources */, + 1BE985DF2813F2430001DCAB /* TopRatedMoviesRouter.swift in Sources */, + 1BE9864F2815D49A0001DCAB /* TabBarViewController.swift in Sources */, + 1BD032A4281C10EF00139C3B /* MovieDetailsInteractorInterface.swift in Sources */, + 1BE9864228140D150001DCAB /* PopularMoviesInteractor.swift in Sources */, + 1B8C5EAF2817F67A0013909D /* ResponseValidatorProtocol.swift in Sources */, + 1BE9863C28140D150001DCAB /* PopularMoviesPresenterInteractorInterface.swift in Sources */, + 1BE985D82813F2430001DCAB /* TopRatedMoviesPresenterRouterInterface.swift in Sources */, + 1BD032CE281C5A8B00139C3B /* MovieInfoContentViewInterface.swift in Sources */, + 1BE985E22813F2500001DCAB /* Interfaces.swift in Sources */, + 1BD032CB281C5A8B00139C3B /* MovieInfoContentPresenter.swift in Sources */, + 1B152876281971BA008071CC /* CoreDataManager.swift in Sources */, + 1BD032A6281C10EF00139C3B /* MovieDetailsRouterInterface.swift in Sources */, + 1BD16F17281AF731003E87B5 /* Extensions.swift in Sources */, + 1BD032A0281C10EF00139C3B /* MovieDetailsPresenterViewInterface.swift in Sources */, + 1BD16F82281B5D9A003E87B5 /* WatchlistMoviesRouterInterface.swift in Sources */, + 1B15287428196CD9008071CC /* FavoriteMovieModel.xcdatamodeld in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1BE985A12813F0A30001DCAB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1BF787D728213E87004B6D2B /* RequestManagerMock.swift in Sources */, + 1BF787D328213DDC004B6D2B /* URLSessionMock.swift in Sources */, + 1BF787DF28213FCB004B6D2B /* TestMoviesService.swift in Sources */, + 1B5B0D4E28202124001136BB /* TestWatchlistMoviesView.swift in Sources */, + 1B5B0D5428202DB3001136BB /* TestWatchlistMoviesRouter.swift in Sources */, + 1B5B0D5628202F22001136BB /* TestTopRatedMoviesRouter.swift in Sources */, + 1BE986522815D5CF0001DCAB /* TestTabBarViewController.swift in Sources */, + 1BF787D528213E34004B6D2B /* URLSessionDataTaskMock.swift in Sources */, + 1BF787D928213EFB004B6D2B /* MockResponseValidator.swift in Sources */, + 1BD03313281F1CF000139C3B /* TestPopularMoviesView.swift in Sources */, + 1B5B0D5828202F83001136BB /* TestPopularMoviesRouter.swift in Sources */, + 1B5B0D5228202CF0001136BB /* TestWatchlistMoviesInteractor.swift in Sources */, + 1B8C5EE128182A980013909D /* TestTopRatedMoviesView.swift in Sources */, + 1BD0330E281F0AB700139C3B /* TestTopRatedMoviesPresenter.swift in Sources */, + 1BE985AA2813F0A30001DCAB /* Movie_ApplicationTests.swift in Sources */, + 1BD03310281F14ED00139C3B /* TestTopRatedMoviesInteractor.swift in Sources */, + 1BD03315281F1E8C00139C3B /* TestPopularMoviesPresenter.swift in Sources */, + 1BD03317281F24A000139C3B /* TestPopularMoviesInteractor.swift in Sources */, + 1BF787DB28213F32004B6D2B /* UnitTestError.swift in Sources */, + 1B5B0D50282024C3001136BB /* TestWatchlistMoviesPresenter.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1BE985AB2813F0A30001DCAB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1BE985B62813F0A30001DCAB /* Movie_ApplicationUITestsLaunchTests.swift in Sources */, + 1BF787CE282127A1004B6D2B /* Movie_ApplicationUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1BF787E62821691B004B6D2B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1B026CD62822BFF9006D2BFE /* MainTabBarController.swift in Sources */, + 1B026CCD282199CC006D2BFE /* MovieCell.swift in Sources */, + 1B026CCC282199BE006D2BFE /* CoreDataProtocol.swift in Sources */, + 1B026CE32822CEB5006D2BFE /* WatchlistMoviesViewModel.swift in Sources */, + 1B026CD42821B4A7006D2BFE /* TopRatedMoviesCoordinator.swift in Sources */, + 1B026CC928219949006D2BFE /* MoviesGeneres.swift in Sources */, + 1B026CDA2822CA0D006D2BFE /* PopularMoviesViewController.swift in Sources */, + 1B026CF928242154006D2BFE /* MovieInfoContentViewModel.swift in Sources */, + 1B026CC12821992C006D2BFE /* RequestError.swift in Sources */, + 1B026CC728219943006D2BFE /* MoviesServiceProtocol.swift in Sources */, + 1B026CCB282199B7006D2BFE /* CoreDataManager.swift in Sources */, + 1B026CBF28219926006D2BFE /* RequestManager.swift in Sources */, + 1B026CF328242088006D2BFE /* BottomSheetContainerViewController.swift in Sources */, + 1B026CB7282193B8006D2BFE /* TopRatedMoviesViewModel.swift in Sources */, + 1B026CD228219EAD006D2BFE /* MovieCollectionViewCell.swift in Sources */, + 1B026CF5282420C9006D2BFE /* MovieInfoContentViewController.swift in Sources */, + 1B026CC428219938006D2BFE /* ResponseLog.swift in Sources */, + 1B026CCA282199B0006D2BFE /* FavoriteMovieModel.xcdatamodeld in Sources */, + 1B026CA9282184F7006D2BFE /* Coordinator.swift in Sources */, + 1B026CF12824200E006D2BFE /* MovieDetailsViewController.swift in Sources */, + 1B026CC52821993C006D2BFE /* URLRequestLoggableProtocol.swift in Sources */, + 1B026CE92823C2D5006D2BFE /* WatchlistMoviesCoordinator.swift in Sources */, + 1B026CC328219935006D2BFE /* ResponseValidator.swift in Sources */, + 1B026CDE2822CBBB006D2BFE /* PopularMoviesCoordinator.swift in Sources */, + 1B026CA528218354006D2BFE /* AppCoordinator.swift in Sources */, + 1B026CF22824206A006D2BFE /* MovieDetailsInfoViewController.swift in Sources */, + 1B026CC228219932006D2BFE /* ResponseValidatorProtocol.swift in Sources */, + 1B026CBB282193EB006D2BFE /* TopRatedMoviesViewController.swift in Sources */, + 1B026CD028219C6A006D2BFE /* MovieCollectionViewDelegate.swift in Sources */, + 1B026CE72823C205006D2BFE /* WatchlistMoviesViewController.swift in Sources */, + 1B026CC828219946006D2BFE /* Movies.swift in Sources */, + 1B026CDC2822CA1D006D2BFE /* PopularMoviesViewModel.swift in Sources */, + 1B026CBE2821990F006D2BFE /* MoviesService.swift in Sources */, + 1BF787ED2821691B004B6D2B /* AppDelegate.swift in Sources */, + 1B026CC028219929006D2BFE /* HTTPMethod.swift in Sources */, + 1B026CB0282188C6006D2BFE /* TabBarPage.swift in Sources */, + 1B026CAB2821858A006D2BFE /* Storyboarded.swift in Sources */, + 1B026CC62821993F006D2BFE /* RequestManagerProtocol.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1BF787FE2821691D004B6D2B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1B026D0428251C5A006D2BFE /* UnitTestError.swift in Sources */, + 1BF788072821691D004B6D2B /* MovieApplicationMVVMTests.swift in Sources */, + 1B026D0E2825B4FF006D2BFE /* TestWatchlistMoviesCoordinator.swift in Sources */, + 1B026D09282582A6006D2BFE /* TestTopRatedMoviesCoordinator.swift in Sources */, + 1B026D0C2825B366006D2BFE /* TestPopularMoviesCoordinator.swift in Sources */, + 1B026D0228251A60006D2BFE /* TestAppCoordinator.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1BF788082821691D004B6D2B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1BF788132821691D004B6D2B /* MovieApplicationMVVMUITestsLaunchTests.swift in Sources */, + 1BF788112821691D004B6D2B /* MovieApplicationMVVMUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 1BE985A72813F0A30001DCAB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 1BE9858E2813F0A20001DCAB /* Movie-Application */; + targetProxy = 1BE985A62813F0A30001DCAB /* PBXContainerItemProxy */; + }; + 1BE985B12813F0A30001DCAB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 1BE9858E2813F0A20001DCAB /* Movie-Application */; + targetProxy = 1BE985B02813F0A30001DCAB /* PBXContainerItemProxy */; + }; + 1BF788042821691D004B6D2B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 1BF787E92821691B004B6D2B /* MovieApplicationMVVM */; + targetProxy = 1BF788032821691D004B6D2B /* PBXContainerItemProxy */; + }; + 1BF7880E2821691D004B6D2B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 1BF787E92821691B004B6D2B /* MovieApplicationMVVM */; + targetProxy = 1BF7880D2821691D004B6D2B /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 1B026D0528256F39006D2BFE /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 1B026D0628256F39006D2BFE /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; + 1BE9859D2813F0A20001DCAB /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 1BE9859E2813F0A20001DCAB /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 1BE985B72813F0A30001DCAB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 1BE985B82813F0A30001DCAB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 1BE985BA2813F0A30001DCAB /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 82D2E5DD96CA7A69AA21161C /* Pods-Movie-Application.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = X289J5G5DP; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "$(SRCROOT)/Movie-Application/Supproting Files/Info.plist"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.Movie-Application"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 1BE985BB2813F0A30001DCAB /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9C3DEA32D4C3AB8BAE6A700C /* Pods-Movie-Application.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = X289J5G5DP; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "$(SRCROOT)/Movie-Application/Supproting Files/Info.plist"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.Movie-Application"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; + 1BE985BD2813F0A30001DCAB /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E0AA62B85B7167696A146DEE /* Pods-Movie-ApplicationTests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = X289J5G5DP; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.Movie-ApplicationTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Movie-Application.app/Movie-Application"; + }; + name = Debug; + }; + 1BE985BE2813F0A30001DCAB /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B8F4B8F4F346FAC3155BD932 /* Pods-Movie-ApplicationTests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = X289J5G5DP; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.Movie-ApplicationTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Movie-Application.app/Movie-Application"; + }; + name = Release; + }; + 1BE985C02813F0A30001DCAB /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = BF201D811AE4556FBC0BA0B9 /* Pods-Movie-Application-Movie-ApplicationUITests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = X289J5G5DP; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.Movie-ApplicationUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "Movie-Application"; + }; + name = Debug; + }; + 1BE985C12813F0A30001DCAB /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3A782884A39CB8B6B643183D /* Pods-Movie-Application-Movie-ApplicationUITests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = X289J5G5DP; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.Movie-ApplicationUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "Movie-Application"; + }; + name = Release; + }; + 1BF788152821691D004B6D2B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = X289J5G5DP; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "$(SRCROOT)/MovieApplicationMVVM/Supporting Files/Info.plist"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.MovieApplicationMVVM; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 1BF788162821691D004B6D2B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = X289J5G5DP; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "$(SRCROOT)/MovieApplicationMVVM/Supporting Files/Info.plist"; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.MovieApplicationMVVM; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; + 1BF788182821691D004B6D2B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = X289J5G5DP; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.MovieApplicationMVVMTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MovieApplicationMVVM.app/MovieApplicationMVVM"; + }; + name = Debug; + }; + 1BF788192821691D004B6D2B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = X289J5G5DP; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.MovieApplicationMVVMTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MovieApplicationMVVM.app/MovieApplicationMVVM"; + }; + name = Release; + }; + 1BF7881B2821691D004B6D2B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = X289J5G5DP; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.MovieApplicationMVVMUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = MovieApplicationMVVM; + }; + name = Debug; + }; + 1BF7881C2821691D004B6D2B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = X289J5G5DP; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.MovieApplicationMVVMUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = MovieApplicationMVVM; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1BE9858A2813F0A10001DCAB /* Build configuration list for PBXProject "Movie-Application" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1BE985B72813F0A30001DCAB /* Debug */, + 1BE985B82813F0A30001DCAB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1BE985B92813F0A30001DCAB /* Build configuration list for PBXNativeTarget "Movie-Application" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1BE985BA2813F0A30001DCAB /* Debug */, + 1BE985BB2813F0A30001DCAB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1BE985BC2813F0A30001DCAB /* Build configuration list for PBXNativeTarget "Movie-ApplicationTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1BE985BD2813F0A30001DCAB /* Debug */, + 1BE985BE2813F0A30001DCAB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1BE985BF2813F0A30001DCAB /* Build configuration list for PBXNativeTarget "Movie-ApplicationUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1BE985C02813F0A30001DCAB /* Debug */, + 1BE985C12813F0A30001DCAB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1BF788142821691D004B6D2B /* Build configuration list for PBXNativeTarget "MovieApplicationMVVM" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1BF788152821691D004B6D2B /* Debug */, + 1BF788162821691D004B6D2B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1BF788172821691D004B6D2B /* Build configuration list for PBXNativeTarget "MovieApplicationMVVMTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1BF788182821691D004B6D2B /* Debug */, + 1BF788192821691D004B6D2B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1BF7881A2821691D004B6D2B /* Build configuration list for PBXNativeTarget "MovieApplicationMVVMUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1BF7881B2821691D004B6D2B /* Debug */, + 1BF7881C2821691D004B6D2B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCVersionGroup section */ + 1B15287228196CD9008071CC /* FavoriteMovieModel.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + 1B15287328196CD9008071CC /* MovieModel.xcdatamodel */, + ); + currentVersion = 1B15287328196CD9008071CC /* MovieModel.xcdatamodel */; + path = FavoriteMovieModel.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ + }; + rootObject = 1BE985872813F0A10001DCAB /* Project object */; +} diff --git a/Movie-Application/Movie-Application.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Movie-Application/Movie-Application.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/Movie-Application/Movie-Application.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Movie-Application/Movie-Application.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Movie-Application/Movie-Application.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/Movie-Application/Movie-Application.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Movie-Application/Movie-Application.xcodeproj/project.xcworkspace/xcuserdata/mohanna.xcuserdatad/IDEFindNavigatorScopes.plist b/Movie-Application/Movie-Application.xcodeproj/project.xcworkspace/xcuserdata/mohanna.xcuserdatad/IDEFindNavigatorScopes.plist new file mode 100644 index 00000000..5dd5da85 --- /dev/null +++ b/Movie-Application/Movie-Application.xcodeproj/project.xcworkspace/xcuserdata/mohanna.xcuserdatad/IDEFindNavigatorScopes.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/Movie-Application/Movie-Application.xcodeproj/xcuserdata/mohanna.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Movie-Application/Movie-Application.xcodeproj/xcuserdata/mohanna.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 00000000..c888890a --- /dev/null +++ b/Movie-Application/Movie-Application.xcodeproj/xcuserdata/mohanna.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/Movie-Application/Movie-Application.xcodeproj/xcuserdata/mohanna.xcuserdatad/xcschemes/xcschememanagement.plist b/Movie-Application/Movie-Application.xcodeproj/xcuserdata/mohanna.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 00000000..6c9abefd --- /dev/null +++ b/Movie-Application/Movie-Application.xcodeproj/xcuserdata/mohanna.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,19 @@ + + + + + SchemeUserState + + Movie-Application.xcscheme_^#shared#^_ + + orderHint + 4 + + MovieApplicationMVVM.xcscheme_^#shared#^_ + + orderHint + 5 + + + + diff --git a/Movie-Application/Movie-Application.xcworkspace/contents.xcworkspacedata b/Movie-Application/Movie-Application.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..beb56dd2 --- /dev/null +++ b/Movie-Application/Movie-Application.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/Movie-Application/Movie-Application.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Movie-Application/Movie-Application.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/Movie-Application/Movie-Application.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Movie-Application/Movie-Application.xcworkspace/xcuserdata/mohanna.xcuserdatad/UserInterfaceState.xcuserstate b/Movie-Application/Movie-Application.xcworkspace/xcuserdata/mohanna.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 00000000..7828269d Binary files /dev/null and b/Movie-Application/Movie-Application.xcworkspace/xcuserdata/mohanna.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Movie-Application/Movie-Application.xcworkspace/xcuserdata/mohanna.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Movie-Application/Movie-Application.xcworkspace/xcuserdata/mohanna.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 00000000..9183385e --- /dev/null +++ b/Movie-Application/Movie-Application.xcworkspace/xcuserdata/mohanna.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/Movie-Application/Movie-Application/.DS_Store b/Movie-Application/Movie-Application/.DS_Store new file mode 100644 index 00000000..c08238f3 Binary files /dev/null and b/Movie-Application/Movie-Application/.DS_Store differ diff --git a/Movie-Application/Movie-Application/App/AppDelegate.swift b/Movie-Application/Movie-Application/App/AppDelegate.swift new file mode 100644 index 00000000..6f23bc24 --- /dev/null +++ b/Movie-Application/Movie-Application/App/AppDelegate.swift @@ -0,0 +1,78 @@ +// +// AppDelegate.swift +// Movie-Application +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import UIKit +import CoreData + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + // swiftlint: disable all + + func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + + lazy var persistentContainer: NSPersistentContainer = { + /* + The persistent container for the application. This implementation + creates and returns a container, having loaded the store for the + application to it. This property is optional since there are legitimate + error conditions that could cause the creation of the store to fail. + */ + let container = NSPersistentContainer(name: "FavoriteMovieModel") + container.loadPersistentStores(completionHandler: { (storeDescription, error) in + if let error = error as NSError? { + + /* + Typical reasons for an error here include: + * The parent directory does not exist, cannot be created, or disallows writing. + * The persistent store is not accessible, due to permissions or data protection when the device is locked. + * The device is out of space. + * The store could not be migrated to the current model version. + Check the error message to determine what the actual problem was. + */ + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + return container + }() + + // MARK: - Core Data Saving support + + func saveContext () { + let context = persistentContainer.viewContext + if context.hasChanges { + do { + try context.save() + } catch { + + let nserror = error as NSError + fatalError("Unresolved error \(nserror), \(nserror.userInfo)") + } + } + } +} diff --git a/Movie-Application/Movie-Application/App/Interfaces.swift b/Movie-Application/Movie-Application/App/Interfaces.swift new file mode 100644 index 00000000..a6417bb7 --- /dev/null +++ b/Movie-Application/Movie-Application/App/Interfaces.swift @@ -0,0 +1,96 @@ +// +// Interfaces.swift +// VIPER +// +// Created by Tibor Bödecs on 2019. 05. 16.. +// Copyright © 2019. Tibor Bödecs. All rights reserved. +// + +import Foundation + +// MARK: - interfaces + +public protocol RouterPresenterInterface: AnyObject { + +} + +public protocol InteractorPresenterInterface: AnyObject { + +} + +public protocol PresenterRouterInterface: AnyObject { + +} + +public protocol PresenterInteractorInterface: AnyObject { + +} + +public protocol PresenterViewInterface: AnyObject { + +} + +public protocol ViewPresenterInterface: AnyObject { + +} + +// MARK: - viper + +public protocol RouterInterface: RouterPresenterInterface { + associatedtype PresenterRouter + + var presenter: PresenterRouter! { get set } +} + +public protocol InteractorInterface: InteractorPresenterInterface { + associatedtype PresenterInteractor + + var presenter: PresenterInteractor! { get set } +} + +public protocol PresenterInterface: PresenterRouterInterface, PresenterInteractorInterface, PresenterViewInterface { + associatedtype RouterPresenter + associatedtype InteractorPresenter + associatedtype ViewPresenter + + var router: RouterPresenter! { get set } + var interactor: InteractorPresenter! { get set } + var view: ViewPresenter! { get set } +} + +public protocol ViewInterface: ViewPresenterInterface { + associatedtype PresenterView + + var presenter: PresenterView! { get set } +} + +public protocol EntityInterface { + +} + +// MARK: - module + +public protocol ModuleInterface { + + associatedtype View where View: ViewInterface + associatedtype Presenter where Presenter: PresenterInterface + associatedtype Router where Router: RouterInterface + associatedtype Interactor where Interactor: InteractorInterface + + func assemble(view: View, presenter: Presenter, router: Router, interactor: Interactor) +} + +public extension ModuleInterface { + + func assemble(view: View, presenter: Presenter, router: Router, interactor: Interactor) { + view.presenter = (presenter as? Self.View.PresenterView) + + presenter.view = (view as? Self.Presenter.ViewPresenter) + presenter.interactor = (interactor as? Self.Presenter.InteractorPresenter) + presenter.router = (router as? Self.Presenter.RouterPresenter) + + interactor.presenter = (presenter as? Self.Interactor.PresenterInteractor) + + router.presenter = (presenter as? Self.Router.PresenterRouter) + } +} diff --git a/Movie-Application/Movie-Application/App/SceneDelegate.swift b/Movie-Application/Movie-Application/App/SceneDelegate.swift new file mode 100644 index 00000000..0152d020 --- /dev/null +++ b/Movie-Application/Movie-Application/App/SceneDelegate.swift @@ -0,0 +1,54 @@ +// +// SceneDelegate.swift +// Movie-Application +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + // swiftlint: disable all + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = (scene as? UIWindowScene) else { return } + + let window = UIWindow(windowScene: windowScene) + window.rootViewController = TabBarViewContorller() + window.makeKeyAndVisible() + self.window = window + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/Movie-Application/Movie-Application/App/TabBarViewController.swift b/Movie-Application/Movie-Application/App/TabBarViewController.swift new file mode 100644 index 00000000..9bc16bc9 --- /dev/null +++ b/Movie-Application/Movie-Application/App/TabBarViewController.swift @@ -0,0 +1,84 @@ +// +// TabBarViewController.swift +// Movie-Application +// +// Created by Mohanna Zakizadeh on 4/24/22. +// + +import UIKit + +final class TabBarViewContorller: UITabBarController { + + // MARK: - Properties + var topRatedMoviesViewController: UIViewController! + var popularMoviesViewController: UIViewController! + var favoriteMoviesViewController: UIViewController! + + let topRatedIcon = UIImage(systemName: "list.number") + let favoriteIcon = UIImage(systemName: "bookmark") + let popularIcon = UIImage(systemName: "flame") + + // MARK: - Lifecycle + deinit { + NotificationCenter.default.removeObserver(self) + } + + override func viewDidLoad() { + super.viewDidLoad() + + topRatedMoviesViewController = setupTopRatedMoviesViewController() + popularMoviesViewController = setupPopularMoviesViewController() + favoriteMoviesViewController = setupFavoriteMoviesViewController() + + self.viewControllers = [popularMoviesViewController, topRatedMoviesViewController, favoriteMoviesViewController] + self.selectedIndex = 1 + self.toolbarItems = [] + + // notification observed in order to switch between tabs + NotificationCenter.default.addObserver(forName: Self.selectedTabNotification, + object: nil, queue: nil) { notification in + if notification.userInfo?["selectedTab"] as? Int == 0 { + self.selectedIndex = 0 + } + } + + } + + // MARK: - Private functions + func setupTopRatedMoviesViewController() -> UIViewController { + let topRatedViewController = TopRatedMoviesModule().build() + let tabBarItem = UITabBarItem(title: "Top Rated", image: topRatedIcon, tag: 0) + topRatedViewController.tabBarItem = tabBarItem + return topRatedViewController + } + + func setupPopularMoviesViewController() -> UIViewController { + let popularViewController = PopularMoviesModule().build() + popularViewController.tabBarItem = UITabBarItem(title: "Popular", + image: popularIcon, + selectedImage: UIImage(systemName: "flame.fill")) + return popularViewController + } + + func setupFavoriteMoviesViewController() -> UIViewController { + let favoriteViewController = WatchlistMoviesModule().build() + favoriteViewController.tabBarItem = UITabBarItem(title: "WatchList", + image: favoriteIcon, + selectedImage: UIImage(systemName: "bookmark.fill")) + return favoriteViewController + } + + override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { + NotificationCenter.default.post(name: TabBarViewContorller.tabBarDidTapNotification, object: nil) + } +} + +extension TabBarViewContorller { + static var tabBarDidTapNotification: NSNotification.Name { + NSNotification.Name(rawValue: "TabBarViewContorller.tabBarDidTapNotification") + } + + static var selectedTabNotification: NSNotification.Name { + NSNotification.Name("TabBarViewContorller.selectedTabNotification") + } +} diff --git a/Movie-Application/Movie-Application/Modules/.DS_Store b/Movie-Application/Movie-Application/Modules/.DS_Store new file mode 100644 index 00000000..7f12ec6c Binary files /dev/null and b/Movie-Application/Movie-Application/Modules/.DS_Store differ diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/.DS_Store b/Movie-Application/Movie-Application/Modules/MovieDetails/.DS_Store new file mode 100644 index 00000000..9edade53 Binary files /dev/null and b/Movie-Application/Movie-Application/Modules/MovieDetails/.DS_Store differ diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/Entity/MovieDetailsEntity.swift b/Movie-Application/Movie-Application/Modules/MovieDetails/Entity/MovieDetailsEntity.swift new file mode 100644 index 00000000..3e27ece0 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/MovieDetails/Entity/MovieDetailsEntity.swift @@ -0,0 +1,8 @@ +// +// MovieDetailsEntity.swift +// MovieDetails +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import Foundation diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/Interactor/MovieDetailsInteractor.swift b/Movie-Application/Movie-Application/Modules/MovieDetails/Interactor/MovieDetailsInteractor.swift new file mode 100644 index 00000000..54ef92b7 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/MovieDetails/Interactor/MovieDetailsInteractor.swift @@ -0,0 +1,17 @@ +// +// MovieDetailsInteractor.swift +// MovieDetails +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import Foundation + +final class MovieDetailsInteractor: InteractorInterface { + + weak var presenter: MovieDetailsPresenterInteractorInterface! +} + +extension MovieDetailsInteractor: MovieDetailsInteractorInterface { + +} diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/Interactor/MovieDetailsInteractorInterface.swift b/Movie-Application/Movie-Application/Modules/MovieDetails/Interactor/MovieDetailsInteractorInterface.swift new file mode 100644 index 00000000..35ca7a7c --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/MovieDetails/Interactor/MovieDetailsInteractorInterface.swift @@ -0,0 +1,12 @@ +// +// MovieDetailsInteractorInterface.swift +// MovieDetails +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import Foundation + +protocol MovieDetailsInteractorInterface: InteractorPresenterInterface { + +} diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/MovieDetailsModule.swift b/Movie-Application/Movie-Application/Modules/MovieDetails/MovieDetailsModule.swift new file mode 100644 index 00000000..cfcaaf1f --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/MovieDetails/MovieDetailsModule.swift @@ -0,0 +1,43 @@ +// +// MovieDetailsModule.swift +// MovieDetails +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import UIKit + +// MARK: - module builder + +final class MovieDetailsModule: ModuleInterface { + + typealias View = MovieDetailsView + typealias Presenter = MovieDetailsPresenter + typealias Router = MovieDetailsRouter + typealias Interactor = MovieDetailsInteractor + + func build(movie: MovieDetail) -> UIViewController { + guard let movieInfoContentView = MovieInfoContentModule().build(movie: movie) as? MovieInfoContentView else { + return UIViewController() + } + let movieDetailsInfoViewController = MovieDetailsInfoViewController() + movieDetailsInfoViewController.movie = movie + + let view = View(contentViewController: movieInfoContentView , + bottomSheetViewController: movieDetailsInfoViewController, + bottomSheetConfiguration: .init(height: UIScreen.main.bounds.height*0.8, + initialOffset: UIScreen.main.bounds.height / 2.2)) + + let navigation = UINavigationController(rootViewController: view) + + let interactor = Interactor() + let presenter = Presenter() + let router = Router() + + self.assemble(view: view, presenter: presenter, router: router, interactor: interactor) + + router.viewController = navigation + + return navigation + } +} diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/Presenter/MovieDetailsPresenter.swift b/Movie-Application/Movie-Application/Modules/MovieDetails/Presenter/MovieDetailsPresenter.swift new file mode 100644 index 00000000..3c1f5aae --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/MovieDetails/Presenter/MovieDetailsPresenter.swift @@ -0,0 +1,32 @@ +// +// MovieDetailsPresenter.swift +// MovieDetails +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import Foundation + +final class MovieDetailsPresenter: PresenterInterface { + + var router: MovieDetailsRouterInterface! + var interactor: MovieDetailsInteractorInterface! + weak var view: MovieDetailsViewInterface! + +} + +extension MovieDetailsPresenter: MovieDetailsPresenterRouterInterface { + +} + +extension MovieDetailsPresenter: MovieDetailsPresenterInteractorInterface { + +} + +extension MovieDetailsPresenter: MovieDetailsPresenterViewInterface { + + func viewDidLoad() { + + } + +} diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/Presenter/MovieDetailsPresenterInteractorInterface.swift b/Movie-Application/Movie-Application/Modules/MovieDetails/Presenter/MovieDetailsPresenterInteractorInterface.swift new file mode 100644 index 00000000..3e6891e3 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/MovieDetails/Presenter/MovieDetailsPresenterInteractorInterface.swift @@ -0,0 +1,12 @@ +// +// MovieDetailsPresenterInteractorInterface.swift +// MovieDetails +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import Foundation + +protocol MovieDetailsPresenterInteractorInterface: PresenterInteractorInterface { + +} diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/Presenter/MovieDetailsPresenterRouterInterface.swift b/Movie-Application/Movie-Application/Modules/MovieDetails/Presenter/MovieDetailsPresenterRouterInterface.swift new file mode 100644 index 00000000..c1ad0e5f --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/MovieDetails/Presenter/MovieDetailsPresenterRouterInterface.swift @@ -0,0 +1,12 @@ +// +// MovieDetailsPresenterRouterInterface.swift +// MovieDetails +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import Foundation + +protocol MovieDetailsPresenterRouterInterface: PresenterRouterInterface { + +} diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/Presenter/MovieDetailsPresenterViewInterface.swift b/Movie-Application/Movie-Application/Modules/MovieDetails/Presenter/MovieDetailsPresenterViewInterface.swift new file mode 100644 index 00000000..1001dd5e --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/MovieDetails/Presenter/MovieDetailsPresenterViewInterface.swift @@ -0,0 +1,13 @@ +// +// MovieDetailsPresenterViewInterface.swift +// MovieDetails +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import Foundation +import UIKit + +protocol MovieDetailsPresenterViewInterface: PresenterViewInterface { + func viewDidLoad() +} diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/Router/MovieDetailsRouter.swift b/Movie-Application/Movie-Application/Modules/MovieDetails/Router/MovieDetailsRouter.swift new file mode 100644 index 00000000..b6e628c4 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/MovieDetails/Router/MovieDetailsRouter.swift @@ -0,0 +1,19 @@ +// +// MovieDetailsRouter.swift +// MovieDetails +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import UIKit + +final class MovieDetailsRouter: RouterInterface { + + weak var presenter: MovieDetailsPresenterRouterInterface! + + weak var viewController: UIViewController? +} + +extension MovieDetailsRouter: MovieDetailsRouterInterface { + +} diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/Router/MovieDetailsRouterInterface.swift b/Movie-Application/Movie-Application/Modules/MovieDetails/Router/MovieDetailsRouterInterface.swift new file mode 100644 index 00000000..42c3c5f7 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/MovieDetails/Router/MovieDetailsRouterInterface.swift @@ -0,0 +1,12 @@ +// +// MovieDetailsRouterInterface.swift +// MovieDetails +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import UIKit + +protocol MovieDetailsRouterInterface: RouterPresenterInterface { + +} diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/View/.DS_Store b/Movie-Application/Movie-Application/Modules/MovieDetails/View/.DS_Store new file mode 100644 index 00000000..dccb0611 Binary files /dev/null and b/Movie-Application/Movie-Application/Modules/MovieDetails/View/.DS_Store differ diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/.DS_Store b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/.DS_Store new file mode 100644 index 00000000..7ca6e289 Binary files /dev/null and b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/.DS_Store differ diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/BottomSheetContainerViewController.swift b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/BottomSheetContainerViewController.swift new file mode 100644 index 00000000..d3c0f93f --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/BottomSheetContainerViewController.swift @@ -0,0 +1,230 @@ +// +// BottomSheetContainerViewController.swift +// Movie-Application +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import UIKit + +open class BottomSheetContainerViewController : UIViewController, + UIGestureRecognizerDelegate { + + // MARK: - Initialization + public init(contentViewController: Content, + bottomSheetViewController: BottomSheet, + bottomSheetConfiguration: BottomSheetConfiguration) { + + self.contentViewController = contentViewController + self.bottomSheetViewController = bottomSheetViewController + self.configuration = bottomSheetConfiguration + + super.init(nibName: nil, bundle: nil) + + self.setupUI() + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) is not supported") + } + + // MARK: - Bottom Sheet Actions + public func showBottomSheet(animated: Bool = true) { + self.topConstraint.constant = -configuration.height + + if animated { + UIView.animate(withDuration: 0.2, animations: { + self.view.layoutIfNeeded() + }, completion: { _ in + self.state = .full + }) + } else { + self.view.layoutIfNeeded() + self.state = .full + } + } + + public func hideBottomSheet(animated: Bool = true) { + self.topConstraint.constant = -configuration.initialOffset + + if animated { + UIView.animate(withDuration: 0.3, + delay: 0, + usingSpringWithDamping: 0.8, + initialSpringVelocity: 0.5, + options: [.curveEaseOut], + animations: { + self.view.layoutIfNeeded() + }, completion: { _ in + self.state = .initial + }) + } else { + self.view.layoutIfNeeded() + self.state = .initial + } + } + + // MARK: - Pan Action + // swiftlint: disable cyclomatic_complexity + @objc func handlePan(_ sender: UIPanGestureRecognizer) { + let translation = sender.translation(in: bottomSheetViewController.view) + let velocity = sender.velocity(in: bottomSheetViewController.view) + + let yTranslationMagnitude = translation.y.magnitude + + switch sender.state { + case .began, .changed: + if self.state == .full { + /* Assert that the user scrolls down ward. + The bottom sheet is already at its full height; no upward scrolling is allowed*/ + guard translation.y > 0 else { return } + + // Change the topConstraint’s constant to match the current position of the user’s finger + topConstraint.constant = -(configuration.height - yTranslationMagnitude) + + // Update the root view to show the constraint’s change + self.view.layoutIfNeeded() + + } else { + + /* Calculate the new constant by using the BottomSheetConfiguration’s initialOffset + and the translation magnitude */ + let newConstant = -(configuration.initialOffset + yTranslationMagnitude) + + // down ward scrollin is allowed + guard translation.y < 0 else { return } + guard newConstant.magnitude < configuration.height else { + self.showBottomSheet() + return + } + + topConstraint.constant = newConstant + + self.view.layoutIfNeeded() + } + case .ended: + if self.state == .full { + + if velocity.y < 0 { + // Bottom Sheet was full initially and the user tried to move it to the top + self.showBottomSheet() + } else if yTranslationMagnitude >= configuration.height / 2 || velocity.y > 1000 { + self.hideBottomSheet() + } else { + self.showBottomSheet() + } + } else { + + if yTranslationMagnitude >= configuration.height / 2 || velocity.y < -1000 { + + self.showBottomSheet() + + } else { + self.hideBottomSheet() + } + } + case .failed: + if self.state == .full { + self.showBottomSheet() + } else { + self.hideBottomSheet() + } + default: break + } + } + // MARK: - Tap Gesture Handler + @objc func contentViewTapped() { + self.hideBottomSheet() + } + + // MARK: - Configuration + public struct BottomSheetConfiguration { + let height: CGFloat + let initialOffset: CGFloat + } + + private let configuration: BottomSheetConfiguration + + // MARK: - State + public enum BottomSheetState { + case initial + case full + } + + var state: BottomSheetState = .initial + + // MARK: - Children + let contentViewController: Content + let bottomSheetViewController: BottomSheet + + // MARK: - Top Constraint + private var topConstraint = NSLayoutConstraint() + + // MARK: - Pan Gesture + lazy var panGesture: UIPanGestureRecognizer = { + let pan = UIPanGestureRecognizer() + pan.delegate = self + pan.addTarget(self, action: #selector(handlePan)) + return pan + }() + + // MARK: - Tap Gesture + + lazy var tapGesture: UITapGestureRecognizer = { + let tap = UITapGestureRecognizer() + tap.addTarget(self, action: #selector(contentViewTapped)) + tap.delegate = self + return tap + }() + + // MARK: - UI Setup + private func setupUI() { + self.addChild(contentViewController) + self.addChild(bottomSheetViewController) + + self.view.addSubview(contentViewController.view) + self.view.addSubview(bottomSheetViewController.view) + bottomSheetViewController.view.addGestureRecognizer(panGesture) + contentViewController.view.addGestureRecognizer(tapGesture) + + contentViewController.view.translatesAutoresizingMaskIntoConstraints = false + bottomSheetViewController.view.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + contentViewController.view.leftAnchor + .constraint(equalTo: self.view.leftAnchor), + contentViewController.view.rightAnchor + .constraint(equalTo: self.view.rightAnchor), + contentViewController.view.topAnchor + .constraint(equalTo: self.view.topAnchor), + contentViewController.view.bottomAnchor + .constraint(equalTo: self.view.bottomAnchor) + ]) + + contentViewController.didMove(toParent: self) + + topConstraint = bottomSheetViewController.view.topAnchor + .constraint(equalTo: self.view.bottomAnchor, + constant: -configuration.initialOffset) + + NSLayoutConstraint.activate([ + bottomSheetViewController.view.heightAnchor + .constraint(equalToConstant: configuration.height), + bottomSheetViewController.view.leftAnchor + .constraint(equalTo: self.view.leftAnchor), + bottomSheetViewController.view.rightAnchor + .constraint(equalTo: self.view.rightAnchor), + topConstraint + ]) + + bottomSheetViewController.didMove(toParent: self) + } + + // MARK: - UIGestureRecognizer Delegate + // swiftlint: disable line_length + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, + shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } +} diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieDetailsInfoViewController.swift b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieDetailsInfoViewController.swift new file mode 100644 index 00000000..9a5bee7a --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieDetailsInfoViewController.swift @@ -0,0 +1,186 @@ +// +// MovieDetailsInfoViewController.swift +// Movie-Application +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import Foundation +import UIKit + +final class MovieDetailsInfoViewController: UIViewController { + + // MARK: - Properties + var movieTitleLabel: UILabel! + var movieDetailsLabel: UILabel! + var movieTitleStackView: UIStackView! + + var addToWatchListButton: UIButton! + + var releaseDateLabel: UILabel! + var releaseTitleLabel: UILabel! + var releaseStackView: UIStackView! + + var userScoreLabel: UILabel! + var userScoreTitleLabel: UILabel! + var userScoreStackView: UIStackView! + + var reviewsLabel: UILabel! + var reviewsTitleLabel: UILabel! + var reviewsStackView: UIStackView! + + var movieInfoStackView: UIStackView! + + var overViewTitleLabel: UILabel! + var overViewLabel: UILabel! + var overViewStackView: UIStackView! + + var totalStackView: UIStackView! + var scrollView: UIScrollView! + + var movie: MovieDetail! + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + movieTitleLabel = setupLabel(with: movie.title) + movieDetailsLabel = setupLabel(with: getMoviesDetails()) + movieTitleStackView = setupStackView(with: [movieTitleLabel, movieDetailsLabel], spacing: 13, axis: .vertical) + movieTitleStackView.distribution = .equalSpacing + + releaseDateLabel = setupLabel(with: movie.releaseDate.components(separatedBy: "-").first) + releaseTitleLabel = setupLabel(with: "Release") + releaseStackView = setupStackView(with: [releaseDateLabel, releaseTitleLabel], spacing: 8, axis: .vertical) + releaseStackView.alignment = .center + + userScoreLabel = setupLabel(with: String(format: "%.0f", movie.voteAverage*10) + "%") + userScoreTitleLabel = setupLabel(with: "User Score") + userScoreStackView = setupStackView(with: [userScoreLabel, userScoreTitleLabel], spacing: 8, axis: .vertical) + userScoreStackView.alignment = .center + + reviewsLabel = setupLabel(with: "\(movie.reviewsCount)") + reviewsTitleLabel = setupLabel(with: "Reviews") + reviewsStackView = setupStackView(with: [reviewsLabel, reviewsTitleLabel], spacing: 8, axis: .vertical) + reviewsStackView.alignment = .center + + movieInfoStackView = setupStackView(with: [releaseStackView, userScoreStackView, reviewsStackView], + spacing: 0, + axis: .horizontal) + movieInfoStackView.layer.cornerRadius = 14 + movieInfoStackView.alignment = .center + + overViewTitleLabel = setupLabel(with: "Overview") + overViewLabel = setupLabel(with: movie.overview) + overViewStackView = setupStackView(with: [overViewTitleLabel, overViewLabel], spacing: 16, axis: .vertical) + overViewStackView.distribution = .fillProportionally + + totalStackView = setupStackView(with: [movieTitleStackView, movieInfoStackView, overViewStackView], + spacing: 32, + axis: .vertical) + + scrollView = setupScrollView() + setTotalStackView(in: scrollView) + + setScrollView(in: self.view) + + self.applyTheme() + } + + // MARK: - Theme + + func applyTheme() { + view.backgroundColor = .systemBackground + view.layer.cornerRadius = 14 + + movieTitleLabel.font = UIFont.boldSystemFont(ofSize: 22) + movieDetailsLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body), size: 15) + movieDetailsLabel.textColor = .secondaryLabel + + releaseDateLabel.font = UIFont.boldSystemFont(ofSize: 22) + releaseTitleLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body), size: 15) + releaseTitleLabel.textColor = .secondaryLabel + + reviewsLabel.font = UIFont.boldSystemFont(ofSize: 22) + reviewsTitleLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body), size: 15) + reviewsTitleLabel.textColor = .secondaryLabel + + userScoreLabel.font = UIFont.boldSystemFont(ofSize: 22) + userScoreTitleLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body), size: 15) + userScoreTitleLabel.textColor = .secondaryLabel + movieInfoStackView.backgroundColor = .secondarySystemBackground + + overViewTitleLabel.font = UIFont.boldSystemFont(ofSize: 28) + } + + // MARK: - Private functions + private func getMoviesDetails() -> String { + let dateYear = movie.releaseDate.components(separatedBy: "-").first + let genre = movie.genres.first?.name + return (dateYear ?? "") + " . " + (genre ?? "") + } + + private func setupLabel(with text: String?) -> UILabel { + let label = UILabel() + label.text = text + label.numberOfLines = 0 + return label + } + + private func setupStackView(with views: [UIView], spacing: CGFloat, axis: NSLayoutConstraint.Axis) -> UIStackView { + let stackView = UIStackView(arrangedSubviews: views) + stackView.axis = axis + stackView.spacing = spacing + stackView.alignment = .leading + stackView.distribution = .fillProportionally + return stackView + } + + private func setupScrollView() -> UIScrollView { + let scrollView = UIScrollView() + scrollView.contentSize.width = scrollView.frame.size.width + scrollView.frame = self.view.bounds + return scrollView + } + + private func setTotalStackView(in view: UIView? = nil) { + if let view = view { + view.addSubview(totalStackView) + totalStackView.alignment = .leading + totalStackView.translatesAutoresizingMaskIntoConstraints = false + totalStackView.layer.cornerRadius = 14 + + NSLayoutConstraint.activate([ + totalStackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + totalStackView.leftAnchor.constraint(equalTo: view.leftAnchor), + totalStackView.widthAnchor.constraint(equalTo: view.widthAnchor), + + movieInfoStackView.heightAnchor.constraint(equalToConstant: 85), + movieInfoStackView.leadingAnchor.constraint(equalTo: totalStackView.leadingAnchor, constant: 16), + movieInfoStackView.trailingAnchor.constraint(equalTo: totalStackView.trailingAnchor, constant: -16), + + movieTitleStackView.leadingAnchor.constraint(equalTo: totalStackView.leadingAnchor, constant: 16), + movieTitleStackView.trailingAnchor.constraint(equalTo: totalStackView.trailingAnchor, constant: -16), + + overViewStackView.leadingAnchor.constraint(equalTo: totalStackView.leadingAnchor, constant: 16), + overViewStackView.trailingAnchor.constraint(equalTo: totalStackView.trailingAnchor, constant: -16) + ]) + } + } + + private func setScrollView(in view: UIView? = nil) { + if let view = view { + view.addSubview(scrollView) + + scrollView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 32), + scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -32), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.widthAnchor.constraint(equalTo: view.widthAnchor) + ]) + } + } +} diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/.DS_Store b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/.DS_Store new file mode 100644 index 00000000..9357eaf5 Binary files /dev/null and b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/.DS_Store differ diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/Interactor/MovieInfoContentInteractor.swift b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/Interactor/MovieInfoContentInteractor.swift new file mode 100644 index 00000000..2db7177a --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/Interactor/MovieInfoContentInteractor.swift @@ -0,0 +1,24 @@ +// +// MovieInfoContentInteractor.swift +// MovieInfoContent +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import Foundation +import UIKit + +final class MovieInfoContentInteractor: InteractorInterface { + + weak var presenter: MovieInfoContentPresenterInteractorInterface! +} + +extension MovieInfoContentInteractor: MovieInfoContentInteractorInterface { + + func getMovieImage(path: String) -> UIImage? { + let url = URL(string: "https://image.tmdb.org/t/p/w400/" + path)! + guard let data = try? Data(contentsOf: url) else { return UIImage(systemName: "film.circle") } + let image = UIImage(data: data) + return image + } +} diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/Interactor/MovieInfoContentInteractorInterface.swift b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/Interactor/MovieInfoContentInteractorInterface.swift new file mode 100644 index 00000000..e8f28644 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/Interactor/MovieInfoContentInteractorInterface.swift @@ -0,0 +1,13 @@ +// +// MovieInfoContentInteractorInterface.swift +// MovieInfoContent +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import Foundation +import UIKit + +protocol MovieInfoContentInteractorInterface: InteractorPresenterInterface { + func getMovieImage(path: String) -> UIImage? +} diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/MovieInfoContentModule.swift b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/MovieInfoContentModule.swift new file mode 100644 index 00000000..2827c26e --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/MovieInfoContentModule.swift @@ -0,0 +1,32 @@ +// +// MovieInfoContentModule.swift +// MovieInfoContent +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import UIKit + +// MARK: - module builder + +final class MovieInfoContentModule: ModuleInterface { + + typealias View = MovieInfoContentView + typealias Presenter = MovieInfoContentPresenter + typealias Router = MovieInfoContentRouter + typealias Interactor = MovieInfoContentInteractor + + func build(movie: MovieDetail) -> UIViewController { + let view = View() + let interactor = Interactor() + let presenter = Presenter() + let router = Router() + view.movie = movie + + self.assemble(view: view, presenter: presenter, router: router, interactor: interactor) + + router.viewController = view + + return view + } +} diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/Presenter/MovieInfoContentPresenter.swift b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/Presenter/MovieInfoContentPresenter.swift new file mode 100644 index 00000000..b449b398 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/Presenter/MovieInfoContentPresenter.swift @@ -0,0 +1,48 @@ +// +// MovieInfoContentPresenter.swift +// MovieInfoContent +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import Foundation +import UIKit + +final class MovieInfoContentPresenter: PresenterInterface { + + var router: MovieInfoContentRouterInterface! + var interactor: MovieInfoContentInteractorInterface! + weak var view: MovieInfoContentViewInterface! + +} + +extension MovieInfoContentPresenter: MovieInfoContentPresenterRouterInterface { + +} + +extension MovieInfoContentPresenter: MovieInfoContentPresenterInteractorInterface { + +} + +extension MovieInfoContentPresenter: MovieInfoContentPresenterViewInterface { + + func viewDidLoad() { + + } + + func getMovieImage(path: String) -> UIImage? { + interactor.getMovieImage(path: path) + } + + func addToWatchListTapped(movie: MovieDetail) { + let url = URL(string: "https://image.tmdb.org/t/p/w300/" + (movie.poster ?? ""))! + guard let data = try? Data(contentsOf: url) else { return } + let coreDataMovie = CoreDataMovie(title: movie.title, + poster: data, + id: movie.id, + date: Date.now, + voteAverage: movie.voteAverage) + CoreDataManager().saveNewMovie(coreDataMovie) + } + +} diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/Presenter/MovieInfoContentPresenterInteractorInterface.swift b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/Presenter/MovieInfoContentPresenterInteractorInterface.swift new file mode 100644 index 00000000..57a2d71e --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/Presenter/MovieInfoContentPresenterInteractorInterface.swift @@ -0,0 +1,12 @@ +// +// MovieInfoContentPresenterInteractorInterface.swift +// MovieInfoContent +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import Foundation + +protocol MovieInfoContentPresenterInteractorInterface: PresenterInteractorInterface { + +} diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/Presenter/MovieInfoContentPresenterRouterInterface.swift b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/Presenter/MovieInfoContentPresenterRouterInterface.swift new file mode 100644 index 00000000..d4ce3d08 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/Presenter/MovieInfoContentPresenterRouterInterface.swift @@ -0,0 +1,12 @@ +// +// MovieInfoContentPresenterRouterInterface.swift +// MovieInfoContent +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import Foundation + +protocol MovieInfoContentPresenterRouterInterface: PresenterRouterInterface { + +} diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/Presenter/MovieInfoContentPresenterViewInterface.swift b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/Presenter/MovieInfoContentPresenterViewInterface.swift new file mode 100644 index 00000000..219acd36 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/Presenter/MovieInfoContentPresenterViewInterface.swift @@ -0,0 +1,15 @@ +// +// MovieInfoContentPresenterViewInterface.swift +// MovieInfoContent +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import Foundation +import UIKit + +protocol MovieInfoContentPresenterViewInterface: PresenterViewInterface { + func viewDidLoad() + func getMovieImage(path: String) -> UIImage? + func addToWatchListTapped(movie: MovieDetail) +} diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/Router/MovieInfoContentRouter.swift b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/Router/MovieInfoContentRouter.swift new file mode 100644 index 00000000..c3c3cd74 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/Router/MovieInfoContentRouter.swift @@ -0,0 +1,19 @@ +// +// MovieInfoContentRouter.swift +// MovieInfoContent +// +// Created by mohannazakizadeh on 4/29/22. +// + +import UIKit + +final class MovieInfoContentRouter: RouterInterface { + + weak var presenter: MovieInfoContentPresenterRouterInterface! + + weak var viewController: UIViewController? +} + +extension MovieInfoContentRouter: MovieInfoContentRouterInterface { + +} diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/Router/MovieInfoContentRouterInterface.swift b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/Router/MovieInfoContentRouterInterface.swift new file mode 100644 index 00000000..c2dbb1d6 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/Router/MovieInfoContentRouterInterface.swift @@ -0,0 +1,12 @@ +// +// MovieInfoContentRouterInterface.swift +// MovieInfoContent +// +// Created by mohannazakizadeh on 4/29/22. +// + +import UIKit + +protocol MovieInfoContentRouterInterface: RouterPresenterInterface { + +} diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/View/MovieInfoContentView.swift b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/View/MovieInfoContentView.swift new file mode 100644 index 00000000..eebc9d51 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/View/MovieInfoContentView.swift @@ -0,0 +1,91 @@ +// +// MovieInfoContentView.swift +// MovieInfoContent +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import UIKit + +final class MovieInfoContentView: UIViewController, ViewInterface { + + var presenter: MovieInfoContentPresenterViewInterface! + + // MARK: - Properties + var imageView: UIImageView! + var addToWatchListButton: UIButton! + + var movie: MovieDetail! + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + addToWatchListButton = setupButton() + imageView = setupImageView() + + setupView() + + self.applyTheme() + self.presenter.viewDidLoad() + } + + // MARK: - Theme + func applyTheme() { + view.backgroundColor = .secondarySystemBackground + addToWatchListButton.backgroundColor = .secondaryLabel + addToWatchListButton.tintColor = .systemBackground + } + + // MARK: - Private functions + private func setupImageView() -> UIImageView { + let image = presenter.getMovieImage(path: movie.poster ?? "") + let imageView = UIImageView(image: image) + imageView.clipsToBounds = true + imageView.layer.cornerRadius = 14 + imageView.translatesAutoresizingMaskIntoConstraints = false + + return imageView + } + + private func setupButton() -> UIButton { + let button = UIButton() + button.addTarget(self, action: #selector(addToWatchListTapped), for: .touchUpInside) + button.setBackgroundImage(UIImage(systemName: "bookmark.circle"), for: .normal) + button.layer.cornerRadius = 20 + return button + } + + private func setupView() { + view.addSubview(imageView) + view.addSubview(addToWatchListButton) + + imageView.translatesAutoresizingMaskIntoConstraints = false + addToWatchListButton.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + imageView.topAnchor.constraint(equalTo: view.topAnchor, constant: 32), + imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 45), + imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -45), + imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor, + constant: -((UIScreen.main.bounds.height / 2.2) + 32)), + + addToWatchListButton.bottomAnchor.constraint(equalTo: imageView.bottomAnchor, constant: -15), + addToWatchListButton.trailingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: -15), + addToWatchListButton.heightAnchor.constraint(equalToConstant: 40), + addToWatchListButton.widthAnchor.constraint(equalToConstant: 40) + ]) + } + + // MARK: - Actions + @objc func addToWatchListTapped() { + presenter.addToWatchListTapped(movie: movie) + } + +} + +extension MovieInfoContentView: MovieInfoContentViewInterface { + +} diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/View/MovieInfoContentViewInterface.swift b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/View/MovieInfoContentViewInterface.swift new file mode 100644 index 00000000..0382b286 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/MovieDetails/View/Components/MovieInfoContent/View/MovieInfoContentViewInterface.swift @@ -0,0 +1,12 @@ +// +// MovieInfoContentViewInterface.swift +// MovieInfoContent +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import UIKit + +protocol MovieInfoContentViewInterface: ViewPresenterInterface { + +} diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/View/MovieDetailsView.swift b/Movie-Application/Movie-Application/Modules/MovieDetails/View/MovieDetailsView.swift new file mode 100644 index 00000000..9a4fceac --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/MovieDetails/View/MovieDetailsView.swift @@ -0,0 +1,29 @@ +// +// MovieDetailsView.swift +// MovieDetails +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import UIKit + +final class MovieDetailsView: BottomSheetContainerViewController +, ViewInterface { + + var presenter: MovieDetailsPresenterViewInterface! + + // MARK: - Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + self.applyTheme() + } + // MARK: - Theme + func applyTheme() { + view.backgroundColor = .systemBackground + } + +} + +extension MovieDetailsView: MovieDetailsViewInterface { + +} diff --git a/Movie-Application/Movie-Application/Modules/MovieDetails/View/MovieDetailsViewInterface.swift b/Movie-Application/Movie-Application/Modules/MovieDetails/View/MovieDetailsViewInterface.swift new file mode 100644 index 00000000..ad9aab77 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/MovieDetails/View/MovieDetailsViewInterface.swift @@ -0,0 +1,12 @@ +// +// MovieDetailsViewInterface.swift +// MovieDetails +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import UIKit + +protocol MovieDetailsViewInterface: ViewPresenterInterface { + +} diff --git a/Movie-Application/Movie-Application/Modules/PopularMovies/Interactor/PopularMoviesInteractor.swift b/Movie-Application/Movie-Application/Modules/PopularMovies/Interactor/PopularMoviesInteractor.swift new file mode 100644 index 00000000..bd177ae6 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/PopularMovies/Interactor/PopularMoviesInteractor.swift @@ -0,0 +1,43 @@ +// +// PopularMoviesInteractor.swift +// PopularMovies +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import Foundation +import UIKit + +final class PopularMoviesInteractor: InteractorInterface { + + weak var presenter: PopularMoviesPresenterInteractorInterface! +} + +extension PopularMoviesInteractor: PopularMoviesInteractorInterface { + func getPopularMovies(page: Int, completionHandler: @escaping MoviesCompletionHandler) { + MoviesService.shared.getPopularMovies(page: page) { result in + completionHandler(result) + } + } + + func getMovieImage(for path: String, completion: @escaping (UIImage) -> Void) { + + DispatchQueue.global(qos: .utility).async { + let url = URL(string: "https://image.tmdb.org/t/p/w300/" + path)! + guard let data = try? Data(contentsOf: url) else { return } + let image = UIImage(data: data) ?? UIImage(systemName: "film.circle")! + + DispatchQueue.main.async { + completion(image) + } + } + + } + + func getMovieDetails(id: Int, completionHandler: @escaping MovieDetailsCompletionHandler) { + MoviesService.shared.getMovieDetails(id: id) { result in + completionHandler(result) + } + } + +} diff --git a/Movie-Application/Movie-Application/Modules/PopularMovies/Interactor/PopularMoviesInteractorInterface.swift b/Movie-Application/Movie-Application/Modules/PopularMovies/Interactor/PopularMoviesInteractorInterface.swift new file mode 100644 index 00000000..1c3c07ee --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/PopularMovies/Interactor/PopularMoviesInteractorInterface.swift @@ -0,0 +1,16 @@ +// +// PopularMoviesInteractorInterface.swift +// PopularMovies +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import Foundation +import UIKit + +protocol PopularMoviesInteractorInterface: InteractorPresenterInterface { + func getPopularMovies(page: Int, completionHandler: @escaping MoviesCompletionHandler) + func getMovieImage(for path: String, completion: @escaping (UIImage) -> Void) + + func getMovieDetails(id: Int, completionHandler: @escaping MovieDetailsCompletionHandler) +} diff --git a/Movie-Application/Movie-Application/Modules/PopularMovies/PopularMoviesModule.swift b/Movie-Application/Movie-Application/Modules/PopularMovies/PopularMoviesModule.swift new file mode 100644 index 00000000..d3a8f7d1 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/PopularMovies/PopularMoviesModule.swift @@ -0,0 +1,39 @@ +// +// PopularMoviesModule.swift +// PopularMovies +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import UIKit + +// MARK: - module builder + +final class PopularMoviesModule: ModuleInterface { + + typealias View = PopularMoviesView + typealias Presenter = PopularMoviesPresenter + typealias Router = PopularMoviesRouter + typealias Interactor = PopularMoviesInteractor + + func build() -> UIViewController { + guard let navigationController = UIStoryboard(name: "PopularMovies", + bundle: nil).instantiateInitialViewController() + as? UINavigationController else { + return UINavigationController() + } + guard let view = navigationController.topViewController as? View else { + return View() + } + + let interactor = Interactor() + let presenter = Presenter() + let router = Router() + + self.assemble(view: view, presenter: presenter, router: router, interactor: interactor) + + router.viewController = navigationController + + return navigationController + } +} diff --git a/Movie-Application/Movie-Application/Modules/PopularMovies/Presenter/PopularMoviesPresenter.swift b/Movie-Application/Movie-Application/Modules/PopularMovies/Presenter/PopularMoviesPresenter.swift new file mode 100644 index 00000000..7628dfae --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/PopularMovies/Presenter/PopularMoviesPresenter.swift @@ -0,0 +1,181 @@ +// +// PopularMoviesPresenter.swift +// PopularMovies +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import Foundation +import UIKit + +final class PopularMoviesPresenter: PresenterInterface { + + var router: PopularMoviesRouterInterface! + var interactor: PopularMoviesInteractorInterface! + weak var view: PopularMoviesViewInterface! + + var movies: [Movie]? + private var currentPage = 1 + + init() { + // in order to scroll top top when user tapped te tab bar again + NotificationCenter.default.addObserver(forName: TabBarViewContorller.tabBarDidTapNotification, + object: nil, + queue: nil) { (_) in + if let view = self.view { + view.scrollToTop() + } + } + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + +} + +extension PopularMoviesPresenter: PopularMoviesPresenterRouterInterface { + +} + +extension PopularMoviesPresenter: PopularMoviesPresenterInteractorInterface { + +} + +extension PopularMoviesPresenter: PopularMoviesPresenterViewInterface { + + func viewDidLoad() { + getPopularMovies() + } + + func alertRetryButtonDidTap() { + getPopularMovies() + } + + func getPopularMovies() { + // movie data base gives 500 pages max. + if currentPage <= 500 { + interactor.getPopularMovies(page: currentPage) { result in + switch result { + case .success(let moviesData): + + if self.currentPage == 1 { + self.movies = moviesData.results + } else { + self.movies! += moviesData.results + } + + self.view.reloadCollectionView() + self.currentPage += 1 + + case .failure(let error): + self.view.showError(with: error) + } + } + } + } + + func getMovieImage(index: Int, completion: @escaping (UIImage) -> Void) { + if let movies = movies { + if let path = movies[index].poster { + return interactor.getMovieImage(for: path, completion: completion) + } + } else { + completion(UIImage(systemName: "film.circle")!) + } + } + + func getMovieTitle(index: Int) -> String { + movies?[index].title ?? "" + } + + func movieSelected(at index: Int) { + if let movies = movies { + interactor.getMovieDetails(id: movies[index].id) { [weak self] result in + switch result { + case .success(let movie): + self?.router.showMovieDetails(movie) + + case .failure(let error): + self?.view.showError(with: error) + } + } + } + + } + + func addToWatchList(index: Int, imageData: Data) { + if let movies = movies { + let savedMovie = CoreDataMovie(title: movies[index].title, + poster: imageData, + id: movies[index].id, + date: Date.now, + voteAverage: movies[index].voteAverage) + CoreDataManager().saveNewMovie(savedMovie) + } + } + + func getSavedMovies() -> [CoreDataMovie] { + CoreDataManager().getSavedMovies() + } + + func configureContextMenu(index: Int, imageData: Data) -> UIContextMenuConfiguration { + // prevents from adding repititious movies to watch list + if !self.getSavedMovies().contains(where: { $0.title == self.getMovieTitle(index: index) }) { + let context = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) -> UIMenu? in + let viewDetails = UIAction(title: "View Details", + image: UIImage(systemName: "text.below.photo.fill"), + identifier: nil, discoverabilityTitle: nil, + state: .off) { (_) in + self.movieSelected(at: index) + } + + let addToWatchList = UIAction(title: "Add to Watchlist", + image: UIImage(systemName: "bookmark"), + identifier: nil, discoverabilityTitle: nil, state: .off) { (_) in + self.addToWatchList(index: index, imageData: imageData) + } + + return UIMenu(title: self.getMovieTitle(index: index), + image: nil, identifier: nil, + options: UIMenu.Options.displayInline, + children: [addToWatchList, viewDetails]) + + } + return context + + } else { + let context = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) -> UIMenu? in + + let viewDetails = UIAction(title: "View Details", + image: UIImage(systemName: "text.below.photo.fill"), + identifier: nil, discoverabilityTitle: nil, + state: .off) { (_) in + self.movieSelected(at: index) + } + + let addToWatchList = UIAction(title: "Added to Watchlist", + image: UIImage(systemName: "bookmark.fill"), + identifier: nil, discoverabilityTitle: nil, + state: .off) { (_) in + } + + return UIMenu(title: self.getMovieTitle(index: index), + image: nil, identifier: nil, + options: UIMenu.Options.displayInline, + children: [addToWatchList, viewDetails]) + + } + return context + } + } + + var numberOfMovies: Int { + return movies?.count ?? 0 + } + + var popularMovies: [Movie] { + return movies ?? [] + } + +} diff --git a/Movie-Application/Movie-Application/Modules/PopularMovies/Presenter/PopularMoviesPresenterInteractorInterface.swift b/Movie-Application/Movie-Application/Modules/PopularMovies/Presenter/PopularMoviesPresenterInteractorInterface.swift new file mode 100644 index 00000000..0360f337 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/PopularMovies/Presenter/PopularMoviesPresenterInteractorInterface.swift @@ -0,0 +1,12 @@ +// +// PopularMoviesPresenterInteractorInterface.swift +// PopularMovies +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import Foundation + +protocol PopularMoviesPresenterInteractorInterface: PresenterInteractorInterface { + +} diff --git a/Movie-Application/Movie-Application/Modules/PopularMovies/Presenter/PopularMoviesPresenterRouterInterface.swift b/Movie-Application/Movie-Application/Modules/PopularMovies/Presenter/PopularMoviesPresenterRouterInterface.swift new file mode 100644 index 00000000..c4cd756d --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/PopularMovies/Presenter/PopularMoviesPresenterRouterInterface.swift @@ -0,0 +1,12 @@ +// +// PopularMoviesPresenterRouterInterface.swift +// PopularMovies +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import Foundation + +protocol PopularMoviesPresenterRouterInterface: PresenterRouterInterface { + +} diff --git a/Movie-Application/Movie-Application/Modules/PopularMovies/Presenter/PopularMoviesPresenterViewInterface.swift b/Movie-Application/Movie-Application/Modules/PopularMovies/Presenter/PopularMoviesPresenterViewInterface.swift new file mode 100644 index 00000000..b6a39cc3 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/PopularMovies/Presenter/PopularMoviesPresenterViewInterface.swift @@ -0,0 +1,24 @@ +// +// PopularMoviesPresenterViewInterface.swift +// PopularMovies +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import Foundation +import UIKit + +protocol PopularMoviesPresenterViewInterface: PresenterViewInterface { + func viewDidLoad() + func alertRetryButtonDidTap() + func getMovieImage(index: Int, completion: @escaping (UIImage) -> Void) + func getMovieTitle(index: Int) -> String + func movieSelected(at index: Int) + func addToWatchList(index: Int, imageData: Data) + func getPopularMovies() + func getSavedMovies() -> [CoreDataMovie] + func configureContextMenu(index: Int, imageData: Data) -> UIContextMenuConfiguration + + var numberOfMovies: Int { get } + var popularMovies: [Movie] { get } +} diff --git a/Movie-Application/Movie-Application/Modules/PopularMovies/Router/PopularMoviesRouter.swift b/Movie-Application/Movie-Application/Modules/PopularMovies/Router/PopularMoviesRouter.swift new file mode 100644 index 00000000..99a49e80 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/PopularMovies/Router/PopularMoviesRouter.swift @@ -0,0 +1,24 @@ +// +// PopularMoviesRouter.swift +// PopularMovies +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import UIKit + +final class PopularMoviesRouter: RouterInterface { + + weak var presenter: PopularMoviesPresenterRouterInterface! + + weak var viewController: UIViewController? +} + +extension PopularMoviesRouter: PopularMoviesRouterInterface { + + func showMovieDetails(_ movie: MovieDetail) { + let movieDetailsViewController = MovieDetailsModule().build(movie: movie) + viewController?.show(movieDetailsViewController, sender: nil) + } + +} diff --git a/Movie-Application/Movie-Application/Modules/PopularMovies/Router/PopularMoviesRouterInterface.swift b/Movie-Application/Movie-Application/Modules/PopularMovies/Router/PopularMoviesRouterInterface.swift new file mode 100644 index 00000000..24a44dbf --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/PopularMovies/Router/PopularMoviesRouterInterface.swift @@ -0,0 +1,12 @@ +// +// PopularMoviesRouterInterface.swift +// PopularMovies +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import UIKit + +protocol PopularMoviesRouterInterface: RouterPresenterInterface { + func showMovieDetails(_ movie: MovieDetail) +} diff --git a/Movie-Application/Movie-Application/Modules/PopularMovies/View/PopularMovies.storyboard b/Movie-Application/Movie-Application/Modules/PopularMovies/View/PopularMovies.storyboard new file mode 100644 index 00000000..8f0d2750 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/PopularMovies/View/PopularMovies.storyboard @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Movie-Application/Movie-Application/Modules/PopularMovies/View/PopularMoviesView.swift b/Movie-Application/Movie-Application/Modules/PopularMovies/View/PopularMoviesView.swift new file mode 100644 index 00000000..7da4436c --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/PopularMovies/View/PopularMoviesView.swift @@ -0,0 +1,169 @@ +// +// PopularMoviesView.swift +// PopularMovies +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import UIKit + +final class PopularMoviesView: UIViewController, ViewInterface { + + var presenter: PopularMoviesPresenterViewInterface! + + // MARK: - Properties + @IBOutlet var collectionView: UICollectionView! + + private let movieImagesCache = NSCache() + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + setupView() + self.presenter.viewDidLoad() + } + + // MARK: - Theme + + func applyTheme() { + view.backgroundColor = .systemBackground + } + + // MARK: - Private functions + private func setupView() { + configureNavigation() + setupCollectionView() + self.applyTheme() + } + + // function to setup and configure navigation details + private func configureNavigation() { + navigationController?.navigationBar.prefersLargeTitles = true + self.title = "Popular" + } + + // function to setup and configure collectionView details + private func setupCollectionView() { + collectionView.delegate = self + collectionView.dataSource = self + collectionView.register(MovieCell.self, forCellWithReuseIdentifier: "MovieCell") + } + + func configureContextMenu(index: Int, imageData: Data) -> UIContextMenuConfiguration { + presenter.configureContextMenu(index: index, imageData: imageData) + } + + func configurePagination(_ cellRow: Int) { + if cellRow == presenter.numberOfMovies - 1 { + presenter.getPopularMovies() + } + } + +} + +extension PopularMoviesView: PopularMoviesViewInterface { + + func showError(with error: RequestError) { + let errorAlert = UIAlertController(title: "Error Occured", + message: error.errorDescription, + preferredStyle: .alert) + let alertAction = UIAlertAction(title: "Retry", style: .default) { [weak self] (_) in + self?.presenter.alertRetryButtonDidTap() + } + errorAlert.addAction(alertAction) + self.present(errorAlert, animated: true, completion: nil) + } + + func reloadCollectionView() { + collectionView.reloadData() + } + + func scrollToTop() { + // checks if collection view has cells then scroll to top + if collectionView?.numberOfItems(inSection: 0) ?? 0 > 0 { + collectionView?.scrollToItem(at: IndexPath(row: 0, section: 0), at: .top, animated: true) + } + } + +} + +extension PopularMoviesView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + presenter.numberOfMovies + } + + func collectionView(_ collectionView: UICollectionView, + willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + configurePagination(indexPath.row) + + // for caching cell movie image + guard let cell = cell as? MovieCell else { return } + let cellNumber = NSNumber(value: indexPath.item) + + if let cachedImage = self.movieImagesCache.object(forKey: cellNumber) { + cell.movieImageView.image = cachedImage + } else { + self.presenter.getMovieImage(index: indexPath.row, completion: { [weak self] (image) in + if collectionView.indexPath(for: cell) == indexPath { + cell.movieImageView.image = image + } + self?.movieImagesCache.setObject(image, forKey: cellNumber) + }) + } + + } + + func collectionView(_ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MovieCell", + for: indexPath) as? MovieCell + else { return UICollectionViewCell() } + cell.layer.cornerRadius = 10 + return cell + } + + func numberOfSections(in collectionView: UICollectionView) -> Int { + 1 + } + + func collectionView(_ collectionView: UICollectionView, + contextMenuConfigurationForItemAt indexPath: IndexPath, + point: CGPoint) -> UIContextMenuConfiguration? { + + let cellNumber = NSNumber(value: indexPath.item) + + if let cachedImage = self.movieImagesCache.object(forKey: cellNumber) { + return configureContextMenu(index: indexPath.row, + imageData: cachedImage.jpegData(compressionQuality: 1.0) ?? Data()) + } + + return configureContextMenu(index: indexPath.row, imageData: Data()) + } + + func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath) -> CGSize { + let noOfCellsInRow = 2 + + guard let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout + else { + return CGSize(width: 0, height: 0) + + } + flowLayout.minimumInteritemSpacing = 10 + flowLayout.minimumLineSpacing = 10 + flowLayout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) + + let totalSpace = flowLayout.sectionInset.left + + flowLayout.sectionInset.right + + (flowLayout.minimumInteritemSpacing * CGFloat(noOfCellsInRow - 1)) + + let size = Int((view.bounds.width - totalSpace) / CGFloat(noOfCellsInRow)) + return CGSize(width: size, height: size + 50) + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + presenter.movieSelected(at: indexPath.row) + } +} diff --git a/Movie-Application/Movie-Application/Modules/PopularMovies/View/PopularMoviesViewInterface.swift b/Movie-Application/Movie-Application/Modules/PopularMovies/View/PopularMoviesViewInterface.swift new file mode 100644 index 00000000..87f6d6d2 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/PopularMovies/View/PopularMoviesViewInterface.swift @@ -0,0 +1,14 @@ +// +// PopularMoviesViewInterface.swift +// PopularMovies +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import UIKit + +protocol PopularMoviesViewInterface: ViewPresenterInterface { + func showError(with error: RequestError) + func reloadCollectionView() + func scrollToTop() +} diff --git a/Movie-Application/Movie-Application/Modules/TopRatedMovies/.DS_Store b/Movie-Application/Movie-Application/Modules/TopRatedMovies/.DS_Store new file mode 100644 index 00000000..92c59b1b Binary files /dev/null and b/Movie-Application/Movie-Application/Modules/TopRatedMovies/.DS_Store differ diff --git a/Movie-Application/Movie-Application/Modules/TopRatedMovies/Interactor/TopRatedMoviesInteractor.swift b/Movie-Application/Movie-Application/Modules/TopRatedMovies/Interactor/TopRatedMoviesInteractor.swift new file mode 100644 index 00000000..1ac3f135 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/TopRatedMovies/Interactor/TopRatedMoviesInteractor.swift @@ -0,0 +1,42 @@ +// +// TopRatedMoviesInteractor.swift +// TopRatedMovies +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import Foundation +import UIKit + +final class TopRatedMoviesInteractor: InteractorInterface { + + weak var presenter: TopRatedMoviesPresenterInteractorInterface! +} + +extension TopRatedMoviesInteractor: TopRatedMoviesInteractorInterface { + func getTopRatedMovies(page: Int, completionHandler: @escaping MoviesCompletionHandler) { + MoviesService.shared.getTopRatedMovies(page: page) { result in + completionHandler(result) + } + } + + func getMovieImage(for path: String, completion: @escaping (UIImage) -> Void) { + + DispatchQueue.global(qos: .utility).async { + let url = URL(string: "https://image.tmdb.org/t/p/w300/" + path)! + guard let data = try? Data(contentsOf: url) else { return } + let image = UIImage(data: data) ?? UIImage(systemName: "film.circle")! + + DispatchQueue.main.async { + completion(image) + } + } + + } + + func getMovieDetails(id: Int, completionHandler: @escaping MovieDetailsCompletionHandler) { + MoviesService.shared.getMovieDetails(id: id) { result in + completionHandler(result) + } + } +} diff --git a/Movie-Application/Movie-Application/Modules/TopRatedMovies/Interactor/TopRatedMoviesInteractorInterface.swift b/Movie-Application/Movie-Application/Modules/TopRatedMovies/Interactor/TopRatedMoviesInteractorInterface.swift new file mode 100644 index 00000000..0f1edb19 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/TopRatedMovies/Interactor/TopRatedMoviesInteractorInterface.swift @@ -0,0 +1,15 @@ +// +// TopRatedMoviesInteractorInterface.swift +// TopRatedMovies +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import Foundation +import UIKit + +protocol TopRatedMoviesInteractorInterface: InteractorPresenterInterface { + func getTopRatedMovies(page: Int, completionHandler: @escaping MoviesCompletionHandler) + func getMovieImage(for path: String, completion: @escaping (UIImage) -> Void) + func getMovieDetails(id: Int, completionHandler: @escaping MovieDetailsCompletionHandler) +} diff --git a/Movie-Application/Movie-Application/Modules/TopRatedMovies/Presenter/TopRatedMoviesPresenter.swift b/Movie-Application/Movie-Application/Modules/TopRatedMovies/Presenter/TopRatedMoviesPresenter.swift new file mode 100644 index 00000000..d725ad70 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/TopRatedMovies/Presenter/TopRatedMoviesPresenter.swift @@ -0,0 +1,186 @@ +// +// TopRatedMoviesPresenter.swift +// TopRatedMovies +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import Foundation +import UIKit + +final class TopRatedMoviesPresenter: PresenterInterface { + + var router: TopRatedMoviesRouterInterface! + var interactor: TopRatedMoviesInteractorInterface! + weak var view: TopRatedMoviesViewInterface! + + var movies: [Movie]? + private var currentPage = 1 + + init() { + // in order to scroll top top when user tapped te tab bar again + NotificationCenter.default.addObserver(forName: TabBarViewContorller.tabBarDidTapNotification, + object: nil, queue: nil) { (_) in + if let view = self.view { + view.scrollToTop() + } + } + } + + deinit { + NotificationCenter.default.removeObserver(self) + } +} + +extension TopRatedMoviesPresenter: TopRatedMoviesPresenterRouterInterface { + +} + +extension TopRatedMoviesPresenter: TopRatedMoviesPresenterInteractorInterface { + +} + +extension TopRatedMoviesPresenter: TopRatedMoviesPresenterViewInterface { + + func viewDidLoad() { + getTopRatedMovies() + } + + func alertRetryButtonDidTap() { + getTopRatedMovies() + } + + // function to get movie image from url that we have + func getMovieImage(index: Int, completion: @escaping (UIImage) -> Void) { + + if let movies = movies { + if let path = movies[index].poster { + return interactor.getMovieImage(for: path, completion: completion) + } + } else { + completion(UIImage(systemName: "film.circle")!) + } + + } + func getMovieTitle(index: Int) -> String { + movies?[index].title ?? "" + } + + func movieSelected(at index: Int) { + if let movies = movies { + interactor.getMovieDetails(id: movies[index].id) { [weak self] result in + switch result { + case .success(let movie): + self?.router.showMovieDetails(movie) + + case .failure(let error): + self?.view.showError(with: error) + } + } + } + + } + + func addToWatchList(index: Int, imageData: Data) { + if let movies = movies { + let savedMovie = CoreDataMovie(title: movies[index].title, + poster: imageData, + id: movies[index].id, + date: Date.now, + voteAverage: movies[index].voteAverage) + CoreDataManager().saveNewMovie(savedMovie) + } + } + + func getTopRatedMovies() { + // movie data base gives 495 pages max. + if currentPage < 496 { + interactor.getTopRatedMovies(page: currentPage) { result in + switch result { + case .success(let moviesData): + + if self.currentPage == 1 { + self.movies = moviesData.results + } else { + self.movies! += moviesData.results + } + + self.view.reloadCollectionView() + self.currentPage += 1 + + case .failure(let error): + self.view.showError(with: error) + } + } + } + + } + + func getSavedMovies() -> [CoreDataMovie] { + CoreDataManager().getSavedMovies() + } + + func configureContextMenu(index: Int, imageData: Data) -> UIContextMenuConfiguration { + + // prevents from adding repititious movies to watch list + if !self.getSavedMovies().contains(where: { $0.title == self.getMovieTitle(index: index)}) { + let context = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) -> UIMenu? in + + let viewDetails = UIAction(title: "View Details", + image: UIImage(systemName: "text.below.photo.fill"), + identifier: nil, + discoverabilityTitle: nil, state: .off) { (_) in + self.movieSelected(at: index) + } + + let addToWatchList = UIAction(title: "Add to Watchlist", + image: UIImage(systemName: "bookmark"), + identifier: nil, + discoverabilityTitle: nil, state: .off) { (_) in + self.addToWatchList(index: index, imageData: imageData) + } + + return UIMenu(title: self.getMovieTitle(index: index), + image: nil, identifier: nil, + options: UIMenu.Options.displayInline, + children: [addToWatchList, viewDetails]) + + } + return context + + } else { + let context = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) -> UIMenu? in + + let viewDetails = UIAction(title: "View Details", + image: UIImage(systemName: "text.below.photo.fill"), + identifier: nil, discoverabilityTitle: nil, + state: .off) { (_) in + self.movieSelected(at: index) + } + + let addToWatchList = UIAction(title: "Added to Watchlist", + image: UIImage(systemName: "bookmark.fill"), + identifier: nil, discoverabilityTitle: nil, + state: .off) { (_) in + + } + + return UIMenu(title: self.getMovieTitle(index: index), + image: nil, identifier: nil, + options: UIMenu.Options.displayInline, + children: [addToWatchList, viewDetails]) + + } + return context + } + } + + var numberOfMovies: Int { + return movies?.count ?? 0 + } + + var topRatedMovies: [Movie] { + return movies ?? [] + } + +} diff --git a/Movie-Application/Movie-Application/Modules/TopRatedMovies/Presenter/TopRatedMoviesPresenterInteractorInterface.swift b/Movie-Application/Movie-Application/Modules/TopRatedMovies/Presenter/TopRatedMoviesPresenterInteractorInterface.swift new file mode 100644 index 00000000..86b9fcdc --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/TopRatedMovies/Presenter/TopRatedMoviesPresenterInteractorInterface.swift @@ -0,0 +1,12 @@ +// +// TopRatedMoviesPresenterInteractorInterface.swift +// TopRatedMovies +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import Foundation + +protocol TopRatedMoviesPresenterInteractorInterface: PresenterInteractorInterface { + +} diff --git a/Movie-Application/Movie-Application/Modules/TopRatedMovies/Presenter/TopRatedMoviesPresenterRouterInterface.swift b/Movie-Application/Movie-Application/Modules/TopRatedMovies/Presenter/TopRatedMoviesPresenterRouterInterface.swift new file mode 100644 index 00000000..45d56495 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/TopRatedMovies/Presenter/TopRatedMoviesPresenterRouterInterface.swift @@ -0,0 +1,12 @@ +// +// TopRatedMoviesPresenterRouterInterface.swift +// TopRatedMovies +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import Foundation + +protocol TopRatedMoviesPresenterRouterInterface: PresenterRouterInterface { + +} diff --git a/Movie-Application/Movie-Application/Modules/TopRatedMovies/Presenter/TopRatedMoviesPresenterViewInterface.swift b/Movie-Application/Movie-Application/Modules/TopRatedMovies/Presenter/TopRatedMoviesPresenterViewInterface.swift new file mode 100644 index 00000000..f1f9118d --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/TopRatedMovies/Presenter/TopRatedMoviesPresenterViewInterface.swift @@ -0,0 +1,24 @@ +// +// TopRatedMoviesPresenterViewInterface.swift +// TopRatedMovies +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import Foundation +import UIKit + +protocol TopRatedMoviesPresenterViewInterface: PresenterViewInterface { + func viewDidLoad() + func alertRetryButtonDidTap() + func getMovieImage(index: Int, completion: @escaping (UIImage) -> Void) + func getMovieTitle(index: Int) -> String + func movieSelected(at index: Int) + func addToWatchList(index: Int, imageData: Data) + func getTopRatedMovies() + func getSavedMovies() -> [CoreDataMovie] + func configureContextMenu(index: Int, imageData: Data) -> UIContextMenuConfiguration + + var topRatedMovies: [Movie] { get } + var numberOfMovies: Int { get } +} diff --git a/Movie-Application/Movie-Application/Modules/TopRatedMovies/Router/TopRatedMoviesRouter.swift b/Movie-Application/Movie-Application/Modules/TopRatedMovies/Router/TopRatedMoviesRouter.swift new file mode 100644 index 00000000..b940d5f8 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/TopRatedMovies/Router/TopRatedMoviesRouter.swift @@ -0,0 +1,23 @@ +// +// TopRatedMoviesRouter.swift +// TopRatedMovies +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import UIKit + +final class TopRatedMoviesRouter: RouterInterface { + + weak var presenter: TopRatedMoviesPresenterRouterInterface! + + weak var viewController: UIViewController? +} + +extension TopRatedMoviesRouter: TopRatedMoviesRouterInterface { + + func showMovieDetails(_ movie: MovieDetail) { + let movieDetailsViewController = MovieDetailsModule().build(movie: movie) + viewController?.show(movieDetailsViewController, sender: nil) + } +} diff --git a/Movie-Application/Movie-Application/Modules/TopRatedMovies/Router/TopRatedMoviesRouterInterface.swift b/Movie-Application/Movie-Application/Modules/TopRatedMovies/Router/TopRatedMoviesRouterInterface.swift new file mode 100644 index 00000000..7a8ac4d7 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/TopRatedMovies/Router/TopRatedMoviesRouterInterface.swift @@ -0,0 +1,12 @@ +// +// TopRatedMoviesRouterInterface.swift +// TopRatedMovies +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import UIKit + +protocol TopRatedMoviesRouterInterface: RouterPresenterInterface { + func showMovieDetails(_ movie: MovieDetail) +} diff --git a/Movie-Application/Movie-Application/Modules/TopRatedMovies/TopRatedMoviesModule.swift b/Movie-Application/Movie-Application/Modules/TopRatedMovies/TopRatedMoviesModule.swift new file mode 100644 index 00000000..267d02b8 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/TopRatedMovies/TopRatedMoviesModule.swift @@ -0,0 +1,39 @@ +// +// TopRatedMoviesModule.swift +// TopRatedMovies +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import UIKit + +// MARK: - module builder + +final class TopRatedMoviesModule: ModuleInterface { + + typealias View = TopRatedMoviesView + typealias Presenter = TopRatedMoviesPresenter + typealias Router = TopRatedMoviesRouter + typealias Interactor = TopRatedMoviesInteractor + + func build() -> UIViewController { + guard let navigationController = UIStoryboard(name: "TopRatedMovies", + bundle: nil).instantiateInitialViewController() + as? UINavigationController else { + return UINavigationController() + } + guard let view = navigationController.topViewController as? View else { + return View() + } + + let interactor = Interactor() + let presenter = Presenter() + let router = Router() + + self.assemble(view: view, presenter: presenter, router: router, interactor: interactor) + + router.viewController = navigationController + + return navigationController + } +} diff --git a/Movie-Application/Movie-Application/Modules/TopRatedMovies/View/TopRatedMovies.storyboard b/Movie-Application/Movie-Application/Modules/TopRatedMovies/View/TopRatedMovies.storyboard new file mode 100644 index 00000000..c4bfdd64 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/TopRatedMovies/View/TopRatedMovies.storyboard @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Movie-Application/Movie-Application/Modules/TopRatedMovies/View/TopRatedMoviesView.swift b/Movie-Application/Movie-Application/Modules/TopRatedMovies/View/TopRatedMoviesView.swift new file mode 100644 index 00000000..8014743b --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/TopRatedMovies/View/TopRatedMoviesView.swift @@ -0,0 +1,168 @@ +// +// TopRatedMoviesView.swift +// TopRatedMovies +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import UIKit + +final class TopRatedMoviesView: UIViewController, ViewInterface { + + var presenter: TopRatedMoviesPresenterViewInterface! + + // MARK: - Properties + @IBOutlet weak var collectionView: UICollectionView! + + private let movieImagesCache = NSCache() + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + setupView() + self.presenter.viewDidLoad() + } + + // MARK: - Theme + + func applyTheme() { + view.backgroundColor = .systemBackground + } + + // MARK: - Private functions + + private func setupView() { + configureNavigation() + setupCollectionView() + + self.applyTheme() + } + + // function to setup and configure navigation details + private func configureNavigation() { + navigationController?.navigationBar.prefersLargeTitles = true + self.title = "Top Rated" + } + + // function to setup and configure collectionView details + private func setupCollectionView() { + collectionView.delegate = self + collectionView.dataSource = self + collectionView.register(MovieCell.self, forCellWithReuseIdentifier: "MovieCell") + } + + func configureContextMenu(index: Int, imageData: Data) -> UIContextMenuConfiguration { + presenter.configureContextMenu(index: index, imageData: imageData) + } + + func configurePagination(_ cellRow: Int) { + if cellRow == presenter.numberOfMovies - 1 { + presenter.getTopRatedMovies() + } + } + +} + +extension TopRatedMoviesView: TopRatedMoviesViewInterface { + + func showError(with error: RequestError) { + let errorAlert = UIAlertController(title: "Error Occured", + message: error.errorDescription, + preferredStyle: .alert) + let alertAction = UIAlertAction(title: "Retry", style: .default) { [weak self] (_) in + self?.presenter.alertRetryButtonDidTap() + } + errorAlert.addAction(alertAction) + self.present(errorAlert, animated: true, completion: nil) + } + + func reloadCollectionView() { + collectionView.reloadData() + } + + func scrollToTop() { + // checks if collection view has cells then scroll to top + if collectionView?.numberOfItems(inSection: 0) ?? 0 > 0 { + collectionView?.scrollToItem(at: IndexPath(row: 0, section: 0), at: .top, animated: true) + } + } +} + +extension TopRatedMoviesView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + presenter.numberOfMovies + } + + func collectionView(_ collectionView: UICollectionView, + willDisplay cell: UICollectionViewCell, + forItemAt indexPath: IndexPath) { + configurePagination(indexPath.row) + + // for caching cell movie image + guard let cell = cell as? MovieCell else { return } + let cellNumber = NSNumber(value: indexPath.item) + + if let cachedImage = movieImagesCache.object(forKey: cellNumber) { + cell.movieImageView.image = cachedImage + } else { + self.presenter.getMovieImage(index: indexPath.row, completion: { [weak self] (image) in + if collectionView.indexPath(for: cell) == indexPath { + cell.movieImageView.image = image + } + self?.movieImagesCache.setObject(image, forKey: cellNumber) + }) + } + + } + + func collectionView(_ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MovieCell", + for: indexPath) as? MovieCell + else { return UICollectionViewCell() } + cell.layer.cornerRadius = 10 + return cell + } + + func numberOfSections(in collectionView: UICollectionView) -> Int { + 1 + } + + func collectionView(_ collectionView: UICollectionView, + contextMenuConfigurationForItemAt indexPath: IndexPath, + point: CGPoint) -> UIContextMenuConfiguration? { + let cellNumber = NSNumber(value: indexPath.item) + + if let cachedImage = self.movieImagesCache.object(forKey: cellNumber) { + return configureContextMenu(index: indexPath.row, + imageData: cachedImage.jpegData(compressionQuality: 1.0) ?? Data()) + } + + return configureContextMenu(index: indexPath.row, imageData: Data()) + } + + func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath) -> CGSize { + let noOfCellsInRow = 2 + + guard let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout else { + return CGSize(width: 0, height: 0) + } + flowLayout.minimumInteritemSpacing = 10 + flowLayout.minimumLineSpacing = 10 + flowLayout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) + + let totalSpace = flowLayout.sectionInset.left + + flowLayout.sectionInset.right + + (flowLayout.minimumInteritemSpacing * CGFloat(noOfCellsInRow - 1)) + + let size = Int((view.bounds.width - totalSpace) / CGFloat(noOfCellsInRow)) + return CGSize(width: size, height: size + 50) + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + presenter.movieSelected(at: indexPath.row) + } +} diff --git a/Movie-Application/Movie-Application/Modules/TopRatedMovies/View/TopRatedMoviesViewInterface.swift b/Movie-Application/Movie-Application/Modules/TopRatedMovies/View/TopRatedMoviesViewInterface.swift new file mode 100644 index 00000000..7eb35606 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/TopRatedMovies/View/TopRatedMoviesViewInterface.swift @@ -0,0 +1,14 @@ +// +// TopRatedMoviesViewInterface.swift +// TopRatedMovies +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import UIKit + +protocol TopRatedMoviesViewInterface: ViewPresenterInterface { + func showError(with error: RequestError) + func reloadCollectionView() + func scrollToTop() +} diff --git a/Movie-Application/Movie-Application/Modules/WatchlistMovies/.DS_Store b/Movie-Application/Movie-Application/Modules/WatchlistMovies/.DS_Store new file mode 100644 index 00000000..2237affe Binary files /dev/null and b/Movie-Application/Movie-Application/Modules/WatchlistMovies/.DS_Store differ diff --git a/Movie-Application/Movie-Application/Modules/WatchlistMovies/Interactor/WatchlistMoviesInteractor.swift b/Movie-Application/Movie-Application/Modules/WatchlistMovies/Interactor/WatchlistMoviesInteractor.swift new file mode 100644 index 00000000..86697a8b --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/WatchlistMovies/Interactor/WatchlistMoviesInteractor.swift @@ -0,0 +1,24 @@ +// +// WatchlistMoviesInteractor.swift +// WatchlistMovies +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import Foundation +import UIKit + +final class WatchlistMoviesInteractor: InteractorInterface { + + weak var presenter: WatchlistMoviesPresenterInteractorInterface! +} + +extension WatchlistMoviesInteractor: WatchlistMoviesInteractorInterface { + + func getMovieDetails(id: Int, completionHandler: @escaping MovieDetailsCompletionHandler) { + MoviesService.shared.getMovieDetails(id: id) { result in + completionHandler(result) + } + } + +} diff --git a/Movie-Application/Movie-Application/Modules/WatchlistMovies/Interactor/WatchlistMoviesInteractorInterface.swift b/Movie-Application/Movie-Application/Modules/WatchlistMovies/Interactor/WatchlistMoviesInteractorInterface.swift new file mode 100644 index 00000000..606e16b2 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/WatchlistMovies/Interactor/WatchlistMoviesInteractorInterface.swift @@ -0,0 +1,13 @@ +// +// WatchlistMoviesInteractorInterface.swift +// WatchlistMovies +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import Foundation +import UIKit + +protocol WatchlistMoviesInteractorInterface: InteractorPresenterInterface { + func getMovieDetails(id: Int, completionHandler: @escaping MovieDetailsCompletionHandler) +} diff --git a/Movie-Application/Movie-Application/Modules/WatchlistMovies/Presenter/WatchlistMoviesPresenter.swift b/Movie-Application/Movie-Application/Modules/WatchlistMovies/Presenter/WatchlistMoviesPresenter.swift new file mode 100644 index 00000000..a5410c68 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/WatchlistMovies/Presenter/WatchlistMoviesPresenter.swift @@ -0,0 +1,175 @@ +// +// WatchlistMoviesPresenter.swift +// WatchlistMovies +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import Foundation +import UIKit + +final class WatchlistMoviesPresenter: PresenterInterface { + + var router: WatchlistMoviesRouterInterface! + var interactor: WatchlistMoviesInteractorInterface! + weak var view: WatchlistMoviesViewInterface! + + var movies: [CoreDataMovie]? + private var deletedMovies = [CoreDataMovie]() + + init() { + // in order to scroll top top when user tapped te tab bar again + NotificationCenter.default.addObserver( + forName: TabBarViewContorller.tabBarDidTapNotification, + object: nil, queue: nil) { (_) in + if let view = self.view { + view.scrollToTop() + } + } + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + +} + +extension WatchlistMoviesPresenter: WatchlistMoviesPresenterRouterInterface { + +} + +extension WatchlistMoviesPresenter: WatchlistMoviesPresenterInteractorInterface { + +} + +extension WatchlistMoviesPresenter: WatchlistMoviesPresenterViewInterface { + + func viewDidLoad() { + getWatchlistMovies() + } + + func getWatchlistMovies() { + movies = CoreDataManager().getSavedMovies() + if let movies = movies { + if movies.isEmpty { + view.setWatchlistEmptyContainerisHidden(to: false) + } else { + view.setWatchlistEmptyContainerisHidden(to: true) + view.reloadCollectionView() + } + } + } + + // function to get movie image from url that we have + func getMovieImage(index: Int) -> UIImage { + + if let movies = movies { + return UIImage(data: movies[index].poster) ?? UIImage(systemName: "film.circle")! + } else { + return UIImage(systemName: "film.circle")! + } + + } + + func getMovieTitle(index: Int) -> String { + movies?[index].title ?? "" + } + + func movieSelected(at index: Int) { + if let movies = movies { + interactor.getMovieDetails(id: movies[index].id) { [weak self] result in + switch result { + case .success(let movie): + self?.router.showMovieDetails(movie) + + case .failure(let error): + self?.view.showError(with: error, index: index) + } + } + } + + } + + func deletefromWatchList(_ index: Int) { + self.movies?.remove(at: index) + view.reloadCollectionView() + + if movies!.isEmpty { + view.setWatchlistEmptyContainerisHidden(to: false) + } + } + + func deleteMovies() { + if let movies = movies { + CoreDataManager().saveMovies(movies: movies) + } + } + + func alertRetryButtonDidTap(_ index: Int) { + if let movies = movies { + interactor.getMovieDetails(id: movies[index].id) { [weak self] result in + switch result { + case .success(let movie): + self?.router.showMovieDetails(movie) + + case .failure(let error): + self?.view.showError(with: error, index: index) + } + + } + } + } + + func sortByDate() { + movies = movies?.sorted(by: { $0.date > $1.date }) + view.reloadCollectionView() + } + + func sortByName() { + movies = movies?.sorted(by: { $0.title > $1.title }) + view.reloadCollectionView() + } + + func sortByUserScore() { + movies = movies?.sorted(by: { $0.voteAverage > $1.voteAverage }) + view.reloadCollectionView() + } + + func browseMoviesDidTap() { + router.showPopularMovies() + } + + func configureContextMenu(_ index: Int) -> UIContextMenuConfiguration { + let context = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) -> UIMenu? in + + let viewDetails = UIAction(title: "View Details", + image: UIImage(systemName: "text.below.photo.fill"), + identifier: nil, + discoverabilityTitle: nil, state: .off) { (_) in + self.movieSelected(at: index) + } + let remove = UIAction(title: "Remove from Watchlist", + image: UIImage(systemName: "trash"), + identifier: nil, + discoverabilityTitle: nil, + attributes: .destructive, state: .off) { (_) in + self.deletefromWatchList(index) + } + + return UIMenu(title: self.getMovieTitle(index: index), + image: nil, identifier: nil, + options: UIMenu.Options.displayInline, children: [viewDetails, remove]) + + } + return context + } + + var numberOfMovies: Int { + return movies?.count ?? 0 + } + + var watchlistMovies: [CoreDataMovie] { + return movies ?? [] + } + +} diff --git a/Movie-Application/Movie-Application/Modules/WatchlistMovies/Presenter/WatchlistMoviesPresenterInteractorInterface.swift b/Movie-Application/Movie-Application/Modules/WatchlistMovies/Presenter/WatchlistMoviesPresenterInteractorInterface.swift new file mode 100644 index 00000000..4ae6d3ed --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/WatchlistMovies/Presenter/WatchlistMoviesPresenterInteractorInterface.swift @@ -0,0 +1,12 @@ +// +// WatchlistMoviesPresenterInteractorInterface.swift +// WatchlistMovies +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import Foundation + +protocol WatchlistMoviesPresenterInteractorInterface: PresenterInteractorInterface { + +} diff --git a/Movie-Application/Movie-Application/Modules/WatchlistMovies/Presenter/WatchlistMoviesPresenterRouterInterface.swift b/Movie-Application/Movie-Application/Modules/WatchlistMovies/Presenter/WatchlistMoviesPresenterRouterInterface.swift new file mode 100644 index 00000000..72e47338 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/WatchlistMovies/Presenter/WatchlistMoviesPresenterRouterInterface.swift @@ -0,0 +1,12 @@ +// +// WatchlistMoviesPresenterRouterInterface.swift +// WatchlistMovies +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import Foundation + +protocol WatchlistMoviesPresenterRouterInterface: PresenterRouterInterface { + +} diff --git a/Movie-Application/Movie-Application/Modules/WatchlistMovies/Presenter/WatchlistMoviesPresenterViewInterface.swift b/Movie-Application/Movie-Application/Modules/WatchlistMovies/Presenter/WatchlistMoviesPresenterViewInterface.swift new file mode 100644 index 00000000..e4ee742c --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/WatchlistMovies/Presenter/WatchlistMoviesPresenterViewInterface.swift @@ -0,0 +1,28 @@ +// +// WatchlistMoviesPresenterViewInterface.swift +// WatchlistMovies +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import Foundation +import UIKit + +protocol WatchlistMoviesPresenterViewInterface: PresenterViewInterface { + func viewDidLoad() + func getMovieImage(index: Int) -> UIImage + func getMovieTitle(index: Int) -> String + func movieSelected(at index: Int) + func deletefromWatchList(_ index: Int) + func getWatchlistMovies() + func deleteMovies() + func alertRetryButtonDidTap(_ index: Int) + func sortByDate() + func sortByName() + func sortByUserScore() + func browseMoviesDidTap() + func configureContextMenu(_ index: Int) -> UIContextMenuConfiguration + + var watchlistMovies: [CoreDataMovie] { get } + var numberOfMovies: Int { get } +} diff --git a/Movie-Application/Movie-Application/Modules/WatchlistMovies/Router/WatchlistMoviesRouter.swift b/Movie-Application/Movie-Application/Modules/WatchlistMovies/Router/WatchlistMoviesRouter.swift new file mode 100644 index 00000000..03a3fe0b --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/WatchlistMovies/Router/WatchlistMoviesRouter.swift @@ -0,0 +1,30 @@ +// +// WatchlistMoviesRouter.swift +// WatchlistMovies +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import UIKit + +final class WatchlistMoviesRouter: RouterInterface { + + weak var presenter: WatchlistMoviesPresenterRouterInterface! + + weak var viewController: UIViewController? +} + +extension WatchlistMoviesRouter: WatchlistMoviesRouterInterface { + + func showMovieDetails(_ movie: MovieDetail) { + let movieDetailsViewController = MovieDetailsModule().build(movie: movie) + viewController?.show(movieDetailsViewController, sender: nil) + } + + // posted notification to switch to popular tab + func showPopularMovies() { + NotificationCenter.default.post(name: TabBarViewContorller.selectedTabNotification, + object: nil, + userInfo: ["selectedTab": 0]) + } +} diff --git a/Movie-Application/Movie-Application/Modules/WatchlistMovies/Router/WatchlistMoviesRouterInterface.swift b/Movie-Application/Movie-Application/Modules/WatchlistMovies/Router/WatchlistMoviesRouterInterface.swift new file mode 100644 index 00000000..04887925 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/WatchlistMovies/Router/WatchlistMoviesRouterInterface.swift @@ -0,0 +1,13 @@ +// +// WatchlistMoviesRouterInterface.swift +// WatchlistMovies +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import UIKit + +protocol WatchlistMoviesRouterInterface: RouterPresenterInterface { + func showMovieDetails(_ movie: MovieDetail) + func showPopularMovies() +} diff --git a/Movie-Application/Movie-Application/Modules/WatchlistMovies/View/WatchlistMovies.storyboard b/Movie-Application/Movie-Application/Modules/WatchlistMovies/View/WatchlistMovies.storyboard new file mode 100644 index 00000000..f8f39ad5 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/WatchlistMovies/View/WatchlistMovies.storyboard @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Movie-Application/Movie-Application/Modules/WatchlistMovies/View/WatchlistMoviesView.swift b/Movie-Application/Movie-Application/Modules/WatchlistMovies/View/WatchlistMoviesView.swift new file mode 100644 index 00000000..d3c9e5c3 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/WatchlistMovies/View/WatchlistMoviesView.swift @@ -0,0 +1,183 @@ +// +// WatchlistMoviesView.swift +// WatchlistMovies +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import UIKit + +final class WatchlistMoviesView: UIViewController, ViewInterface { + + var presenter: WatchlistMoviesPresenterViewInterface! + + // MARK: - Properties + @IBOutlet var collectionView: UICollectionView! + + @IBOutlet var emptyWatchlistContainerView: UIStackView! + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + setupView() + self.presenter.viewDidLoad() + } + + override func viewWillAppear(_ animated: Bool) { + presenter.getWatchlistMovies() + } + + override func viewWillDisappear(_ animated: Bool) { + presenter.deleteMovies() + } + // MARK: - Theme + + func applyTheme() { + view.backgroundColor = .systemBackground + } + + // MARK: - Private functions + + private func setupView() { + configureNavigation() + setupCollectionView() + + self.applyTheme() + } + + // function to setup and configure navigation details + private func configureNavigation() { + navigationController?.navigationBar.prefersLargeTitles = true + self.title = "Watchlist" + + let dateAddedAction = UIAction(title: "Date Added", + image: nil, identifier: nil, + discoverabilityTitle: nil, + state: .off) { (_) in + self.presenter.sortByDate() + } + + let nameSortAction = UIAction(title: "Name", + image: nil, identifier: nil, + discoverabilityTitle: nil, + state: .off) { (_) in + self.presenter.sortByName() + } + + let userScoreSortAction = UIAction(title: "User Score", + image: nil, identifier: nil, + discoverabilityTitle: nil, + state: .off) { (_) in + self.presenter.sortByUserScore() + } + + let sortMenu = UIMenu(title: "", + image: nil, identifier: nil, + options: .singleSelection, + children: [dateAddedAction, nameSortAction, userScoreSortAction]) + + navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Sort", image: nil, primaryAction: nil, menu: sortMenu) + } + + // function to setup and configure collectionView details + private func setupCollectionView() { + collectionView.delegate = self + collectionView.dataSource = self + collectionView.register(MovieCell.self, forCellWithReuseIdentifier: "MovieCell") + } + + func configureContextMenu(_ index: Int) -> UIContextMenuConfiguration { + presenter.configureContextMenu(index) + } + + // MARK: - Actions + + @IBAction func browseMoviesDidTap(_ sender: UIButton) { + presenter.browseMoviesDidTap() + } + +} + +extension WatchlistMoviesView: WatchlistMoviesViewInterface { + + func reloadCollectionView() { + collectionView.reloadData() + } + + func scrollToTop() { + // checks if collection view has cells then scroll to top + if collectionView?.numberOfItems(inSection: 0) ?? 0 > 0 { + collectionView?.scrollToItem(at: IndexPath(row: 0, section: 0), at: .top, animated: true) + } + } + + func showError(with error: RequestError, index: Int) { + let errorAlert = UIAlertController(title: "Error Occured", + message: error.errorDescription, + preferredStyle: .alert) + let alertAction = UIAlertAction(title: "Retry", style: .default) { [weak self] (_) in + self?.presenter.alertRetryButtonDidTap(index) + } + errorAlert.addAction(alertAction) + self.present(errorAlert, animated: true, completion: nil) + } + + func setWatchlistEmptyContainerisHidden(to isHidden: Bool) { + emptyWatchlistContainerView.isHidden = isHidden + } +} + +extension WatchlistMoviesView: UICollectionViewDelegate, + UICollectionViewDataSource, + UICollectionViewDelegateFlowLayout { + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + presenter.numberOfMovies + } + + func collectionView(_ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MovieCell", + for: indexPath) as? MovieCell + else { return UICollectionViewCell() } + + cell.movieImageView.image = presenter.getMovieImage(index: indexPath.row) + cell.layer.cornerRadius = 10 + return cell + } + + func numberOfSections(in collectionView: UICollectionView) -> Int { + 1 + } + + func collectionView(_ collectionView: UICollectionView, + contextMenuConfigurationForItemAt indexPath: IndexPath, + point: CGPoint) -> UIContextMenuConfiguration? { + configureContextMenu(indexPath.row) + } + + func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath) -> CGSize { + let noOfCellsInRow = 2 + + guard let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout else { + return CGSize(width: 0, height: 0) + } + flowLayout.minimumInteritemSpacing = 10 + flowLayout.minimumLineSpacing = 10 + flowLayout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) + + let totalSpace = flowLayout.sectionInset.left + + flowLayout.sectionInset.right + + (flowLayout.minimumInteritemSpacing * CGFloat(noOfCellsInRow - 1)) + + let size = Int((view.bounds.width - totalSpace) / CGFloat(noOfCellsInRow)) + return CGSize(width: size, height: size + 50) + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + presenter.movieSelected(at: indexPath.row) + } +} diff --git a/Movie-Application/Movie-Application/Modules/WatchlistMovies/View/WatchlistMoviesViewInterface.swift b/Movie-Application/Movie-Application/Modules/WatchlistMovies/View/WatchlistMoviesViewInterface.swift new file mode 100644 index 00000000..e748d2ea --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/WatchlistMovies/View/WatchlistMoviesViewInterface.swift @@ -0,0 +1,15 @@ +// +// WatchlistMoviesViewInterface.swift +// WatchlistMovies +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import UIKit + +protocol WatchlistMoviesViewInterface: ViewPresenterInterface { + func reloadCollectionView() + func scrollToTop() + func showError(with error: RequestError, index: Int) + func setWatchlistEmptyContainerisHidden(to isHidden: Bool) +} diff --git a/Movie-Application/Movie-Application/Modules/WatchlistMovies/WatchlistMoviesModule.swift b/Movie-Application/Movie-Application/Modules/WatchlistMovies/WatchlistMoviesModule.swift new file mode 100644 index 00000000..908a8800 --- /dev/null +++ b/Movie-Application/Movie-Application/Modules/WatchlistMovies/WatchlistMoviesModule.swift @@ -0,0 +1,39 @@ +// +// WatchlistMoviesModule.swift +// WatchlistMovies +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import UIKit + +// MARK: - module builder + +final class WatchlistMoviesModule: ModuleInterface { + + typealias View = WatchlistMoviesView + typealias Presenter = WatchlistMoviesPresenter + typealias Router = WatchlistMoviesRouter + typealias Interactor = WatchlistMoviesInteractor + + func build() -> UIViewController { + guard let navigationController = UIStoryboard(name: "WatchlistMovies", + bundle: nil).instantiateInitialViewController() + as? UINavigationController else { + return UINavigationController() + } + guard let view = navigationController.topViewController as? View else { + return View() + } + + let interactor = Interactor() + let presenter = Presenter() + let router = Router() + + self.assemble(view: view, presenter: presenter, router: router, interactor: interactor) + + router.viewController = navigationController + + return navigationController + } +} diff --git a/Movie-Application/Movie-Application/Supproting Files/.DS_Store b/Movie-Application/Movie-Application/Supproting Files/.DS_Store new file mode 100644 index 00000000..19587d52 Binary files /dev/null and b/Movie-Application/Movie-Application/Supproting Files/.DS_Store differ diff --git a/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/.DS_Store b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/.DS_Store new file mode 100644 index 00000000..b81ec467 Binary files /dev/null and b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/.DS_Store differ diff --git a/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AccentColor.colorset/Contents.json b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/1024.png b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 00000000..13735c8d Binary files /dev/null and b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/1024.png differ diff --git a/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/114.png b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/114.png new file mode 100644 index 00000000..3c714870 Binary files /dev/null and b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/114.png differ diff --git a/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/120.png b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 00000000..09b4649c Binary files /dev/null and b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/120.png differ diff --git a/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/180.png b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 00000000..e7dc5fc1 Binary files /dev/null and b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/180.png differ diff --git a/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/29.png b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/29.png new file mode 100644 index 00000000..624c7970 Binary files /dev/null and b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/29.png differ diff --git a/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/40.png b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 00000000..3ba0c251 Binary files /dev/null and b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/40.png differ diff --git a/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/57.png b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/57.png new file mode 100644 index 00000000..582b93e6 Binary files /dev/null and b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/57.png differ diff --git a/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/58.png b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 00000000..c89d9779 Binary files /dev/null and b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/58.png differ diff --git a/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/60.png b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 00000000..690f196d Binary files /dev/null and b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/60.png differ diff --git a/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/80.png b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 00000000..62c597f7 Binary files /dev/null and b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/80.png differ diff --git a/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/87.png b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/87.png new file mode 100644 index 00000000..fba51c59 Binary files /dev/null and b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/87.png differ diff --git a/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/Contents.json b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..73d3b7f6 --- /dev/null +++ b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1 @@ +{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"}]} \ No newline at end of file diff --git a/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/Contents.json b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/filmIcon.imageset/Contents.json b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/filmIcon.imageset/Contents.json new file mode 100644 index 00000000..8b3ad4a8 --- /dev/null +++ b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/filmIcon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Icon-1024px.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Icon-1024px@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Icon-1024px@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/filmIcon.imageset/Icon-1024px.png b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/filmIcon.imageset/Icon-1024px.png new file mode 100644 index 00000000..9937e8c1 Binary files /dev/null and b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/filmIcon.imageset/Icon-1024px.png differ diff --git a/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/filmIcon.imageset/Icon-1024px@2x.png b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/filmIcon.imageset/Icon-1024px@2x.png new file mode 100644 index 00000000..8316ec96 Binary files /dev/null and b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/filmIcon.imageset/Icon-1024px@2x.png differ diff --git a/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/filmIcon.imageset/Icon-1024px@3x.png b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/filmIcon.imageset/Icon-1024px@3x.png new file mode 100644 index 00000000..13735c8d Binary files /dev/null and b/Movie-Application/Movie-Application/Supproting Files/Assets.xcassets/filmIcon.imageset/Icon-1024px@3x.png differ diff --git a/Movie-Application/Movie-Application/Supproting Files/Base.lproj/LaunchScreen.storyboard b/Movie-Application/Movie-Application/Supproting Files/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..b82387ed --- /dev/null +++ b/Movie-Application/Movie-Application/Supproting Files/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Movie-Application/Movie-Application/Supproting Files/Info.plist b/Movie-Application/Movie-Application/Supproting Files/Info.plist new file mode 100644 index 00000000..0eb786dc --- /dev/null +++ b/Movie-Application/Movie-Application/Supproting Files/Info.plist @@ -0,0 +1,23 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + + diff --git a/Movie-Application/Movie-Application/Utilities/.DS_Store b/Movie-Application/Movie-Application/Utilities/.DS_Store new file mode 100644 index 00000000..4495f7d8 Binary files /dev/null and b/Movie-Application/Movie-Application/Utilities/.DS_Store differ diff --git a/Movie-Application/Movie-Application/Utilities/CoreData/.DS_Store b/Movie-Application/Movie-Application/Utilities/CoreData/.DS_Store new file mode 100644 index 00000000..b456d596 Binary files /dev/null and b/Movie-Application/Movie-Application/Utilities/CoreData/.DS_Store differ diff --git a/Movie-Application/Movie-Application/Utilities/CoreData/CoreDataManager.swift b/Movie-Application/Movie-Application/Utilities/CoreData/CoreDataManager.swift new file mode 100644 index 00000000..6875ea6b --- /dev/null +++ b/Movie-Application/Movie-Application/Utilities/CoreData/CoreDataManager.swift @@ -0,0 +1,109 @@ +// +// CoreDataManager.swift +// Movie-Application +// +// Created by Mohanna Zakizadeh on 4/27/22. +// + +import CoreData +import Foundation +import UIKit + +final class CoreDataManager: CoreDataManagerProtocol { + + func saveNewMovie(_ movie: CoreDataMovie) { + guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return } + + // prevents core data from saving repititious data + let savedMovies = getSavedMovies() + if savedMovies.contains(where: {$0.id == movie.id}) { + return + } + + let managedContext = appDelegate.persistentContainer.viewContext + + let entity = NSEntityDescription.entity(forEntityName: "FavoriteMovie", in: managedContext)! + + let favoriteMovie = NSManagedObject(entity: entity, insertInto: managedContext) + + favoriteMovie.setValue(movie.title, forKey: "title") + favoriteMovie.setValue(movie.poster, forKey: "poster") + favoriteMovie.setValue(movie.id, forKey: "id") + favoriteMovie.setValue(movie.voteAverage, forKey: "voteAverage") + + do { + try managedContext.save() + } catch let error as NSError { + print("Could not save. \(error), \(error.userInfo)") + } + } + + func getSavedMovies() -> [CoreDataMovie] { + guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { + return [] + } + var movies = [CoreDataMovie]() + let managedContext = appDelegate.persistentContainer.viewContext + + let fetchRequest = NSFetchRequest(entityName: "FavoriteMovie") + + do { + let objects = try managedContext.fetch(fetchRequest) + for object in objects { + movies.append(CoreDataMovie(title: object.value(forKey: "title") as? String ?? "", + poster: object.value(forKey: "poster") as? Data ?? Data(), + id: object.value(forKey: "id") as? Int ?? 0, + date: Date.now, + voteAverage: object.value(forKey: "voteAverage") as? Double ?? 0)) + } + } catch let error as NSError { + print("Could not fetch. \(error), \(error.userInfo)") + } + return movies + } + + func saveMovies(movies: [CoreDataMovie]) { + deleteMovies() + for movie in movies { + saveNewMovie(movie) + } + } + + private func deleteMovies() { + guard let appDelegate = + UIApplication.shared.delegate as? AppDelegate else { return } + let context = + appDelegate.persistentContainer.viewContext + let fetchRequest: NSFetchRequest + fetchRequest = NSFetchRequest(entityName: "FavoriteMovie") + + // Create a batch delete request for the + // fetch request + let deleteRequest = NSBatchDeleteRequest( + fetchRequest: fetchRequest + ) + deleteRequest.resultType = .resultTypeObjectIDs + + do { + let batchDelete = try context.execute(deleteRequest) + as? NSBatchDeleteResult + + guard let deleteResult = batchDelete?.result + as? [NSManagedObjectID] + else { return } + let deletedObjects: [AnyHashable: Any] = [ + NSDeletedObjectsKey: deleteResult + ] + + NSManagedObjectContext.mergeChanges( + fromRemoteContextSave: deletedObjects, + into: [context] + ) + + } catch let error as NSError { + print("Could not delete. \(error), \(error.userInfo)") + } + + } + +} diff --git a/Movie-Application/Movie-Application/Utilities/CoreData/CoreDataProtocol.swift b/Movie-Application/Movie-Application/Utilities/CoreData/CoreDataProtocol.swift new file mode 100644 index 00000000..ec676925 --- /dev/null +++ b/Movie-Application/Movie-Application/Utilities/CoreData/CoreDataProtocol.swift @@ -0,0 +1,14 @@ +// +// CoreDataProtocol.swift +// Movie-Application +// +// Created by Mohanna Zakizadeh on 4/27/22. +// + +import Foundation + +protocol CoreDataManagerProtocol { + func saveNewMovie(_ movie: CoreDataMovie) + func getSavedMovies() -> [CoreDataMovie] + func saveMovies(movies: [CoreDataMovie]) +} diff --git a/Movie-Application/Movie-Application/Utilities/CoreData/FavoriteMovieModel.xcdatamodeld/MovieModel.xcdatamodel/contents b/Movie-Application/Movie-Application/Utilities/CoreData/FavoriteMovieModel.xcdatamodeld/MovieModel.xcdatamodel/contents new file mode 100644 index 00000000..bffd23ca --- /dev/null +++ b/Movie-Application/Movie-Application/Utilities/CoreData/FavoriteMovieModel.xcdatamodeld/MovieModel.xcdatamodel/contents @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Movie-Application/Movie-Application/Utilities/Extensions.swift b/Movie-Application/Movie-Application/Utilities/Extensions.swift new file mode 100644 index 00000000..7e0bd0ba --- /dev/null +++ b/Movie-Application/Movie-Application/Utilities/Extensions.swift @@ -0,0 +1,26 @@ +// +// Extensions.swift +// Movie-Application +// +// Created by Mohanna Zakizadeh on 4/28/22. +// + +import Foundation +import UIKit + +extension UIViewController { + public func add(asChildViewController viewController: UIViewController, to parentView: UIView) { + // Add Child View Controller + addChild(viewController) + + // Add Child View as Subview + parentView.addSubview(viewController.view) + + // Configure Child View + viewController.view.frame = parentView.bounds + viewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] + + // Notify Child View Controller + viewController.didMove(toParent: self) + } +} diff --git a/Movie-Application/Movie-Application/Utilities/Model/Movies.swift b/Movie-Application/Movie-Application/Utilities/Model/Movies.swift new file mode 100644 index 00000000..f0e7cbef --- /dev/null +++ b/Movie-Application/Movie-Application/Utilities/Model/Movies.swift @@ -0,0 +1,52 @@ +// +// Movies.swift +// Movie-Application +// +// Created by Mohanna Zakizadeh on 4/26/22. +// + +import Foundation + +struct Movies: Decodable { + let results: [Movie] +} + +struct Movie: Decodable { + let title: String + let poster: String? + let id: Int + let voteAverage: Double + + enum CodingKeys: String, CodingKey { + case poster = "poster_path" + case voteAverage = "vote_average" + case title, id + } +} + +struct CoreDataMovie { + let title: String + let poster: Data + let id: Int + let date: Date + let voteAverage: Double +} + +struct MovieDetail: Decodable { + let title: String + let poster: String? + let id: Int + let genres: [Genres] + let overview: String? + let voteAverage: Double + let releaseDate: String + let reviewsCount: Int + + enum CodingKeys: String, CodingKey { + case poster = "poster_path" + case releaseDate = "release_date" + case reviewsCount = "vote_count" + case voteAverage = "vote_average" + case title, id, genres, overview + } +} diff --git a/Movie-Application/Movie-Application/Utilities/Model/MoviesGeneres.swift b/Movie-Application/Movie-Application/Utilities/Model/MoviesGeneres.swift new file mode 100644 index 00000000..04723e78 --- /dev/null +++ b/Movie-Application/Movie-Application/Utilities/Model/MoviesGeneres.swift @@ -0,0 +1,17 @@ +// +// MoviesGeneres.swift +// Movie-Application +// +// Created by Mohanna Zakizadeh on 4/26/22. +// + +import Foundation + +struct MoviesGeneres: Decodable { + let genres: [Genres] +} + +struct Genres: Decodable { + let id: Int + let name: String +} diff --git a/Movie-Application/Movie-Application/Utilities/MovieCell.swift b/Movie-Application/Movie-Application/Utilities/MovieCell.swift new file mode 100644 index 00000000..42cebc5a --- /dev/null +++ b/Movie-Application/Movie-Application/Utilities/MovieCell.swift @@ -0,0 +1,50 @@ +// +// MovieCell.swift +// Movie-Application +// +// Created by Mohanna Zakizadeh on 4/28/22. +// + +import UIKit + +final class MovieCell: UICollectionViewCell { + + let movieImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFill + imageView.clipsToBounds = true + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.layer.cornerRadius = 10 + return imageView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + self.contentView.addSubview(movieImageView) + + NSLayoutConstraint.activate([ + movieImageView.topAnchor + .constraint(equalTo: self.contentView.topAnchor), + movieImageView.leftAnchor + .constraint(equalTo: self.contentView.leftAnchor), + movieImageView.rightAnchor + .constraint(equalTo: self.contentView.rightAnchor), + movieImageView.bottomAnchor + .constraint(equalTo: self.contentView.bottomAnchor) + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + movieImageView.image = nil + } + +} + +extension MovieCell: MovieCollectionViewCell { + typealias CellViewModel = Movie +} diff --git a/Movie-Application/Movie-Application/Utilities/Network/.DS_Store b/Movie-Application/Movie-Application/Utilities/Network/.DS_Store new file mode 100644 index 00000000..28524943 Binary files /dev/null and b/Movie-Application/Movie-Application/Utilities/Network/.DS_Store differ diff --git a/Movie-Application/Movie-Application/Utilities/Network/APILogger/ResponseLog.swift b/Movie-Application/Movie-Application/Utilities/Network/APILogger/ResponseLog.swift new file mode 100644 index 00000000..604577a1 --- /dev/null +++ b/Movie-Application/Movie-Application/Utilities/Network/APILogger/ResponseLog.swift @@ -0,0 +1,61 @@ +// +// ResponseLog.swift +// Movie-Application +// +// Created by Mohanna Zakizadeh on 4/26/22. +// + +import Foundation + +struct MovieResponseLog: URLRequestLoggableProtocol { + + let ENABLELOG = true + + func logResponse(_ response: HTTPURLResponse?, data: Data?, error: Error?, HTTPMethod: String?) { + guard ENABLELOG else { return } + print("\n💛 ========== Start logResponse ========== 💛") + defer { + print("🟡 ========== End logResponse ========== 🟡\n") + } + guard let response = response else { + print("==", "❌ NULL Response ERROR: ❌") + return + } + if let url = response.url?.absoluteString { + print("==", "Request URL: `\(url)`") + print("==", "Response CallBack Status Code: `\(response.statusCode)`") + } else { + print("==", "❌ LOG ERROR: ❌") + print("==", "Empty URL") + } + if let method = HTTPMethod { + print("==", "Request HTTPMethod: `\(method)`") + } + if let error = error { + print("==", "❌ GOT URL REQUEST ERROR: ❌") + print(error) + } + guard let data = data else { + print("==", "❌ Empty Response ERROR: ❌") + return + } + print("==", "✅ Response CallBack Data: ✅") + if let json = data.prettyPrintedJSONString { + print(json) + } else { + let responseDataString: String = String(data: data, encoding: .utf8) ?? "BAD ENCODING" + print(responseDataString) + } + } +} + +extension Data { + var prettyPrintedJSONString: String? { + guard let object = try? JSONSerialization.jsonObject(with: self, options: []), + let data = try? JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted]), + let prettyPrintedString = NSString(data: data, + encoding: String.Encoding.utf8.rawValue) as String? else { return nil } + + return prettyPrintedString + } +} diff --git a/Movie-Application/Movie-Application/Utilities/Network/APILogger/URLRequestLoggableProtocol.swift b/Movie-Application/Movie-Application/Utilities/Network/APILogger/URLRequestLoggableProtocol.swift new file mode 100644 index 00000000..4b1ccbd4 --- /dev/null +++ b/Movie-Application/Movie-Application/Utilities/Network/APILogger/URLRequestLoggableProtocol.swift @@ -0,0 +1,12 @@ +// +// URLRequestLoggableProtocol.swift +// Movie-Application +// +// Created by Mohanna Zakizadeh on 4/26/22. +// + +import Foundation + +protocol URLRequestLoggableProtocol { + func logResponse(_ response: HTTPURLResponse?, data: Data?, error: Error?, HTTPMethod: String?) +} diff --git a/Movie-Application/Movie-Application/Utilities/Network/HTTPMethod.swift b/Movie-Application/Movie-Application/Utilities/Network/HTTPMethod.swift new file mode 100644 index 00000000..ceb9a565 --- /dev/null +++ b/Movie-Application/Movie-Application/Utilities/Network/HTTPMethod.swift @@ -0,0 +1,12 @@ +// +// HTTPMethod.swift +// Movie-Application +// +// Created by Mohanna Zakizadeh on 4/26/22. +// + +import Foundation + +enum HTTPMethod: String { + case get = "GET" +} diff --git a/Movie-Application/Movie-Application/Utilities/Network/Protocols/RequestManagerProtocol.swift b/Movie-Application/Movie-Application/Utilities/Network/Protocols/RequestManagerProtocol.swift new file mode 100644 index 00000000..30252365 --- /dev/null +++ b/Movie-Application/Movie-Application/Utilities/Network/Protocols/RequestManagerProtocol.swift @@ -0,0 +1,34 @@ +// +// RequestManagerProtocol.swift +// Movie-Application +// +// Created by Mohanna Zakizadeh on 4/26/22. +// + +import Foundation + +protocol RequestManagerProtocol { + /// Provided URL Session + var session: URLSession! {get set} + + /// Timeout interval is interval for a request to be timedOut + var timeOutInterval: Double { get } + + var baseApi: String { get set } + + var responseValidator: ResponseValidatorProtocol { get set } + + var reponseLog: URLRequestLoggableProtocol? { get set } + /** + To make 'get' request to url. + + - Parameter url: url of interest to retrieve data. It should be String + - Parameter httpMethod: http method with associated value + + - Returns: completionHandler, which is Swift 5 Result Type , on Success returns the type which is Decodable . + On failure returns RequestError based on your server RequestError. + */ + func performRequestWith(url: String, + httpMethod: HTTPMethod, + completionHandler: @escaping DecodableResponse) +} diff --git a/Movie-Application/Movie-Application/Utilities/Network/RequestError.swift b/Movie-Application/Movie-Application/Utilities/Network/RequestError.swift new file mode 100644 index 00000000..eada6a6b --- /dev/null +++ b/Movie-Application/Movie-Application/Utilities/Network/RequestError.swift @@ -0,0 +1,36 @@ +// +// RequestError.swift +// Movie-Application +// +// Created by Mohanna Zakizadeh on 4/26/22. +// + +import Foundation + +enum RequestError: Error, LocalizedError { + case unknownError + case connectionError + case badHTTPStatus(status: Int, message: String?) + case authorizationError + case invalidRequest + case notFound + case serverUnavailable + case jsonParseError +} + +extension RequestError { + public var errorDescription: String? { + switch self { + case .connectionError: + return "Internet Connection Error" + case .badHTTPStatus(status: let status, message: let message): + return "Error with status `\(status) and message `\(message ?? "nil")` was thrown" + case .notFound: + return "Request not found" + case .jsonParseError: + return "JSON parsing probelm, make sure response has a valid JSON" + default: + return "Network Error with` \(self)` thrown" + } + } +} diff --git a/Movie-Application/Movie-Application/Utilities/Network/RequestManager.swift b/Movie-Application/Movie-Application/Utilities/Network/RequestManager.swift new file mode 100644 index 00000000..3c5f1583 --- /dev/null +++ b/Movie-Application/Movie-Application/Utilities/Network/RequestManager.swift @@ -0,0 +1,95 @@ +// +// RequestManager.swift +// Movie-Application +// +// Created by Mohanna Zakizadeh on 4/26/22. +// + +import Foundation + +typealias DecodableResponse = (Result) -> Void + +final class RequestManager: NSObject, URLSessionDelegate { + + // swiftlint: disable identifier_name + var baseApi: String = "https://api.themoviedb.org/3/" + var api_key: String = "api_key=496d0a5328d28334263194a131fb242b" + + var session: URLSession! + + var responseValidator: ResponseValidatorProtocol + + var reponseLog: URLRequestLoggableProtocol? + + typealias Headers = [String: String] + + private override init() { + self.reponseLog = MovieResponseLog() + self.responseValidator = MovieResponseValidator() + super.init() + self.session = URLSession(configuration: URLSessionConfiguration.ephemeral, + delegate: self, delegateQueue: OperationQueue.main) + } + + public init(session: URLSession, validator: ResponseValidatorProtocol) { + self.session = session + self.responseValidator = validator + } + + static let shared = RequestManager() + +} + +extension RequestManager: RequestManagerProtocol { + + var timeOutInterval: Double { + return 6 + } + + func performRequestWith(url: String, httpMethod: HTTPMethod, + completionHandler: @escaping DecodableResponse) { + + let headers = headerBuilder() + + let urlRequest = urlRequestBuilder(url: url, header: headers, httpMethod: httpMethod) + + performURLRequest(urlRequest, completionHandler: completionHandler) + } + + private func headerBuilder() -> Headers { + let headers = [ + "Content-Type": "application/json" + ] + return headers + } + + private func urlRequestBuilder(url: String, header: Headers, httpMethod: HTTPMethod) -> URLRequest { + + var urlRequest = URLRequest(url: URL(string: baseApi + url + api_key)!, + cachePolicy: .useProtocolCachePolicy, + timeoutInterval: timeOutInterval) + urlRequest.allHTTPHeaderFields = header + + urlRequest.httpMethod = httpMethod.rawValue + + return urlRequest + } + + private func performURLRequest(_ request: URLRequest, + completionHandler: @escaping DecodableResponse) { + + session.dataTask(with: request) { (data, response, error) in + self.reponseLog?.logResponse(response as? HTTPURLResponse, + data: data, + error: error, HTTPMethod: request.httpMethod) + if error != nil { + return completionHandler(.failure(RequestError.connectionError)) + } else { + let validationResult: (Result) = self.responseValidator.validation( + response: response as? HTTPURLResponse, + data: data) + return completionHandler(validationResult) + } + }.resume() + } +} diff --git a/Movie-Application/Movie-Application/Utilities/Network/ResponseValidator/ResponseValidator.swift b/Movie-Application/Movie-Application/Utilities/Network/ResponseValidator/ResponseValidator.swift new file mode 100644 index 00000000..6c381169 --- /dev/null +++ b/Movie-Application/Movie-Application/Utilities/Network/ResponseValidator/ResponseValidator.swift @@ -0,0 +1,34 @@ +// +// ResponseValidator.swift +// Movie-Application +// +// Created by Mohanna Zakizadeh on 4/26/22. +// + +import Foundation + +struct MovieResponseValidator: ResponseValidatorProtocol { + + func validation(response: HTTPURLResponse?, data: Data?) -> (Result) { + guard let response = response, let data = data else { + return .failure(RequestError.invalidRequest) + } + switch response.statusCode { + case 200: + do { + let model = try JSONDecoder().decode(T.self, from: data) + return .success(model) + } catch { + print("JSON Parse Error") + print(error) + return .failure(.jsonParseError) + } + case 400...499: + return .failure(RequestError.authorizationError) + case 500...599: + return .failure(.serverUnavailable) + default: + return .failure(RequestError.unknownError) + } + } +} diff --git a/Movie-Application/Movie-Application/Utilities/Network/ResponseValidator/ResponseValidatorProtocol.swift b/Movie-Application/Movie-Application/Utilities/Network/ResponseValidator/ResponseValidatorProtocol.swift new file mode 100644 index 00000000..0cb7c3cb --- /dev/null +++ b/Movie-Application/Movie-Application/Utilities/Network/ResponseValidator/ResponseValidatorProtocol.swift @@ -0,0 +1,12 @@ +// +// ResponseValidatorProtocol.swift +// Movie-Application +// +// Created by Mohanna Zakizadeh on 4/26/22. +// + +import Foundation + +protocol ResponseValidatorProtocol { + func validation(response: HTTPURLResponse?, data: Data?) -> Result +} diff --git a/Movie-Application/Movie-Application/Utilities/Service/MoviesService.swift b/Movie-Application/Movie-Application/Utilities/Service/MoviesService.swift new file mode 100644 index 00000000..37b11fb7 --- /dev/null +++ b/Movie-Application/Movie-Application/Utilities/Service/MoviesService.swift @@ -0,0 +1,85 @@ +// +// MoviesService.swift +// Movie-Application +// +// Created by Mohanna Zakizadeh on 4/26/22. +// + +import Foundation +import UIKit + +private enum MoviesEndpoint { + case generes + case topRatedMovies(Int) + case popularMovies(Int) + case movie(Int) + + var path: String { + switch self { + + case .generes: + return "genre/movie/list?" + case .topRatedMovies(let page): + return "movie/top_rated?page=\(page)&" + case .popularMovies(let page): + return "movie/popular?page=\(page)&" + case .movie(let id): + return "movie/\(id)?" + } + } +} + +final class MoviesService: MoviesServiceProtocol { + + private let requestManager: RequestManagerProtocol + + public static let shared: MoviesServiceProtocol = MoviesService(requestManager: RequestManager.shared) + + init(requestManager: RequestManagerProtocol) { + self.requestManager = requestManager + } + + func getTopRatedMovies(page: Int, completionHandler: @escaping MoviesCompletionHandler) { + self.requestManager.performRequestWith(url: MoviesEndpoint.topRatedMovies(page).path, + httpMethod: .get) { (result: Result) in + // Taking Data to main thread so we can update UI. + DispatchQueue.main.async { + completionHandler(result) + } + } + } + + func getPopularMovies(page: Int, completionHandler: @escaping MoviesCompletionHandler) { + self.requestManager.performRequestWith(url: MoviesEndpoint.popularMovies(page).path, + httpMethod: .get) { (result: Result) in + // Taking Data to main thread so we can update UI. + DispatchQueue.main.async { + completionHandler(result) + } + } + } + + func getMovieDetails(id: Int, completionHandler: @escaping MovieDetailsCompletionHandler) { + self.requestManager.performRequestWith(url: MoviesEndpoint.movie(id).path, + httpMethod: .get) { (result: Result) in + // Taking Data to main thread so we can update UI. + DispatchQueue.main.async { + completionHandler(result) + } + + } + } + + func getMovieImage(for path: String, completion: @escaping (UIImage) -> Void) { + DispatchQueue.global(qos: .utility).async { + let url = URL(string: "https://image.tmdb.org/t/p/w300/" + path)! + guard let data = try? Data(contentsOf: url) else { return } + let image = UIImage(data: data) ?? UIImage(systemName: "film.circle")! + + DispatchQueue.main.async { + completion(image) + } + } + } + +} diff --git a/Movie-Application/Movie-Application/Utilities/Service/MoviesServiceProtocol.swift b/Movie-Application/Movie-Application/Utilities/Service/MoviesServiceProtocol.swift new file mode 100644 index 00000000..8a619582 --- /dev/null +++ b/Movie-Application/Movie-Application/Utilities/Service/MoviesServiceProtocol.swift @@ -0,0 +1,19 @@ +// +// MoviesServiceProtocol.swift +// Movie-Application +// +// Created by Mohanna Zakizadeh on 4/29/22. +// + +import Foundation +import UIKit + +typealias MoviesCompletionHandler = (Result) -> Void +typealias MovieDetailsCompletionHandler = (Result) -> Void + +protocol MoviesServiceProtocol { + func getTopRatedMovies(page: Int, completionHandler: @escaping MoviesCompletionHandler) + func getPopularMovies(page: Int, completionHandler: @escaping MoviesCompletionHandler) + func getMovieDetails(id: Int, completionHandler: @escaping MovieDetailsCompletionHandler) + func getMovieImage(for path: String, completion: @escaping (UIImage) -> Void) +} diff --git a/Movie-Application/Movie-ApplicationTests/.DS_Store b/Movie-Application/Movie-ApplicationTests/.DS_Store new file mode 100644 index 00000000..8cb5d7f7 Binary files /dev/null and b/Movie-Application/Movie-ApplicationTests/.DS_Store differ diff --git a/Movie-Application/Movie-ApplicationTests/Movie_ApplicationTests.swift b/Movie-Application/Movie-ApplicationTests/Movie_ApplicationTests.swift new file mode 100644 index 00000000..0027d932 --- /dev/null +++ b/Movie-Application/Movie-ApplicationTests/Movie_ApplicationTests.swift @@ -0,0 +1,35 @@ +// +// Movie_ApplicationTests.swift +// Movie-ApplicationTests +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import XCTest +@testable import Movie_Application + +// swiftlint: disable type_name +class Movie_ApplicationTests: XCTestCase { + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/Movie-Application/Movie-ApplicationTests/PopularMoviesTests/TestPopularMoviesInteractor.swift b/Movie-Application/Movie-ApplicationTests/PopularMoviesTests/TestPopularMoviesInteractor.swift new file mode 100644 index 00000000..6e6ecefd --- /dev/null +++ b/Movie-Application/Movie-ApplicationTests/PopularMoviesTests/TestPopularMoviesInteractor.swift @@ -0,0 +1,41 @@ +// +// TestPopularMoviesInteractor.swift +// Movie-ApplicationTests +// +// Created by Mohanna Zakizadeh on 5/2/22. +// + +import XCTest +@testable import Movie_Application + +final class TestPopularMoviesInteractor: XCTestCase { + + var interactor: PopularMoviesInteractor! + + override func setUpWithError() throws { + interactor = .init() + } + + override func tearDownWithError() throws { + interactor = nil + } + + func testInteractorHasGetTopRatedMoviesMethod() throws { + interactor.getPopularMovies(page: 0) { _ in + return + } + } + + func testInteractorHasGetMovieImage() throws { + interactor.getMovieImage(for: "") { _ in + return + } + } + + func testInteractorHasGetMovieDetails() throws { + interactor.getMovieDetails(id: 0) { _ in + return + } + } + +} diff --git a/Movie-Application/Movie-ApplicationTests/PopularMoviesTests/TestPopularMoviesPresenter.swift b/Movie-Application/Movie-ApplicationTests/PopularMoviesTests/TestPopularMoviesPresenter.swift new file mode 100644 index 00000000..7ed42970 --- /dev/null +++ b/Movie-Application/Movie-ApplicationTests/PopularMoviesTests/TestPopularMoviesPresenter.swift @@ -0,0 +1,127 @@ +// +// TestPopularMoviesPresenter.swift +// Movie-ApplicationTests +// +// Created by Mohanna Zakizadeh on 5/2/22. +// + +import XCTest +@testable import Movie_Application + +final class TestPopularMoviesPresenter: XCTestCase { + + var presenter: PopularMoviesPresenter! + // swiftlint: disable identifier_name + var notificationCallsViewScrollToTopExpectation: XCTestExpectation? + var getPopularMoviesCallsInteractorGetTopMoviesExpectation: XCTestExpectation? + var getMovieImageCallsInteractorGetMovieImageExpectation: XCTestExpectation? + var presenterMovieSelectedCallsInteractorGetMoviesDetails: XCTestExpectation? + + override func setUpWithError() throws { + presenter = .init() + presenter.router = self + presenter.view = self + presenter.interactor = self + presenter.movies = [Movie(title: "", poster: "", id: 0, voteAverage: 0)] + } + + override func tearDownWithError() throws { + presenter.view = nil + presenter.interactor = nil + presenter.router = nil + presenter.movies = nil + presenter = nil + } + + func testNotificationCallsViewScrollToTop() throws { + notificationCallsViewScrollToTopExpectation = + expectation(description: "expect to view scroll to top fullfill this expectation") + NotificationCenter.default.post(name: TabBarViewContorller.tabBarDidTapNotification, object: nil) + wait(for: [notificationCallsViewScrollToTopExpectation!], timeout: 1) + } + + func testGetTopRatedMoviesCallsInteractorGetTopMovies() throws { + getPopularMoviesCallsInteractorGetTopMoviesExpectation = + expectation(description: "expect interactor getTopRatedMovies to fullfill this expectation") + presenter.getPopularMovies() + wait(for: [getPopularMoviesCallsInteractorGetTopMoviesExpectation!], timeout: 1) + } + + func testGetMovieImageCallsInteractorGetMovieImage() throws { + getMovieImageCallsInteractorGetMovieImageExpectation = + expectation(description: "expect interactor getMovieImage to fullfill this expectation") + presenter.getMovieImage(index: 0) { (_) in + return + } + wait(for: [getMovieImageCallsInteractorGetMovieImageExpectation!], timeout: 1) + } + + func testPresenterGetMovieTitleReturnsString() throws { + XCTAssertNotNil(presenter.getMovieTitle(index: 0)) + } + + func testPresenterMovieSelectedCallsInteractorGetMoviesDetails() throws { + presenterMovieSelectedCallsInteractorGetMoviesDetails = + expectation(description: "expect interactor getMovieDetails to fullfill this expectation") + presenter.movieSelected(at: 0) + wait(for: [presenterMovieSelectedCallsInteractorGetMoviesDetails!], timeout: 1) + } + + func testPresenterHasAddToWatchListMethod() throws { + presenter.addToWatchList(index: 0, imageData: Data()) + } + + func testPresenterHasNumberOfMovies() throws { + XCTAssertNotNil(presenter.numberOfMovies) + } + + func testPresenterHasTopRatedMovies() throws { + XCTAssert(type(of: presenter.popularMovies) == [Movie].self) + } + +} + +extension TestPopularMoviesPresenter: PopularMoviesViewInterface { + func showError(with error: RequestError) { + + } + + func reloadCollectionView() { + + } + + func scrollToTop() { + DispatchQueue.main.async { + self.notificationCallsViewScrollToTopExpectation?.fulfill() + } + } + +} + +extension TestPopularMoviesPresenter: PopularMoviesInteractorInterface { + func getPopularMovies(page: Int, completionHandler: @escaping MoviesCompletionHandler) { + DispatchQueue.main.async { + self.getPopularMoviesCallsInteractorGetTopMoviesExpectation?.fulfill() + } + } + + func getMovieImage(for path: String, completion: @escaping (UIImage) -> Void) { + DispatchQueue.main.async { + self.getMovieImageCallsInteractorGetMovieImageExpectation?.fulfill() + } + } + + func getMovieDetails(id: Int, completionHandler: @escaping MovieDetailsCompletionHandler) { + DispatchQueue.main.async { + self.presenterMovieSelectedCallsInteractorGetMoviesDetails?.fulfill() + } + } + +} + +extension TestPopularMoviesPresenter: PopularMoviesRouterInterface { + func showMovieDetails(_ movie: MovieDetail) { + + } + +} diff --git a/Movie-Application/Movie-ApplicationTests/PopularMoviesTests/TestPopularMoviesRouter.swift b/Movie-Application/Movie-ApplicationTests/PopularMoviesTests/TestPopularMoviesRouter.swift new file mode 100644 index 00000000..2734eef2 --- /dev/null +++ b/Movie-Application/Movie-ApplicationTests/PopularMoviesTests/TestPopularMoviesRouter.swift @@ -0,0 +1,30 @@ +// +// TestPopularMoviesRouter.swift +// Movie-ApplicationTests +// +// Created by Mohanna Zakizadeh on 5/2/22. +// + +import XCTest +@testable import Movie_Application + +final class TestPopularMoviesRouter: XCTestCase { + + var router: PopularMoviesRouter! + + override func setUpWithError() throws { + router = .init() + } + + override func tearDownWithError() throws { + router = nil + } + + func testRouterHasShowMovieDetailsMethod() throws { + router.showMovieDetails(MovieDetail(title: "", poster: "", id: 0, + genres: [Genres(id: 0, name: "")], + overview: nil, voteAverage: 0, + releaseDate: "", reviewsCount: 0)) + } + +} diff --git a/Movie-Application/Movie-ApplicationTests/PopularMoviesTests/TestPopularMoviesView.swift b/Movie-Application/Movie-ApplicationTests/PopularMoviesTests/TestPopularMoviesView.swift new file mode 100644 index 00000000..040ef071 --- /dev/null +++ b/Movie-Application/Movie-ApplicationTests/PopularMoviesTests/TestPopularMoviesView.swift @@ -0,0 +1,154 @@ +// +// TestPopularMoviesView.swift +// Movie-ApplicationTests +// +// Created by Mohanna Zakizadeh on 5/2/22. +// + +import XCTest +@testable import Movie_Application + +final class TestPopularMoviesView: XCTestCase { + + var view: PopularMoviesView! + + // swiftlint: disable identifier_name + var collectionViewDidSelectRowATCallsPresenterMovieSelectedExpectation: XCTestExpectation? + var collectionViewNumberOfItemsCallsPresenterNumberOfMoviesExpectation: XCTestExpectation? + var configureContextMenuCallsPresenterGetSavedMoviesExpectation: XCTestExpectation? + var configureContextMenuCallsPresenterGetMovieTitleExpectation: XCTestExpectation? + var configurePaginationCallsPresenterNumberOfMoviesExpectation: XCTestExpectation? + + override func setUpWithError() throws { + let navigation = UIStoryboard(name: "PopularMovies", + bundle: nil).instantiateInitialViewController() as? UINavigationController + view = navigation?.topViewController as? PopularMoviesView + view.presenter = self + view.loadView() + view.viewDidLoad() + } + + override func tearDownWithError() throws { + view.presenter = nil + view = nil + } + + func testViewHasCollectionView() throws { + XCTAssert(type(of: view.collectionView) == UICollectionView?.self) + } + + func testViewHasTopRatedTitle() throws { + XCTAssert(view.navigationItem.title == "Popular") + } + + func testViewHasLargeTitle() throws { + XCTAssert(view.navigationController!.navigationBar.prefersLargeTitles) + } + + func testCollectionViewHasDataSource() throws { + XCTAssertNotNil(view.collectionView.dataSource) + } + + func testCollectionViewHasDelegate() throws { + XCTAssertNotNil(view.collectionView.delegate) + } + + func testViewConformsToCollectionViewDataSource() throws { + XCTAssertNotNil(view as? UICollectionViewDataSource) + } + + func testViewConformsToCollectionViewDelegate() throws { + XCTAssertNotNil(view as? UICollectionViewDelegate) + } + + func testViewConformsToCollectionViewDelegateFlowLayout() { + XCTAssertNotNil(view as? UICollectionViewDelegateFlowLayout) + } + + func testCollectionViewHasACellWithCityCellID() throws { + XCTAssertNotNil(view.collectionView.dequeueReusableCell(withReuseIdentifier: "MovieCell", + for: IndexPath(index: 1))) + } + + func testCollectionViewDidSelectRowATCallsPresenterMovieSelected() throws { + collectionViewDidSelectRowATCallsPresenterMovieSelectedExpectation = + expectation(description: "expect presenter movieSelected to fullfill this expectation") + view.collectionView(view.collectionView, didSelectItemAt: IndexPath(row: 0, section: 0)) + wait(for: [collectionViewDidSelectRowATCallsPresenterMovieSelectedExpectation!], timeout: 2) + } + + func testCollectionViewNumberOfItemsCallsPresenterNumberOfMovies() throws { + collectionViewNumberOfItemsCallsPresenterNumberOfMoviesExpectation = + expectation(description: "expect presenter numberOfMovies fullfill this expectation") + _ = view.collectionView(view.collectionView, numberOfItemsInSection: 0) + wait(for: [collectionViewNumberOfItemsCallsPresenterNumberOfMoviesExpectation!], timeout: 2) + } + + func testConfigurePaginationCallsPresenterNumberOfMovies() throws { + configurePaginationCallsPresenterNumberOfMoviesExpectation = + expectation(description: "expect presenter numberOfMovies fullfill this expectation") + view.configurePagination(0) + wait(for: [configurePaginationCallsPresenterNumberOfMoviesExpectation!], timeout: 1) + } + +} + +extension TestPopularMoviesView: PopularMoviesPresenterViewInterface { + + func configureContextMenu(index: Int, imageData: Data) -> UIContextMenuConfiguration { + UIContextMenuConfiguration() + } + + func viewDidLoad() { + + } + + func alertRetryButtonDidTap() { + + } + + func getMovieImage(index: Int, completion: @escaping (UIImage) -> Void) { + + } + + func getMovieTitle(index: Int) -> String { + DispatchQueue.main.async { + self.configureContextMenuCallsPresenterGetMovieTitleExpectation?.fulfill() + } + return "" + } + + func movieSelected(at index: Int) { + DispatchQueue.main.async { + self.collectionViewDidSelectRowATCallsPresenterMovieSelectedExpectation?.fulfill() + } + } + + func addToWatchList(index: Int, imageData: Data) { + + } + + func getPopularMovies() { + + } + + func getSavedMovies() -> [CoreDataMovie] { + DispatchQueue.main.async { + self.configureContextMenuCallsPresenterGetSavedMoviesExpectation?.fulfill() + } + return [CoreDataMovie(title: "", poster: Data(), id: 0, date: Date(), voteAverage: 0.0)] + } + + var popularMovies: [Movie] { + return [Movie(title: "", poster: nil, id: 1, voteAverage: 1.0)] + } + + var numberOfMovies: Int { + DispatchQueue.main.async { + self.collectionViewNumberOfItemsCallsPresenterNumberOfMoviesExpectation?.fulfill() + self.configurePaginationCallsPresenterNumberOfMoviesExpectation?.fulfill() + } + return 1 + } + +} diff --git a/Movie-Application/Movie-ApplicationTests/TabBarViewControllerTest/TestTabBarViewController.swift b/Movie-Application/Movie-ApplicationTests/TabBarViewControllerTest/TestTabBarViewController.swift new file mode 100644 index 00000000..548aa38b --- /dev/null +++ b/Movie-Application/Movie-ApplicationTests/TabBarViewControllerTest/TestTabBarViewController.swift @@ -0,0 +1,91 @@ +// +// TestTabBarViewController.swift +// Movie-ApplicationTests +// +// Created by Mohanna Zakizadeh on 4/24/22. +// + +import XCTest +@testable import Movie_Application + +final class TestTabBarViewController: XCTestCase { + var view: TabBarViewContorller! + + override func setUpWithError() throws { + view = TabBarViewContorller() + view.loadView() + view.viewDidLoad() + } + + override func tearDownWithError() throws { + view = nil + } + + func testViewHasTopRatedIcon() throws { + XCTAssert(type(of: view.topRatedIcon) == UIImage?.self) + } + + func testViewHasFavoriteIcon() throws { + XCTAssert(type(of: view.favoriteIcon) == UIImage?.self) + } + + func testViewHasPopularIcon() throws { + XCTAssert(type(of: view.popularIcon) == UIImage?.self) + } + + func testTopRatedMovieViewControllerExists() throws { + XCTAssertNotNil(view.topRatedMoviesViewController) + } + + func testPopularMovieViewControllerExists() throws { + XCTAssertNotNil(view.popularMoviesViewController) + } + + func testFavoriteMovieViewControllerExists() throws { + XCTAssertNotNil(view.favoriteMoviesViewController) + } + + func testViewControllerHasSetupTopRatedMoviesViewControllerMethod() throws { + _ = view.setupTopRatedMoviesViewController() + } + + func testViewControllerHasSetupPopularMoviesViewControllerMethod() throws { + _ = view.setupPopularMoviesViewController() + } + + func testViewControllerHasSetupFavoritesMoviesViewControllerMethod() throws { + _ = view.setupFavoriteMoviesViewController() + } + + func testTopRatedMoviesTabBarItemNotNil() throws { + let tabBarItem = UITabBarItem(title: "Top Rated", image: UIImage(systemName: "list.number"), tag: 0) + let viewController = view.setupTopRatedMoviesViewController() + XCTAssertEqual(tabBarItem.title, viewController.tabBarItem.title) + XCTAssertEqual(tabBarItem.image, viewController.tabBarItem.image) + } + + func testPopularMoviesTabBarItemNotNil() throws { + let tabBarItem = UITabBarItem(title: "Popular", + image: UIImage(systemName: "flame"), + selectedImage: UIImage(systemName: "flame.fill")) + let viewController = view.setupPopularMoviesViewController() + XCTAssertEqual(tabBarItem.title, viewController.tabBarItem.title) + XCTAssertEqual(tabBarItem.image, viewController.tabBarItem.image) + XCTAssertEqual(tabBarItem.selectedImage, viewController.tabBarItem.selectedImage) + } + + func testFavoriteMoviesTabBarItemNotNil() throws { + // given + let tabBarItem = UITabBarItem(title: "WatchList", + image: UIImage(systemName: "bookmark"), + selectedImage: UIImage(systemName: "bookmark.fill")) + + // when + let viewController = view.setupFavoriteMoviesViewController() + + // then + XCTAssertEqual(tabBarItem.title, viewController.tabBarItem.title) + XCTAssertEqual(tabBarItem.image, viewController.tabBarItem.image) + XCTAssertEqual(tabBarItem.selectedImage, viewController.tabBarItem.selectedImage) + } +} diff --git a/Movie-Application/Movie-ApplicationTests/TopRatedMoviesTests/TestTopRatedMoviesInteractor.swift b/Movie-Application/Movie-ApplicationTests/TopRatedMoviesTests/TestTopRatedMoviesInteractor.swift new file mode 100644 index 00000000..a02fbac1 --- /dev/null +++ b/Movie-Application/Movie-ApplicationTests/TopRatedMoviesTests/TestTopRatedMoviesInteractor.swift @@ -0,0 +1,41 @@ +// +// TestTopRatedMoviesInteractor.swift +// Movie-ApplicationTests +// +// Created by Mohanna Zakizadeh on 5/1/22. +// + +import XCTest +@testable import Movie_Application + +final class TestTopRatedMoviesInteractor: XCTestCase { + + var interactor: TopRatedMoviesInteractor! + + override func setUpWithError() throws { + interactor = .init() + } + + override func tearDownWithError() throws { + interactor = nil + } + + func testInteractorHasGetTopRatedMoviesMethod() throws { + interactor.getTopRatedMovies(page: 0) { _ in + return + } + } + + func testInteractorHasGetMovieImage() throws { + interactor.getMovieImage(for: "") { _ in + return + } + } + + func testInteractorHasGetMovieDetails() throws { + interactor.getMovieDetails(id: 0) { _ in + return + } + } + +} diff --git a/Movie-Application/Movie-ApplicationTests/TopRatedMoviesTests/TestTopRatedMoviesPresenter.swift b/Movie-Application/Movie-ApplicationTests/TopRatedMoviesTests/TestTopRatedMoviesPresenter.swift new file mode 100644 index 00000000..6ac4a90f --- /dev/null +++ b/Movie-Application/Movie-ApplicationTests/TopRatedMoviesTests/TestTopRatedMoviesPresenter.swift @@ -0,0 +1,126 @@ +// +// TestTopRatedMoviesPresenter.swift +// Movie-ApplicationTests +// +// Created by Mohanna Zakizadeh on 5/1/22. +// + +import XCTest +@testable import Movie_Application + +final class TestTopRatedMoviesPresenter: XCTestCase { + + var presenter: TopRatedMoviesPresenter! + // swiftlint: disable identifier_name + var notificationCallsViewScrollToTopExpectation: XCTestExpectation? + var getTopRatedMoviesCallsInteractorGetTopMoviesExpectation: XCTestExpectation? + var getMovieImageCallsInteractorGetMovieImageExpectation: XCTestExpectation? + var presenterMovieSelectedCallsInteractorGetMoviesDetails: XCTestExpectation? + + override func setUpWithError() throws { + presenter = .init() + presenter.router = self + presenter.view = self + presenter.interactor = self + presenter.movies = [Movie(title: "", poster: "", id: 0, voteAverage: 0)] + } + + override func tearDownWithError() throws { + presenter.view = nil + presenter.interactor = nil + presenter.router = nil + presenter.movies = nil + presenter = nil + } + + func testNotificationCallsViewScrollToTop() throws { + notificationCallsViewScrollToTopExpectation = + expectation(description: "expect to view scroll to top fullfill this expectation") + NotificationCenter.default.post(name: TabBarViewContorller.tabBarDidTapNotification, object: nil) + wait(for: [notificationCallsViewScrollToTopExpectation!], timeout: 1) + } + + func testGetTopRatedMoviesCallsInteractorGetTopMovies() throws { + getTopRatedMoviesCallsInteractorGetTopMoviesExpectation = + expectation(description: "expect interactor getTopRatedMovies to fullfill this expectation") + presenter.getTopRatedMovies() + wait(for: [getTopRatedMoviesCallsInteractorGetTopMoviesExpectation!], timeout: 1) + } + + func testGetMovieImageCallsInteractorGetMovieImage() throws { + getMovieImageCallsInteractorGetMovieImageExpectation = + expectation(description: "expect interactor getMovieImage to fullfill this expectation") + presenter.getMovieImage(index: 0) { (_) in + return + } + wait(for: [getMovieImageCallsInteractorGetMovieImageExpectation!], timeout: 1) + } + + func testPresenterGetMovieTitleReturnsString() throws { + XCTAssertNotNil(presenter.getMovieTitle(index: 0)) + } + + func testPresenterMovieSelectedCallsInteractorGetMoviesDetails() throws { + presenterMovieSelectedCallsInteractorGetMoviesDetails = + expectation(description: "expect interactor getMovieDetails to fullfill this expectation") + presenter.movieSelected(at: 0) + wait(for: [presenterMovieSelectedCallsInteractorGetMoviesDetails!], timeout: 1) + } + + func testPresenterHasAddToWatchListMethod() throws { + presenter.addToWatchList(index: 0, imageData: Data()) + } + + func testPresenterHasNumberOfMovies() throws { + XCTAssertNotNil(presenter.numberOfMovies) + } + + func testPresenterHasTopRatedMovies() throws { + XCTAssert(type(of: presenter.topRatedMovies) == [Movie].self) + } + +} + +extension TestTopRatedMoviesPresenter: TopRatedMoviesViewInterface { + func showError(with error: RequestError) { + + } + + func reloadCollectionView() { + + } + + func scrollToTop() { + DispatchQueue.main.async { + self.notificationCallsViewScrollToTopExpectation?.fulfill() + } + } + +} + +extension TestTopRatedMoviesPresenter: TopRatedMoviesInteractorInterface { + func getTopRatedMovies(page: Int, completionHandler: @escaping MoviesCompletionHandler) { + DispatchQueue.main.async { + self.getTopRatedMoviesCallsInteractorGetTopMoviesExpectation?.fulfill() + } + } + + func getMovieImage(for path: String, completion: @escaping (UIImage) -> Void) { + DispatchQueue.main.async { + self.getMovieImageCallsInteractorGetMovieImageExpectation?.fulfill() + } + } + + func getMovieDetails(id: Int, completionHandler: @escaping MovieDetailsCompletionHandler) { + DispatchQueue.main.async { + self.presenterMovieSelectedCallsInteractorGetMoviesDetails?.fulfill() + } + } + +} + +extension TestTopRatedMoviesPresenter: TopRatedMoviesRouterInterface { + func showMovieDetails(_ movie: MovieDetail) { + + } +} diff --git a/Movie-Application/Movie-ApplicationTests/TopRatedMoviesTests/TestTopRatedMoviesRouter.swift b/Movie-Application/Movie-ApplicationTests/TopRatedMoviesTests/TestTopRatedMoviesRouter.swift new file mode 100644 index 00000000..95e8e559 --- /dev/null +++ b/Movie-Application/Movie-ApplicationTests/TopRatedMoviesTests/TestTopRatedMoviesRouter.swift @@ -0,0 +1,29 @@ +// +// TestTopRatedMoviesRouter.swift +// Movie-ApplicationTests +// +// Created by Mohanna Zakizadeh on 5/2/22. +// + +import XCTest +@testable import Movie_Application + +final class TestTopRatedMoviesRouter: XCTestCase { + + var router: TopRatedMoviesRouter! + + override func setUpWithError() throws { + router = .init() + } + + override func tearDownWithError() throws { + router = nil + } + + func testRouterHasShowMovieDetailsMethod() throws { + router.showMovieDetails(MovieDetail(title: "", poster: "", id: 0, + genres: [Genres(id: 0, name: "")], + overview: nil, voteAverage: 0, releaseDate: "", + reviewsCount: 0)) + } +} diff --git a/Movie-Application/Movie-ApplicationTests/TopRatedMoviesTests/TestTopRatedMoviesView.swift b/Movie-Application/Movie-ApplicationTests/TopRatedMoviesTests/TestTopRatedMoviesView.swift new file mode 100644 index 00000000..f4305a2f --- /dev/null +++ b/Movie-Application/Movie-ApplicationTests/TopRatedMoviesTests/TestTopRatedMoviesView.swift @@ -0,0 +1,174 @@ +// +// TestTopRatedMoviesView.swift +// Movie-ApplicationTests +// +// Created by Mohanna Zakizadeh on 4/26/22. +// + +import XCTest +@testable import Movie_Application + +final class TestTopRatedMoviesView: XCTestCase { + + var view: TopRatedMoviesView! +// swiftlint: disable identifier_name + var collectionViewDidSelectRowATCallsPresenterMovieSelectedExpectation: XCTestExpectation? + var collectionViewNumberOfItemsCallsPresenterNumberOfMoviesExpectation: XCTestExpectation? + var configureContextMenuCallsPresenterGetSavedMoviesExpectation: XCTestExpectation? + var configureContextMenuCallsPresenterGetMovieTitleExpectation: XCTestExpectation? + var configurePaginationCallsPresenterNumberOfMoviesExpectation: XCTestExpectation? + + override func setUpWithError() throws { + let navigation = UIStoryboard(name: "TopRatedMovies", + bundle: nil).instantiateInitialViewController() as? UINavigationController + view = navigation?.topViewController as? TopRatedMoviesView + view.presenter = self + view.loadView() + view.viewDidLoad() + + } + + override func tearDownWithError() throws { + view = nil + } + + func testViewHasCollectionView() throws { + XCTAssert(type(of: view.collectionView) == UICollectionView?.self) + } + + func testViewHasTopRatedTitle() throws { + // Given + let title = "Top Rated" + + // When + let viewTitle = view.navigationItem.title + + // Then + XCTAssert(viewTitle == title) + } + + func testViewHasLargeTitle() throws { + // Given + let prefersLargeTitles = true + + // When + let viewPrefersLargeTitles = view.navigationController!.navigationBar.prefersLargeTitles + + // Then + XCTAssert(prefersLargeTitles == viewPrefersLargeTitles) + } + + func testCollectionViewHasDataSource() throws { + XCTAssertNotNil(view.collectionView.dataSource) + } + + func testCollectionViewHasDelegate() throws { + XCTAssertNotNil(view.collectionView.delegate) + } + + func testViewConformsToCollectionViewDataSource() throws { + XCTAssertNotNil(view as? UICollectionViewDataSource) + } + + func testViewConformsToCollectionViewDelegate() throws { + XCTAssertNotNil(view as? UICollectionViewDelegate) + } + + func testViewConformsToCollectionViewDelegateFlowLayout() { + XCTAssertNotNil(view as? UICollectionViewDelegateFlowLayout) + } + + func testCollectionViewHasACellWithCityCellID() throws { + XCTAssertNotNil(view.collectionView.dequeueReusableCell(withReuseIdentifier: "MovieCell", + for: IndexPath(index: 1))) + } + + func testCollectionViewDidSelectRowATCallsPresenterMovieSelected() throws { + collectionViewDidSelectRowATCallsPresenterMovieSelectedExpectation = + expectation(description: "expect presenter movieSelected to fullfill this expectation") + + view.collectionView(view.collectionView, didSelectItemAt: IndexPath(row: 0, section: 0)) + wait(for: [collectionViewDidSelectRowATCallsPresenterMovieSelectedExpectation!], timeout: 2) + } + + func testCollectionViewNumberOfItemsCallsPresenterNumberOfMovies() throws { + collectionViewNumberOfItemsCallsPresenterNumberOfMoviesExpectation = + expectation(description: "expect presenter numberOfMovies fullfill this expectation") + + _ = view.collectionView(view.collectionView, numberOfItemsInSection: 0) + wait(for: [collectionViewNumberOfItemsCallsPresenterNumberOfMoviesExpectation!], timeout: 2) + } + + func testConfigurePaginationCallsPresenterNumberOfMovies() throws { + configurePaginationCallsPresenterNumberOfMoviesExpectation = + expectation(description: "expect presenter numberOfMovies fullfill this expectation") + + view.configurePagination(0) + wait(for: [configurePaginationCallsPresenterNumberOfMoviesExpectation!], timeout: 1) + } + +} + +extension TestTopRatedMoviesView: TopRatedMoviesPresenterViewInterface { + + func configureContextMenu(index: Int, imageData: Data) -> UIContextMenuConfiguration { + UIContextMenuConfiguration() + } + + var movieImagesCache: NSCache { + NSCache() + } + + func viewDidLoad() { + + } + + func alertRetryButtonDidTap() { + + } + + func getMovieImage(index: Int, completion: @escaping (UIImage) -> Void) { + + } + + func getMovieTitle(index: Int) -> String { + DispatchQueue.main.async { + self.configureContextMenuCallsPresenterGetMovieTitleExpectation?.fulfill() + } + return "" + } + + func movieSelected(at index: Int) { + DispatchQueue.main.async { + self.collectionViewDidSelectRowATCallsPresenterMovieSelectedExpectation?.fulfill() + } + } + + func addToWatchList(index: Int, imageData: Data) { + + } + + func getTopRatedMovies() { + + } + + func getSavedMovies() -> [CoreDataMovie] { + DispatchQueue.main.async { + self.configureContextMenuCallsPresenterGetSavedMoviesExpectation?.fulfill() + } + return [CoreDataMovie(title: "", poster: Data(), id: 0, date: Date(), voteAverage: 0.0)] + } + + var topRatedMovies: [Movie] { + return [Movie(title: "", poster: nil, id: 1, voteAverage: 1.0)] + } + + var numberOfMovies: Int { + DispatchQueue.main.async { + self.collectionViewNumberOfItemsCallsPresenterNumberOfMoviesExpectation?.fulfill() + self.configurePaginationCallsPresenterNumberOfMoviesExpectation?.fulfill() + } + return 1 + } + +} diff --git a/Movie-Application/Movie-ApplicationTests/UtilitiesTest/Mocks/MockResponseValidator.swift b/Movie-Application/Movie-ApplicationTests/UtilitiesTest/Mocks/MockResponseValidator.swift new file mode 100644 index 00000000..8fd62d08 --- /dev/null +++ b/Movie-Application/Movie-ApplicationTests/UtilitiesTest/Mocks/MockResponseValidator.swift @@ -0,0 +1,26 @@ +// +// MockResponseValidator.swift +// Movie-ApplicationTests +// +// Created by Mohanna Zakizadeh on 5/3/22. +// + +import Foundation +@testable import Movie_Application + +struct MockResponseValidator: ResponseValidatorProtocol { + + func validation(response: HTTPURLResponse? = nil, data: Data?) -> (Result) { + guard let data = data else { + return .failure(RequestError.invalidRequest) + } + do { + let model = try JSONDecoder().decode(T.self, from: data) + return .success(model) + } catch { + print("JSON Parse Error") + print(error) + return .failure(.jsonParseError) + } + } +} diff --git a/Movie-Application/Movie-ApplicationTests/UtilitiesTest/Mocks/Movie.json b/Movie-Application/Movie-ApplicationTests/UtilitiesTest/Mocks/Movie.json new file mode 100644 index 00000000..ed32c7e6 --- /dev/null +++ b/Movie-Application/Movie-ApplicationTests/UtilitiesTest/Mocks/Movie.json @@ -0,0 +1,84 @@ +{ + "adult":false, + "backdrop_path":"/rr7E0NoGKxvbkb89eR1GwfoYjpA.jpg", + "belongs_to_collection":null, + "budget":63000000, + "genres":[ + { + "id":18, + "name":"Drama", + } + ], + "homepage":"http://www.foxmovies.com/movies/fight-club", + "id":550, + "imdb_id":"tt0137523", + "original_language":"en", + "original_title":"Fight Club", + "overview":"A ticking-time-bomb insomniac and a slippery soap salesman channel primal male aggression into a shocking new form of therapy. Their concept catches on, with underground forming in every town, until an eccentric gets in the way and ignites an out-of-control spiral toward oblivion.", + "popularity":51.679, + "poster_path":"/pB8BM7pdSp6B6Ih7QZ4DrQ3PmJK.jpg", + "production_companies":[ + { + "id":508, + "logo_path":"/7PzJdsLGlR7oW4J0J5Xcd0pHGRg.png", + "name":"Regency Enterprises", + "origin_country":"US", + }, + { + "id":711, + "logo_path":"/tEiIH5QesdheJmDAqQwvtN60727.png", + "name":"Fox 2000 Pictures", + "origin_country":"US", + }, + { + "id":20555, + "logo_path":"/hD8yEGUBlHOcfHYbujp71vD8gZp.png", + "name":"Taurus Film", + "origin_country":"DE", + }, + { + "id":54051, + "logo_path":null, + "name":"Atman Entertainment", + "origin_country":"", + }, + { + "id":54052, + "logo_path":null, + "name":"Knickerbocker Films", + "origin_country":"US", + }, + { + "id":4700, + "logo_path":"/A32wmjrs9Psf4zw0uaixF0GXfxq.png", + "name":"The Linson Company", + "origin_country":"US", + } + ], + "production_countries":[ + { + "iso_3166_1":"DE", + "name":"Germany", + }, + { + "iso_3166_1":"US", + "name":"United States of America", + } + ], + "release_date":"1999-10-15", + "revenue":100853753, + "runtime":139, + "spoken_languages":[ + { + "english_name":"English", + "iso_639_1":"en", + "name":"English", + } + ], + "status":"Released", + "tagline":"Mischief. Mayhem. Soap.", + "title":"Fight Club", + "video":false, + "vote_average":8.4, + "vote_count":23989, +} diff --git a/Movie-Application/Movie-ApplicationTests/UtilitiesTest/Mocks/Movies.json b/Movie-Application/Movie-ApplicationTests/UtilitiesTest/Mocks/Movies.json new file mode 100644 index 00000000..25894362 --- /dev/null +++ b/Movie-Application/Movie-ApplicationTests/UtilitiesTest/Mocks/Movies.json @@ -0,0 +1,403 @@ +{ + "page":1, + "results":[ + { + "adult":false, + "backdrop_path":"/aEGiJJP91HsKVTEPy1HhmN0wRLm.jpg", + "genre_ids":[ + 28, + 12, + 10751, + ], + "id":335787, + "original_language":"en", + "original_title":"Uncharted", + "overview":"A young street-smart, Nathan Drake and his wisecracking partner Victor “Sully” Sullivan embark on a dangerous pursuit of “the greatest treasure never found” while also tracking clues that may lead to Nathan’s long-lost brother.", + "popularity":9026.362, + "poster_path":"/tlZpSxYuBRoVJBOpUrPdQe9FmFq.jpg", + "release_date":"2022-02-10", + "title":"Uncharted", + "video":false, + "vote_average":7.2, + "vote_count":1563, + }, + { + "adult":false, + "backdrop_path":"/tRS6jvPM9qPrrnx2KRp3ew96Yot.jpg", + "genre_ids":[ + 80, + 9648, + 53, + ], + "id":414906, + "original_language":"en", + "original_title":"The Batman", + "overview":"In his second year of fighting crime, Batman uncovers corruption in Gotham City that connects to his own family while facing a serial killer known as the Riddler.", + "popularity":8195.597, + "poster_path":"/74xTEgt7R36Fpooo50r9T25onhq.jpg", + "release_date":"2022-03-01", + "title":"The Batman", + "video":false, + "vote_average":7.8, + "vote_count":4161, + }, + { + "adult":false, + "backdrop_path":"/iQFcwSGbZXMkeyKrxbPnwnRo5fl.jpg", + "genre_ids":[ + 28, + 12, + 878, + ], + "id":634649, + "original_language":"en", + "original_title":"Spider-Man: No Way Home", + "overview":"Peter Parker is unmasked and no longer able to separate his normal life from the high-stakes of being a super-hero. When he asks for help from Doctor Strange the stakes become even more dangerous, forcing him to discover what it truly means to be Spider-Man.", + "popularity":4716.995, + "poster_path":"/1g0dhYtq4irTY1GPXvft6k4YLjm.jpg", + "release_date":"2021-12-15", + "title":"Spider-Man: No Way Home", + "video":false, + "vote_average":8.1, + "vote_count":12294, + }, + { + "adult":false, + "backdrop_path":"/egoyMDLqCxzjnSrWOz50uLlJWmD.jpg", + "genre_ids":[ + 28, + 878, + 35, + 10751, + ], + "id":675353, + "original_language":"en", + "original_title":"Sonic the Hedgehog 2", + "overview":"After settling in Green Hills, Sonic is eager to prove he has what it takes to be a true hero. His test comes when Dr. Robotnik returns, this time with a new partner, Knuckles, in search for an emerald that has the power to destroy civilizations. Sonic teams up with his own sidekick, Tails, and together they embark on a globe-trotting journey to find the emerald before it falls into the wrong hands.", + "popularity":4056.245, + "poster_path":"/6DrHO1jr3qVrViUO6s6kFiAGM7.jpg", + "release_date":"2022-03-30", + "title":"Sonic the Hedgehog 2", + "video":false, + "vote_average":7.6, + "vote_count":786, + }, + { + "adult":false, + "backdrop_path":"/zBG5Mg29NH9xxpWMMG7BIvKwYhL.jpg", + "genre_ids":[ + 10749, + 18, + ], + "id":829557, + "original_language":"pl", + "original_title":"365 Days: This Day", + "overview":"Laura and Massimo are back and hotter than ever. But the reunited couple's new beginning is complicated by Massimo’s family ties and a mysterious man who enters Laura’s life to win her heart and trust, at any cost.", + "popularity":4922.217, + "poster_path":"/3UPXQtdWLdOkU6zHNP3gIZ2M4K4.jpg", + "release_date":"2022-04-27", + "title":"365 Days: This Day", + "video":false, + "vote_average":6.1, + "vote_count":242, + }, + { + "adult":false, + "backdrop_path":"/fOy2Jurz9k6RnJnMUMRDAgBwru2.jpg", + "genre_ids":[ + 16, + 10751, + 35, + 14, + ], + "id":508947, + "original_language":"en", + "original_title":"Turning Red", + "overview":"Thirteen-year-old Mei is experiencing the awkwardness of being a teenager with a twist – when she gets too excited, she transforms into a giant red panda.", + "popularity":3513.967, + "poster_path":"/qsdjk9oAKSQMWs0Vt5Pyfh6O4GZ.jpg", + "release_date":"2022-03-10", + "title":"Turning Red", + "video":false, + "vote_average":7.4, + "vote_count":1980, + }, + { + "adult":false, + "backdrop_path":"/2n95p9isIi1LYTscTcGytlI4zYd.jpg", + "genre_ids":[ + 18, + 53, + 80, + ], + "id":799876, + "original_language":"en", + "original_title":"The Outfit", + "overview":"Leonard is an English tailor who used to craft suits on London’s world-famous Savile Row. After a personal tragedy, he’s ended up in Chicago, operating a small tailor shop in a rough part of town where he makes beautiful clothes for the only people around who can afford them: a family of vicious gangsters.", + "popularity":2752.077, + "poster_path":"/ieOVuwnoFC49m7bekmdQ4AynciS.jpg", + "release_date":"2022-02-25", + "title":"The Outfit", + "video":false, + "vote_average":7, + "vote_count":161, + }, + { + "adult":false, + "backdrop_path":"/x747ZvF0CcYYTTpPRCoUrxA2cYy.jpg", + "genre_ids":[ + 28, + 12, + 878, + ], + "id":406759, + "original_language":"en", + "original_title":"Moonfall", + "overview":"A mysterious force knocks the moon from its orbit around Earth and sends it hurtling on a collision course with life as we know it.", + "popularity":2291.114, + "poster_path":"/odVv1sqVs0KxBXiA8bhIBlPgalx.jpg", + "release_date":"2022-02-03", + "title":"Moonfall", + "video":false, + "vote_average":6.5, + "vote_count":865, + }, + { + "adult":false, + "backdrop_path":"/hXTWVJMsI9BkxMLliqL1j0FT55t.jpg", + "genre_ids":[ + 28, + ], + "id":606402, + "original_language":"ko", + "original_title":"야차", + "overview":"Nicknamed after a human-devouring spirit, the ruthless leader of an overseas black ops team takes up a dangerous mission in a city riddled with spies.", + "popularity":1866.502, + "poster_path":"/7MDgiFOPUCeG74nQsMKJuzTJrtc.jpg", + "release_date":"2022-04-08", + "title":"Yaksha: Ruthless Operations", + "video":false, + "vote_average":6.2, + "vote_count":72, + }, + { + "adult":false, + "backdrop_path":"/3G1Q5xF40HkUBJXxt2DQgQzKTp5.jpg", + "genre_ids":[ + 16, + 35, + 10751, + 14, + ], + "id":568124, + "original_language":"en", + "original_title":"Encanto", + "overview":"The tale of an extraordinary family, the Madrigals, who live hidden in the mountains of Colombia, in a magical house, in a vibrant town, in a wondrous, charmed place called an Encanto. The magic of the Encanto has blessed every child in the family with a unique gift from super strength to the power to heal—every child except one, Mirabel. But when she discovers that the magic surrounding the Encanto is in danger, Mirabel decides that she, the only ordinary Madrigal, might just be her exceptional family's last hope.", + "popularity":1674.855, + "poster_path":"/4j0PNHkMr5ax3IA8tjtxcmPU3QT.jpg", + "release_date":"2021-11-24", + "title":"Encanto", + "video":false, + "vote_average":7.7, + "vote_count":6268, + }, + { + "adult":false, + "backdrop_path":"/xicKILMzPn6XZYCOpWwaxlUzg6S.jpg", + "genre_ids":[ + 53, + 28, + ], + "id":294793, + "original_language":"en", + "original_title":"All the Old Knives", + "overview":"When the CIA discovers one of its agents leaked information that cost more than 100 people their lives, veteran operative Henry Pelham is assigned to root out the mole with his former lover and colleague Celia Harrison.", + "popularity":1690.875, + "poster_path":"/g4tMniKxol1TBJrHlAtiDjjlx4Q.jpg", + "release_date":"2022-04-08", + "title":"All the Old Knives", + "video":false, + "vote_average":6, + "vote_count":187, + }, + { + "adult":false, + "backdrop_path":"/iDeWAGnmloZ5Oz3bocDp4rSbUXd.jpg", + "genre_ids":[ + 28, + 53, + ], + "id":823625, + "original_language":"en", + "original_title":"Blacklight", + "overview":"Travis Block is a shadowy Government agent who specializes in removing operatives whose covers have been exposed. He then has to uncover a deadly conspiracy within his own ranks that reaches the highest echelons of power.", + "popularity":1635.723, + "poster_path":"/bv9dy8mnwftdY2j6gG39gCfSFpV.jpg", + "release_date":"2022-02-10", + "title":"Blacklight", + "video":false, + "vote_average":6.2, + "vote_count":328, + }, + { + "adult":false, + "backdrop_path":"/ewUqXnwiRLhgmGhuksOdLgh49Ch.jpg", + "genre_ids":[ + 28, + 12, + 35, + 878, + ], + "id":696806, + "original_language":"en", + "original_title":"The Adam Project", + "overview":"After accidentally crash-landing in 2022, time-traveling fighter pilot Adam Reed teams up with his 12-year-old self on a mission to save the future.", + "popularity":1395.056, + "poster_path":"/wFjboE0aFZNbVOF05fzrka9Fqyx.jpg", + "release_date":"2022-03-11", + "title":"The Adam Project", + "video":false, + "vote_average":7, + "vote_count":2044, + }, + { + "adult":false, + "backdrop_path":"/dqWiut9F30jkiKHHkYTf2RIy1g7.jpg", + "genre_ids":[ + 878, + 28, + ], + "id":919689, + "original_language":"en", + "original_title":"War of the Worlds: Annihilation", + "overview":"A mother and son find themselves faced with a brutal alien invasion where survival will depend on discovering the unthinkable truth about the enemy.", + "popularity":1556.313, + "poster_path":"/9eiUNsUAw2iwVyMeXNNiNQQad4E.jpg", + "release_date":"2021-12-22", + "title":"War of the Worlds: Annihilation", + "video":false, + "vote_average":5.6, + "vote_count":37, + }, + { + "adult":false, + "backdrop_path":"/An1nKjXahrChfEbZ3MAE8fsiut1.jpg", + "genre_ids":[ + 27, + ], + "id":661791, + "original_language":"es", + "original_title":"La abuela", + "overview":"A Paris model must return to Madrid where her grandmother, who raised her, has had a stroke. But spending just a few days with this relative turns into an unexpected nightmare.", + "popularity":1248.638, + "poster_path":"/eIUixNvox4U4foL5Z9KbN9HXYSM.jpg", + "release_date":"2022-01-28", + "title":"The Grandmother", + "video":false, + "vote_average":6.1, + "vote_count":141, + }, + { + "adult":false, + "backdrop_path":"/tq3klWQevRK0Or0cGhsw0h3FDWQ.jpg", + "genre_ids":[ + 12, + 16, + 35, + 10751, + 14, + ], + "id":676705, + "original_language":"fr", + "original_title":"Pil", + "overview":"Pil, a little vagabond girl, lives on the streets of the medieval city of Roc-en-Brume, along with her three tame weasels. She survives of food stolen from the castle of the sinister Regent Tristain. One day, to escape his guards, Pil disguises herself as a princess. Thus she embarks upon a mad, delirious adventure, together with Crobar, a big clumsy guard who thinks she's a noble, and Rigolin, a young crackpot jester. Pil is going to have to save Roland, rightful heir to the throne under the curse of a spell. This adventure will turn the entire kingdom upside down, and teach Pil that nobility can be found in all of us.", + "popularity":1270.482, + "poster_path":"/abPQVYyNfVuGoFUfGVhlNecu0QG.jpg", + "release_date":"2021-08-11", + "title":"Pil's Adventures", + "video":false, + "vote_average":6.7, + "vote_count":73, + }, + { + "adult":false, + "backdrop_path":"/i0zbSmiyyylh7H3Qb4jgscz46Pm.jpg", + "genre_ids":[ + 27, + ], + "id":893370, + "original_language":"es", + "original_title":"Virus-32", + "overview":"A virus is unleashed and a chilling massacre runs through the streets of Montevideo.", + "popularity":1954.425, + "poster_path":"/wZiF79hbhLK1U2Pj9bF67NAKXQR.jpg", + "release_date":"2022-04-21", + "title":"Virus:32", + "video":false, + "vote_average":6.9, + "vote_count":26, + }, + { + "adult":false, + "backdrop_path":"/qBLEWvJNVsehJkEJqIigPsWyBse.jpg", + "genre_ids":[ + 16, + 10751, + 14, + 35, + 12, + ], + "id":585083, + "original_language":"en", + "original_title":"Hotel Transylvania: Transformania", + "overview":"When Van Helsing's mysterious invention, the goes haywire, Drac and his monster pals are all transformed into humans, and Johnny becomes a monster. In their new mismatched bodies, Drac and Johnny must team up and race across the globe to find a cure before it's too late, and before they drive each other crazy.", + "popularity":1138.026, + "poster_path":"/teCy1egGQa0y8ULJvlrDHQKnxBL.jpg", + "release_date":"2022-02-25", + "title":"Hotel Transylvania: Transformania", + "video":false, + "vote_average":7.1, + "vote_count":800, + }, + { + "adult":false, + "backdrop_path":"/33wnBK5NxvuKQv0Cxo3wMv0eR7F.jpg", + "genre_ids":[ + 27, + 53, + ], + "id":833425, + "original_language":"en", + "original_title":"No Exit", + "overview":"Stranded at a rest stop in the mountains during a blizzard, a recovering addict discovers a kidnapped child hidden in a car belonging to one of the people inside the building which sets her on a terrifying struggle to identify who among them is the kidnapper.", + "popularity":1206.748, + "poster_path":"/5cnLoWq9o5tuLe1Zq4BTX4LwZ2B.jpg", + "release_date":"2022-02-25", + "title":"No Exit", + "video":false, + "vote_average":6.7, + "vote_count":409, + }, + { + "adult":false, + "backdrop_path":"/t7I942V5U1Ggn6OevN75u3sNYH9.jpg", + "genre_ids":[ + 28, + 53, + ], + "id":760868, + "original_language":"sv", + "original_title":"Svart krabba", + "overview":"To end an apocalyptic war and save her daughter, a reluctant soldier embarks on a desperate mission to cross a frozen sea carrying a top-secret cargo.", + "popularity":1116.696, + "poster_path":"/mcIYHZYwUbvhvUt8Lb5nENJ7AlX.jpg", + "release_date":"2022-03-18", + "title":"Black Crab", + "video":false, + "vote_average":6.3, + "vote_count":389, + } + ], + "total_pages":33387, + "total_results":667725, +} diff --git a/Movie-Application/Movie-ApplicationTests/UtilitiesTest/Mocks/RequestManagerMock.swift b/Movie-Application/Movie-ApplicationTests/UtilitiesTest/Mocks/RequestManagerMock.swift new file mode 100644 index 00000000..d301773d --- /dev/null +++ b/Movie-Application/Movie-ApplicationTests/UtilitiesTest/Mocks/RequestManagerMock.swift @@ -0,0 +1,50 @@ +// +// RequestManagerMock.swift +// Movie-ApplicationTests +// +// Created by Mohanna Zakizadeh on 5/3/22. +// + +import Foundation +@testable import Movie_Application + +final class RequestManagerMock: RequestManagerProtocol { + + var session: URLSession! + + var timeOutInterval: Double { + return 5 + } + + var baseApi: String + + var responseValidator: ResponseValidatorProtocol + + var reponseLog: URLRequestLoggableProtocol? + + public init(session: URLSession, validator: ResponseValidatorProtocol) { + self.baseApi = "" + self.session = session + self.responseValidator = validator + } + + func performRequestWith(url: String, httpMethod: HTTPMethod, + completionHandler: @escaping DecodableResponse) where T: Decodable { + guard let url = URL(string: url) else { + completionHandler(.failure(.invalidRequest)) + return + } + session.dataTask(with: url) { (data, response, error) in + usleep(400) + if error != nil { + return completionHandler(.failure(RequestError.connectionError)) + } else { + let validationResult: (Result) = + self.responseValidator.validation(response: response as? HTTPURLResponse, + data: data) + return completionHandler(validationResult) + } + }.resume() + } + +} diff --git a/Movie-Application/Movie-ApplicationTests/UtilitiesTest/Mocks/URLSessionDataTaskMock.swift b/Movie-Application/Movie-ApplicationTests/UtilitiesTest/Mocks/URLSessionDataTaskMock.swift new file mode 100644 index 00000000..99bb140d --- /dev/null +++ b/Movie-Application/Movie-ApplicationTests/UtilitiesTest/Mocks/URLSessionDataTaskMock.swift @@ -0,0 +1,22 @@ +// +// URLSessionDataTaskMock.swift +// Movie-ApplicationTests +// +// Created by Mohanna Zakizadeh on 5/3/22. +// + +import Foundation + +final class URLSessionDataTaskMock: URLSessionDataTask { + private let closure: () -> Void + + init(closure: @escaping () -> Void) { + self.closure = closure + } + + // We override the 'resume' method and simply call our closure + // instead of actually resuming any task. + override func resume() { + closure() + } +} diff --git a/Movie-Application/Movie-ApplicationTests/UtilitiesTest/Mocks/URLSessionMock.swift b/Movie-Application/Movie-ApplicationTests/UtilitiesTest/Mocks/URLSessionMock.swift new file mode 100644 index 00000000..01026e0d --- /dev/null +++ b/Movie-Application/Movie-ApplicationTests/UtilitiesTest/Mocks/URLSessionMock.swift @@ -0,0 +1,37 @@ +// +// URLSessionMock.swift +// Movie-ApplicationTests +// +// Created by Mohanna Zakizadeh on 5/3/22. +// + +import Foundation +final class URLSessionMock: URLSession { + typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void + + // Properties that enable us to set exactly what data or error + // we want our mocked URLSession to return for any request. + var data: Data? + var error: Error? + + override func dataTask( + with url: URL, + completionHandler: @escaping CompletionHandler) -> URLSessionDataTask { + let data = self.data + let error = self.error + + return URLSessionDataTaskMock { + completionHandler(data, nil, error) + } + } + + override func dataTask(with request: URLRequest, + completionHandler: @escaping CompletionHandler) -> URLSessionDataTask { + let data = self.data + let error = self.error + + return URLSessionDataTaskMock { + completionHandler(data, nil, error) + } + } +} diff --git a/Movie-Application/Movie-ApplicationTests/UtilitiesTest/TestMoviesService.swift b/Movie-Application/Movie-ApplicationTests/UtilitiesTest/TestMoviesService.swift new file mode 100644 index 00000000..d0844689 --- /dev/null +++ b/Movie-Application/Movie-ApplicationTests/UtilitiesTest/TestMoviesService.swift @@ -0,0 +1,126 @@ +// +// TestMoviesService.swift +// Movie-ApplicationTests +// +// Created by Mohanna Zakizadeh on 5/3/22. +// + +import XCTest +@testable import Movie_Application + +final class TestMoviesService: XCTestCase { + + var sut: MoviesService? + var moviesJson: Data? + var movieDetails: Data? + + override func setUpWithError() throws { + let bundle = Bundle(for: type(of: self)) + if let path = bundle.path(forResource: "Movies", ofType: "json") { + do { + let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) + self.moviesJson = data + } catch { + + } + } + + if let path = bundle.path(forResource: "Movie", ofType: "json") { + do { + let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) + self.movieDetails = data + } catch { + + } + } + + } + + override func tearDownWithError() throws { + moviesJson = nil + sut = nil + } + + func testGetPopularMovies() throws { + + // Given + let urlSessionMock = URLSessionMock() + urlSessionMock.data = moviesJson + let mockRequestManager = RequestManagerMock(session: urlSessionMock, validator: MockResponseValidator()) + sut = MoviesService(requestManager: mockRequestManager) + let expectation = XCTestExpectation(description: "Async popular movies test") + var movies: Movies? + + // When + sut?.getPopularMovies(page: 1, completionHandler: { (result) in + defer { + expectation.fulfill() + } + switch result { + case .success(let popularMovies): + movies = popularMovies + case .failure(let error): + XCTFail(error.localizedDescription) + } + }) + + // Then + wait(for: [expectation], timeout: 5) + XCTAssertTrue(movies?.results.count == 20) + } + + func testGetTopRatedMovies() throws { + // Given + let urlSessionMock = URLSessionMock() + urlSessionMock.data = moviesJson + let mockRequestManager = RequestManagerMock(session: urlSessionMock, validator: MockResponseValidator()) + sut = MoviesService(requestManager: mockRequestManager) + let expectation = XCTestExpectation(description: "Async top rated movies test") + var movies: Movies? + + // When + sut?.getTopRatedMovies(page: 1, completionHandler: { (result) in + defer { + expectation.fulfill() + } + switch result { + case .success(let topRatedMovies): + movies = topRatedMovies + case .failure(let error): + XCTFail(error.localizedDescription) + } + }) + + // Then + wait(for: [expectation], timeout: 5) + XCTAssertTrue(movies?.results.count == 20) + } + + func testGetMoviedetails() throws { + // Given + let urlSessionMock = URLSessionMock() + urlSessionMock.data = movieDetails + let mockRequestManager = RequestManagerMock(session: urlSessionMock, validator: MockResponseValidator()) + sut = MoviesService(requestManager: mockRequestManager) + let expectation = XCTestExpectation(description: "Async movie details test") + var movie: MovieDetail? + + // When + sut?.getMovieDetails(id: 550, completionHandler: { (result) in + defer { + expectation.fulfill() + } + switch result { + case .success(let topRatedMovies): + movie = topRatedMovies + case .failure(let error): + XCTFail(error.localizedDescription) + } + }) + + // Then + wait(for: [expectation], timeout: 5) + XCTAssertTrue(movie?.id == 550) + } + +} diff --git a/Movie-Application/Movie-ApplicationTests/UtilitiesTest/UnitTestError.swift b/Movie-Application/Movie-ApplicationTests/UtilitiesTest/UnitTestError.swift new file mode 100644 index 00000000..bd9f65f2 --- /dev/null +++ b/Movie-Application/Movie-ApplicationTests/UtilitiesTest/UnitTestError.swift @@ -0,0 +1,10 @@ +// +// UnitTestError.swift +// Movie-ApplicationTests +// +// Created by Mohanna Zakizadeh on 5/3/22. +// + +import Foundation + +struct UnitTestError: Error {} diff --git a/Movie-Application/Movie-ApplicationTests/WatchlistMoviesTests/TestWatchlistMoviesInteractor.swift b/Movie-Application/Movie-ApplicationTests/WatchlistMoviesTests/TestWatchlistMoviesInteractor.swift new file mode 100644 index 00000000..152aeebe --- /dev/null +++ b/Movie-Application/Movie-ApplicationTests/WatchlistMoviesTests/TestWatchlistMoviesInteractor.swift @@ -0,0 +1,29 @@ +// +// TestWatchlistMoviesInteractor.swift +// Movie-ApplicationTests +// +// Created by Mohanna Zakizadeh on 5/2/22. +// + +import XCTest +@testable import Movie_Application + +final class TestWatchlistMoviesInteractor: XCTestCase { + + var interactor: WatchlistMoviesInteractor! + + override func setUpWithError() throws { + interactor = .init() + } + + override func tearDownWithError() throws { + interactor = nil + } + + func testInteractorHasGetMovieDetails() throws { + interactor.getMovieDetails(id: 0) { _ in + return + } + } + +} diff --git a/Movie-Application/Movie-ApplicationTests/WatchlistMoviesTests/TestWatchlistMoviesPresenter.swift b/Movie-Application/Movie-ApplicationTests/WatchlistMoviesTests/TestWatchlistMoviesPresenter.swift new file mode 100644 index 00000000..86f29de9 --- /dev/null +++ b/Movie-Application/Movie-ApplicationTests/WatchlistMoviesTests/TestWatchlistMoviesPresenter.swift @@ -0,0 +1,115 @@ +// +// TestWatchlistMoviesPresenter.swift +// Movie-ApplicationTests +// +// Created by Mohanna Zakizadeh on 5/2/22. +// + +import XCTest +@testable import Movie_Application + +final class TestWatchlistMoviesPresenter: XCTestCase { + +// swiftlint: disable identifier_name + var presenter: WatchlistMoviesPresenter! + + var notificationCallsViewScrollToTopExpectation: XCTestExpectation? + var presenterMovieSelectedCallsInteractorGetMoviesDetailsExpectation: XCTestExpectation? + var presenterCallsSetWatchlistEmptyContainerisHiddenExpectation: XCTestExpectation? + var presenterDeleteFromWatchlistCallsViewReloadCollectionViewExpectation: XCTestExpectation? + + override func setUpWithError() throws { + presenter = .init() + presenter.router = self + presenter.view = self + presenter.interactor = self + + presenter.movies = [CoreDataMovie(title: "", poster: Data(), id: 0, date: Date(), voteAverage: 0)] + } + + override func tearDownWithError() throws { + presenter.view = nil + presenter.interactor = nil + presenter.router = nil + presenter.movies = nil + presenter = nil + } + + func testNotificationCallsViewScrollToTop() throws { + notificationCallsViewScrollToTopExpectation = + expectation(description: "expect to view scroll to top fullfill this expectation") + + NotificationCenter.default.post(name: TabBarViewContorller.tabBarDidTapNotification, object: nil) + wait(for: [notificationCallsViewScrollToTopExpectation!], timeout: 1) + } + + func testPresenterMovieSelectedCallsInteractorGetMoviesDetails() throws { + presenterMovieSelectedCallsInteractorGetMoviesDetailsExpectation = + expectation(description: "expect interactor getMovieDetails to fullfill this expectation") + + presenter.movieSelected(at: 0) + wait(for: [presenterMovieSelectedCallsInteractorGetMoviesDetailsExpectation!], timeout: 1) + } + + func testPresenterCallsSetWatchlistEmptyContainerisHidden() throws { + presenterCallsSetWatchlistEmptyContainerisHiddenExpectation = + expectation(description: "expect view setWatchlistEmptyContainerisHidden to fullfill this expectation") + + presenter.getWatchlistMovies() + wait(for: [presenterCallsSetWatchlistEmptyContainerisHiddenExpectation!], timeout: 1) + } + + func testPresenterDeleteFromWatchlistCallsViewReloadCollectionView() throws { + presenterDeleteFromWatchlistCallsViewReloadCollectionViewExpectation = + expectation(description: "expect view reloadCollectionView to fullfill this expectation") + + presenter.deletefromWatchList(0) + wait(for: [presenterDeleteFromWatchlistCallsViewReloadCollectionViewExpectation!], timeout: 1) + } + +} + +extension TestWatchlistMoviesPresenter: WatchlistMoviesViewInterface { + func reloadCollectionView() { + DispatchQueue.main.async { + self.presenterDeleteFromWatchlistCallsViewReloadCollectionViewExpectation?.fulfill() + } + } + + func scrollToTop() { + DispatchQueue.main.async { + self.notificationCallsViewScrollToTopExpectation?.fulfill() + } + } + + func showError(with error: RequestError, index: Int) { + + } + + func setWatchlistEmptyContainerisHidden(to isHidden: Bool) { + DispatchQueue.main.async { + self.presenterCallsSetWatchlistEmptyContainerisHiddenExpectation?.fulfill() + } + } + +} + +extension TestWatchlistMoviesPresenter: WatchlistMoviesRouterInterface { + func showMovieDetails(_ movie: MovieDetail) { + + } + + func showPopularMovies() { + + } + +} + +extension TestWatchlistMoviesPresenter: WatchlistMoviesInteractorInterface { + func getMovieDetails(id: Int, completionHandler: @escaping MovieDetailsCompletionHandler) { + DispatchQueue.main.async { + self.presenterMovieSelectedCallsInteractorGetMoviesDetailsExpectation?.fulfill() + } + } + +} diff --git a/Movie-Application/Movie-ApplicationTests/WatchlistMoviesTests/TestWatchlistMoviesRouter.swift b/Movie-Application/Movie-ApplicationTests/WatchlistMoviesTests/TestWatchlistMoviesRouter.swift new file mode 100644 index 00000000..23ac16cb --- /dev/null +++ b/Movie-Application/Movie-ApplicationTests/WatchlistMoviesTests/TestWatchlistMoviesRouter.swift @@ -0,0 +1,34 @@ +// +// TestWatchlistMoviesRouter.swift +// Movie-ApplicationTests +// +// Created by Mohanna Zakizadeh on 5/2/22. +// + +import XCTest +@testable import Movie_Application + +final class TestWatchlistMoviesRouter: XCTestCase { + + var router: WatchlistMoviesRouter! + + override func setUpWithError() throws { + router = .init() + } + + override func tearDownWithError() throws { + router = nil + } + + func testRouterHasShowMovieDetailsMethod() throws { + router.showMovieDetails(MovieDetail(title: "", poster: "", id: 0, + genres: [Genres(id: 0, name: "")], + overview: nil, voteAverage: 0, + releaseDate: "", reviewsCount: 0)) + } + + func testRouterHasShowPopulerMoviesMethod() throws { + router.showPopularMovies() + } + +} diff --git a/Movie-Application/Movie-ApplicationTests/WatchlistMoviesTests/TestWatchlistMoviesView.swift b/Movie-Application/Movie-ApplicationTests/WatchlistMoviesTests/TestWatchlistMoviesView.swift new file mode 100644 index 00000000..c000d2a5 --- /dev/null +++ b/Movie-Application/Movie-ApplicationTests/WatchlistMoviesTests/TestWatchlistMoviesView.swift @@ -0,0 +1,173 @@ +// +// TestWatchlistMoviesView.swift +// Movie-ApplicationTests +// +// Created by Mohanna Zakizadeh on 5/2/22. +// + +import XCTest +@testable import Movie_Application + +final class TestWatchlistMoviesView: XCTestCase { + + var view: WatchlistMoviesView! + // swiftlint: disable identifier_name + var collectionViewDidSelectRowATCallsPresenterMovieSelectedExpectation: XCTestExpectation? + var collectionViewNumberOfItemsCallsPresenterNumberOfMoviesExpectation: XCTestExpectation? + + override func setUpWithError() throws { + let navigation = UIStoryboard(name: "WatchlistMovies", + bundle: nil).instantiateInitialViewController() as? UINavigationController + view = navigation?.topViewController as? WatchlistMoviesView + view.presenter = self + view.loadView() + view.viewDidLoad() + } + + override func tearDownWithError() throws { + view = nil + } + + func testViewHasWatchlistTitle() throws { + // Given + let title = "Watchlist" + + // When + let viewTitle = view.navigationItem.title + + // Then + XCTAssert(viewTitle == title) + } + + func testViewHasLargeTitle() throws { + // Given + let prefersLargeTitles = true + + // When + let viewPrefersLargeTitles = view.navigationController!.navigationBar.prefersLargeTitles + + // Then + XCTAssert(prefersLargeTitles == viewPrefersLargeTitles) + } + + func testViewHasNavigationSortButton() throws { + XCTAssertNotNil(view.navigationItem.rightBarButtonItem) + } + + func testViewHasNavigationSortButtonTitle() throws { + // Given + let title = "Sort" + // When + let viewNavigationButtonTitle = view.navigationItem.rightBarButtonItem!.title! + // Then + XCTAssert(title == viewNavigationButtonTitle) + } + + func testCollectionViewHasDataSource() throws { + XCTAssertNotNil(view.collectionView.dataSource) + } + + func testCollectionViewHasDelegate() throws { + XCTAssertNotNil(view.collectionView.delegate) + } + + func testViewConformsToCollectionViewDataSource() throws { + XCTAssertNotNil(view as? UICollectionViewDataSource) + } + + func testViewConformsToCollectionViewDelegate() throws { + XCTAssertNotNil(view as? UICollectionViewDelegate) + } + + func testViewConformsToCollectionViewDelegateFlowLayout() { + XCTAssertNotNil(view as? UICollectionViewDelegateFlowLayout) + } + + func testCollectionViewHasACellWithCityCellID() throws { + XCTAssertNotNil(view.collectionView.dequeueReusableCell(withReuseIdentifier: "MovieCell", + for: IndexPath(index: 1))) + } + + func testCollectionViewDidSelectRowATCallsPresenterMovieSelected() throws { + collectionViewDidSelectRowATCallsPresenterMovieSelectedExpectation = + expectation(description: "expect presenter movieSelected to fullfill this expectation") + view.collectionView(view.collectionView, didSelectItemAt: IndexPath(row: 0, section: 0)) + wait(for: [collectionViewDidSelectRowATCallsPresenterMovieSelectedExpectation!], timeout: 2) + } + + func testCollectionViewNumberOfItemsCallsPresenterNumberOfMovies() throws { + collectionViewNumberOfItemsCallsPresenterNumberOfMoviesExpectation = + expectation(description: "expect presenter numberOfMovies fullfill this expectation") + _ = view.collectionView(view.collectionView, numberOfItemsInSection: 0) + wait(for: [collectionViewNumberOfItemsCallsPresenterNumberOfMoviesExpectation!], timeout: 2) + } + +} + +extension TestWatchlistMoviesView: WatchlistMoviesPresenterViewInterface { + func configureContextMenu(_ index: Int) -> UIContextMenuConfiguration { + UIContextMenuConfiguration() + } + + func viewDidLoad() { + + } + + func getMovieImage(index: Int) -> UIImage { + UIImage() + } + + func getMovieTitle(index: Int) -> String { + "" + } + + func movieSelected(at index: Int) { + DispatchQueue.main.async { + self.collectionViewDidSelectRowATCallsPresenterMovieSelectedExpectation?.fulfill() + } + } + + func deletefromWatchList(_ index: Int) { + + } + + func getWatchlistMovies() { + + } + + func deleteMovies() { + + } + + func alertRetryButtonDidTap(_ index: Int) { + + } + + func sortByDate() { + + } + + func sortByName() { + + } + + func sortByUserScore() { + + } + + func browseMoviesDidTap() { + + } + + var watchlistMovies: [CoreDataMovie] { + [CoreDataMovie(title: "", poster: Data(), id: 0, date: Date(), voteAverage: 0)] + } + + var numberOfMovies: Int { + DispatchQueue.main.async { + self.collectionViewNumberOfItemsCallsPresenterNumberOfMoviesExpectation?.fulfill() + } + return 1 + } + +} diff --git a/Movie-Application/Movie-ApplicationUITests/Movie_ApplicationUITests.swift b/Movie-Application/Movie-ApplicationUITests/Movie_ApplicationUITests.swift new file mode 100644 index 00000000..d9b0ca2e --- /dev/null +++ b/Movie-Application/Movie-ApplicationUITests/Movie_ApplicationUITests.swift @@ -0,0 +1,41 @@ +// +// Movie_ApplicationUITests.swift +// Movie-ApplicationUITests +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import XCTest +// swiftlint: disable type_name +class Movie_ApplicationUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/Movie-Application/Movie-ApplicationUITests/Movie_ApplicationUITestsLaunchTests.swift b/Movie-Application/Movie-ApplicationUITests/Movie_ApplicationUITestsLaunchTests.swift new file mode 100644 index 00000000..6df2254a --- /dev/null +++ b/Movie-Application/Movie-ApplicationUITests/Movie_ApplicationUITestsLaunchTests.swift @@ -0,0 +1,33 @@ +// +// Movie_ApplicationUITestsLaunchTests.swift +// Movie-ApplicationUITests +// +// Created by Mohanna Zakizadeh on 4/23/22. +// + +import XCTest + +// swiftlint: disable type_name +class Movie_ApplicationUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/Movie-Application/MovieApplicationMVVM/.DS_Store b/Movie-Application/MovieApplicationMVVM/.DS_Store new file mode 100644 index 00000000..08a7cd14 Binary files /dev/null and b/Movie-Application/MovieApplicationMVVM/.DS_Store differ diff --git a/Movie-Application/MovieApplicationMVVM/App/AppCoordinator.swift b/Movie-Application/MovieApplicationMVVM/App/AppCoordinator.swift new file mode 100644 index 00000000..462d8264 --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/App/AppCoordinator.swift @@ -0,0 +1,37 @@ +// +// AppCoordinator.swift +// MovieApplicationMVVM +// +// Created by Mohanna Zakizadeh on 5/3/22. +// + +import UIKit + +final class AppCoordinator: NSObject, Coordinator { + + var navigationController: UINavigationController? + // Since AppCoordinator is top of all coordinators of our app, it has no parent and is nil. + var parentCoordinator: Coordinator? + + let window: UIWindow? + var tabBarController: UITabBarController? + + var childCoordinators = [Coordinator]() + + init(window: UIWindow?) { + self.window = window + super.init() + tabBarController = MainTabBarController(parentCoordinator: self) + } + + func start() { + guard let window = window else { return } + + window.rootViewController = tabBarController + window.makeKeyAndVisible() + } + + func changeTabBarIndex(to index: Int) { + tabBarController?.selectedIndex = index + } +} diff --git a/Movie-Application/MovieApplicationMVVM/App/AppDelegate.swift b/Movie-Application/MovieApplicationMVVM/App/AppDelegate.swift new file mode 100644 index 00000000..14d3372a --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/App/AppDelegate.swift @@ -0,0 +1,70 @@ +// +// AppDelegate.swift +// MovieApplicationMVVM +// +// Created by Mohanna Zakizadeh on 5/3/22. +// + +import UIKit +import CoreData + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + var appCoordinator: AppCoordinator! + // swiftlint: disable all + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + + setupCoordinator() + return true + } + + fileprivate func setupCoordinator() { + window = UIWindow(frame: UIScreen.main.bounds) + + appCoordinator = AppCoordinator(window: window) + appCoordinator.start() + } + + lazy var persistentContainer: NSPersistentContainer = { + /* + The persistent container for the application. This implementation + creates and returns a container, having loaded the store for the + application to it. This property is optional since there are legitimate + error conditions that could cause the creation of the store to fail. + */ + let container = NSPersistentContainer(name: "FavoriteMovieModel") + container.loadPersistentStores(completionHandler: { (storeDescription, error) in + if let error = error as NSError? { + + /* + Typical reasons for an error here include: + * The parent directory does not exist, cannot be created, or disallows writing. + * The persistent store is not accessible, due to permissions or data protection when the device is locked. + * The device is out of space. + * The store could not be migrated to the current model version. + Check the error message to determine what the actual problem was. + */ + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + return container + }() + + // MARK: - Core Data Saving support + + func saveContext () { + let context = persistentContainer.viewContext + if context.hasChanges { + do { + try context.save() + } catch { + + let nserror = error as NSError + fatalError("Unresolved error \(nserror), \(nserror.userInfo)") + } + } + } + +} + diff --git a/Movie-Application/MovieApplicationMVVM/App/TabBar/MainTabBarController.swift b/Movie-Application/MovieApplicationMVVM/App/TabBar/MainTabBarController.swift new file mode 100644 index 00000000..46f21c3a --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/App/TabBar/MainTabBarController.swift @@ -0,0 +1,58 @@ +// +// MainTabBarController.swift +// MovieApplicationMVVM +// +// Created by Mohanna Zakizadeh on 5/4/22. +// + +import UIKit + +final class MainTabBarController: UITabBarController { + + // MARK: - Properties + let topRatedTabBarItem = UITabBarItem(title: TabBarPage.topRated.pageTitleValue(), + image: TabBarPage.topRated.pageIcon(), + selectedImage: TabBarPage.topRated.pageSelectedIcon()) + let popularTabBarItem = UITabBarItem(title: TabBarPage.popular.pageTitleValue(), + image: TabBarPage.popular.pageIcon(), + selectedImage: TabBarPage.popular.pageSelectedIcon()) + let watchlistTabBarItem = UITabBarItem(title: TabBarPage.watchlist.pageTitleValue(), + image: TabBarPage.watchlist.pageIcon(), + selectedImage: TabBarPage.watchlist.pageSelectedIcon()) + + var topRatedMovies: TopRatedMoviesCoordinator? + var popularMovies: PopularMoviesCoordinator? + var watchlistMovies: WatchlistMoviesCoordinator? + + let parentCoordinator: Coordinator + + init(parentCoordinator: Coordinator) { + self.parentCoordinator = parentCoordinator + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + self.viewControllers = configureViewControllers() + self.selectedIndex = 1 + } + + private func configureViewControllers() -> [UIViewController] { + topRatedMovies = TopRatedMoviesCoordinator(tabBarItem: topRatedTabBarItem, + parentCoordinator: parentCoordinator) + popularMovies = PopularMoviesCoordinator(tabBarItem: popularTabBarItem, + parentCoordinator: parentCoordinator) + watchlistMovies = WatchlistMoviesCoordinator(tabBarItem: watchlistTabBarItem, + parentCoordinator: parentCoordinator) + + return [popularMovies!.navigationController!, + topRatedMovies!.navigationController!, + watchlistMovies!.navigationController!] + } + +} diff --git a/Movie-Application/MovieApplicationMVVM/App/TabBar/TabBarPage.swift b/Movie-Application/MovieApplicationMVVM/App/TabBar/TabBarPage.swift new file mode 100644 index 00000000..060e614b --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/App/TabBar/TabBarPage.swift @@ -0,0 +1,72 @@ +// +// TabBarPage.swift +// MovieApplicationMVVM +// +// Created by Mohanna Zakizadeh on 5/3/22. +// + +import UIKit + +enum TabBarPage { + case popular + case topRated + case watchlist + + init?(index: Int) { + switch index { + case 0: + self = .popular + case 1: + self = .topRated + case 2: + self = .watchlist + default: + return nil + } + } + + func pageTitleValue() -> String { + switch self { + case .popular: + return "Popular" + case .topRated: + return "Top Rated" + case .watchlist: + return "Watchlist" + } + } + + func pageOrderNumber() -> Int { + switch self { + case .popular: + return 0 + case .topRated: + return 1 + case .watchlist: + return 2 + } + } + + func pageIcon() -> UIImage? { + switch self { + case .popular: + return UIImage(systemName: "flame") + case .topRated: + return UIImage(systemName: "list.number") + case .watchlist: + return UIImage(systemName: "bookmark") + } + } + + func pageSelectedIcon() -> UIImage? { + switch self { + case .popular: + return UIImage(systemName: "flame.fill") + case .topRated: + return UIImage(systemName: "list.number") + case .watchlist: + return UIImage(systemName: "bookmark.fill") + } + } + +} diff --git a/Movie-Application/MovieApplicationMVVM/App/TabBar/TabCoordinator.swift b/Movie-Application/MovieApplicationMVVM/App/TabBar/TabCoordinator.swift new file mode 100644 index 00000000..bf7b810d --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/App/TabBar/TabCoordinator.swift @@ -0,0 +1,99 @@ +// +// TabCoordinator.swift +// MovieApplicationMVVM +// +// Created by Mohanna Zakizadeh on 5/3/22. +// + +import UIKit + +protocol TabCoordinatorProtocol: Coordinator { + var tabBarController: UITabBarController { get set } + + func selectPage(_ page: TabBarPage) + + func setSelectedIndex(_ index: Int) + + func currentPage() -> TabBarPage? +} + +class TabCoordinator: NSObject, TabCoordinatorProtocol { + var tabBarController: UITabBarController + + var childCoordinators: [Coordinator] = [] + + override init() { + self.tabBarController = .init() + } + + func start() { + let pages: [TabBarPage] = [.popular, .topRated, .watchlist] + .sorted(by: { $0.pageOrderNumber() < $1.pageOrderNumber() }) + + // Initialization of ViewControllers or these pages + let controllers: [UINavigationController] = pages.map({ getTabController($0) }) + + prepareTabBarController(withTabControllers: controllers) + } + + deinit { + print("TabCoordinator deinit") + } + + private func prepareTabBarController(withTabControllers tabControllers: [UIViewController]) { + // Set delegate for UITabBarController + tabBarController.delegate = self + // Assign page's controllers + tabBarController.setViewControllers(tabControllers, animated: true) + // Let set index + tabBarController.selectedIndex = TabBarPage.topRated.pageOrderNumber() + // Styling + tabBarController.tabBar.isTranslucent = false + + } + + private func getTabController(_ page: TabBarPage) -> UINavigationController { + let navController = UINavigationController() + navController.setNavigationBarHidden(false, animated: false) + + navController.tabBarItem = UITabBarItem.init(title: page.pageTitleValue(), + image: page.pageIcon(), + selectedImage: page.pageSelectedIcon()) + + switch page { + case .popular: +// let popularVC = pop + selectPage(.popular) + + case .topRated: + let topRatedScene = TopRatedMoviesCoordinator() + self.childCoordinators.append(topRatedScene) + selectPage(.topRated) + + case .watchlist: + selectPage(.watchlist) + } + + return navController + } + + func currentPage() -> TabBarPage? { TabBarPage.init(index: tabBarController.selectedIndex) } + + func selectPage(_ page: TabBarPage) { + tabBarController.selectedIndex = page.pageOrderNumber() + } + + func setSelectedIndex(_ index: Int) { + guard let page = TabBarPage.init(index: index) else { return } + + tabBarController.selectedIndex = page.pageOrderNumber() + } +} + +// MARK: - UITabBarControllerDelegate +extension TabCoordinator: UITabBarControllerDelegate { + func tabBarController(_ tabBarController: UITabBarController, + didSelect viewController: UIViewController) { + // Some implementation + } +} diff --git a/Movie-Application/MovieApplicationMVVM/MovieApplicationMVVM.xcdatamodeld/.xccurrentversion b/Movie-Application/MovieApplicationMVVM/MovieApplicationMVVM.xcdatamodeld/.xccurrentversion new file mode 100644 index 00000000..0c67376e --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/MovieApplicationMVVM.xcdatamodeld/.xccurrentversion @@ -0,0 +1,5 @@ + + + + + diff --git a/Movie-Application/MovieApplicationMVVM/MovieApplicationMVVM.xcdatamodeld/MovieApplicationMVVM.xcdatamodel/contents b/Movie-Application/MovieApplicationMVVM/MovieApplicationMVVM.xcdatamodeld/MovieApplicationMVVM.xcdatamodel/contents new file mode 100644 index 00000000..50d2514e --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/MovieApplicationMVVM.xcdatamodeld/MovieApplicationMVVM.xcdatamodel/contents @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Movie-Application/MovieApplicationMVVM/MovieDetails/View/MovieDetailsViewController.swift b/Movie-Application/MovieApplicationMVVM/MovieDetails/View/MovieDetailsViewController.swift new file mode 100644 index 00000000..44d93ab3 --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/MovieDetails/View/MovieDetailsViewController.swift @@ -0,0 +1,13 @@ +// +// MovieDetailsViewController.swift +// MovieApplicationMVVM +// +// Created by Mohanna Zakizadeh on 5/5/22. +// + +import Foundation + +final class MovieDetailsViewController: BottomSheetContainerViewController + { + +} diff --git a/Movie-Application/MovieApplicationMVVM/MovieDetails/View/MovieInfoContent/View/MovieInfoContentViewController.swift b/Movie-Application/MovieApplicationMVVM/MovieDetails/View/MovieInfoContent/View/MovieInfoContentViewController.swift new file mode 100644 index 00000000..41816305 --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/MovieDetails/View/MovieInfoContent/View/MovieInfoContentViewController.swift @@ -0,0 +1,94 @@ +// +// MovieInfoContentViewController.swift +// MovieApplicationMVVM +// +// Created by Mohanna Zakizadeh on 5/5/22. +// + +import UIKit + +final class MovieInfoContentViewController: UIViewController { + + let viewModel = MovieInfoContentViewModel() + + // MARK: - Properties + var imageView: UIImageView! + var addToWatchListButton: UIButton! + + var movie: MovieDetail! + // MARK: - Lifecycle + init(movie: MovieDetail) { + self.movie = movie + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + addToWatchListButton = setupButton() + imageView = setupImageView() + + setupView() + + self.applyTheme() + } + + // MARK: - Theme + func applyTheme() { + view.backgroundColor = .secondarySystemBackground + addToWatchListButton.backgroundColor = .secondaryLabel + addToWatchListButton.tintColor = .systemBackground + } + + // MARK: - Private functions + private func setupImageView() -> UIImageView { + let image = viewModel.getMovieImage(path: movie.poster ?? "") + let imageView = UIImageView(image: image) + imageView.clipsToBounds = true + imageView.layer.cornerRadius = 14 + imageView.translatesAutoresizingMaskIntoConstraints = false + + return imageView + } + + private func setupButton() -> UIButton { + let button = UIButton() + button.addTarget(self, action: #selector(addToWatchListTapped), for: .touchUpInside) + button.setBackgroundImage(UIImage(systemName: "bookmark.circle"), for: .normal) + button.layer.cornerRadius = 20 + return button + } + + private func setupView() { + view.addSubview(imageView) + view.addSubview(addToWatchListButton) + + imageView.translatesAutoresizingMaskIntoConstraints = false + addToWatchListButton.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + imageView.topAnchor.constraint(equalTo: view.topAnchor, constant: 32), + imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 45), + imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -45), + imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor, + constant: -((UIScreen.main.bounds.height / 2.2) + 32)), + + addToWatchListButton.bottomAnchor.constraint(equalTo: imageView.bottomAnchor, constant: -15), + addToWatchListButton.trailingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: -15), + addToWatchListButton.heightAnchor.constraint(equalToConstant: 40), + addToWatchListButton.widthAnchor.constraint(equalToConstant: 40) + ]) + } + + // MARK: - Actions + @objc func addToWatchListTapped() { + viewModel.addToWatchListTapped(movie: movie) + } + +} diff --git a/Movie-Application/MovieApplicationMVVM/MovieDetails/View/MovieInfoContent/ViewModel/MovieInfoContentViewModel.swift b/Movie-Application/MovieApplicationMVVM/MovieDetails/View/MovieInfoContent/ViewModel/MovieInfoContentViewModel.swift new file mode 100644 index 00000000..eb130117 --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/MovieDetails/View/MovieInfoContent/ViewModel/MovieInfoContentViewModel.swift @@ -0,0 +1,29 @@ +// +// MovieInfoContentViewModel.swift +// MovieApplicationMVVM +// +// Created by Mohanna Zakizadeh on 5/5/22. +// + +import UIKit + +final class MovieInfoContentViewModel { + + func getMovieImage(path: String) -> UIImage? { + let url = URL(string: "https://image.tmdb.org/t/p/w500/" + path)! + guard let data = try? Data(contentsOf: url) else { return UIImage(systemName: "film.circle") } + let image = UIImage(data: data) + return image + } + + func addToWatchListTapped(movie: MovieDetail) { + let url = URL(string: "https://image.tmdb.org/t/p/w500/" + (movie.poster ?? ""))! + guard let data = try? Data(contentsOf: url) else { return } + let coreDataMovie = CoreDataMovie(title: movie.title, + poster: data, + id: movie.id, + date: Date.now, + voteAverage: movie.voteAverage) + CoreDataManager().saveNewMovie(coreDataMovie) + } +} diff --git a/Movie-Application/MovieApplicationMVVM/PopularMovies/PopularMoviesCoordinator.swift b/Movie-Application/MovieApplicationMVVM/PopularMovies/PopularMoviesCoordinator.swift new file mode 100644 index 00000000..75b1415c --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/PopularMovies/PopularMoviesCoordinator.swift @@ -0,0 +1,28 @@ +// +// PopularMoviesCoordinator.swift +// MovieApplicationMVVM +// +// Created by Mohanna Zakizadeh on 5/4/22. +// + +import UIKit + +final class PopularMoviesCoordinator: Coordinator { + var navigationController: UINavigationController? + var parentCoordinator: Coordinator? + + init(tabBarItem: UITabBarItem, parentCoordinator: Coordinator) { + navigationController = UINavigationController() + self.parentCoordinator = parentCoordinator + let viewController = PopularMoviesViewController.instantiate(coordinator: self) + viewController.tabBarItem = tabBarItem + viewController.popularMoviesViewModel = PopularMoviesViewModel(moviesService: MoviesService.shared) + navigationController?.viewControllers = [viewController] + navigationController?.navigationBar.prefersLargeTitles = true + } + + func changeTabBarIndex(to index: Int) { + parentCoordinator?.changeTabBarIndex(to: index) + } + +} diff --git a/Movie-Application/MovieApplicationMVVM/PopularMovies/View/PopularMovies.storyboard b/Movie-Application/MovieApplicationMVVM/PopularMovies/View/PopularMovies.storyboard new file mode 100644 index 00000000..b5a7b44b --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/PopularMovies/View/PopularMovies.storyboard @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Movie-Application/MovieApplicationMVVM/PopularMovies/View/PopularMoviesViewController.swift b/Movie-Application/MovieApplicationMVVM/PopularMovies/View/PopularMoviesViewController.swift new file mode 100644 index 00000000..3b1ace78 --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/PopularMovies/View/PopularMoviesViewController.swift @@ -0,0 +1,152 @@ +// +// PopularMoviesViewController.swift +// MovieApplicationMVVM +// +// Created by Mohanna Zakizadeh on 5/4/22. +// + +import UIKit + +final class PopularMoviesViewController: UIViewController, Storyboarded { + // MARK: - Properties + var moviesCollectionViewDataSource: MovieCollectionViewDataSource! + + weak var coordinator: PopularMoviesCoordinator? + @IBOutlet var collectionView: UICollectionView! + + var popularMoviesViewModel: PopularMoviesViewModel! + private let movieImagesCache = NSCache() + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + popularMoviesViewModel.getPopularMovies() + setupView() + } + + // MARK: - Theme + + func applyTheme() { + view.backgroundColor = .systemBackground + } + + // MARK: - Private functions + + private func setupView() { + configureNavigation() + setupCollectionView() + setupBindings() + self.applyTheme() + } + + // bind view to viewModel + private func setupBindings() { + popularMoviesViewModel.movies = { [weak self] movies in + guard let self = self else { return } + self.moviesCollectionViewDataSource.appendItemsToCollectionView(movies) + } + + popularMoviesViewModel.errorHandler = { [weak self] error in + guard let self = self else { return } + let errorAlert = UIAlertController(title: "Error Occured", + message: error, + preferredStyle: .alert) + let alertAction = UIAlertAction(title: "Retry", style: .default) { [weak self] (_) in + self?.popularMoviesViewModel.alertRetryButtonDidTap() + } + errorAlert.addAction(alertAction) + self.present(errorAlert, animated: true, completion: nil) + } + + popularMoviesViewModel.movieDetails = { [weak self] movieDetail in + guard let self = self else { return } + self.coordinator?.showMovieDetails(movieDetail) + } + } + + // function to setup and configure navigation details + private func configureNavigation() { + coordinator?.navigationController?.navigationBar.prefersLargeTitles = true + self.title = "Popular" + } + + // function to setup and configure collectionView details + private func setupCollectionView() { + moviesCollectionViewDataSource = MovieCollectionViewDataSource(items: [], + collectionView: collectionView, + delegate: self) + collectionView.delegate = moviesCollectionViewDataSource + collectionView.dataSource = moviesCollectionViewDataSource + collectionView.showsHorizontalScrollIndicator = false + } + + func configurePagination(_ cellRow: Int) { + if cellRow == popularMoviesViewModel.numberOfMovies - 1 { + popularMoviesViewModel.getPopularMovies() + } + } + + // function to configure contextMenu for each collectionView cell + func configureContextMenu(index: Int, imageData: Data) -> UIContextMenuConfiguration { + popularMoviesViewModel.configureContextMenu(index: index, imageData: imageData) + } + +} + +extension PopularMoviesViewController: MovieCollectionViewDelegate { + func collection(willDisplay cellIndexPath: IndexPath, cell: UICollectionViewCell) { + configurePagination(cellIndexPath.row) + + // for caching cell movie image + guard let cell = cell as? MovieCell else { return } + let cellNumber = NSNumber(value: cellIndexPath.item) + + if let cachedImage = self.movieImagesCache.object(forKey: cellNumber) { + cell.movieImageView.image = cachedImage + } else { + self.popularMoviesViewModel.getMovieImage(index: cellIndexPath.row, completion: { [weak self] (image) in + cell.movieImageView.image = image + self?.movieImagesCache.setObject(image, forKey: cellNumber) + }) + } + } + + func collection(_ collectionView: UICollectionView, didSelectItem index: IndexPath) { + self.popularMoviesViewModel.movieSelected(at: index.row) + } + + func collection(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath) -> CGSize { + let noOfCellsInRow = 2 + + guard let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout else { + return CGSize(width: 0, height: 0) + } + flowLayout.minimumInteritemSpacing = 10 + flowLayout.minimumLineSpacing = 10 + flowLayout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) + + let totalSpace = flowLayout.sectionInset.left + + flowLayout.sectionInset.right + + (flowLayout.minimumInteritemSpacing * CGFloat(noOfCellsInRow - 1)) + + let size = Int((view.bounds.width - totalSpace) / CGFloat(noOfCellsInRow)) + return CGSize(width: size, height: size + 50) + } + + func collection(_ collectionView: UICollectionView, + contextMenuConfigurationForItemAt indexPath: IndexPath, + point: CGPoint) -> UIContextMenuConfiguration? { + let cellNumber = NSNumber(value: indexPath.item) + + if let cachedImage = self.movieImagesCache.object(forKey: cellNumber) { + return configureContextMenu(index: indexPath.row, + imageData: cachedImage.jpegData(compressionQuality: 1.0) ?? Data()) + } + + return configureContextMenu(index: indexPath.row, imageData: Data()) + } + +} diff --git a/Movie-Application/MovieApplicationMVVM/PopularMovies/ViewModel/PopularMoviesViewModel.swift b/Movie-Application/MovieApplicationMVVM/PopularMovies/ViewModel/PopularMoviesViewModel.swift new file mode 100644 index 00000000..034b026e --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/PopularMovies/ViewModel/PopularMoviesViewModel.swift @@ -0,0 +1,158 @@ +// +// PopularMoviesViewModel.swift +// MovieApplicationMVVM +// +// Created by Mohanna Zakizadeh on 5/4/22. +// + +import UIKit + +final class PopularMoviesViewModel { + var moviesService: MoviesServiceProtocol + var movies: (([Movie]) -> Void)? + var movieDetails: ((MovieDetail) -> Void)? + var errorHandler: ((String) -> Void)? + + // movie data base gives 500 pages max. + private var maxPages: Int = 500 + + private var currentPage = 1 + private var allMovies: [Movie]? + + init(moviesService: MoviesServiceProtocol) { + self.moviesService = moviesService + } + + var numberOfMovies: Int { + return allMovies?.count ?? 0 + } + + var topRatedMovies: [Movie] { + return allMovies ?? [] + } + + func alertRetryButtonDidTap() { + getPopularMovies() + } + + // function to get movie image from url that we have + func getMovieImage(index: Int, completion: @escaping (UIImage) -> Void) { + + if let movies = allMovies { + if let path = movies[index].poster { + return moviesService.getMovieImage(for: path, completion: completion) + } + } else { + completion(UIImage(systemName: "film.circle")!) + } + + } + + func getMovieTitle(index: Int) -> String { + allMovies?[index].title ?? "" + } + + func movieSelected(at index: Int) { + if let movies = allMovies { + moviesService.getMovieDetails(id: movies[index].id) { [weak self] result in + switch result { + case .success(let movieDetail): + self?.movieDetails?(movieDetail) + case .failure(let error): + self?.errorHandler?(error.errorDescription ?? error.localizedDescription) + } + } + } + } + + func addToWatchList(index: Int, imageData: Data) { + if let movies = allMovies { + let savedMovie = CoreDataMovie(title: movies[index].title, + poster: imageData, + id: movies[index].id, + date: Date.now, + voteAverage: movies[index].voteAverage) + CoreDataManager().saveNewMovie(savedMovie) + } + } + + func getPopularMovies() { + + if currentPage <= maxPages { + moviesService.getPopularMovies(page: currentPage) { result in + switch result { + case .success(let moviesData): + if self.currentPage == 1 { + self.allMovies = moviesData.results + } else { + self.allMovies! += moviesData.results + } + self.currentPage += 1 + self.movies?(moviesData.results) + + case .failure(let error): + self.errorHandler?(error.errorDescription ?? error.localizedDescription) + } + } + } + + } + + func getSavedMovies() -> [CoreDataMovie] { + CoreDataManager().getSavedMovies() + } + + func configureContextMenu(index: Int, imageData: Data) -> UIContextMenuConfiguration { + // prevents from adding repititious movies to watch list + if !self.getSavedMovies().contains(where: { $0.title == self.getMovieTitle(index: index)}) { + let context = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) -> UIMenu? in + + let viewDetails = UIAction(title: "View Details", + image: UIImage(systemName: "text.below.photo.fill"), + identifier: nil, + discoverabilityTitle: nil, state: .off) { (_) in + self.self.movieSelected(at: index) + } + + let addToWatchList = UIAction(title: "Add to Watchlist", + image: UIImage(systemName: "bookmark"), + identifier: nil, + discoverabilityTitle: nil, state: .off) { (_) in + self.addToWatchList(index: index, imageData: imageData) + } + + return UIMenu(title: self.getMovieTitle(index: index), + image: nil, identifier: nil, + options: UIMenu.Options.displayInline, + children: [addToWatchList, viewDetails]) + + } + return context + + } else { + let context = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) -> UIMenu? in + + let viewDetails = UIAction(title: "View Details", + image: UIImage(systemName: "text.below.photo.fill"), + identifier: nil, discoverabilityTitle: nil, + state: .off) { (_) in + self.movieSelected(at: index) + } + + let addToWatchList = UIAction(title: "Added to Watchlist", + image: UIImage(systemName: "bookmark.fill"), + identifier: nil, discoverabilityTitle: nil, + state: .off) { (_) in + + } + + return UIMenu(title: self.getMovieTitle(index: index), + image: nil, identifier: nil, + options: UIMenu.Options.displayInline, + children: [addToWatchList, viewDetails]) + + } + return context + } + } +} diff --git a/Movie-Application/MovieApplicationMVVM/Supporting Files/.DS_Store b/Movie-Application/MovieApplicationMVVM/Supporting Files/.DS_Store new file mode 100644 index 00000000..b4511e42 Binary files /dev/null and b/Movie-Application/MovieApplicationMVVM/Supporting Files/.DS_Store differ diff --git a/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/.DS_Store b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/.DS_Store new file mode 100644 index 00000000..198f5b13 Binary files /dev/null and b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/.DS_Store differ diff --git a/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AccentColor.colorset/Contents.json b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/1024.png b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 00000000..de033f79 Binary files /dev/null and b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/1024.png differ diff --git a/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/114.png b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/114.png new file mode 100644 index 00000000..2727d8ef Binary files /dev/null and b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/114.png differ diff --git a/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/120.png b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 00000000..f84efece Binary files /dev/null and b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/120.png differ diff --git a/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/180.png b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 00000000..4e381c12 Binary files /dev/null and b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/180.png differ diff --git a/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/29.png b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/29.png new file mode 100644 index 00000000..d806024b Binary files /dev/null and b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/29.png differ diff --git a/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/40.png b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 00000000..d73a1c52 Binary files /dev/null and b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/40.png differ diff --git a/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/57.png b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/57.png new file mode 100644 index 00000000..782f2293 Binary files /dev/null and b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/57.png differ diff --git a/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/58.png b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 00000000..04b3dde8 Binary files /dev/null and b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/58.png differ diff --git a/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/60.png b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 00000000..455f11dd Binary files /dev/null and b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/60.png differ diff --git a/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/80.png b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 00000000..8eb58e65 Binary files /dev/null and b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/80.png differ diff --git a/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/87.png b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/87.png new file mode 100644 index 00000000..5ca436a6 Binary files /dev/null and b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/87.png differ diff --git a/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..73d3b7f6 --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1 @@ +{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"}]} \ No newline at end of file diff --git a/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/Contents.json b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/movieIcon.imageset/Contents.json b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/movieIcon.imageset/Contents.json new file mode 100644 index 00000000..578331f0 --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/movieIcon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "MVVM-1024px.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "MVVM-1024px@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "MVVM-1024px@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/movieIcon.imageset/MVVM-1024px.png b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/movieIcon.imageset/MVVM-1024px.png new file mode 100644 index 00000000..bde65bcc Binary files /dev/null and b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/movieIcon.imageset/MVVM-1024px.png differ diff --git a/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/movieIcon.imageset/MVVM-1024px@2x.png b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/movieIcon.imageset/MVVM-1024px@2x.png new file mode 100644 index 00000000..45a187ce Binary files /dev/null and b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/movieIcon.imageset/MVVM-1024px@2x.png differ diff --git a/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/movieIcon.imageset/MVVM-1024px@3x.png b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/movieIcon.imageset/MVVM-1024px@3x.png new file mode 100644 index 00000000..de033f79 Binary files /dev/null and b/Movie-Application/MovieApplicationMVVM/Supporting Files/Assets.xcassets/movieIcon.imageset/MVVM-1024px@3x.png differ diff --git a/Movie-Application/MovieApplicationMVVM/Supporting Files/Base.lproj/LaunchScreen.storyboard b/Movie-Application/MovieApplicationMVVM/Supporting Files/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..fe18a14a --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/Supporting Files/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Movie-Application/MovieApplicationMVVM/Supporting Files/Info.plist b/Movie-Application/MovieApplicationMVVM/Supporting Files/Info.plist new file mode 100644 index 00000000..0c67376e --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/Supporting Files/Info.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/Movie-Application/MovieApplicationMVVM/TopRatedMovies/TopRatedMoviesCoordinator.swift b/Movie-Application/MovieApplicationMVVM/TopRatedMovies/TopRatedMoviesCoordinator.swift new file mode 100644 index 00000000..bbfdbd20 --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/TopRatedMovies/TopRatedMoviesCoordinator.swift @@ -0,0 +1,28 @@ +// +// TopRatedMoviesCoordinator.swift +// MovieApplicationMVVM +// +// Created by Mohanna Zakizadeh on 5/3/22. +// + +import UIKit + +final class TopRatedMoviesCoordinator: Coordinator { + var parentCoordinator: Coordinator? + var navigationController: UINavigationController? + + init(tabBarItem: UITabBarItem, parentCoordinator: Coordinator) { + navigationController = UINavigationController() + self.parentCoordinator = parentCoordinator + let viewController = TopRatedMoviesViewController.instantiate(coordinator: self) + viewController.tabBarItem = tabBarItem + viewController.topRatedMoviesViewModel = TopRatedMoviesViewModel(moviesService: MoviesService.shared) + navigationController?.viewControllers = [viewController] + navigationController?.navigationBar.prefersLargeTitles = true + } + + func changeTabBarIndex(to index: Int) { + parentCoordinator?.changeTabBarIndex(to: index) + } + +} diff --git a/Movie-Application/MovieApplicationMVVM/TopRatedMovies/View/TopRatedMovies.storyboard b/Movie-Application/MovieApplicationMVVM/TopRatedMovies/View/TopRatedMovies.storyboard new file mode 100644 index 00000000..4517c83e --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/TopRatedMovies/View/TopRatedMovies.storyboard @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Movie-Application/MovieApplicationMVVM/TopRatedMovies/View/TopRatedMoviesViewController.swift b/Movie-Application/MovieApplicationMVVM/TopRatedMovies/View/TopRatedMoviesViewController.swift new file mode 100644 index 00000000..777548b5 --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/TopRatedMovies/View/TopRatedMoviesViewController.swift @@ -0,0 +1,152 @@ +// +// TopRatedMoviesViewController.swift +// MovieApplicationMVVM +// +// Created by Mohanna Zakizadeh on 5/3/22. +// + +import UIKit + +final class TopRatedMoviesViewController: UIViewController, Storyboarded { + // MARK: - Properties + var moviesCollectionViewDataSource: MovieCollectionViewDataSource! + + @IBOutlet var collectionView: UICollectionView! + weak var coordinator: TopRatedMoviesCoordinator? + + var topRatedMoviesViewModel: TopRatedMoviesViewModel! + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + topRatedMoviesViewModel.getTopRatedMovies() + setupView() + } + + // MARK: - Theme + + func applyTheme() { + view.backgroundColor = .systemBackground + } + + // MARK: - Private functions + + private func setupView() { + configureNavigation() + setupCollectionView() + setupBindings() + + self.applyTheme() + } + + // bind view to viewModel + private func setupBindings() { + topRatedMoviesViewModel.movies = { [weak self] movies in + guard let self = self else { return } + self.moviesCollectionViewDataSource.appendItemsToCollectionView(movies) + } + + topRatedMoviesViewModel.errorHandler = { [weak self] error in + guard let self = self else { return } + let errorAlert = UIAlertController(title: "Error Occured", + message: error, + preferredStyle: .alert) + let alertAction = UIAlertAction(title: "Retry", style: .default) { [weak self] (_) in + self?.topRatedMoviesViewModel.alertRetryButtonDidTap() + } + errorAlert.addAction(alertAction) + self.present(errorAlert, animated: true, completion: nil) + } + + topRatedMoviesViewModel.movieDetails = { [weak self] movieDetail in + guard let self = self else { return } + self.coordinator?.showMovieDetails(movieDetail) + } + } + + // function to setup and configure navigation details + private func configureNavigation() { + self.title = "Top Rated" + } + + // function to setup and configure collectionView details + private func setupCollectionView() { + moviesCollectionViewDataSource = MovieCollectionViewDataSource(items: [], + collectionView: collectionView, + delegate: self) + collectionView.delegate = moviesCollectionViewDataSource + collectionView.dataSource = moviesCollectionViewDataSource + collectionView.showsHorizontalScrollIndicator = false + } + + func configurePagination(_ cellRow: Int) { + if cellRow == topRatedMoviesViewModel.numberOfMovies - 1 { + topRatedMoviesViewModel.getTopRatedMovies() + } + } + + // function to configure contextMenu for each collectionView cell + func configureContextMenu(index: Int, imageData: Data) -> UIContextMenuConfiguration { + topRatedMoviesViewModel.configureContextMenu(index: index, imageData: imageData) + } +} + +extension TopRatedMoviesViewController: MovieCollectionViewDelegate { + + func collection(willDisplay cellIndexPath: IndexPath, cell: UICollectionViewCell) { + configurePagination(cellIndexPath.row) + + // for caching cell movie image + guard let cell = cell as? MovieCell else { return } + let cellNumber = NSNumber(value: cellIndexPath.item) + + if let cachedImage = self.topRatedMoviesViewModel.movieImagesCache.object(forKey: cellNumber) { + cell.movieImageView.image = cachedImage + } else { + self.topRatedMoviesViewModel.getMovieImage(index: cellIndexPath.row, completion: { [weak self] (image) in + if self?.collectionView.indexPath(for: cell) == cellIndexPath { + cell.movieImageView.image = image + } + self?.topRatedMoviesViewModel.movieImagesCache.setObject(image, forKey: cellNumber) + }) + } + } + + func collection(_ collectionView: UICollectionView, didSelectItem index: IndexPath) { + self.topRatedMoviesViewModel.movieSelected(at: index.row) + } + + func collection(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath) -> CGSize { + let noOfCellsInRow = 2 + + guard let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout else { + return CGSize(width: 0, height: 0) + } + flowLayout.minimumInteritemSpacing = 10 + flowLayout.minimumLineSpacing = 10 + flowLayout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) + + let totalSpace = flowLayout.sectionInset.left + + flowLayout.sectionInset.right + + (flowLayout.minimumInteritemSpacing * CGFloat(noOfCellsInRow - 1)) + + let size = Int((view.bounds.width - totalSpace) / CGFloat(noOfCellsInRow)) + return CGSize(width: size, height: size + 50) + } + + func collection(_ collectionView: UICollectionView, + contextMenuConfigurationForItemAt indexPath: IndexPath, + point: CGPoint) -> UIContextMenuConfiguration? { + let cellNumber = NSNumber(value: indexPath.item) + + if let cachedImage = self.topRatedMoviesViewModel.movieImagesCache.object(forKey: cellNumber) { + return configureContextMenu(index: indexPath.row, + imageData: cachedImage.jpegData(compressionQuality: 1.0) ?? Data()) + } + + return configureContextMenu(index: indexPath.row, imageData: Data()) + } + +} diff --git a/Movie-Application/MovieApplicationMVVM/TopRatedMovies/ViewModel/TopRatedMoviesViewModel.swift b/Movie-Application/MovieApplicationMVVM/TopRatedMovies/ViewModel/TopRatedMoviesViewModel.swift new file mode 100644 index 00000000..4ac06d35 --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/TopRatedMovies/ViewModel/TopRatedMoviesViewModel.swift @@ -0,0 +1,156 @@ +// +// TopRatedMoviesViewModel.swift +// MovieApplicationMVVM +// +// Created by Mohanna Zakizadeh on 5/3/22. +// + +import Foundation +import UIKit + +final class TopRatedMoviesViewModel { + var moviesService: MoviesServiceProtocol + var movies: (([Movie]) -> Void)? + var movieDetails: ((MovieDetail) -> Void)? + var errorHandler: ((String) -> Void)? + let movieImagesCache = NSCache() + + private var currentPage = 1 + private var allMovies: [Movie]? + + init(moviesService: MoviesServiceProtocol) { + self.moviesService = moviesService + } + + func alertRetryButtonDidTap() { + getTopRatedMovies() + } + + // function to get movie image from url that we have + func getMovieImage(index: Int, completion: @escaping (UIImage) -> Void) { + + if let movie = allMovies?[index] { + if let path = movie.poster { + return moviesService.getMovieImage(for: path, completion: completion) + } + } else { + completion(UIImage(systemName: "film.circle")!) + } + + } + + func getMovieTitle(index: Int) -> String { + allMovies?[index].title ?? "" + } + + func movieSelected(at index: Int) { + if let movies = allMovies { + moviesService.getMovieDetails(id: movies[index].id) { [weak self] result in + switch result { + case .success(let movieDetail): + self?.movieDetails?(movieDetail) + case .failure(let error): + self?.errorHandler?(error.errorDescription ?? error.localizedDescription) + } + } + } + } + + func addToWatchList(index: Int, imageData: Data) { + if let movies = allMovies { + let savedMovie = CoreDataMovie(title: movies[index].title, + poster: imageData, + id: movies[index].id, + date: Date.now, + voteAverage: movies[index].voteAverage) + CoreDataManager().saveNewMovie(savedMovie) + } + } + + func getTopRatedMovies() { + // movie data base gives 495 pages max. + if currentPage < 496 { + moviesService.getTopRatedMovies(page: currentPage) { result in + switch result { + case .success(let moviesData): + if self.currentPage == 1 { + self.allMovies = moviesData.results + } else { + self.allMovies! += moviesData.results + } + self.currentPage += 1 + self.movies?(moviesData.results) + case .failure(let error): + self.errorHandler?(error.errorDescription ?? error.localizedDescription) + } + } + } + + } + + func getSavedMovies() -> [CoreDataMovie] { + CoreDataManager().getSavedMovies() + } + + func configureContextMenu(index: Int, imageData: Data) -> UIContextMenuConfiguration { + // prevents from adding repititious movies to watch list + if !self.getSavedMovies().contains(where: { $0.title == self.getMovieTitle(index: index)}) { + let context = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) -> UIMenu? in + + let viewDetails = UIAction(title: "View Details", + image: UIImage(systemName: "text.below.photo.fill"), + identifier: nil, + discoverabilityTitle: nil, state: .off) { (_) in + self.self.movieSelected(at: index) + } + + let addToWatchList = UIAction(title: "Add to Watchlist", + image: UIImage(systemName: "bookmark"), + identifier: nil, + discoverabilityTitle: nil, state: .off) { (_) in + self.addToWatchList(index: index, imageData: imageData) + } + + return UIMenu(title: self.getMovieTitle(index: index), + image: nil, identifier: nil, + options: UIMenu.Options.displayInline, + children: [addToWatchList, viewDetails]) + + } + return context + + } else { + let context = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) -> UIMenu? in + + let viewDetails = UIAction(title: "View Details", + image: UIImage(systemName: "text.below.photo.fill"), + identifier: nil, discoverabilityTitle: nil, + state: .off) { (_) in + self.movieSelected(at: index) + } + + let addToWatchList = UIAction(title: "Added to Watchlist", + image: UIImage(systemName: "bookmark.fill"), + identifier: nil, discoverabilityTitle: nil, + state: .off) { (_) in + + } + + return UIMenu(title: self.getMovieTitle(index: index), + image: nil, identifier: nil, + options: UIMenu.Options.displayInline, + children: [addToWatchList, viewDetails]) + + } + return context + } + } + + var numberOfMovies: Int { + return allMovies?.count ?? 0 + } + + var topRatedMovies: [Movie] { + return allMovies ?? [] + } +} diff --git a/Movie-Application/MovieApplicationMVVM/UIKitUtilities/Coordinator/Coordinator.swift b/Movie-Application/MovieApplicationMVVM/UIKitUtilities/Coordinator/Coordinator.swift new file mode 100644 index 00000000..34494953 --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/UIKitUtilities/Coordinator/Coordinator.swift @@ -0,0 +1,32 @@ +// +// Coordinator.swift +// MovieApplicationMVVM +// +// Created by Mohanna Zakizadeh on 5/3/22. +// + +import UIKit + +// MARK: - Coordinator +protocol Coordinator: AnyObject { + var parentCoordinator: Coordinator? { get set } + func changeTabBarIndex(to index: Int) + var navigationController: UINavigationController? { get set } + func showMovieDetails(_ movie: MovieDetail) +} + +extension Coordinator { + func showMovieDetails(_ movie: MovieDetail) { + + // swiftlint: disable line_length + let movieDetailsInfoViewController = MovieDetailsInfoViewController() + movieDetailsInfoViewController.movie = movie + let movieContentViewController = MovieInfoContentViewController(movie: movie) + let movieDetailsViewController = MovieDetailsViewController(contentViewController: movieContentViewController, + bottomSheetViewController: movieDetailsInfoViewController, + bottomSheetConfiguration: .init(height: UIScreen.main.bounds.height*0.8, + initialOffset: UIScreen.main.bounds.height / 2.2)) + movieDetailsViewController.modalPresentationStyle = .popover + self.navigationController?.present(movieDetailsViewController, animated: true, completion: nil) + } +} diff --git a/Movie-Application/MovieApplicationMVVM/UIKitUtilities/Coordinator/Storyboarded.swift b/Movie-Application/MovieApplicationMVVM/UIKitUtilities/Coordinator/Storyboarded.swift new file mode 100644 index 00000000..96b9949e --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/UIKitUtilities/Coordinator/Storyboarded.swift @@ -0,0 +1,73 @@ +// +// Storyboarded.swift +// MovieApplicationMVVM +// +// Created by Mohanna Zakizadeh on 5/3/22. +// + +import UIKit +/* + + Insted of setting dependency of ViewController for coordinator, + by confirming to Storyboarded protocol we can easily instantiate our ViewControllers + + */ + +protocol Storyboarded { + associatedtype ConcreteCoordinator + var coordinator: ConcreteCoordinator? { get set } + static func instantiate() -> Self +} + +extension Storyboarded where Self: UIViewController { + + private static var fileName: String { + NSStringFromClass(self) + } + + private static var className: String { + fileName.components(separatedBy: ".")[1] + } + + private static var storyboardName: String { + className.deletingSuffix("ViewController") + } + + private static var storyboard: UIStoryboard { + UIStoryboard(name: storyboardName, bundle: Bundle.main) + } + + static func instantiate() -> Self { + guard let viewController = storyboard.instantiateViewController(withIdentifier: className) as? Self else { + fatalError("Could not find View Controller named \(className)") + } + return viewController + } +} + +extension Storyboarded where Self: UIViewController { + + static func instantiate(coordinator: ConcreteCoordinator?) -> Self { + var viewController = instantiate() + viewController.coordinator = coordinator + return viewController + } +} + +fileprivate extension String { + + /// Removes the given String from the end of the string String. + /// If the text is not present, returns the original String intact. + /// + /// - Parameters: + /// - suffix: The text to be removed, e.g. "ViewController" + /// + /// - Returns: + /// - If suffix was found, String with the suffix removed, e.g. "MainViewController" -> "Main" + /// - If no suffix was found, the original string intact. e.g. "MainCoordinator" -> "MainCoordinator" + /// + func deletingSuffix(_ suffix: String) -> String { + guard self.hasSuffix(suffix) else { return self } + return String(self.dropLast(suffix.count)) + } +} diff --git a/Movie-Application/MovieApplicationMVVM/UIKitUtilities/UICollectionViewDelegate/MovieCollectionViewCell.swift b/Movie-Application/MovieApplicationMVVM/UIKitUtilities/UICollectionViewDelegate/MovieCollectionViewCell.swift new file mode 100644 index 00000000..39f90efe --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/UIKitUtilities/UICollectionViewDelegate/MovieCollectionViewCell.swift @@ -0,0 +1,12 @@ +// +// MovieCollectionViewCell.swift +// MovieApplicationMVVM +// +// Created by Mohanna Zakizadeh on 5/3/22. +// + +import UIKit + +protocol MovieCollectionViewCell: UICollectionViewCell { + associatedtype CellViewModel +} diff --git a/Movie-Application/MovieApplicationMVVM/UIKitUtilities/UICollectionViewDelegate/MovieCollectionViewDelegate.swift b/Movie-Application/MovieApplicationMVVM/UIKitUtilities/UICollectionViewDelegate/MovieCollectionViewDelegate.swift new file mode 100644 index 00000000..4db01026 --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/UIKitUtilities/UICollectionViewDelegate/MovieCollectionViewDelegate.swift @@ -0,0 +1,109 @@ +// +// MovieCollectionViewDelegate.swift +// MovieApplicationMVVM +// +// Created by Mohanna Zakizadeh on 5/3/22. +// + +import UIKit + +protocol MovieCollectionViewDelegate: AnyObject { + func collection(willDisplay cellIndexPath: IndexPath, cell: UICollectionViewCell) + func collection(_ collectionView: UICollectionView, didSelectItem index: IndexPath) + func collection(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath) -> CGSize + func collection(_ collectionView: UICollectionView, + contextMenuConfigurationForItemAt indexPath: IndexPath, + point: CGPoint) -> UIContextMenuConfiguration? +} + +extension MovieCollectionViewDelegate { + func collection(willDisplay cellIndexPath: IndexPath, cell: UICollectionViewCell) {} +} + +class MovieCollectionViewDataSource: NSObject, UICollectionViewDataSource, + UICollectionViewDelegate, + UICollectionViewDelegateFlowLayout { + // MARK: - Variables + var items: [T.CellViewModel] = [] + var selectItem: IndexPath? + var collectionView: UICollectionView + + weak var delegate: MovieCollectionViewDelegate? + + // MARK: - Initializer + init(items: [T.CellViewModel], collectionView: UICollectionView, delegate: MovieCollectionViewDelegate) { + self.items = items + self.collectionView = collectionView + // Register cell to collectionView + self.collectionView.register(T.self, forCellWithReuseIdentifier: String.init(describing: T.self)) + self.delegate = delegate + } + + // MARK: - UICollectionView DataSource + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return items.count + } + + func collectionView(_ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: + String.init(describing: T.self), for: indexPath) + as? T else { + return UICollectionViewCell() + } + return cell + } + + // MARK: - UICollectionView Delegate + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + delegate?.collection(collectionView, didSelectItem: indexPath) + } + + public func appendItemsToCollectionView( _ newItems: [T.CellViewModel]) { + // append to last of list + self.items.append(contentsOf: newItems) + // Now performing insert + + // Get the last row index (numberOfRows - 1) + var lastRowIndex = collectionView.numberOfItems(inSection: 0) - 1 + if lastRowIndex < 0 { + lastRowIndex = 0 + self.collectionView.reloadData() + } else { + let indexPaths = newItems.enumerated().map { (index, _) -> IndexPath in + IndexPath(item: items.count - 1 - index, section: 0) + } + self.collectionView.performBatchUpdates({ + self.collectionView.insertItems(at: indexPaths) + }, completion: nil) + } + } + + public func refreshWithNewItems(_ newItems: [T.CellViewModel]) { + self.items = newItems + self.collectionView.reloadData() + } + + func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath) -> CGSize { + delegate?.collection(collectionView, + layout: collectionViewLayout, + sizeForItemAt: indexPath) ?? CGSize(width: 50, height: 50) + } + + func collectionView(_ collectionView: UICollectionView, + willDisplay cell: UICollectionViewCell, + forItemAt indexPath: IndexPath) { + delegate?.collection(willDisplay: indexPath, cell: cell) + } + + func collectionView(_ collectionView: UICollectionView, + contextMenuConfigurationForItemAt indexPath: IndexPath, + point: CGPoint) -> UIContextMenuConfiguration? { + delegate?.collection(collectionView, contextMenuConfigurationForItemAt: indexPath, point: point) + } + +} diff --git a/Movie-Application/MovieApplicationMVVM/WatchlistMovies/View/WatchlistMovies.storyboard b/Movie-Application/MovieApplicationMVVM/WatchlistMovies/View/WatchlistMovies.storyboard new file mode 100644 index 00000000..81454f0e --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/WatchlistMovies/View/WatchlistMovies.storyboard @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Movie-Application/MovieApplicationMVVM/WatchlistMovies/View/WatchlistMoviesViewController.swift b/Movie-Application/MovieApplicationMVVM/WatchlistMovies/View/WatchlistMoviesViewController.swift new file mode 100644 index 00000000..3268a888 --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/WatchlistMovies/View/WatchlistMoviesViewController.swift @@ -0,0 +1,174 @@ +// +// WatchlistMoviesViewController.swift +// MovieApplicationMVVM +// +// Created by Mohanna Zakizadeh on 5/5/22. +// + +import UIKit + +final class WatchlistMoviesViewController: UIViewController, Storyboarded { + // MARK: - Properties + var moviesCollectionViewDataSource: MovieCollectionViewDataSource! + + weak var coordinator: WatchlistMoviesCoordinator? + var viewModel: WatchlistMoviesViewModel! + + @IBOutlet var emptyWatchlistView: UIStackView! + @IBOutlet var collectionView: UICollectionView! + + @IBAction func browseButtonDidTap(_ sender: UIButton) { + coordinator?.changeTabBarIndex(to: 0) + } + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + viewModel.getWatchlistMovies() + setupView() + } + + override func viewWillAppear(_ animated: Bool) { + viewModel.getWatchlistMovies() + } + + override func viewWillDisappear(_ animated: Bool) { + viewModel.deleteMovies() + } + // MARK: - Theme + + func applyTheme() { + view.backgroundColor = .systemBackground + } + + // MARK: - Private functions + + private func setupView() { + configureNavigation() + setupCollectionView() + setupBindings() + self.applyTheme() + } + + // bind view to viewModel + private func setupBindings() { + viewModel.movies = { [weak self] coreDataMovies in + guard let self = self else { return } + if coreDataMovies.isEmpty { + self.emptyWatchlistView.isHidden = false + self.collectionView.isHidden = true + } else { + self.emptyWatchlistView.isHidden = true + self.collectionView.isHidden = false + let movies = coreDataMovies.map({ Movie(title: $0.title, + poster: nil, id: $0.id, + voteAverage: $0.voteAverage)}) + self.moviesCollectionViewDataSource.refreshWithNewItems(movies) + } + } + + viewModel.movieDetails = { [weak self] movieDetail in + guard let self = self else { return } + self.coordinator?.showMovieDetails(movieDetail) + } + + viewModel.errorHandler = { [weak self] error in + guard let self = self else { return } + let errorAlert = UIAlertController(title: "Error Occured", + message: error, + preferredStyle: .alert) + let errorAction = UIAlertAction(title: "Ok", style: .default, handler: nil) + errorAlert.addAction(errorAction) + self.present(errorAlert, animated: true, completion: nil) + } + } + + // function to setup and configure navigation details + private func configureNavigation() { + self.title = "Watchlist" + let dateAddedAction = UIAction(title: "Date Added", + image: nil, identifier: nil, + discoverabilityTitle: nil, + state: .off) { (_) in + self.viewModel.sortByDate() + } + + let nameSortAction = UIAction(title: "Name", + image: nil, identifier: nil, + discoverabilityTitle: nil, + state: .off) { (_) in + self.viewModel.sortByName() + } + + let userScoreSortAction = UIAction(title: "User Score", + image: nil, identifier: nil, + discoverabilityTitle: nil, + state: .off) { (_) in + self.viewModel.sortByUserScore() + } + + let sortMenu = UIMenu(title: "", + image: nil, identifier: nil, + options: .singleSelection, + children: [dateAddedAction, nameSortAction, userScoreSortAction]) + + navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Sort", + image: nil, + primaryAction: nil, + menu: sortMenu) + } + + // function to setup and configure collectionView details + private func setupCollectionView() { + moviesCollectionViewDataSource = MovieCollectionViewDataSource(items: [], + collectionView: collectionView, + delegate: self) + collectionView.delegate = moviesCollectionViewDataSource + collectionView.dataSource = moviesCollectionViewDataSource + collectionView.showsHorizontalScrollIndicator = false + } + + private func configureContextMenu(_ index: Int) -> UIContextMenuConfiguration { + viewModel.configureContextMenu(index: index) + } +} + +extension WatchlistMoviesViewController: MovieCollectionViewDelegate { + + func collection(_ collectionView: UICollectionView, didSelectItem index: IndexPath) { + self.viewModel.movieSelected(at: index.row) + } + + func collection(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath) -> CGSize { + let noOfCellsInRow = 2 + + guard let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout else { + return CGSize(width: 0, height: 0) + } + flowLayout.minimumInteritemSpacing = 10 + flowLayout.minimumLineSpacing = 10 + flowLayout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) + + let totalSpace = flowLayout.sectionInset.left + + flowLayout.sectionInset.right + + (flowLayout.minimumInteritemSpacing * CGFloat(noOfCellsInRow - 1)) + + let size = Int((view.bounds.width - totalSpace) / CGFloat(noOfCellsInRow)) + return CGSize(width: size, height: size + 50) + } + + func collection(_ collectionView: UICollectionView, + contextMenuConfigurationForItemAt indexPath: IndexPath, + point: CGPoint) -> UIContextMenuConfiguration? { + configureContextMenu(indexPath.row) + } + + func collection(willDisplay cellIndexPath: IndexPath, cell: UICollectionViewCell) { + guard let cell = cell as? MovieCell else { return } + cell.movieImageView.image = viewModel.getMovieImage(index: cellIndexPath.row) + } + +} diff --git a/Movie-Application/MovieApplicationMVVM/WatchlistMovies/ViewModel/WatchlistMoviesViewModel.swift b/Movie-Application/MovieApplicationMVVM/WatchlistMovies/ViewModel/WatchlistMoviesViewModel.swift new file mode 100644 index 00000000..9d1fd3f1 --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/WatchlistMovies/ViewModel/WatchlistMoviesViewModel.swift @@ -0,0 +1,129 @@ +// +// WatchlistMoviesViewModel.swift +// MovieApplicationMVVM +// +// Created by Mohanna Zakizadeh on 5/4/22. +// + +import UIKit + +final class WatchlistMoviesViewModel { + var moviesService: MoviesServiceProtocol + var movies: (([CoreDataMovie]) -> Void)? + var movieDetails: ((MovieDetail) -> Void)? + var errorHandler: ((String) -> Void)? + + private var allMovies: [CoreDataMovie]? + + init(moviesService: MoviesServiceProtocol) { + self.moviesService = moviesService + } + + // function to get movie image from url that we have + func getMovieImage(index: Int) -> UIImage { + + if let movies = allMovies { + return UIImage(data: movies[index].poster) ?? UIImage(systemName: "film.circle")! + } else { + return UIImage(systemName: "film.circle")! + } + + } + + func getMovieTitle(index: Int) -> String { + if index <= allMovies?.count ?? 0 { + return allMovies?[index].title ?? "" + } + return "" + } + + func movieSelected(at index: Int) { + if let movies = allMovies { + moviesService.getMovieDetails(id: movies[index].id) { [weak self] result in + switch result { + case .success(let movieDetail): + self?.movieDetails?(movieDetail) + case .failure(let error): + self?.errorHandler?(error.errorDescription ?? error.localizedDescription) + } + } + } + } + + func addToWatchList(index: Int, imageData: Data) { + if let movies = allMovies { + let savedMovie = CoreDataMovie(title: movies[index].title, + poster: imageData, + id: movies[index].id, + date: Date.now, + voteAverage: movies[index].voteAverage) + CoreDataManager().saveNewMovie(savedMovie) + } + } + + func deleteFromWatchlist(_ index: Int) { + allMovies?.remove(at: index) + movies?(allMovies ?? []) + } + + func getWatchlistMovies() { + allMovies = CoreDataManager().getSavedMovies() + movies?(allMovies ?? []) + } + + func sortByDate() { + allMovies = allMovies?.sorted(by: { $0.date > $1.date }) + movies?(allMovies ?? []) + } + + func sortByName() { + allMovies = allMovies?.sorted(by: { $0.title < $1.title }) + movies?(allMovies ?? []) + } + + func sortByUserScore() { + allMovies = allMovies?.sorted(by: { $0.voteAverage > $1.voteAverage }) + movies?(allMovies ?? []) + } + + func deleteMovies() { + if let movies = allMovies { + CoreDataManager().saveMovies(movies: movies) + } + } + + func configureContextMenu(index: Int) -> UIContextMenuConfiguration { + let context = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) -> UIMenu? in + + let viewDetails = UIAction(title: "View Details", + image: UIImage(systemName: "text.below.photo.fill"), + identifier: nil, + discoverabilityTitle: nil, state: .off) { (_) in + + self.movieSelected(at: index) + + } + let remove = UIAction(title: "Remove from Watchlist", + image: UIImage(systemName: "trash"), + identifier: nil, + discoverabilityTitle: nil, + attributes: .destructive, state: .off) { (_) in + self.deleteFromWatchlist(index) + } + + return UIMenu(title: self.getMovieTitle(index: index), + image: nil, identifier: nil, + options: UIMenu.Options.displayInline, children: [viewDetails, remove]) + + } + return context + } + + var numberOfMovies: Int { + return allMovies?.count ?? 0 + } + + var topRatedMovies: [CoreDataMovie] { + return allMovies ?? [] + } +} diff --git a/Movie-Application/MovieApplicationMVVM/WatchlistMovies/WatchlistMoviesCoordinator.swift b/Movie-Application/MovieApplicationMVVM/WatchlistMovies/WatchlistMoviesCoordinator.swift new file mode 100644 index 00000000..82c5c8c4 --- /dev/null +++ b/Movie-Application/MovieApplicationMVVM/WatchlistMovies/WatchlistMoviesCoordinator.swift @@ -0,0 +1,33 @@ +// +// WatchlistMoviesCoordinator.swift +// MovieApplicationMVVM +// +// Created by Mohanna Zakizadeh on 5/5/22. +// + +import UIKit + +final class WatchlistMoviesCoordinator: Coordinator { + + var navigationController: UINavigationController? + var parentCoordinator: Coordinator? + + init(tabBarItem: UITabBarItem, parentCoordinator: Coordinator) { + navigationController = UINavigationController() + self.parentCoordinator = parentCoordinator + let viewController = WatchlistMoviesViewController.instantiate(coordinator: self) + viewController.tabBarItem = tabBarItem + viewController.viewModel = WatchlistMoviesViewModel(moviesService: MoviesService.shared) + navigationController?.viewControllers = [viewController] + navigationController?.navigationBar.prefersLargeTitles = true + } + + deinit { + print("removed \(self) from memory") + } + + func changeTabBarIndex(to index: Int) { + parentCoordinator?.changeTabBarIndex(to: index) + } + +} diff --git a/Movie-Application/MovieApplicationMVVMTests/CoordinatorTests/TestAppCoordinator.swift b/Movie-Application/MovieApplicationMVVMTests/CoordinatorTests/TestAppCoordinator.swift new file mode 100644 index 00000000..991d6cb4 --- /dev/null +++ b/Movie-Application/MovieApplicationMVVMTests/CoordinatorTests/TestAppCoordinator.swift @@ -0,0 +1,51 @@ +// +// TestAppCoordinator.swift +// MovieApplicationMVVMTests +// +// Created by Mohanna Zakizadeh on 5/6/22. +// + +import XCTest +@testable import MovieApplicationMVVM + +final class TestAppCoordinator: XCTestCase { + + var coordinator: AppCoordinator? + var window: UIWindow? + + override func setUpWithError() throws { + window = UIWindow(frame: UIScreen.main.bounds) + coordinator = AppCoordinator(window: window) + } + + override func tearDownWithError() throws { + window = nil + coordinator = nil + } + + func testStart() throws { + // given + guard let coordinator = coordinator else { + throw UnitTestError() + } + + // when + coordinator.start() + + // then + XCTAssertNotNil(coordinator.window?.rootViewController) + + } + + func testChangeTabBarIndex() throws { + // given + let index = 0 + + // when + coordinator?.changeTabBarIndex(to: 0) + + // then + XCTAssertEqual(index, coordinator?.tabBarController?.selectedIndex) + } + +} diff --git a/Movie-Application/MovieApplicationMVVMTests/CoordinatorTests/TestPopularMoviesCoordinator.swift b/Movie-Application/MovieApplicationMVVMTests/CoordinatorTests/TestPopularMoviesCoordinator.swift new file mode 100644 index 00000000..2e0ade73 --- /dev/null +++ b/Movie-Application/MovieApplicationMVVMTests/CoordinatorTests/TestPopularMoviesCoordinator.swift @@ -0,0 +1,29 @@ +// +// TestPopularMoviesCoordinator.swift +// MovieApplicationMVVMTests +// +// Created by Mohanna Zakizadeh on 5/7/22. +// + +import XCTest +@testable import MovieApplicationMVVM + +final class TestPopularMoviesCoordinator: XCTestCase { + + var coordinator: PopularMoviesCoordinator? + + override func setUpWithError() throws { + coordinator = PopularMoviesCoordinator(tabBarItem: UITabBarItem(), + parentCoordinator: AppCoordinator(window: nil)) + + } + + override func tearDownWithError() throws { + coordinator = nil + } + + func testCoordinatorHasNavigationController() throws { + XCTAssertNotNil(coordinator?.navigationController) + } + +} diff --git a/Movie-Application/MovieApplicationMVVMTests/CoordinatorTests/TestTopRatedMoviesCoordinator.swift b/Movie-Application/MovieApplicationMVVMTests/CoordinatorTests/TestTopRatedMoviesCoordinator.swift new file mode 100644 index 00000000..dd8fd2d3 --- /dev/null +++ b/Movie-Application/MovieApplicationMVVMTests/CoordinatorTests/TestTopRatedMoviesCoordinator.swift @@ -0,0 +1,27 @@ +// +// TestTopRatedMoviesCoordinator.swift +// MovieApplicationMVVMTests +// +// Created by Mohanna Zakizadeh on 5/6/22. +// + +import XCTest +@testable import MovieApplicationMVVM + +final class TestTopRatedMoviesCoordinator: XCTestCase { + var coordinator: TopRatedMoviesCoordinator? + + override func setUpWithError() throws { + coordinator = TopRatedMoviesCoordinator(tabBarItem: UITabBarItem(), + parentCoordinator: AppCoordinator(window: nil)) + + } + + override func tearDownWithError() throws { + coordinator = nil + } + + func testCoordinatorHasNavigationController() throws { + XCTAssertNotNil(coordinator?.navigationController) + } +} diff --git a/Movie-Application/MovieApplicationMVVMTests/CoordinatorTests/TestWatchlistMoviesCoordinator.swift b/Movie-Application/MovieApplicationMVVMTests/CoordinatorTests/TestWatchlistMoviesCoordinator.swift new file mode 100644 index 00000000..fcaf3ce4 --- /dev/null +++ b/Movie-Application/MovieApplicationMVVMTests/CoordinatorTests/TestWatchlistMoviesCoordinator.swift @@ -0,0 +1,28 @@ +// +// TestWatchlistMoviesCoordinator.swift +// MovieApplicationMVVMTests +// +// Created by Mohanna Zakizadeh on 5/7/22. +// + +import XCTest +@testable import MovieApplicationMVVM + +final class TestWatchlistMoviesCoordinator: XCTestCase { + var coordinator: WatchlistMoviesCoordinator? + + override func setUpWithError() throws { + coordinator = WatchlistMoviesCoordinator(tabBarItem: UITabBarItem(), + parentCoordinator: AppCoordinator(window: nil)) + + } + + override func tearDownWithError() throws { + coordinator = nil + } + + func testCoordinatorHasNavigationController() throws { + XCTAssertNotNil(coordinator?.navigationController) + } + +} diff --git a/Movie-Application/MovieApplicationMVVMTests/MovieApplicationMVVMTests.swift b/Movie-Application/MovieApplicationMVVMTests/MovieApplicationMVVMTests.swift new file mode 100644 index 00000000..c623be73 --- /dev/null +++ b/Movie-Application/MovieApplicationMVVMTests/MovieApplicationMVVMTests.swift @@ -0,0 +1,35 @@ +// +// MovieApplicationMVVMTests.swift +// MovieApplicationMVVMTests +// +// Created by Mohanna Zakizadeh on 5/3/22. +// + +import XCTest +@testable import MovieApplicationMVVM + +class MovieApplicationMVVMTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/Movie-Application/MovieApplicationMVVMTests/UnitTestError.swift b/Movie-Application/MovieApplicationMVVMTests/UnitTestError.swift new file mode 100644 index 00000000..b972f5af --- /dev/null +++ b/Movie-Application/MovieApplicationMVVMTests/UnitTestError.swift @@ -0,0 +1,10 @@ +// +// UnitTestError.swift +// MovieApplicationMVVMTests +// +// Created by Mohanna Zakizadeh on 5/6/22. +// + +import Foundation + +struct UnitTestError: Error {} diff --git a/Movie-Application/MovieApplicationMVVMUITests/MovieApplicationMVVMUITests.swift b/Movie-Application/MovieApplicationMVVMUITests/MovieApplicationMVVMUITests.swift new file mode 100644 index 00000000..c526f251 --- /dev/null +++ b/Movie-Application/MovieApplicationMVVMUITests/MovieApplicationMVVMUITests.swift @@ -0,0 +1,40 @@ +// +// MovieApplicationMVVMUITests.swift +// MovieApplicationMVVMUITests +// +// Created by Mohanna Zakizadeh on 5/3/22. +// + +import XCTest + +class MovieApplicationMVVMUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/Movie-Application/MovieApplicationMVVMUITests/MovieApplicationMVVMUITestsLaunchTests.swift b/Movie-Application/MovieApplicationMVVMUITests/MovieApplicationMVVMUITestsLaunchTests.swift new file mode 100644 index 00000000..0c8d3c20 --- /dev/null +++ b/Movie-Application/MovieApplicationMVVMUITests/MovieApplicationMVVMUITestsLaunchTests.swift @@ -0,0 +1,32 @@ +// +// MovieApplicationMVVMUITestsLaunchTests.swift +// MovieApplicationMVVMUITests +// +// Created by Mohanna Zakizadeh on 5/3/22. +// + +import XCTest + +class MovieApplicationMVVMUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/Movie-Application/Podfile b/Movie-Application/Podfile new file mode 100644 index 00000000..cb01c0c7 --- /dev/null +++ b/Movie-Application/Podfile @@ -0,0 +1,20 @@ +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' + +target 'Movie-Application' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + # Pods for Movie-Application +pod 'SwiftLint' + + target 'Movie-ApplicationTests' do + inherit! :search_paths + # Pods for testing + end + + target 'Movie-ApplicationUITests' do + # Pods for testing + end + +end diff --git a/Movie-Application/Podfile.lock b/Movie-Application/Podfile.lock new file mode 100644 index 00000000..b4ef71d1 --- /dev/null +++ b/Movie-Application/Podfile.lock @@ -0,0 +1,16 @@ +PODS: + - SwiftLint (0.43.1) + +DEPENDENCIES: + - SwiftLint + +SPEC REPOS: + trunk: + - SwiftLint + +SPEC CHECKSUMS: + SwiftLint: 99f82d07b837b942dd563c668de129a03fc3fb52 + +PODFILE CHECKSUM: 2b9fd59ff3fa94638354a986fe133707f5fd7049 + +COCOAPODS: 1.11.2 diff --git a/Movie-Application/Pods/.DS_Store b/Movie-Application/Pods/.DS_Store new file mode 100644 index 00000000..17d2cd7a Binary files /dev/null and b/Movie-Application/Pods/.DS_Store differ diff --git a/Movie-Application/Pods/Manifest.lock b/Movie-Application/Pods/Manifest.lock new file mode 100644 index 00000000..b4ef71d1 --- /dev/null +++ b/Movie-Application/Pods/Manifest.lock @@ -0,0 +1,16 @@ +PODS: + - SwiftLint (0.43.1) + +DEPENDENCIES: + - SwiftLint + +SPEC REPOS: + trunk: + - SwiftLint + +SPEC CHECKSUMS: + SwiftLint: 99f82d07b837b942dd563c668de129a03fc3fb52 + +PODFILE CHECKSUM: 2b9fd59ff3fa94638354a986fe133707f5fd7049 + +COCOAPODS: 1.11.2 diff --git a/Movie-Application/Pods/Pods.xcodeproj/project.pbxproj b/Movie-Application/Pods/Pods.xcodeproj/project.pbxproj new file mode 100644 index 00000000..5a410e9e --- /dev/null +++ b/Movie-Application/Pods/Pods.xcodeproj/project.pbxproj @@ -0,0 +1,868 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXAggregateTarget section */ + 52B60EC2A583F24ACBB69C113F5488B9 /* SwiftLint */ = { + isa = PBXAggregateTarget; + buildConfigurationList = AE7B4FB01588B9E6DF09CB79FC7CE7BD /* Build configuration list for PBXAggregateTarget "SwiftLint" */; + buildPhases = ( + ); + dependencies = ( + ); + name = SwiftLint; + productName = SwiftLint; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 088EC7292E8D5904FEADDD1413032636 /* Pods-Movie-Application-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 5DC615774AA0A7BA84B754EC69967A25 /* Pods-Movie-Application-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1A3068750930B55C297DA2804602D285 /* Pods-Movie-Application-Movie-ApplicationUITests-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 87E26D1D5297070E94225F4310B05503 /* Pods-Movie-Application-Movie-ApplicationUITests-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 32022D3904DFF37BFC7E4E10B0BDACD8 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73010CC983E3809BECEE5348DA1BB8C6 /* Foundation.framework */; }; + 617B4AC3B9CC3809A57079260D3AC16A /* Pods-Movie-Application-Movie-ApplicationUITests-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 60ADE285B0AD73894EB10EAEB17D90A9 /* Pods-Movie-Application-Movie-ApplicationUITests-dummy.m */; }; + 6E5312936F496AD8B8241C2B7842171F /* Pods-Movie-Application-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FA8769234563DC2782C159D06DDC071 /* Pods-Movie-Application-dummy.m */; }; + 80511D8AED19B204052FE5BE50AA4BC1 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73010CC983E3809BECEE5348DA1BB8C6 /* Foundation.framework */; }; + D086E998A181D15D1C1C38477833F373 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 73010CC983E3809BECEE5348DA1BB8C6 /* Foundation.framework */; }; + E6133DD52CC298E557AF5D84BA14B117 /* Pods-Movie-ApplicationTests-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = EAE69F818D0E8A0C39A228188FA4CA99 /* Pods-Movie-ApplicationTests-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + FA362453619DACB45E77163D2CBEFF23 /* Pods-Movie-ApplicationTests-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 926E1A196064C52085B031D52B0D4B3C /* Pods-Movie-ApplicationTests-dummy.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 23128C94A062138F32C111017658FAED /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = C885DC71076F878D853ADD01EAA07AB5; + remoteInfo = "Pods-Movie-Application"; + }; + 60BE007270207BA55BC5BAF6E93E5534 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 52B60EC2A583F24ACBB69C113F5488B9; + remoteInfo = SwiftLint; + }; + 9C95EDA32BFA17ECA9276583CB34AF65 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 52B60EC2A583F24ACBB69C113F5488B9; + remoteInfo = SwiftLint; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 1BFF7061A1F4133B05034659C9871E99 /* Pods-Movie-Application-Movie-ApplicationUITests-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-Movie-Application-Movie-ApplicationUITests-Info.plist"; sourceTree = ""; }; + 1DD502E3C292A7B1960EDEBBFEF44E4B /* Pods-Movie-Application.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-Movie-Application.modulemap"; sourceTree = ""; }; + 21DEC5480B49326B5DC4014EF630EA76 /* Pods-Movie-ApplicationTests.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-Movie-ApplicationTests.modulemap"; sourceTree = ""; }; + 3EC0124719FE8EF5FDDD72C26F8A2F57 /* SwiftLint.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = SwiftLint.debug.xcconfig; sourceTree = ""; }; + 477CB6AEB1199DAF1AC6940863F59784 /* Pods-Movie-ApplicationTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-Movie-ApplicationTests.debug.xcconfig"; sourceTree = ""; }; + 57D900B7BCCC797ACCC67ED6DAB5C037 /* Pods-Movie-Application-Movie-ApplicationUITests */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = "Pods-Movie-Application-Movie-ApplicationUITests"; path = Pods_Movie_Application_Movie_ApplicationUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5DC615774AA0A7BA84B754EC69967A25 /* Pods-Movie-Application-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-Movie-Application-umbrella.h"; sourceTree = ""; }; + 60ADE285B0AD73894EB10EAEB17D90A9 /* Pods-Movie-Application-Movie-ApplicationUITests-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-Movie-Application-Movie-ApplicationUITests-dummy.m"; sourceTree = ""; }; + 643B63285001F11707C524E9DB75276F /* Pods-Movie-ApplicationTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-Movie-ApplicationTests.release.xcconfig"; sourceTree = ""; }; + 696BF1ACAD12673E38334D2C9297AE6E /* Pods-Movie-ApplicationTests-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-Movie-ApplicationTests-Info.plist"; sourceTree = ""; }; + 6A7653E3F970DE4740CE9C81F477136D /* Pods-Movie-Application-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-Movie-Application-acknowledgements.plist"; sourceTree = ""; }; + 708B1968254A5D6C62E3FBAAF15C63CB /* Pods-Movie-Application-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-Movie-Application-acknowledgements.markdown"; sourceTree = ""; }; + 73010CC983E3809BECEE5348DA1BB8C6 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; + 7C62C2E1DDF543F6CE5921BC2C41DCD1 /* Pods-Movie-Application.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-Movie-Application.debug.xcconfig"; sourceTree = ""; }; + 7E753795499878CBEBF1AAF26D20D94E /* Pods-Movie-ApplicationTests-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-Movie-ApplicationTests-acknowledgements.markdown"; sourceTree = ""; }; + 7EAB1DA481EDDC005A251D3A5E6BE4CD /* SwiftLint.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = SwiftLint.release.xcconfig; sourceTree = ""; }; + 87E26D1D5297070E94225F4310B05503 /* Pods-Movie-Application-Movie-ApplicationUITests-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-Movie-Application-Movie-ApplicationUITests-umbrella.h"; sourceTree = ""; }; + 8B2AC8608AD7EC056CA8FF345791925B /* Pods-Movie-Application-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-Movie-Application-Info.plist"; sourceTree = ""; }; + 8FA8769234563DC2782C159D06DDC071 /* Pods-Movie-Application-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-Movie-Application-dummy.m"; sourceTree = ""; }; + 926E1A196064C52085B031D52B0D4B3C /* Pods-Movie-ApplicationTests-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-Movie-ApplicationTests-dummy.m"; sourceTree = ""; }; + 977119CF588BAF7B10A9C48A7C6B9C6B /* Pods-Movie-Application-Movie-ApplicationUITests.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-Movie-Application-Movie-ApplicationUITests.modulemap"; sourceTree = ""; }; + 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; + A7ADAF3FE53D427495181634939FB212 /* Pods-Movie-Application-Movie-ApplicationUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-Movie-Application-Movie-ApplicationUITests.release.xcconfig"; sourceTree = ""; }; + B17E6B9EDA5352A0A2B1CFF2069A524B /* Pods-Movie-Application-Movie-ApplicationUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-Movie-Application-Movie-ApplicationUITests.debug.xcconfig"; sourceTree = ""; }; + C50F5A57494BDFAF07F40520DE30B162 /* Pods-Movie-Application.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-Movie-Application.release.xcconfig"; sourceTree = ""; }; + C8AE2673202947D67498F0A0FA5C35C9 /* Pods-Movie-Application-Movie-ApplicationUITests-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-Movie-Application-Movie-ApplicationUITests-acknowledgements.plist"; sourceTree = ""; }; + CF883D8648A6D9110DAF07A014912C7A /* Pods-Movie-ApplicationTests-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-Movie-ApplicationTests-acknowledgements.plist"; sourceTree = ""; }; + CFF3DDE8E33D8AB4E634AD7397EDF142 /* Pods-Movie-Application-Movie-ApplicationUITests-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-Movie-Application-Movie-ApplicationUITests-acknowledgements.markdown"; sourceTree = ""; }; + E9AFDEB7CDF4C389D33017FD8435A2D3 /* Pods-Movie-Application */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = "Pods-Movie-Application"; path = Pods_Movie_Application.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + EAE69F818D0E8A0C39A228188FA4CA99 /* Pods-Movie-ApplicationTests-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-Movie-ApplicationTests-umbrella.h"; sourceTree = ""; }; + F6BE35E9934DDABF5ACA87061AA15F92 /* Pods-Movie-ApplicationTests */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = "Pods-Movie-ApplicationTests"; path = Pods_Movie_ApplicationTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3860492F6520909E3CC1E57358D61551 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D086E998A181D15D1C1C38477833F373 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 81A2C324845428ECD2145BC9F658F168 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 32022D3904DFF37BFC7E4E10B0BDACD8 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A6448C7DB26E4AC6B27E105766CFD839 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 80511D8AED19B204052FE5BE50AA4BC1 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 437BBD89DD4A83FD441D19E4A176C73B /* Products */ = { + isa = PBXGroup; + children = ( + E9AFDEB7CDF4C389D33017FD8435A2D3 /* Pods-Movie-Application */, + 57D900B7BCCC797ACCC67ED6DAB5C037 /* Pods-Movie-Application-Movie-ApplicationUITests */, + F6BE35E9934DDABF5ACA87061AA15F92 /* Pods-Movie-ApplicationTests */, + ); + name = Products; + sourceTree = ""; + }; + 578452D2E740E91742655AC8F1636D1F /* iOS */ = { + isa = PBXGroup; + children = ( + 73010CC983E3809BECEE5348DA1BB8C6 /* Foundation.framework */, + ); + name = iOS; + sourceTree = ""; + }; + 7B1C6652F49778646D59F05AD12F7AE9 /* Pods-Movie-ApplicationTests */ = { + isa = PBXGroup; + children = ( + 21DEC5480B49326B5DC4014EF630EA76 /* Pods-Movie-ApplicationTests.modulemap */, + 7E753795499878CBEBF1AAF26D20D94E /* Pods-Movie-ApplicationTests-acknowledgements.markdown */, + CF883D8648A6D9110DAF07A014912C7A /* Pods-Movie-ApplicationTests-acknowledgements.plist */, + 926E1A196064C52085B031D52B0D4B3C /* Pods-Movie-ApplicationTests-dummy.m */, + 696BF1ACAD12673E38334D2C9297AE6E /* Pods-Movie-ApplicationTests-Info.plist */, + EAE69F818D0E8A0C39A228188FA4CA99 /* Pods-Movie-ApplicationTests-umbrella.h */, + 477CB6AEB1199DAF1AC6940863F59784 /* Pods-Movie-ApplicationTests.debug.xcconfig */, + 643B63285001F11707C524E9DB75276F /* Pods-Movie-ApplicationTests.release.xcconfig */, + ); + name = "Pods-Movie-ApplicationTests"; + path = "Target Support Files/Pods-Movie-ApplicationTests"; + sourceTree = ""; + }; + 7FDA596DBE0337ACDFE55A210D54EF26 /* Support Files */ = { + isa = PBXGroup; + children = ( + 3EC0124719FE8EF5FDDD72C26F8A2F57 /* SwiftLint.debug.xcconfig */, + 7EAB1DA481EDDC005A251D3A5E6BE4CD /* SwiftLint.release.xcconfig */, + ); + name = "Support Files"; + path = "../Target Support Files/SwiftLint"; + sourceTree = ""; + }; + 8DC8DC796700F4E899A97248D9EC1817 /* Pods-Movie-Application-Movie-ApplicationUITests */ = { + isa = PBXGroup; + children = ( + 977119CF588BAF7B10A9C48A7C6B9C6B /* Pods-Movie-Application-Movie-ApplicationUITests.modulemap */, + CFF3DDE8E33D8AB4E634AD7397EDF142 /* Pods-Movie-Application-Movie-ApplicationUITests-acknowledgements.markdown */, + C8AE2673202947D67498F0A0FA5C35C9 /* Pods-Movie-Application-Movie-ApplicationUITests-acknowledgements.plist */, + 60ADE285B0AD73894EB10EAEB17D90A9 /* Pods-Movie-Application-Movie-ApplicationUITests-dummy.m */, + 1BFF7061A1F4133B05034659C9871E99 /* Pods-Movie-Application-Movie-ApplicationUITests-Info.plist */, + 87E26D1D5297070E94225F4310B05503 /* Pods-Movie-Application-Movie-ApplicationUITests-umbrella.h */, + B17E6B9EDA5352A0A2B1CFF2069A524B /* Pods-Movie-Application-Movie-ApplicationUITests.debug.xcconfig */, + A7ADAF3FE53D427495181634939FB212 /* Pods-Movie-Application-Movie-ApplicationUITests.release.xcconfig */, + ); + name = "Pods-Movie-Application-Movie-ApplicationUITests"; + path = "Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests"; + sourceTree = ""; + }; + 94BB1DE400DAD447EDB6F3811D86811F /* Targets Support Files */ = { + isa = PBXGroup; + children = ( + FF8CBF5B59EFEF4907CFEF4B1F42F3F5 /* Pods-Movie-Application */, + 8DC8DC796700F4E899A97248D9EC1817 /* Pods-Movie-Application-Movie-ApplicationUITests */, + 7B1C6652F49778646D59F05AD12F7AE9 /* Pods-Movie-ApplicationTests */, + ); + name = "Targets Support Files"; + sourceTree = ""; + }; + 965877409E01FB3D85D85E90E6B30185 /* Pods */ = { + isa = PBXGroup; + children = ( + D96179D43A67C5D5323B0682D32C133C /* SwiftLint */, + ); + name = Pods; + sourceTree = ""; + }; + CF1408CF629C7361332E53B88F7BD30C = { + isa = PBXGroup; + children = ( + 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */, + D210D550F4EA176C3123ED886F8F87F5 /* Frameworks */, + 965877409E01FB3D85D85E90E6B30185 /* Pods */, + 437BBD89DD4A83FD441D19E4A176C73B /* Products */, + 94BB1DE400DAD447EDB6F3811D86811F /* Targets Support Files */, + ); + sourceTree = ""; + }; + D210D550F4EA176C3123ED886F8F87F5 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 578452D2E740E91742655AC8F1636D1F /* iOS */, + ); + name = Frameworks; + sourceTree = ""; + }; + D96179D43A67C5D5323B0682D32C133C /* SwiftLint */ = { + isa = PBXGroup; + children = ( + 7FDA596DBE0337ACDFE55A210D54EF26 /* Support Files */, + ); + path = SwiftLint; + sourceTree = ""; + }; + FF8CBF5B59EFEF4907CFEF4B1F42F3F5 /* Pods-Movie-Application */ = { + isa = PBXGroup; + children = ( + 1DD502E3C292A7B1960EDEBBFEF44E4B /* Pods-Movie-Application.modulemap */, + 708B1968254A5D6C62E3FBAAF15C63CB /* Pods-Movie-Application-acknowledgements.markdown */, + 6A7653E3F970DE4740CE9C81F477136D /* Pods-Movie-Application-acknowledgements.plist */, + 8FA8769234563DC2782C159D06DDC071 /* Pods-Movie-Application-dummy.m */, + 8B2AC8608AD7EC056CA8FF345791925B /* Pods-Movie-Application-Info.plist */, + 5DC615774AA0A7BA84B754EC69967A25 /* Pods-Movie-Application-umbrella.h */, + 7C62C2E1DDF543F6CE5921BC2C41DCD1 /* Pods-Movie-Application.debug.xcconfig */, + C50F5A57494BDFAF07F40520DE30B162 /* Pods-Movie-Application.release.xcconfig */, + ); + name = "Pods-Movie-Application"; + path = "Target Support Files/Pods-Movie-Application"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 7D4EEF7452B77D04E31DA0DE44FB9F82 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 1A3068750930B55C297DA2804602D285 /* Pods-Movie-Application-Movie-ApplicationUITests-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E2B6B52D5496298522E7E1C91C6F0919 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 088EC7292E8D5904FEADDD1413032636 /* Pods-Movie-Application-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F522AE662FCF62B32F1BB9D214DBC9F8 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + E6133DD52CC298E557AF5D84BA14B117 /* Pods-Movie-ApplicationTests-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 35BCBC8DE220CDC9F22AD04801E26067 /* Pods-Movie-ApplicationTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8D6FEB4A2C704F7CE349EBCD29914BE2 /* Build configuration list for PBXNativeTarget "Pods-Movie-ApplicationTests" */; + buildPhases = ( + F522AE662FCF62B32F1BB9D214DBC9F8 /* Headers */, + FB6CFB2A23C7D18AE3192B924DD74BA1 /* Sources */, + 81A2C324845428ECD2145BC9F658F168 /* Frameworks */, + 70B1F986B7D822A70C5441F638212D6A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 146ABF76033C429625FA2D57A0EF2AA8 /* PBXTargetDependency */, + ); + name = "Pods-Movie-ApplicationTests"; + productName = Pods_Movie_ApplicationTests; + productReference = F6BE35E9934DDABF5ACA87061AA15F92 /* Pods-Movie-ApplicationTests */; + productType = "com.apple.product-type.framework"; + }; + 48D9F8FA7768414C08FD7D1127EDDF8E /* Pods-Movie-Application-Movie-ApplicationUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F81F31A82E8849AFAFD31E91EE46DD31 /* Build configuration list for PBXNativeTarget "Pods-Movie-Application-Movie-ApplicationUITests" */; + buildPhases = ( + 7D4EEF7452B77D04E31DA0DE44FB9F82 /* Headers */, + 903B480FD22992D515749D79421C82B6 /* Sources */, + A6448C7DB26E4AC6B27E105766CFD839 /* Frameworks */, + D8BE1476B066BEE0BF436B0920CC9CD6 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 3746067D9F1190DAC794D25BA9704DFD /* PBXTargetDependency */, + ); + name = "Pods-Movie-Application-Movie-ApplicationUITests"; + productName = Pods_Movie_Application_Movie_ApplicationUITests; + productReference = 57D900B7BCCC797ACCC67ED6DAB5C037 /* Pods-Movie-Application-Movie-ApplicationUITests */; + productType = "com.apple.product-type.framework"; + }; + C885DC71076F878D853ADD01EAA07AB5 /* Pods-Movie-Application */ = { + isa = PBXNativeTarget; + buildConfigurationList = DC653BCA5A87F4255CAEF389E87AE652 /* Build configuration list for PBXNativeTarget "Pods-Movie-Application" */; + buildPhases = ( + E2B6B52D5496298522E7E1C91C6F0919 /* Headers */, + 41CFDB8CAF894FD8C8275DFF5619E3DC /* Sources */, + 3860492F6520909E3CC1E57358D61551 /* Frameworks */, + E8B485B105237B10E14E60A2F0F0A5F6 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 45ABFD05AA01F26832619A12D50047FF /* PBXTargetDependency */, + ); + name = "Pods-Movie-Application"; + productName = Pods_Movie_Application; + productReference = E9AFDEB7CDF4C389D33017FD8435A2D3 /* Pods-Movie-Application */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + BFDFE7DC352907FC980B868725387E98 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1240; + LastUpgradeCheck = 1320; + }; + buildConfigurationList = 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = CF1408CF629C7361332E53B88F7BD30C; + productRefGroup = 437BBD89DD4A83FD441D19E4A176C73B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + C885DC71076F878D853ADD01EAA07AB5 /* Pods-Movie-Application */, + 48D9F8FA7768414C08FD7D1127EDDF8E /* Pods-Movie-Application-Movie-ApplicationUITests */, + 35BCBC8DE220CDC9F22AD04801E26067 /* Pods-Movie-ApplicationTests */, + 52B60EC2A583F24ACBB69C113F5488B9 /* SwiftLint */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 70B1F986B7D822A70C5441F638212D6A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D8BE1476B066BEE0BF436B0920CC9CD6 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E8B485B105237B10E14E60A2F0F0A5F6 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 41CFDB8CAF894FD8C8275DFF5619E3DC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6E5312936F496AD8B8241C2B7842171F /* Pods-Movie-Application-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 903B480FD22992D515749D79421C82B6 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 617B4AC3B9CC3809A57079260D3AC16A /* Pods-Movie-Application-Movie-ApplicationUITests-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FB6CFB2A23C7D18AE3192B924DD74BA1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FA362453619DACB45E77163D2CBEFF23 /* Pods-Movie-ApplicationTests-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 146ABF76033C429625FA2D57A0EF2AA8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = "Pods-Movie-Application"; + target = C885DC71076F878D853ADD01EAA07AB5 /* Pods-Movie-Application */; + targetProxy = 23128C94A062138F32C111017658FAED /* PBXContainerItemProxy */; + }; + 3746067D9F1190DAC794D25BA9704DFD /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = SwiftLint; + target = 52B60EC2A583F24ACBB69C113F5488B9 /* SwiftLint */; + targetProxy = 9C95EDA32BFA17ECA9276583CB34AF65 /* PBXContainerItemProxy */; + }; + 45ABFD05AA01F26832619A12D50047FF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = SwiftLint; + target = 52B60EC2A583F24ACBB69C113F5488B9 /* SwiftLint */; + targetProxy = 60BE007270207BA55BC5BAF6E93E5534 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 2AF0B07D25C907EB8705510B3D8C00B9 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 477CB6AEB1199DAF1AC6940863F59784 /* Pods-Movie-ApplicationTests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 4214E6946993DF02ED98D89047C64016 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "POD_CONFIGURATION_DEBUG=1", + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + SYMROOT = "${SRCROOT}/../build"; + }; + name = Debug; + }; + 9DA2BF3F8536C40EB8A38DE628EBC77B /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7C62C2E1DDF543F6CE5921BC2C41DCD1 /* Pods-Movie-Application.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "Target Support Files/Pods-Movie-Application/Pods-Movie-Application-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-Movie-Application/Pods-Movie-Application.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + AD81E62ACCB0B7A923FC8AA288F9921E /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7EAB1DA481EDDC005A251D3A5E6BE4CD /* SwiftLint.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + BD326061CEBCC28F845A58DD84F837BC /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C50F5A57494BDFAF07F40520DE30B162 /* Pods-Movie-Application.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "Target Support Files/Pods-Movie-Application/Pods-Movie-Application-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-Movie-Application/Pods-Movie-Application.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + CD85FD90473CFBE797E4264E7BBC70AF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "POD_CONFIGURATION_RELEASE=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + SYMROOT = "${SRCROOT}/../build"; + }; + name = Release; + }; + E0A16BE17F08E36E3BB1A5B29AD43B44 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A7ADAF3FE53D427495181634939FB212 /* Pods-Movie-Application-Movie-ApplicationUITests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + EADD1F50ABC8096A0D6CB18822BB4EE4 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3EC0124719FE8EF5FDDD72C26F8A2F57 /* SwiftLint.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + FA689D35EDF915D1F3C086B27791685E /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 643B63285001F11707C524E9DB75276F /* Pods-Movie-ApplicationTests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + FD7B8C6B40E011BB9B78CC44CF4BA293 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B17E6B9EDA5352A0A2B1CFF2069A524B /* Pods-Movie-Application-Movie-ApplicationUITests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4214E6946993DF02ED98D89047C64016 /* Debug */, + CD85FD90473CFBE797E4264E7BBC70AF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8D6FEB4A2C704F7CE349EBCD29914BE2 /* Build configuration list for PBXNativeTarget "Pods-Movie-ApplicationTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2AF0B07D25C907EB8705510B3D8C00B9 /* Debug */, + FA689D35EDF915D1F3C086B27791685E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AE7B4FB01588B9E6DF09CB79FC7CE7BD /* Build configuration list for PBXAggregateTarget "SwiftLint" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EADD1F50ABC8096A0D6CB18822BB4EE4 /* Debug */, + AD81E62ACCB0B7A923FC8AA288F9921E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DC653BCA5A87F4255CAEF389E87AE652 /* Build configuration list for PBXNativeTarget "Pods-Movie-Application" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9DA2BF3F8536C40EB8A38DE628EBC77B /* Debug */, + BD326061CEBCC28F845A58DD84F837BC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F81F31A82E8849AFAFD31E91EE46DD31 /* Build configuration list for PBXNativeTarget "Pods-Movie-Application-Movie-ApplicationUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FD7B8C6B40E011BB9B78CC44CF4BA293 /* Debug */, + E0A16BE17F08E36E3BB1A5B29AD43B44 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = BFDFE7DC352907FC980B868725387E98 /* Project object */; +} diff --git a/Movie-Application/Pods/Pods.xcodeproj/xcuserdata/mohanna.xcuserdatad/xcschemes/Pods-Movie-Application-Movie-ApplicationUITests.xcscheme b/Movie-Application/Pods/Pods.xcodeproj/xcuserdata/mohanna.xcuserdatad/xcschemes/Pods-Movie-Application-Movie-ApplicationUITests.xcscheme new file mode 100644 index 00000000..8ad0a7a9 --- /dev/null +++ b/Movie-Application/Pods/Pods.xcodeproj/xcuserdata/mohanna.xcuserdatad/xcschemes/Pods-Movie-Application-Movie-ApplicationUITests.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Movie-Application/Pods/Pods.xcodeproj/xcuserdata/mohanna.xcuserdatad/xcschemes/Pods-Movie-Application.xcscheme b/Movie-Application/Pods/Pods.xcodeproj/xcuserdata/mohanna.xcuserdatad/xcschemes/Pods-Movie-Application.xcscheme new file mode 100644 index 00000000..6599b6e2 --- /dev/null +++ b/Movie-Application/Pods/Pods.xcodeproj/xcuserdata/mohanna.xcuserdatad/xcschemes/Pods-Movie-Application.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Movie-Application/Pods/Pods.xcodeproj/xcuserdata/mohanna.xcuserdatad/xcschemes/Pods-Movie-ApplicationTests.xcscheme b/Movie-Application/Pods/Pods.xcodeproj/xcuserdata/mohanna.xcuserdatad/xcschemes/Pods-Movie-ApplicationTests.xcscheme new file mode 100644 index 00000000..17958f9e --- /dev/null +++ b/Movie-Application/Pods/Pods.xcodeproj/xcuserdata/mohanna.xcuserdatad/xcschemes/Pods-Movie-ApplicationTests.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Movie-Application/Pods/Pods.xcodeproj/xcuserdata/mohanna.xcuserdatad/xcschemes/SwiftLint.xcscheme b/Movie-Application/Pods/Pods.xcodeproj/xcuserdata/mohanna.xcuserdatad/xcschemes/SwiftLint.xcscheme new file mode 100644 index 00000000..ccad242c --- /dev/null +++ b/Movie-Application/Pods/Pods.xcodeproj/xcuserdata/mohanna.xcuserdatad/xcschemes/SwiftLint.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Movie-Application/Pods/Pods.xcodeproj/xcuserdata/mohanna.xcuserdatad/xcschemes/xcschememanagement.plist b/Movie-Application/Pods/Pods.xcodeproj/xcuserdata/mohanna.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 00000000..29596b98 --- /dev/null +++ b/Movie-Application/Pods/Pods.xcodeproj/xcuserdata/mohanna.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,39 @@ + + + + + SchemeUserState + + Pods-Movie-Application-Movie-ApplicationUITests.xcscheme + + isShown + + orderHint + 1 + + Pods-Movie-Application.xcscheme + + isShown + + orderHint + 0 + + Pods-Movie-ApplicationTests.xcscheme + + isShown + + orderHint + 2 + + SwiftLint.xcscheme + + isShown + + orderHint + 3 + + + SuppressBuildableAutocreation + + + diff --git a/Movie-Application/Pods/SwiftLint/LICENSE b/Movie-Application/Pods/SwiftLint/LICENSE new file mode 100644 index 00000000..04203762 --- /dev/null +++ b/Movie-Application/Pods/SwiftLint/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 Realm Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Movie-Application/Pods/SwiftLint/swiftlint b/Movie-Application/Pods/SwiftLint/swiftlint new file mode 100755 index 00000000..6837f762 Binary files /dev/null and b/Movie-Application/Pods/SwiftLint/swiftlint differ diff --git a/Movie-Application/Pods/Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests-Info.plist b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests-Info.plist new file mode 100644 index 00000000..2243fe6e --- /dev/null +++ b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/Movie-Application/Pods/Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests-acknowledgements.markdown b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests-acknowledgements.markdown new file mode 100644 index 00000000..ca921c3d --- /dev/null +++ b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests-acknowledgements.markdown @@ -0,0 +1,28 @@ +# Acknowledgements +This application makes use of the following third party libraries: + +## SwiftLint + +The MIT License (MIT) + +Copyright (c) 2020 Realm Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Generated by CocoaPods - https://cocoapods.org diff --git a/Movie-Application/Pods/Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests-acknowledgements.plist b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests-acknowledgements.plist new file mode 100644 index 00000000..ddd65521 --- /dev/null +++ b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests-acknowledgements.plist @@ -0,0 +1,60 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + Type + PSGroupSpecifier + + + FooterText + The MIT License (MIT) + +Copyright (c) 2020 Realm Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + License + MIT + Title + SwiftLint + Type + PSGroupSpecifier + + + FooterText + Generated by CocoaPods - https://cocoapods.org + Title + + Type + PSGroupSpecifier + + + StringsTable + Acknowledgements + Title + Acknowledgements + + diff --git a/Movie-Application/Pods/Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests-dummy.m b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests-dummy.m new file mode 100644 index 00000000..677bf625 --- /dev/null +++ b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Pods_Movie_Application_Movie_ApplicationUITests : NSObject +@end +@implementation PodsDummy_Pods_Movie_Application_Movie_ApplicationUITests +@end diff --git a/Movie-Application/Pods/Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests-umbrella.h b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests-umbrella.h new file mode 100644 index 00000000..dc0f4063 --- /dev/null +++ b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double Pods_Movie_Application_Movie_ApplicationUITestsVersionNumber; +FOUNDATION_EXPORT const unsigned char Pods_Movie_Application_Movie_ApplicationUITestsVersionString[]; + diff --git a/Movie-Application/Pods/Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests.debug.xcconfig b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests.debug.xcconfig new file mode 100644 index 00000000..1d245763 --- /dev/null +++ b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests.debug.xcconfig @@ -0,0 +1,9 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Movie-Application/Pods/Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests.modulemap b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests.modulemap new file mode 100644 index 00000000..704ac3b0 --- /dev/null +++ b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests.modulemap @@ -0,0 +1,6 @@ +framework module Pods_Movie_Application_Movie_ApplicationUITests { + umbrella header "Pods-Movie-Application-Movie-ApplicationUITests-umbrella.h" + + export * + module * { export * } +} diff --git a/Movie-Application/Pods/Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests.release.xcconfig b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests.release.xcconfig new file mode 100644 index 00000000..1d245763 --- /dev/null +++ b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application-Movie-ApplicationUITests/Pods-Movie-Application-Movie-ApplicationUITests.release.xcconfig @@ -0,0 +1,9 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Movie-Application/Pods/Target Support Files/Pods-Movie-Application/Pods-Movie-Application-Info.plist b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application/Pods-Movie-Application-Info.plist new file mode 100644 index 00000000..2243fe6e --- /dev/null +++ b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application/Pods-Movie-Application-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/Movie-Application/Pods/Target Support Files/Pods-Movie-Application/Pods-Movie-Application-acknowledgements.markdown b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application/Pods-Movie-Application-acknowledgements.markdown new file mode 100644 index 00000000..ca921c3d --- /dev/null +++ b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application/Pods-Movie-Application-acknowledgements.markdown @@ -0,0 +1,28 @@ +# Acknowledgements +This application makes use of the following third party libraries: + +## SwiftLint + +The MIT License (MIT) + +Copyright (c) 2020 Realm Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Generated by CocoaPods - https://cocoapods.org diff --git a/Movie-Application/Pods/Target Support Files/Pods-Movie-Application/Pods-Movie-Application-acknowledgements.plist b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application/Pods-Movie-Application-acknowledgements.plist new file mode 100644 index 00000000..ddd65521 --- /dev/null +++ b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application/Pods-Movie-Application-acknowledgements.plist @@ -0,0 +1,60 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + Type + PSGroupSpecifier + + + FooterText + The MIT License (MIT) + +Copyright (c) 2020 Realm Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + License + MIT + Title + SwiftLint + Type + PSGroupSpecifier + + + FooterText + Generated by CocoaPods - https://cocoapods.org + Title + + Type + PSGroupSpecifier + + + StringsTable + Acknowledgements + Title + Acknowledgements + + diff --git a/Movie-Application/Pods/Target Support Files/Pods-Movie-Application/Pods-Movie-Application-dummy.m b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application/Pods-Movie-Application-dummy.m new file mode 100644 index 00000000..c0a26ab9 --- /dev/null +++ b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application/Pods-Movie-Application-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Pods_Movie_Application : NSObject +@end +@implementation PodsDummy_Pods_Movie_Application +@end diff --git a/Movie-Application/Pods/Target Support Files/Pods-Movie-Application/Pods-Movie-Application-umbrella.h b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application/Pods-Movie-Application-umbrella.h new file mode 100644 index 00000000..aa1dbdf0 --- /dev/null +++ b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application/Pods-Movie-Application-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double Pods_Movie_ApplicationVersionNumber; +FOUNDATION_EXPORT const unsigned char Pods_Movie_ApplicationVersionString[]; + diff --git a/Movie-Application/Pods/Target Support Files/Pods-Movie-Application/Pods-Movie-Application.debug.xcconfig b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application/Pods-Movie-Application.debug.xcconfig new file mode 100644 index 00000000..1d245763 --- /dev/null +++ b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application/Pods-Movie-Application.debug.xcconfig @@ -0,0 +1,9 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Movie-Application/Pods/Target Support Files/Pods-Movie-Application/Pods-Movie-Application.modulemap b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application/Pods-Movie-Application.modulemap new file mode 100644 index 00000000..54f22f6d --- /dev/null +++ b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application/Pods-Movie-Application.modulemap @@ -0,0 +1,6 @@ +framework module Pods_Movie_Application { + umbrella header "Pods-Movie-Application-umbrella.h" + + export * + module * { export * } +} diff --git a/Movie-Application/Pods/Target Support Files/Pods-Movie-Application/Pods-Movie-Application.release.xcconfig b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application/Pods-Movie-Application.release.xcconfig new file mode 100644 index 00000000..1d245763 --- /dev/null +++ b/Movie-Application/Pods/Target Support Files/Pods-Movie-Application/Pods-Movie-Application.release.xcconfig @@ -0,0 +1,9 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Movie-Application/Pods/Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests-Info.plist b/Movie-Application/Pods/Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests-Info.plist new file mode 100644 index 00000000..2243fe6e --- /dev/null +++ b/Movie-Application/Pods/Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/Movie-Application/Pods/Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests-acknowledgements.markdown b/Movie-Application/Pods/Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests-acknowledgements.markdown new file mode 100644 index 00000000..102af753 --- /dev/null +++ b/Movie-Application/Pods/Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests-acknowledgements.markdown @@ -0,0 +1,3 @@ +# Acknowledgements +This application makes use of the following third party libraries: +Generated by CocoaPods - https://cocoapods.org diff --git a/Movie-Application/Pods/Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests-acknowledgements.plist b/Movie-Application/Pods/Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests-acknowledgements.plist new file mode 100644 index 00000000..7acbad1e --- /dev/null +++ b/Movie-Application/Pods/Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests-acknowledgements.plist @@ -0,0 +1,29 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + Type + PSGroupSpecifier + + + FooterText + Generated by CocoaPods - https://cocoapods.org + Title + + Type + PSGroupSpecifier + + + StringsTable + Acknowledgements + Title + Acknowledgements + + diff --git a/Movie-Application/Pods/Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests-dummy.m b/Movie-Application/Pods/Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests-dummy.m new file mode 100644 index 00000000..b9de8131 --- /dev/null +++ b/Movie-Application/Pods/Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Pods_Movie_ApplicationTests : NSObject +@end +@implementation PodsDummy_Pods_Movie_ApplicationTests +@end diff --git a/Movie-Application/Pods/Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests-umbrella.h b/Movie-Application/Pods/Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests-umbrella.h new file mode 100644 index 00000000..eb4d071a --- /dev/null +++ b/Movie-Application/Pods/Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double Pods_Movie_ApplicationTestsVersionNumber; +FOUNDATION_EXPORT const unsigned char Pods_Movie_ApplicationTestsVersionString[]; + diff --git a/Movie-Application/Pods/Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests.debug.xcconfig b/Movie-Application/Pods/Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests.debug.xcconfig new file mode 100644 index 00000000..26f2c773 --- /dev/null +++ b/Movie-Application/Pods/Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests.debug.xcconfig @@ -0,0 +1,8 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Movie-Application/Pods/Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests.modulemap b/Movie-Application/Pods/Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests.modulemap new file mode 100644 index 00000000..68d0d5fd --- /dev/null +++ b/Movie-Application/Pods/Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests.modulemap @@ -0,0 +1,6 @@ +framework module Pods_Movie_ApplicationTests { + umbrella header "Pods-Movie-ApplicationTests-umbrella.h" + + export * + module * { export * } +} diff --git a/Movie-Application/Pods/Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests.release.xcconfig b/Movie-Application/Pods/Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests.release.xcconfig new file mode 100644 index 00000000..26f2c773 --- /dev/null +++ b/Movie-Application/Pods/Target Support Files/Pods-Movie-ApplicationTests/Pods-Movie-ApplicationTests.release.xcconfig @@ -0,0 +1,8 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Movie-Application/Pods/Target Support Files/SwiftLint/SwiftLint.debug.xcconfig b/Movie-Application/Pods/Target Support Files/SwiftLint/SwiftLint.debug.xcconfig new file mode 100644 index 00000000..003a1f43 --- /dev/null +++ b/Movie-Application/Pods/Target Support Files/SwiftLint/SwiftLint.debug.xcconfig @@ -0,0 +1,11 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwiftLint +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/SwiftLint +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Movie-Application/Pods/Target Support Files/SwiftLint/SwiftLint.release.xcconfig b/Movie-Application/Pods/Target Support Files/SwiftLint/SwiftLint.release.xcconfig new file mode 100644 index 00000000..003a1f43 --- /dev/null +++ b/Movie-Application/Pods/Target Support Files/SwiftLint/SwiftLint.release.xcconfig @@ -0,0 +1,11 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwiftLint +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/SwiftLint +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/README.md b/README.md index 8bb0c38d..8c50975c 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,49 @@ -# Work sample - Application developer - -## Assignment - -- Build an awesome movie app that shows popular and high rated movies. -- Code it for the platform (Android, iOS, web) you applied for or the one you prefer. - -## Requirements - -- Use an open https://developers.themoviedb.org/open source API. Please read the API [Authentication section](https://developers.themoviedb.org/3/getting-started/authentication) to get started. -- Discover most popular and highly rated movies. -- Display the movies with creative look and feel of an app to meet design guidelines for your platform (Material Design etc.). -- Launch a detail screen whenever a particular movie is selected. - -## Examples of bonus features - -- Allow user to save a favorite movie for offline access. -- Allow user to read movie reviews. - -## We expect you to - -- Write clean code. -- Create a responsive design. -- Handle error cases. -- Use the latest libraries and technologies. -- Tested code is a big plus. - -### User experience - -The features of the app might be few, but we expect you to deliver a solution with a high user experience. Imagine this application to be used by real users, with real needs. Make it interesting, fun and intuitive to use. And of course you are allowed to extend your applications functionality. - -### Code - -We expect that the code is of high quality and under source control. Expect the solution to be continuously worked on by other developers and should therefore be easy to understand, adjust and extend. True beauty starts on the inside! - -## Delivery - -Fork the repository, code in your fork and make a pull request when done. A nice commit history describing your work is preferred over squashing it into one commit. -Also send us an e-mail to let us know! - -### Good luck! - ---- - +## Technical features +* VIPER Architecture Module +* MVVMC Architectures Module +* Completely native Swift(without dependencies and libraries) +* Clean swift code(Swiftlint included) +* Testable (unit test included) +* Generic collection view DataSource +* Pagination System +* Caching System +* Testable network layer +* Custom bottom sheet with animations +* Offline saved movies(watchlist with Core data) +* Have sorting system based on Name, Date Added, User Score for watchlist movies +* Included unit tests +* Context menu for each movie cell +* Support Dark and light appearance +* Smooth and easy to use design +* Creative design for showing handful details of each movie +* Included launchScreen and application icon + +## Expected Features: +- [x] Build an awesome movie app that shows popular and high-rated movies. +- [x] Discover top rated movies +- [x] Discover the most popular movies. +- [x] Display the movies with the creative look and feel of an app to meet design guidelines for your platform (Material Design etc.). +- [x] Launch a detail screen whenever a particular movie is selected. +- [x] Create a responsive design. +- [x] Write clean code. +- [x] Handle error cases. +- [x] Tested code +- [x] Use the latest technologies. +- [x] Use an open https://developers.themoviedb.org/open source API. Please read the API Authentication section to get started. + +## Bonus features +* Allowed user to save list of favorite movies as watchlist with offline access +* Unit Test +* Pagination system +* Caching system(testable network layer +* Custom bottom sheet with animations for movie details +* Used Core data as saving offline system +* Sorting system for watchlist +![Simulator Screen Shot - iPhone 13 mini - 2022-05-07 at 00 41 25](https://user-images.githubusercontent.com/66467669/167210681-fe72e942-f4ea-4381-baf2-6ba4582b8cf5.png)![Simulator Screen Shot - iPhone 13 mini - 2022-05-07 at 00 41 31](https://user-images.githubusercontent.com/66467669/167210731-172f8606-f41c-4e03-b037-860575265cce.png) +![Simulator Screen Shot - iPhone 13 mini - 2022-05-07 at 00 41 40](https://user-images.githubusercontent.com/66467669/167210800-c25366b8-3823-4dc0-9323-f9c6df6ded35.png) +![Simulator Screen Shot - iPhone 13 mini - 2022-05-07 at 00 41 44](https://user-images.githubusercontent.com/66467669/167210831-94045ca5-5921-4406-8029-73f6f079710b.png)![Simulator Screen Shot - iPhone 13 mini - 2022-05-07 at 00 41 59](https://user-images.githubusercontent.com/66467669/167210850-69fe1eec-4570-43c7-93e6-cfd32fde609a.png) +![Simulator Screen Shot - iPhone 13 mini - 2022-05![Simulator Screen Shot - iPhone 13 mini - 2022-05-07 at 00 42 06](https://user-images.githubusercontent.com![Simulator Screen Shot - iPhone 13 mini - 2022-05-07 at 00 42 29](https://user-images.githubusercontent.com/66467669/167210994-ce524395-55ad-4e7c-badc-6d0fcc693ab2.png) +/66467669/167210964-a541f233-935d-4072-9995-8af5a1607b1a.png) +-07 at 00 48 00](https://user-images.githubusercontent.com/66467669/167210916-284958a1-a1d2-413e-b417-a9211e2fee65.png) +![Simulator Screen Shot - iPhone 13 mini - 2022-05-07 at 00 42 29](https://user-images.githubusercontent.com/66467669/167211323-7672df19-0934-45d2-a23a-14ed6c0ff342.png) +![Simulator Screen Shot - iPhone 13 mini - 2022-05-07 at 00 43 45](https://user-images.githubusercontent.com/66467669/167211343-39786479-bd88-438c-a1b8-b670d0047a43.png)