diff --git a/MovieDB.xcodeproj/project.pbxproj b/MovieDB.xcodeproj/project.pbxproj new file mode 100644 index 00000000..08e7970e --- /dev/null +++ b/MovieDB.xcodeproj/project.pbxproj @@ -0,0 +1,657 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 0E4C82C02974A55600D96A83 /* URL+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E4C82BF2974A55600D96A83 /* URL+Extension.swift */; }; + 0E4C82C22974A62500D96A83 /* Data+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E4C82C12974A62500D96A83 /* Data+Extension.swift */; }; + 0E7C210E2973D88300882E04 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7C210D2973D88300882E04 /* AppDelegate.swift */; }; + 0E7C21102973D88300882E04 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7C210F2973D88300882E04 /* SceneDelegate.swift */; }; + 0E7C21122973D88300882E04 /* MoviesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7C21112973D88300882E04 /* MoviesViewController.swift */; }; + 0E7C21172973D88500882E04 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0E7C21162973D88500882E04 /* Assets.xcassets */; }; + 0E7C211A2973D88500882E04 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0E7C21182973D88500882E04 /* LaunchScreen.storyboard */; }; + 0E7C21242973EF6900882E04 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 0E7C21232973EF6900882E04 /* SnapKit */; }; + 0E7C21262974126F00882E04 /* MoviesCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7C21252974126F00882E04 /* MoviesCollectionViewCell.swift */; }; + 0E7C21292974138E00882E04 /* UICollectionView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7C21282974138E00882E04 /* UICollectionView+Extension.swift */; }; + 0E7C212C29741A1400882E04 /* ReusableViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7C212B29741A1400882E04 /* ReusableViewProtocol.swift */; }; + 0E7C212E29741A4900882E04 /* UICollectionViewCell+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7C212D29741A4900882E04 /* UICollectionViewCell+Extension.swift */; }; + 0E7C21312974236C00882E04 /* MoviesCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7C21302974236C00882E04 /* MoviesCollectionReusableView.swift */; }; + 0E7C21342974351300882E04 /* Constant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7C21332974351300882E04 /* Constant.swift */; }; + 0E7C213F297438A400882E04 /* MovieDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7C213E297438A400882E04 /* MovieDetailsViewController.swift */; }; + 0E7C214229747CCB00882E04 /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7C214129747CCB00882E04 /* NetworkService.swift */; }; + 0E7C2144297483B400882E04 /* MoviesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7C2143297483B400882E04 /* MoviesViewModel.swift */; }; + 0E7C21462974869B00882E04 /* MoviesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7C21452974869B00882E04 /* MoviesModel.swift */; }; + 0EE1BDC329775BE9007FEB59 /* ImageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE1BDC229775BE9007FEB59 /* ImageService.swift */; }; + 0EE1BDC52977814C007FEB59 /* UIActivityIndicatorView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE1BDC42977814C007FEB59 /* UIActivityIndicatorView+Extension.swift */; }; + 0EE1BDC72978960D007FEB59 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE1BDC62978960D007FEB59 /* Double+Extension.swift */; }; + 0EE1BDC929789A62007FEB59 /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE1BDC829789A62007FEB59 /* UIColor+Extension.swift */; }; + 0EE1BDD42978BE69007FEB59 /* MovieViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE1BDD32978BE69007FEB59 /* MovieViewModelTests.swift */; }; + 0EE1BDDB297A023A007FEB59 /* Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE1BDDA297A023A007FEB59 /* Helper.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 0EE1BDD52978BE69007FEB59 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0E7C21022973D88300882E04 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 0E7C21092973D88300882E04; + remoteInfo = MovieDB; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 0E4C82BF2974A55600D96A83 /* URL+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extension.swift"; sourceTree = ""; }; + 0E4C82C12974A62500D96A83 /* Data+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extension.swift"; sourceTree = ""; }; + 0E7C210A2973D88300882E04 /* MovieDB.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MovieDB.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 0E7C210D2973D88300882E04 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 0E7C210F2973D88300882E04 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 0E7C21112973D88300882E04 /* MoviesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesViewController.swift; sourceTree = ""; }; + 0E7C21162973D88500882E04 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 0E7C21192973D88500882E04 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 0E7C211B2973D88500882E04 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 0E7C21252974126F00882E04 /* MoviesCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesCollectionViewCell.swift; sourceTree = ""; }; + 0E7C21282974138E00882E04 /* UICollectionView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICollectionView+Extension.swift"; sourceTree = ""; }; + 0E7C212B29741A1400882E04 /* ReusableViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableViewProtocol.swift; sourceTree = ""; }; + 0E7C212D29741A4900882E04 /* UICollectionViewCell+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICollectionViewCell+Extension.swift"; sourceTree = ""; }; + 0E7C21302974236C00882E04 /* MoviesCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesCollectionReusableView.swift; sourceTree = ""; }; + 0E7C21332974351300882E04 /* Constant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constant.swift; sourceTree = ""; }; + 0E7C213E297438A400882E04 /* MovieDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieDetailsViewController.swift; sourceTree = ""; }; + 0E7C214129747CCB00882E04 /* NetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = ""; }; + 0E7C2143297483B400882E04 /* MoviesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesViewModel.swift; sourceTree = ""; }; + 0E7C21452974869B00882E04 /* MoviesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesModel.swift; sourceTree = ""; }; + 0EE1BDC229775BE9007FEB59 /* ImageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageService.swift; sourceTree = ""; }; + 0EE1BDC42977814C007FEB59 /* UIActivityIndicatorView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIActivityIndicatorView+Extension.swift"; sourceTree = ""; }; + 0EE1BDC62978960D007FEB59 /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = ""; }; + 0EE1BDC829789A62007FEB59 /* UIColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extension.swift"; sourceTree = ""; }; + 0EE1BDD12978BE69007FEB59 /* MovieDBTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MovieDBTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 0EE1BDD32978BE69007FEB59 /* MovieViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieViewModelTests.swift; sourceTree = ""; }; + 0EE1BDDA297A023A007FEB59 /* Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helper.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 0E7C21072973D88300882E04 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0E7C21242973EF6900882E04 /* SnapKit in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0EE1BDCE2978BE69007FEB59 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0E7C21012973D88300882E04 = { + isa = PBXGroup; + children = ( + 0E7C210C2973D88300882E04 /* MovieDB */, + 0EE1BDD22978BE69007FEB59 /* MovieDBTests */, + 0E7C210B2973D88300882E04 /* Products */, + ); + sourceTree = ""; + }; + 0E7C210B2973D88300882E04 /* Products */ = { + isa = PBXGroup; + children = ( + 0E7C210A2973D88300882E04 /* MovieDB.app */, + 0EE1BDD12978BE69007FEB59 /* MovieDBTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 0E7C210C2973D88300882E04 /* MovieDB */ = { + isa = PBXGroup; + children = ( + 0E7C2132297434F200882E04 /* App */, + 0E7C21212973D9C400882E04 /* Movies */, + 0E7C213A2974385900882E04 /* MovieDetails */, + 0E7C212F2974227C00882E04 /* Component */, + 0E7C212A297419F900882E04 /* Protocol */, + 0EE1BDC129775BDC007FEB59 /* Service */, + 0E7C21272974137E00882E04 /* Extension */, + 0E7C21352974360E00882E04 /* Helper */, + 0E7C21162973D88500882E04 /* Assets.xcassets */, + 0E7C21182973D88500882E04 /* LaunchScreen.storyboard */, + 0E7C211B2973D88500882E04 /* Info.plist */, + ); + path = MovieDB; + sourceTree = ""; + }; + 0E7C21212973D9C400882E04 /* Movies */ = { + isa = PBXGroup; + children = ( + 0E7C21362974367F00882E04 /* Model */, + 0E7C21372974368800882E04 /* View */, + 0E7C21382974368D00882E04 /* ViewModel */, + ); + path = Movies; + sourceTree = ""; + }; + 0E7C21272974137E00882E04 /* Extension */ = { + isa = PBXGroup; + children = ( + 0E7C21282974138E00882E04 /* UICollectionView+Extension.swift */, + 0E7C212D29741A4900882E04 /* UICollectionViewCell+Extension.swift */, + 0EE1BDC42977814C007FEB59 /* UIActivityIndicatorView+Extension.swift */, + 0E4C82BF2974A55600D96A83 /* URL+Extension.swift */, + 0E4C82C12974A62500D96A83 /* Data+Extension.swift */, + 0EE1BDC62978960D007FEB59 /* Double+Extension.swift */, + 0EE1BDC829789A62007FEB59 /* UIColor+Extension.swift */, + ); + path = Extension; + sourceTree = ""; + }; + 0E7C212A297419F900882E04 /* Protocol */ = { + isa = PBXGroup; + children = ( + 0E7C212B29741A1400882E04 /* ReusableViewProtocol.swift */, + ); + path = Protocol; + sourceTree = ""; + }; + 0E7C212F2974227C00882E04 /* Component */ = { + isa = PBXGroup; + children = ( + 0E7C21302974236C00882E04 /* MoviesCollectionReusableView.swift */, + ); + path = Component; + sourceTree = ""; + }; + 0E7C2132297434F200882E04 /* App */ = { + isa = PBXGroup; + children = ( + 0E7C210D2973D88300882E04 /* AppDelegate.swift */, + 0E7C210F2973D88300882E04 /* SceneDelegate.swift */, + ); + path = App; + sourceTree = ""; + }; + 0E7C21352974360E00882E04 /* Helper */ = { + isa = PBXGroup; + children = ( + 0E7C21332974351300882E04 /* Constant.swift */, + 0EE1BDDA297A023A007FEB59 /* Helper.swift */, + ); + path = Helper; + sourceTree = ""; + }; + 0E7C21362974367F00882E04 /* Model */ = { + isa = PBXGroup; + children = ( + 0E7C21452974869B00882E04 /* MoviesModel.swift */, + ); + path = Model; + sourceTree = ""; + }; + 0E7C21372974368800882E04 /* View */ = { + isa = PBXGroup; + children = ( + 0E7C21392974369A00882E04 /* Cell */, + 0E7C21112973D88300882E04 /* MoviesViewController.swift */, + ); + path = View; + sourceTree = ""; + }; + 0E7C21382974368D00882E04 /* ViewModel */ = { + isa = PBXGroup; + children = ( + 0E7C2143297483B400882E04 /* MoviesViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 0E7C21392974369A00882E04 /* Cell */ = { + isa = PBXGroup; + children = ( + 0E7C21252974126F00882E04 /* MoviesCollectionViewCell.swift */, + ); + path = Cell; + sourceTree = ""; + }; + 0E7C213A2974385900882E04 /* MovieDetails */ = { + isa = PBXGroup; + children = ( + 0E7C213E297438A400882E04 /* MovieDetailsViewController.swift */, + ); + path = MovieDetails; + sourceTree = ""; + }; + 0EE1BDC129775BDC007FEB59 /* Service */ = { + isa = PBXGroup; + children = ( + 0EE1BDC229775BE9007FEB59 /* ImageService.swift */, + 0E7C214129747CCB00882E04 /* NetworkService.swift */, + ); + path = Service; + sourceTree = ""; + }; + 0EE1BDD22978BE69007FEB59 /* MovieDBTests */ = { + isa = PBXGroup; + children = ( + 0EE1BDD32978BE69007FEB59 /* MovieViewModelTests.swift */, + ); + path = MovieDBTests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 0E7C21092973D88300882E04 /* MovieDB */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0E7C211E2973D88500882E04 /* Build configuration list for PBXNativeTarget "MovieDB" */; + buildPhases = ( + 0E7C21062973D88300882E04 /* Sources */, + 0E7C21072973D88300882E04 /* Frameworks */, + 0E7C21082973D88300882E04 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MovieDB; + packageProductDependencies = ( + 0E7C21232973EF6900882E04 /* SnapKit */, + ); + productName = MovieDB; + productReference = 0E7C210A2973D88300882E04 /* MovieDB.app */; + productType = "com.apple.product-type.application"; + }; + 0EE1BDD02978BE69007FEB59 /* MovieDBTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0EE1BDD72978BE69007FEB59 /* Build configuration list for PBXNativeTarget "MovieDBTests" */; + buildPhases = ( + 0EE1BDCD2978BE69007FEB59 /* Sources */, + 0EE1BDCE2978BE69007FEB59 /* Frameworks */, + 0EE1BDCF2978BE69007FEB59 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 0EE1BDD62978BE69007FEB59 /* PBXTargetDependency */, + ); + name = MovieDBTests; + productName = MovieDBTests; + productReference = 0EE1BDD12978BE69007FEB59 /* MovieDBTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 0E7C21022973D88300882E04 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1400; + LastUpgradeCheck = 1400; + TargetAttributes = { + 0E7C21092973D88300882E04 = { + CreatedOnToolsVersion = 14.0.1; + }; + 0EE1BDD02978BE69007FEB59 = { + CreatedOnToolsVersion = 14.0.1; + TestTargetID = 0E7C21092973D88300882E04; + }; + }; + }; + buildConfigurationList = 0E7C21052973D88300882E04 /* Build configuration list for PBXProject "MovieDB" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 0E7C21012973D88300882E04; + packageReferences = ( + 0E7C21222973EF6900882E04 /* XCRemoteSwiftPackageReference "SnapKit" */, + ); + productRefGroup = 0E7C210B2973D88300882E04 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 0E7C21092973D88300882E04 /* MovieDB */, + 0EE1BDD02978BE69007FEB59 /* MovieDBTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 0E7C21082973D88300882E04 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0E7C211A2973D88500882E04 /* LaunchScreen.storyboard in Resources */, + 0E7C21172973D88500882E04 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0EE1BDCF2978BE69007FEB59 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 0E7C21062973D88300882E04 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0E7C213F297438A400882E04 /* MovieDetailsViewController.swift in Sources */, + 0EE1BDC929789A62007FEB59 /* UIColor+Extension.swift in Sources */, + 0E7C21122973D88300882E04 /* MoviesViewController.swift in Sources */, + 0EE1BDC72978960D007FEB59 /* Double+Extension.swift in Sources */, + 0E4C82C22974A62500D96A83 /* Data+Extension.swift in Sources */, + 0E7C212E29741A4900882E04 /* UICollectionViewCell+Extension.swift in Sources */, + 0E7C21342974351300882E04 /* Constant.swift in Sources */, + 0EE1BDDB297A023A007FEB59 /* Helper.swift in Sources */, + 0E7C214229747CCB00882E04 /* NetworkService.swift in Sources */, + 0E4C82C02974A55600D96A83 /* URL+Extension.swift in Sources */, + 0E7C21292974138E00882E04 /* UICollectionView+Extension.swift in Sources */, + 0E7C210E2973D88300882E04 /* AppDelegate.swift in Sources */, + 0E7C21462974869B00882E04 /* MoviesModel.swift in Sources */, + 0EE1BDC52977814C007FEB59 /* UIActivityIndicatorView+Extension.swift in Sources */, + 0E7C2144297483B400882E04 /* MoviesViewModel.swift in Sources */, + 0E7C21312974236C00882E04 /* MoviesCollectionReusableView.swift in Sources */, + 0E7C21102973D88300882E04 /* SceneDelegate.swift in Sources */, + 0EE1BDC329775BE9007FEB59 /* ImageService.swift in Sources */, + 0E7C21262974126F00882E04 /* MoviesCollectionViewCell.swift in Sources */, + 0E7C212C29741A1400882E04 /* ReusableViewProtocol.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0EE1BDCD2978BE69007FEB59 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0EE1BDD42978BE69007FEB59 /* MovieViewModelTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 0EE1BDD62978BE69007FEB59 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 0E7C21092973D88300882E04 /* MovieDB */; + targetProxy = 0EE1BDD52978BE69007FEB59 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 0E7C21182973D88500882E04 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 0E7C21192973D88500882E04 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 0E7C211C2973D88500882E04 /* 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++20"; + 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 = 16.0; + 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; + }; + 0E7C211D2973D88500882E04 /* 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++20"; + 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 = 16.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 0E7C211F2973D88500882E04 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MovieDB/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + 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 = su.MovieDB; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 0E7C21202973D88500882E04 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MovieDB/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + 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 = su.MovieDB; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 0EE1BDD82978BE69007FEB59 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = su.MovieDBTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MovieDB.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MovieDB"; + }; + name = Debug; + }; + 0EE1BDD92978BE69007FEB59 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = su.MovieDBTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MovieDB.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MovieDB"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 0E7C21052973D88300882E04 /* Build configuration list for PBXProject "MovieDB" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0E7C211C2973D88500882E04 /* Debug */, + 0E7C211D2973D88500882E04 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 0E7C211E2973D88500882E04 /* Build configuration list for PBXNativeTarget "MovieDB" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0E7C211F2973D88500882E04 /* Debug */, + 0E7C21202973D88500882E04 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 0EE1BDD72978BE69007FEB59 /* Build configuration list for PBXNativeTarget "MovieDBTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0EE1BDD82978BE69007FEB59 /* Debug */, + 0EE1BDD92978BE69007FEB59 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 0E7C21222973EF6900882E04 /* XCRemoteSwiftPackageReference "SnapKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/SnapKit/SnapKit.git"; + requirement = { + branch = develop; + kind = branch; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 0E7C21232973EF6900882E04 /* SnapKit */ = { + isa = XCSwiftPackageProductDependency; + package = 0E7C21222973EF6900882E04 /* XCRemoteSwiftPackageReference "SnapKit" */; + productName = SnapKit; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 0E7C21022973D88300882E04 /* Project object */; +} diff --git a/MovieDB.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/MovieDB.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/MovieDB.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/MovieDB.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/MovieDB.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/MovieDB.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/MovieDB.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/MovieDB.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..6790c942 --- /dev/null +++ b/MovieDB.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "snapkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SnapKit/SnapKit.git", + "state" : { + "branch" : "develop", + "revision" : "58320fe80522414bf3a7e24c88123581dc586752" + } + } + ], + "version" : 2 +} diff --git a/MovieDB.xcodeproj/project.xcworkspace/xcuserdata/sinanulusoy.xcuserdatad/UserInterfaceState.xcuserstate b/MovieDB.xcodeproj/project.xcworkspace/xcuserdata/sinanulusoy.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 00000000..d9204d16 Binary files /dev/null and b/MovieDB.xcodeproj/project.xcworkspace/xcuserdata/sinanulusoy.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/MovieDB.xcodeproj/xcuserdata/sinanulusoy.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/MovieDB.xcodeproj/xcuserdata/sinanulusoy.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 00000000..b576b419 --- /dev/null +++ b/MovieDB.xcodeproj/xcuserdata/sinanulusoy.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/MovieDB.xcodeproj/xcuserdata/sinanulusoy.xcuserdatad/xcschemes/xcschememanagement.plist b/MovieDB.xcodeproj/xcuserdata/sinanulusoy.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 00000000..ff38dd85 --- /dev/null +++ b/MovieDB.xcodeproj/xcuserdata/sinanulusoy.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,35 @@ + + + + + SchemeUserState + + MovieDB.xcscheme_^#shared#^_ + + orderHint + 0 + + SnapKitPlayground (Playground) 1.xcscheme + + isShown + + orderHint + 2 + + SnapKitPlayground (Playground) 2.xcscheme + + isShown + + orderHint + 3 + + SnapKitPlayground (Playground).xcscheme + + isShown + + orderHint + 1 + + + + diff --git a/MovieDB/App/AppDelegate.swift b/MovieDB/App/AppDelegate.swift new file mode 100644 index 00000000..ad0ccb1f --- /dev/null +++ b/MovieDB/App/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// MovieDB +// +// Created by Sinan Ulusoy on 15.01.2023. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + 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. + } + + +} + diff --git a/MovieDB/App/SceneDelegate.swift b/MovieDB/App/SceneDelegate.swift new file mode 100644 index 00000000..91ad828b --- /dev/null +++ b/MovieDB/App/SceneDelegate.swift @@ -0,0 +1,58 @@ +// +// SceneDelegate.swift +// MovieDB +// +// Created by Sinan Ulusoy on 15.01.2023. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let windowScene = (scene as? UIWindowScene) else { return } + let window = UIWindow(windowScene: windowScene) + let moviesViewController = MoviesViewController() + let moviesNavigationController = UINavigationController(rootViewController: moviesViewController) + self.window = window + window.rootViewController = moviesNavigationController + window.makeKeyAndVisible() + } + + 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/MovieDB/Assets.xcassets/AccentColor.colorset/Contents.json b/MovieDB/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/MovieDB/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MovieDB/Assets.xcassets/AppIcon.appiconset/Contents.json b/MovieDB/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..13613e3e --- /dev/null +++ b/MovieDB/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MovieDB/Assets.xcassets/Contents.json b/MovieDB/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/MovieDB/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MovieDB/Assets.xcassets/defaultPosterHorizontal.imageset/Contents.json b/MovieDB/Assets.xcassets/defaultPosterHorizontal.imageset/Contents.json new file mode 100644 index 00000000..19574190 --- /dev/null +++ b/MovieDB/Assets.xcassets/defaultPosterHorizontal.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "defaultPosterHorizontal.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MovieDB/Assets.xcassets/defaultPosterHorizontal.imageset/defaultPosterHorizontal.png b/MovieDB/Assets.xcassets/defaultPosterHorizontal.imageset/defaultPosterHorizontal.png new file mode 100644 index 00000000..7fd86b34 Binary files /dev/null and b/MovieDB/Assets.xcassets/defaultPosterHorizontal.imageset/defaultPosterHorizontal.png differ diff --git a/MovieDB/Assets.xcassets/defaultPosterVertical.imageset/Contents.json b/MovieDB/Assets.xcassets/defaultPosterVertical.imageset/Contents.json new file mode 100644 index 00000000..455353af --- /dev/null +++ b/MovieDB/Assets.xcassets/defaultPosterVertical.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "defaultPosterVertical.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MovieDB/Assets.xcassets/defaultPosterVertical.imageset/defaultPosterVertical.png b/MovieDB/Assets.xcassets/defaultPosterVertical.imageset/defaultPosterVertical.png new file mode 100644 index 00000000..d96f1ec1 Binary files /dev/null and b/MovieDB/Assets.xcassets/defaultPosterVertical.imageset/defaultPosterVertical.png differ diff --git a/MovieDB/Base.lproj/LaunchScreen.storyboard b/MovieDB/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/MovieDB/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MovieDB/Component/MoviesCollectionReusableView.swift b/MovieDB/Component/MoviesCollectionReusableView.swift new file mode 100644 index 00000000..fb1dd1b2 --- /dev/null +++ b/MovieDB/Component/MoviesCollectionReusableView.swift @@ -0,0 +1,59 @@ +// +// MoviesCollectionViewHeader.swift +// MovieDB +// +// Created by Sinan Ulusoy on 15.01.2023. +// + +import UIKit +import SnapKit + +final class MoviesCollectionReusableView: UICollectionReusableView { + + static let identifier = String(describing: MoviesCollectionReusableView.self) + private var segmentedControl: UISegmentedControl! + var handleSegmentChange: ((Int) -> Void)? + + override init(frame: CGRect) { + super.init(frame: frame) + + setupView() + setupHierarchy() + setupLayout() + } + + private func setupView() { + backgroundColor = .bg + let titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.text] + let segmentItems = Constant.MoviesCollectionReusableView.segmentItems + segmentedControl = UISegmentedControl(items: segmentItems) + segmentedControl.selectedSegmentIndex = 0 + segmentedControl.addTarget(self, action: #selector(self.segmentedValueChanged(_:)), for: .valueChanged) + segmentedControl.selectedSegmentTintColor = .bg + segmentedControl.setTitleTextAttributes(titleTextAttributes, for: .normal) + segmentedControl.setTitleTextAttributes(titleTextAttributes, for: .selected) + } + + private func setupHierarchy() { + addSubview(segmentedControl) + } + + private func setupLayout() { + segmentedControl.snp.makeConstraints { make in + make.center.equalToSuperview() + make.width.equalTo(350) + } + } + + required init?(coder: NSCoder) { + fatalError() + } +} + + +extension MoviesCollectionReusableView { + + @objc func segmentedValueChanged(_ sender: UISegmentedControl!) { + handleSegmentChange?(sender.selectedSegmentIndex) + } +} diff --git a/MovieDB/Extension/Data+Extension.swift b/MovieDB/Extension/Data+Extension.swift new file mode 100644 index 00000000..8372e6be --- /dev/null +++ b/MovieDB/Extension/Data+Extension.swift @@ -0,0 +1,20 @@ +// +// Data+Extension.swift +// MovieDB +// +// Created by Sinan Ulusoy on 15.01.2023. +// + +import Foundation + +extension Data { + + func decodeData() -> T? { + do { + let decodedData = try JSONDecoder().decode(T.self, from: self) + return decodedData + } catch { + return nil + } + } +} diff --git a/MovieDB/Extension/Double+Extension.swift b/MovieDB/Extension/Double+Extension.swift new file mode 100644 index 00000000..488b4053 --- /dev/null +++ b/MovieDB/Extension/Double+Extension.swift @@ -0,0 +1,16 @@ +// +// Double+Extension.swift +// MovieDB +// +// Created by Sinan Ulusoy on 18.01.2023. +// + +import Foundation + +extension Double { + + func rounded(toPlaces places:Int) -> Double { + let divisor = pow(10.0, Double(places)) + return (self * divisor).rounded() / divisor + } +} diff --git a/MovieDB/Extension/UIActivityIndicatorView+Extension.swift b/MovieDB/Extension/UIActivityIndicatorView+Extension.swift new file mode 100644 index 00000000..cee15c5c --- /dev/null +++ b/MovieDB/Extension/UIActivityIndicatorView+Extension.swift @@ -0,0 +1,23 @@ +// +// UIActivityIndicatorView+Extension.swift +// MovieDB +// +// Created by Sinan Ulusoy on 18.01.2023. +// + +import UIKit + +extension UIActivityIndicatorView { + + func startAnimatingAsync() { + DispatchQueue.main.async { [weak self] in + self?.startAnimating() + } + } + + func stopAnimatingAsync() { + DispatchQueue.main.async { [weak self] in + self?.stopAnimating() + } + } +} diff --git a/MovieDB/Extension/UICollectionView+Extension.swift b/MovieDB/Extension/UICollectionView+Extension.swift new file mode 100644 index 00000000..1f084d01 --- /dev/null +++ b/MovieDB/Extension/UICollectionView+Extension.swift @@ -0,0 +1,28 @@ +// +// UICollectionView+Extension.swift +// MovieDB +// +// Created by Sinan Ulusoy on 15.01.2023. +// + +import UIKit + +extension UICollectionView { + + func reloadDataAsync() { + DispatchQueue.main.async { [weak self] in + self?.reloadData() + } + } + + func register(_ type: UICollectionViewCell.Type) { + register(type, forCellWithReuseIdentifier: type.reuseIdentifier) + } + + func dequeueReusableCell(for indexPath: IndexPath) -> T { + guard let cell = dequeueReusableCell(withReuseIdentifier: T.reuseIdentifier, for: indexPath) as? T else { + fatalError("Unable to Dequeue Reusable CollectionViewCell") + } + return cell + } +} diff --git a/MovieDB/Extension/UICollectionViewCell+Extension.swift b/MovieDB/Extension/UICollectionViewCell+Extension.swift new file mode 100644 index 00000000..578e01de --- /dev/null +++ b/MovieDB/Extension/UICollectionViewCell+Extension.swift @@ -0,0 +1,10 @@ +// +// UICollectionViewCell+Extension.swift +// MovieDB +// +// Created by Sinan Ulusoy on 15.01.2023. +// + +import UIKit + +extension UICollectionViewCell: ReusableViewProtocol {} diff --git a/MovieDB/Extension/UIColor+Extension.swift b/MovieDB/Extension/UIColor+Extension.swift new file mode 100644 index 00000000..8c6131d5 --- /dev/null +++ b/MovieDB/Extension/UIColor+Extension.swift @@ -0,0 +1,43 @@ +// +// UIColor+Extension.swift +// MovieDB +// +// Created by Sinan Ulusoy on 18.01.2023. +// + +import UIKit + +extension UIColor { + + convenience init(red: Int, green: Int, blue: Int, a: CGFloat = 1.0) { + self.init( + red: CGFloat(red) / 255.0, + green: CGFloat(green) / 255.0, + blue: CGFloat(blue) / 255.0, + alpha: a + ) + } + + convenience init(rgb: Int, a: CGFloat = 1.0) { + self.init( + red: (rgb >> 16) & 0xFF, + green: (rgb >> 8) & 0xFF, + blue: rgb & 0xFF, + a: a + ) + } + + class var bg: UIColor { + UIColor(rgb: 0xFFDCA9) + } + + class var bg2: UIColor { + UIColor(rgb: 0xFFD8A9).withAlphaComponent(0.9) + } + + class var text: UIColor { + UIColor(rgb: 0xFF6E31) + } +} + + diff --git a/MovieDB/Extension/URL+Extension.swift b/MovieDB/Extension/URL+Extension.swift new file mode 100644 index 00000000..4bd4f400 --- /dev/null +++ b/MovieDB/Extension/URL+Extension.swift @@ -0,0 +1,47 @@ +// +// URL+Extension.swift +// MovieDB +// +// Created by Sinan Ulusoy on 15.01.2023. +// + +import UIKit + +extension URL { + + typealias ImageResult = Result + + func fetchImage(completion: @escaping (ImageResult) -> Void) { + ImageService.shared.checkCache(from: self) { image in + if let image = image { + completion(.success(image)) + return + } + NetworkService.shared.fetchData(url: self) { imageResponse in + switch imageResponse { + case .success(let data): + let image = ImageService.shared.dataToImage(data: data) + completion(.success(image)) + case .failure(let error): + completion(.failure(error)) + } + } + } + } + + func fetchJsonData(completion: @escaping (Result) -> Void) { + NetworkService.shared.fetchData(url: self) { dataResponse in + switch dataResponse { + case .success(let data): + do { + let res = try JSONDecoder().decode(T.self, from: data) + completion(.success(res)) + } catch { + completion(.failure(.decodeError)) + } + case .failure(let error): + completion(.failure(error)) + } + } + } +} diff --git a/MovieDB/Helper/Constant.swift b/MovieDB/Helper/Constant.swift new file mode 100644 index 00000000..6191caee --- /dev/null +++ b/MovieDB/Helper/Constant.swift @@ -0,0 +1,38 @@ +// +// Constant.swift +// MovieDB +// +// Created by Sinan Ulusoy on 15.01.2023. +// + +import Foundation + +struct Constant { + + struct MoviesCollectionReusableView { + static let segmentItems = [ + "Top Rated", + "Most Popular" + ] + } + + struct MoviesViewController { + static let title = "Movies" + } + + struct Api { + static let key = "d90d3f8fb7f2d89dd5603d1a182fd2ca" + + struct KeyWord { + static let apiKey = "api_key" + static let page = "page" + } + + struct Url { + static let baseMovieUrl = "https://api.themoviedb.org" + static let baseImageUrl = "https://image.tmdb.org" + static let pathMovie = "3/movie" + static let pathImage = "t/p/w500" + } + } +} diff --git a/MovieDB/Helper/Helper.swift b/MovieDB/Helper/Helper.swift new file mode 100644 index 00000000..27883e8e --- /dev/null +++ b/MovieDB/Helper/Helper.swift @@ -0,0 +1,16 @@ +// +// Helper.swift +// MovieDB +// +// Created by Sinan Ulusoy on 19.01.2023. +// + +import UIKit + +final class Helper { + + static func distanceBetween(bottomOf view1: UIView, andTopOf view2: UIView) -> CGFloat { + let frame2 = view1.convert(view2.bounds, from: view2) + return abs(frame2.minY - view1.bounds.maxY) + } +} diff --git a/MovieDB/Info.plist b/MovieDB/Info.plist new file mode 100644 index 00000000..0eb786dc --- /dev/null +++ b/MovieDB/Info.plist @@ -0,0 +1,23 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + + diff --git a/MovieDB/MovieDetails/MovieDetailsViewController.swift b/MovieDB/MovieDetails/MovieDetailsViewController.swift new file mode 100644 index 00000000..398256d0 --- /dev/null +++ b/MovieDB/MovieDetails/MovieDetailsViewController.swift @@ -0,0 +1,163 @@ +// +// MovieDetailsViewController.swift +// MovieDB +// +// Created by Sinan Ulusoy on 15.01.2023. +// + +import UIKit +import SnapKit + +final class MovieDetailsViewController: UIViewController { + + private var movieModel: MovieModel! + private let scrollView = UIScrollView() + private let titleLabel = UILabel() + private let backdropImageView = UIImageView() + private let overviewTextView = UITextView() + private let detailBackgroudView = UIView() + private let releaseDateLabel = UILabel() + private let voteLabel = UILabel() + private let imageActivityIndicatorView = UIActivityIndicatorView() + + convenience init(movieModel: MovieModel) { + self.init() + self.movieModel = movieModel + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupView() + setupHierarchy() + setupLayout() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + let dis = Helper.distanceBetween(bottomOf: overviewTextView, andTopOf: backdropImageView) + scrollView.contentSize.height = dis * 1.2 + scrollView.contentSize.width = view.frame.size.width + } + + private func setupView() { + imageActivityIndicatorView.startAnimatingAsync() + + view.backgroundColor = .bg + title = movieModel.title + navigationController?.navigationBar.prefersLargeTitles = false + navigationController?.navigationBar.tintColor = .text + + scrollView.alwaysBounceVertical = true + scrollView.showsVerticalScrollIndicator = false + scrollView.showsHorizontalScrollIndicator = false + + titleLabel.text = movieModel.title + titleLabel.font = .boldSystemFont(ofSize: 25) + titleLabel.numberOfLines = 0 + titleLabel.textColor = .text + + overviewTextView.text = movieModel.overview + overviewTextView.textColor = .text + overviewTextView.font = .systemFont(ofSize: 12) + overviewTextView.backgroundColor = .bg + overviewTextView.isScrollEnabled = false + overviewTextView.isUserInteractionEnabled = false + overviewTextView.layer.masksToBounds = true + overviewTextView.layer.cornerRadius = 10 + overviewTextView.layer.borderWidth = 1 + overviewTextView.layer.borderColor = UIColor.text.cgColor + + detailBackgroudView.backgroundColor = .bg + detailBackgroudView.layer.masksToBounds = true + detailBackgroudView.layer.cornerRadius = 10 + detailBackgroudView.layer.borderWidth = 1 + detailBackgroudView.layer.borderColor = UIColor.text.cgColor + + releaseDateLabel.text = movieModel.release_date + releaseDateLabel.textColor = .text + + backdropImageView.clipsToBounds = true + backdropImageView.contentMode = .scaleAspectFit + backdropImageView.layer.masksToBounds = true + backdropImageView.layer.cornerRadius = 10 + + voteLabel.textColor = .text + if let vote = movieModel.vote_average { + voteLabel.text = String(describing: vote.rounded(toPlaces: 1)) + " / 10" + } + + guard let imagePath = self.movieModel.backdrop_path, + let imageUrl = URL(string: [ + Constant.Api.Url.baseImageUrl, + Constant.Api.Url.pathImage, + imagePath].joined(separator: "/")) else { + return + } + imageUrl.fetchImage { [weak self] res in + switch res { + case .success(let image): + DispatchQueue.main.async { [weak self] in + self?.backdropImageView.image = image + } + case .failure(_): + self?.backdropImageView.image = UIImage(named: "defaultPosterHorizontal") + } + self?.imageActivityIndicatorView.stopAnimatingAsync() + } + } + + private func setupHierarchy() { + view.addSubview(scrollView) + scrollView.addSubview(backdropImageView) + scrollView.addSubview(overviewTextView) + scrollView.addSubview(detailBackgroudView) + detailBackgroudView.addSubview(releaseDateLabel) + detailBackgroudView.addSubview(voteLabel) + backdropImageView.addSubview(imageActivityIndicatorView) + } + + private func setupLayout() { + scrollView.snp.makeConstraints { make in + make.edges.equalTo(view.safeAreaLayoutGuide) + } + + backdropImageView.snp.makeConstraints { make in + make.top.equalToSuperview().offset(20) + make.leading.equalTo(view.safeAreaLayoutGuide.snp.leading).offset(20) + make.trailing.equalTo(view.safeAreaLayoutGuide.snp.trailing).offset(-20) + make.height.equalToSuperview().multipliedBy(0.3) + } + + imageActivityIndicatorView.snp.makeConstraints { make in + make.center.equalToSuperview() + } + + detailBackgroudView.snp.makeConstraints { make in + make.top.equalTo(backdropImageView.snp.bottom).offset(20) + make.leading.equalTo(view.safeAreaLayoutGuide.snp.leading).offset(20) + make.trailing.equalTo(view.safeAreaLayoutGuide.snp.trailing).offset(-20) + make.height.equalTo(50) + } + + releaseDateLabel.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.leading.equalToSuperview().offset(10) + } + + voteLabel.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.trailing.equalToSuperview().offset(-10) + } + + overviewTextView.snp.makeConstraints { make in + let sizeThatFits = overviewTextView.sizeThatFits( + CGSize(width: view.frame.width, height: CGFloat(MAXFLOAT))) + make.top.equalTo(detailBackgroudView.snp.bottom).offset(20) + make.leading.equalTo(view.safeAreaLayoutGuide.snp.leading).offset(20) + make.trailing.equalTo(view.safeAreaLayoutGuide.snp.trailing).offset(-20) + make.height.equalTo(sizeThatFits) + } + } +} diff --git a/MovieDB/Movies/Model/MoviesModel.swift b/MovieDB/Movies/Model/MoviesModel.swift new file mode 100644 index 00000000..10682801 --- /dev/null +++ b/MovieDB/Movies/Model/MoviesModel.swift @@ -0,0 +1,26 @@ +// +// MoviesModel.swift +// MovieDB +// +// Created by Sinan Ulusoy on 15.01.2023. +// + +import Foundation + +// MARK: - MoviesResponse +struct MoviesModel: Decodable { + let total_pages: Int + let results: [MovieModel] +} + + +// MARK: - Movie +struct MovieModel: Decodable { + let id: Int? + let title: String? + let overview: String? + let release_date: String? + let vote_average: Double? + let poster_path: String? + let backdrop_path: String? +} diff --git a/MovieDB/Movies/View/Cell/MoviesCollectionViewCell.swift b/MovieDB/Movies/View/Cell/MoviesCollectionViewCell.swift new file mode 100644 index 00000000..5340583c --- /dev/null +++ b/MovieDB/Movies/View/Cell/MoviesCollectionViewCell.swift @@ -0,0 +1,117 @@ +// +// MoviesCollectionViewCell.swift +// MovieDB +// +// Created by Sinan Ulusoy on 15.01.2023. +// + +import UIKit +import SnapKit + +protocol MoviesCollectionViewCellProtocol: AnyObject { + func didTapCell() +} + +final class MoviesCollectionViewCell: UICollectionViewCell { + + weak var delegate: MoviesCollectionViewCellProtocol? + + private var movieModel: MovieModel! + private var movieImageView = UIImageView() + private let foregroundView = UIView() + private let titleLabel = UILabel() + private let imageActivityIndicatorView = UIActivityIndicatorView() + + override init(frame: CGRect) { + super.init(frame: frame) + + setupView() + setupHierarchy() + setupLayout() + } + + private func setupView() { + imageActivityIndicatorView.startAnimatingAsync() + + backgroundColor = .bg + layer.masksToBounds = true + layer.cornerRadius = 10 + + movieImageView.layer.masksToBounds = true + movieImageView.layer.cornerRadius = 10 + + foregroundView.backgroundColor = .bg2 + foregroundView.layer.masksToBounds = true + foregroundView.layer.cornerRadius = 10 + + titleLabel.textAlignment = .center + titleLabel.numberOfLines = 0 + titleLabel.textColor = .text + titleLabel.font = .boldSystemFont(ofSize: 10) + } + + private func setupHierarchy() { + contentView.addSubview(movieImageView) + contentView.addSubview(foregroundView) + contentView.addSubview(imageActivityIndicatorView) + foregroundView.addSubview(titleLabel) + } + + private func setupLayout() { + movieImageView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + foregroundView.snp.makeConstraints { make in + make.leading.trailing.bottom.equalToSuperview() + make.height.equalToSuperview().multipliedBy(0.2) + } + + titleLabel.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + imageActivityIndicatorView.snp.makeConstraints { make in + make.center.equalToSuperview() + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + +// MARK: - MoviesCollectionViewCell configuration +extension MoviesCollectionViewCell { + + func configure(movieModel: MovieModel) { + self.movieModel = movieModel + setData() + } + + private func setData() { + self.titleLabel.text = self.movieModel.title + + guard let imagePath = self.movieModel.poster_path, + let imageUrl = URL(string: [ + Constant.Api.Url.baseImageUrl, + Constant.Api.Url.pathImage, + imagePath].joined(separator: "/")) else { + self.movieImageView.image = UIImage(named: "defaultPosterVertical") + imageActivityIndicatorView.stopAnimatingAsync() + return + } + imageUrl.fetchImage { res in + switch res { + case .success(let image): + DispatchQueue.main.async { [weak self] in + self?.movieImageView.image = image + } + case .failure(_): + self.movieImageView.image = UIImage(named: "defaultPosterVertical") + } + self.imageActivityIndicatorView.stopAnimatingAsync() + } + } +} diff --git a/MovieDB/Movies/View/MoviesViewController.swift b/MovieDB/Movies/View/MoviesViewController.swift new file mode 100644 index 00000000..32827511 --- /dev/null +++ b/MovieDB/Movies/View/MoviesViewController.swift @@ -0,0 +1,159 @@ +// +// MoviesViewController.swift +// MovieDB +// +// Created by Sinan Ulusoy on 15.01.2023. +// + +import UIKit +import SnapKit + +class MoviesViewController: UIViewController { + + private let moviesViewModel = MoviesViewModel() + private let moviesCollectionView = UICollectionView( + frame: .zero, + collectionViewLayout: UICollectionViewFlowLayout() + ) + private var movieModelList: [MovieModel] = [] + + override func viewDidLoad() { + super.viewDidLoad() + setup() + setupView() + setupHierarchy() + setupLayout() + } + + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + moviesCollectionView.collectionViewLayout.invalidateLayout() + } + + private func setup() { + moviesViewModel.delegate = self + moviesViewModel.getData() + } + + private func setupView() { + view.backgroundColor = .bg + title = Constant.MoviesViewController.title + navigationController?.navigationBar.prefersLargeTitles = true + navigationController?.navigationBar.barTintColor = .bg + navigationController?.navigationBar.largeTitleTextAttributes = [ + NSAttributedString.Key.foregroundColor: UIColor.text] + navigationController?.navigationBar.titleTextAttributes = [ + NSAttributedString.Key.foregroundColor: UIColor.text] + + moviesCollectionView.backgroundColor = .bg + moviesCollectionView.delegate = self + moviesCollectionView.dataSource = self + moviesCollectionView.register(MoviesCollectionViewCell.self) + moviesCollectionView.register( + MoviesCollectionReusableView.self, + forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, + withReuseIdentifier: MoviesCollectionReusableView.identifier + ) + } + + private func setupHierarchy() { + view.addSubview(moviesCollectionView) + } + + private func setupLayout() { + moviesCollectionView.snp.makeConstraints { make in + make.edges.equalTo(view.safeAreaLayoutGuide) + } + } +} + + +// MARK: - MovieCollectionView delegates +extension MoviesViewController: UICollectionViewDelegate, UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return self.moviesViewModel.movieModelListCount + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell: MoviesCollectionViewCell = collectionView.dequeueReusableCell(for: indexPath) + cell.configure(movieModel: self.movieModelList[indexPath.row]) + return cell + } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let movieDetailsViewController = MovieDetailsViewController(movieModel: movieModelList[indexPath.row]) + show(movieDetailsViewController, sender: self) + } +} + + +// MARK: - MovieCollectionView layout delegates +extension MoviesViewController: UICollectionViewDelegateFlowLayout { + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + return UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + return CGSize(width: collectionView.frame.size.width / 2.5, height: collectionView.frame.size.width / 2) + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + return 20 + } +} + + +// MARK: - MoviesViewController header +extension MoviesViewController { + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { + return CGSize( + width: view.frame.size.width, + height: 50 + ) + } + + func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { + guard let header = collectionView.dequeueReusableSupplementaryView( + ofKind: kind, + withReuseIdentifier: MoviesCollectionReusableView.identifier, + for: indexPath + ) as? MoviesCollectionReusableView else { + return UICollectionReusableView() + } + header.handleSegmentChange = { [weak self] segmentIndex in + self?.moviesViewModel.setSegmentChange(segmentIndex) + self?.moviesViewModel.showMovies() + } + return header + } +} + + +//MARK: - MoviesViewController MoviesViewModelProtocol +extension MoviesViewController: MoviesViewModelProtocol { + + func setMoviesData(movieModelList: [MovieModel]) { + self.movieModelList = movieModelList + } + + func reloadCollectionView() { + moviesCollectionView.reloadDataAsync() + } +} + + +//MARK: - MoviesViewController +extension MoviesViewController: UIScrollViewDelegate { + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + let currentOffset = scrollView.contentOffset.y + let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height + + if maximumOffset - currentOffset <= 50.0 { + moviesViewModel.appendNewMovies() + } + } +} diff --git a/MovieDB/Movies/ViewModel/MoviesViewModel.swift b/MovieDB/Movies/ViewModel/MoviesViewModel.swift new file mode 100644 index 00000000..606d26e9 --- /dev/null +++ b/MovieDB/Movies/ViewModel/MoviesViewModel.swift @@ -0,0 +1,165 @@ +// +// MoviesViewModel.swift +// MovieDB +// +// Created by Sinan Ulusoy on 15.01.2023. +// + +import Foundation + +protocol MoviesViewModelProtocol: AnyObject { + func setMoviesData(movieModelList: [MovieModel]) + func reloadCollectionView() +} + +final class MoviesViewModel { + + private enum SegmentName: Int { + case topRated + case mostPopular + + var value: String { + switch self { + case .topRated: + return "top_rated" + case .mostPopular: + return "popular" + } + } + } + + weak var delegate: MoviesViewModelProtocol? + + private var topRatedMoviesPage = 1 + private var mostPopularMoviesPage = 1 + private var currentSegment: SegmentName = .topRated + + private(set) var topRatedMovieModelList: [MovieModel] = [] { + didSet { + showMovies() + } + } + + private(set) var mostPopularMovieModelList: [MovieModel] = [] { + didSet { + showMovies() + } + } + + var movieModelListCount: Int { + get { + switch currentSegment { + case .topRated: + return topRatedMovieModelList.count + case .mostPopular: + return mostPopularMovieModelList.count + } + } + } + + func getData() { + fetchTopRatedMovies() + fetchMostPopularMovies() + } + + func showMovies() { + switch currentSegment { + case .topRated: + self.delegate?.setMoviesData(movieModelList: topRatedMovieModelList) + case .mostPopular: + self.delegate?.setMoviesData(movieModelList: mostPopularMovieModelList) + } + self.delegate?.reloadCollectionView() + } +} + + +// MARK: - MoviesViewModel new movies +extension MoviesViewModel { + + func appendNewMovies() { + switch currentSegment { + case .topRated: + appendNewTopRatedMovies() + case .mostPopular: + appendNewMostPopularMovies() + } + } + + private func appendNewTopRatedMovies() { + topRatedMoviesPage += 1 + fetchTopRatedMovies() + } + + private func appendNewMostPopularMovies() { + mostPopularMoviesPage += 1 + fetchMostPopularMovies() + } +} + + +// MARK: - MoviesViewModel network functions +extension MoviesViewModel { + + private func fetchTopRatedMovies() { + guard let movieTypeUrl = URL(string: [ + Constant.Api.Url.baseMovieUrl, + Constant.Api.Url.pathMovie, + SegmentName.topRated.value].joined(separator: "/")), + var urlComponents = URLComponents(url: movieTypeUrl, resolvingAgainstBaseURL: false) else { + return + } + var urlQueryItem = [URLQueryItem(name: Constant.Api.KeyWord.apiKey, value: Constant.Api.key)] + urlQueryItem.append(URLQueryItem(name: Constant.Api.KeyWord.page, value: String(describing: topRatedMoviesPage))) + urlComponents.queryItems = urlQueryItem + guard let url = urlComponents.url else { return } + + url.fetchJsonData { [weak self] (result: Result) in + switch result { + case .success(let res): + self?.topRatedMovieModelList.append(contentsOf: res.results) + case .failure(let error): + print(error) + } + } + } + + private func fetchMostPopularMovies() { + guard let movieTypeUrl = URL(string: [ + Constant.Api.Url.baseMovieUrl, + Constant.Api.Url.pathMovie, + SegmentName.mostPopular.value].joined(separator: "/")), + var urlComponents = URLComponents(url: movieTypeUrl, resolvingAgainstBaseURL: false) else { + return + } + var urlQueryItem = [URLQueryItem(name: Constant.Api.KeyWord.apiKey, value: Constant.Api.key)] + urlQueryItem.append(URLQueryItem(name: Constant.Api.KeyWord.page, value: String(describing: mostPopularMoviesPage))) + urlComponents.queryItems = urlQueryItem + guard let url = urlComponents.url else { return } + + url.fetchJsonData { [weak self] (result: Result) in + switch result { + case .success(let res): + self?.mostPopularMovieModelList.append(contentsOf: res.results) + case .failure(let error): + print(error) + } + } + } +} + + +// MARK: - MoviesViewModel segment operations +extension MoviesViewModel { + + func setSegmentChange(_ segmentIndex: Int) { + switch segmentIndex { + case SegmentName.topRated.rawValue: + currentSegment = .topRated + case SegmentName.mostPopular.rawValue: + currentSegment = .mostPopular + default: + return + } + } +} diff --git a/MovieDB/Protocol/ReusableViewProtocol.swift b/MovieDB/Protocol/ReusableViewProtocol.swift new file mode 100644 index 00000000..b2fd767b --- /dev/null +++ b/MovieDB/Protocol/ReusableViewProtocol.swift @@ -0,0 +1,20 @@ +// +// ReusableViewProtocol.swift +// MovieDB +// +// Created by Sinan Ulusoy on 15.01.2023. +// + +import Foundation + +protocol ReusableViewProtocol { + + static var reuseIdentifier: String { get } +} + +extension ReusableViewProtocol { + + static var reuseIdentifier: String { + return String(describing: self) + } +} diff --git a/MovieDB/Service/ImageService.swift b/MovieDB/Service/ImageService.swift new file mode 100644 index 00000000..255ae7ed --- /dev/null +++ b/MovieDB/Service/ImageService.swift @@ -0,0 +1,32 @@ +// +// ImageService.swift +// MovieDB +// +// Created by Sinan Ulusoy on 17.01.2023. +// + +import UIKit + +final class ImageService { + + static let shared = ImageService() + private init() {} + + private let imageCache = NSCache() + + func checkCache(from imageUrl: URL, completion: @escaping (UIImage?) -> Void) { + let cacheKey = NSString(string: imageUrl.absoluteString) + if let image = imageCache.object(forKey: cacheKey) { + completion(image) + return + } + completion(nil) + } + + func dataToImage(data: Data) -> UIImage? { + if let image = UIImage(data: data) { + return image + } + return nil + } +} diff --git a/MovieDB/Service/NetworkService.swift b/MovieDB/Service/NetworkService.swift new file mode 100644 index 00000000..4712616b --- /dev/null +++ b/MovieDB/Service/NetworkService.swift @@ -0,0 +1,39 @@ +// +// NetworkManager.swift +// MovieDB +// +// Created by Sinan Ulusoy on 15.01.2023. +// + +import Foundation + +enum APIError: Error { + case responseFailed + case invalidURL + case invalidData + case decodeError +} + +final class NetworkService { + + static let shared = NetworkService() + private init() {} + + func fetchData(url: URL, completion: @escaping (Result) -> Void) { + URLSession.shared.dataTask(with: url, completionHandler: {(data, response, error) in + if let _ = error { + completion(.failure(.responseFailed)) + return + } + guard let _ = response else { + completion(.failure(.responseFailed)) + return + } + guard let data = data else { + completion(.failure(.invalidData)) + return + } + completion(.success(data)) + }).resume() + } +} diff --git a/MovieDBTests/MovieViewModelTests.swift b/MovieDBTests/MovieViewModelTests.swift new file mode 100644 index 00000000..d29c6531 --- /dev/null +++ b/MovieDBTests/MovieViewModelTests.swift @@ -0,0 +1,20 @@ +// +// MovieDBTests.swift +// MovieDBTests +// +// Created by Sinan Ulusoy on 19.01.2023. +// + +import XCTest +@testable import MovieDB + +final class MovieViewModelTests: XCTestCase { + + let vm = MoviesViewModel() + + func test() { + vm.getData() +// XCTAssertEqual("The Godfather", vm.topRatedMovieModelList[0].title) + } + +}