diff --git a/Daresay Movies/.gitignore b/Daresay Movies/.gitignore new file mode 100644 index 00000000..ba1ff083 --- /dev/null +++ b/Daresay Movies/.gitignore @@ -0,0 +1,90 @@ +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ \ No newline at end of file diff --git a/Daresay Movies/.swiftlint.yml b/Daresay Movies/.swiftlint.yml new file mode 100644 index 00000000..91f8f8b0 --- /dev/null +++ b/Daresay Movies/.swiftlint.yml @@ -0,0 +1,107 @@ +# trailing_whitespace: +# ignores_empty_lines: true +# ignores_comments: true + +# excluded: +# - Pods + +# identifier_name: +# excluded: +# - id + +# line_length: 300 + +# # opt_in_rules: +# # - force_unwrapping + +# # force_cast: warning +# force_try: warning + +# disabled_rules: +# - force_cast +# - redundant_void_return +# - nesting +# - redundant_string_enum_value +# - function_body_length + + +disabled_rules: # rule identifiers to exclude from running + - identifier_name + - nesting + - function_parameter_count + - trailing_whitespace + - empty_count + - function_body_length + - force_cast + - type_body_length + - no_fallthrough_only + - file_length + - unused_optional_binding + - class_delegate_protocol + - is_disjoint + - todo + - redundant_string_enum_value + - block_based_kvo +#- type_name +opt_in_rules: # some rules are only opt-in + - control_statement + - empty_count + - trailing_newline + - colon + - comma +included: # paths to include during linting. `--path` is ignored if present. + - Daresay Movies + - Daresay MoviesTests +excluded: # paths to ignore during linting. Takes precedence over `included`. + - Pods + +# configurable rules can be customized from this configuration file +# binary rules can set their severity level +# force_cast: warning # implicitly. Give warning only for force casting + +# force_try: +# severity: warning # explicitly. Give warning only for force try + +# type_body_length: +# warning: 300 +#error: 400 + +# or they can set both explicitly +# file_length: +# warning: 750 +#error: 800 + +large_tuple: # warn user when using 3 values in tuple, give error if there are 4 + - 3 + - 4 + +line_length: 750 + +# naming rules can set warnings/errors for min_length and max_length +# additionally they can set excluded names +type_name: + min_length: 3 # only warning + max_length: 60 # warning and error + warning: 47 + excluded: iPhone # excluded via string +reporter: "xcode" + +cyclomatic_complexity: + ignores_case_statements: true + warning: 40 + error: 39 + +#custom_rules: +# localizable_usage: # rule identifier +# name: "Localizable Usage" # rule name. optional. +# regex: '@Localizable' # matching pattern +# capture_group: 0 # number of regex capture group to highlight the rule violation at. optional. +# message: "@Localizable is deprecated. You should use: SetareYekStrings Enum" # violation message. optional. +# severity: warning # violation severity. optional. +# +# localizable_usage2: # rule identifier +# name: "Localizable Usage" # rule name. optional. +# regex: 'NSLocalizedString\(' # matching pattern +# capture_group: 0 #number of regex capture group to highlight the rule violation at. optional. +# message: "You should use: SetareYekStrings Enum" # violation message. optional. +# severity: warning # violation severity. optional. diff --git a/Daresay Movies/Daresay Movies.xcodeproj/project.pbxproj b/Daresay Movies/Daresay Movies.xcodeproj/project.pbxproj new file mode 100644 index 00000000..62698dad --- /dev/null +++ b/Daresay Movies/Daresay Movies.xcodeproj/project.pbxproj @@ -0,0 +1,1388 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 6531228A1E3EBB90B2155EBB /* libPods-Daresay Movies.a in Frameworks */ = {isa = PBXBuildFile; fileRef = AFEDA65F33F74A422D79DBF0 /* libPods-Daresay Movies.a */; }; + 7B24DF4E276943460092DEF3 /* FavoritesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B24DF4D276943460092DEF3 /* FavoritesViewController.swift */; }; + 7B24DF50276943810092DEF3 /* Favorites.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7B24DF4F276943810092DEF3 /* Favorites.storyboard */; }; + 7B42E73F2767D0C1004D1473 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7B42E7412767D0C1004D1473 /* Localizable.strings */; }; + 7B42E7492767D2AA004D1473 /* UserDefaultStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B42E7482767D2AA004D1473 /* UserDefaultStorage.swift */; }; + 7B42E74B2767D3A7004D1473 /* UserDefaultData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B42E74A2767D3A7004D1473 /* UserDefaultData.swift */; }; + 7B42E74D2767D8FF004D1473 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B42E74C2767D8FF004D1473 /* main.swift */; }; + 7B42E7502767D957004D1473 /* LanguageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B42E74F2767D957004D1473 /* LanguageManager.swift */; }; + 7B42E7532767DA81004D1473 /* Bundle+SetLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B42E7522767DA81004D1473 /* Bundle+SetLanguage.swift */; }; + 7B42E7552767DBE8004D1473 /* LocalizedStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B42E7542767DBE8004D1473 /* LocalizedStrings.swift */; }; + 7B42E7582767DE37004D1473 /* FlowControlViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B42E7572767DE37004D1473 /* FlowControlViewController.swift */; }; + 7B42E75A2767E238004D1473 /* FlowControl.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7B42E7592767E238004D1473 /* FlowControl.storyboard */; }; + 7B42E7632767E9EE004D1473 /* HomeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B42E7622767E9EE004D1473 /* HomeCoordinator.swift */; }; + 7B42E7672767F34A004D1473 /* HomeMovieCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B42E7662767F34A004D1473 /* HomeMovieCell.swift */; }; + 7B42E76B2767F443004D1473 /* HomeMovieCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7B42E76A2767F443004D1473 /* HomeMovieCell.xib */; }; + 7B42E76E2767F4D3004D1473 /* DaMoviesCollectionViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B42E76D2767F4D3004D1473 /* DaMoviesCollectionViewDataSource.swift */; }; + 7B42E7702767F51E004D1473 /* DaMoviesCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B42E76F2767F51E004D1473 /* DaMoviesCollectionViewCell.swift */; }; + 7B42E7742767F8AB004D1473 /* RequestManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B42E7732767F8AB004D1473 /* RequestManagerProtocol.swift */; }; + 7B42E7772767F8FD004D1473 /* URLRequestLoggableProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B42E7762767F8FD004D1473 /* URLRequestLoggableProtocol.swift */; }; + 7B42E7792767F918004D1473 /* ResponseLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B42E7782767F918004D1473 /* ResponseLog.swift */; }; + 7B42E77C2767F958004D1473 /* ResponseValidatableProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B42E77B2767F958004D1473 /* ResponseValidatableProtocol.swift */; }; + 7B42E77E2767F99B004D1473 /* ResponseValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B42E77D2767F99B004D1473 /* ResponseValidator.swift */; }; + 7B42E7812767F9DD004D1473 /* CacheManagableProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B42E7802767F9DD004D1473 /* CacheManagableProtocol.swift */; }; + 7B42E7832767FA43004D1473 /* CacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B42E7822767FA43004D1473 /* CacheManager.swift */; }; + 7B42E7852767FACC004D1473 /* RequestError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B42E7842767FACC004D1473 /* RequestError.swift */; }; + 7B42E7872767FAEB004D1473 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B42E7862767FAEB004D1473 /* HTTPMethod.swift */; }; + 7B42E7892767FB03004D1473 /* RequestManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B42E7882767FB03004D1473 /* RequestManager.swift */; }; + 7B42E78D2767FB90004D1473 /* Bundle+info.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B42E78C2767FB90004D1473 /* Bundle+info.swift */; }; + 7B42E78F276801F7004D1473 /* HomeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B42E78E276801F7004D1473 /* HomeService.swift */; }; + 7B42E7912768028F004D1473 /* MovieListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B42E7902768028F004D1473 /* MovieListModel.swift */; }; + 7B42E79327680494004D1473 /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B42E79227680494004D1473 /* HomeViewModel.swift */; }; + 7B42E795276804D2004D1473 /* GeneralTypealias.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B42E794276804D2004D1473 /* GeneralTypealias.swift */; }; + 7B4F3F0E276A0F8100951B4D /* URLSessionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4F3F0D276A0F8100951B4D /* URLSessionMock.swift */; }; + 7B4F3F10276A0F9D00951B4D /* URLSessionDataTaskMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4F3F0F276A0F9D00951B4D /* URLSessionDataTaskMock.swift */; }; + 7B4F3F12276A0FC200951B4D /* RequestManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4F3F11276A0FC200951B4D /* RequestManagerMock.swift */; }; + 7B4F3F14276A106100951B4D /* ResponseValidatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4F3F13276A106100951B4D /* ResponseValidatorMock.swift */; }; + 7B4F3F17276A10A000951B4D /* Test_HomeServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4F3F16276A10A000951B4D /* Test_HomeServices.swift */; }; + 7B4F3F19276A198B00951B4D /* Movies.json in Resources */ = {isa = PBXBuildFile; fileRef = 7B4F3F0C276A0ECE00951B4D /* Movies.json */; }; + 7B56CDC92768BCF1009EA164 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B56CDC82768BCF1009EA164 /* Constants.swift */; }; + 7B56CDCC2768C713009EA164 /* LoadingViewable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B56CDCB2768C713009EA164 /* LoadingViewable.swift */; }; + 7B56CDCF2768C7CE009EA164 /* UIImageView+LoadImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B56CDCE2768C7CE009EA164 /* UIImageView+LoadImage.swift */; }; + 7B56CDD12768CDB7009EA164 /* ConfigModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B56CDD02768CDB7009EA164 /* ConfigModel.swift */; }; + 7B56CDD42768D4EF009EA164 /* RequestCallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B56CDD32768D4EF009EA164 /* RequestCallback.swift */; }; + 7B56CDD727690FE3009EA164 /* ImageBaseUrlBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B56CDD627690FE3009EA164 /* ImageBaseUrlBuilder.swift */; }; + 7B56CDDA276922DD009EA164 /* DaMoviesNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B56CDD9276922DD009EA164 /* DaMoviesNavigationController.swift */; }; + 7B56CDDC276923C2009EA164 /* UIView+Radius.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B56CDDB276923C2009EA164 /* UIView+Radius.swift */; }; + 7B56CDE02769319D009EA164 /* FavoriteMoviesHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B56CDDF2769319D009EA164 /* FavoriteMoviesHandler.swift */; }; + 7B655AC427675C690045C710 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B655AC327675C690045C710 /* AppDelegate.swift */; }; + 7B655AC827675C690045C710 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B655AC727675C690045C710 /* HomeViewController.swift */; }; + 7B655ACB27675C690045C710 /* Home.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7B655AC927675C690045C710 /* Home.storyboard */; }; + 7B655ACE27675C690045C710 /* Daresay_Movies.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 7B655ACC27675C690045C710 /* Daresay_Movies.xcdatamodeld */; }; + 7B655AD027675C6B0045C710 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7B655ACF27675C6B0045C710 /* Assets.xcassets */; }; + 7B655AD327675C6B0045C710 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7B655AD127675C6B0045C710 /* LaunchScreen.storyboard */; }; + 7B655AE827675C6B0045C710 /* Daresay_MoviesUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B655AE727675C6B0045C710 /* Daresay_MoviesUITests.swift */; }; + 7B655AEA27675C6B0045C710 /* Daresay_MoviesUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B655AE927675C6B0045C710 /* Daresay_MoviesUITestsLaunchTests.swift */; }; + 7B655AFA276778060045C710 /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B655AF9276778060045C710 /* Coordinator.swift */; }; + 7B655AFD276779040045C710 /* Storyboarded.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B655AFC276779040045C710 /* Storyboarded.swift */; }; + 7B93E24D2767B3A900D5DD07 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93E24C2767B3A900D5DD07 /* AppCoordinator.swift */; }; + 7B93E2502767B47700D5DD07 /* Deeplink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93E24F2767B47700D5DD07 /* Deeplink.swift */; }; + 7BBA59F72769E7540011ED20 /* MovieDetail.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7BBA59F62769E7540011ED20 /* MovieDetail.storyboard */; }; + 7BBA59F92769E76D0011ED20 /* MovieDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBA59F82769E76D0011ED20 /* MovieDetailViewController.swift */; }; + 7BBA59FB2769F4500011ED20 /* UIView+Shadow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBA59FA2769F4500011ED20 /* UIView+Shadow.swift */; }; + 7BBA59FE2769F7B10011ED20 /* FavoriteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBA59FD2769F7B10011ED20 /* FavoriteButton.swift */; }; + 7BBA5A04276A03970011ED20 /* UnitTestError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBA5A03276A03970011ED20 /* UnitTestError.swift */; }; + 7BBA5A06276A05490011ED20 /* Test_AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBA5A05276A05490011ED20 /* Test_AppCoordinator.swift */; }; + 8BC23F020EEEC97A687CBCA6 /* libPods-Daresay Movies-Daresay MoviesUITests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0FC598325C86BD80EB642891 /* libPods-Daresay Movies-Daresay MoviesUITests.a */; }; + D7E22CDA3B0D3AB9EE5C584B /* libPods-Daresay MoviesTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FB21DA4A5DE2AC346CC51C29 /* libPods-Daresay MoviesTests.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 7B655ADA27675C6B0045C710 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7B655AB827675C690045C710 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7B655ABF27675C690045C710; + remoteInfo = "Daresay Movies"; + }; + 7B655AE427675C6B0045C710 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7B655AB827675C690045C710 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7B655ABF27675C690045C710; + remoteInfo = "Daresay Movies"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 0FC598325C86BD80EB642891 /* libPods-Daresay Movies-Daresay MoviesUITests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Daresay Movies-Daresay MoviesUITests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 6A86DAB5A31D669AB61C228D /* Pods-Daresay Movies.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Daresay Movies.debug.xcconfig"; path = "Target Support Files/Pods-Daresay Movies/Pods-Daresay Movies.debug.xcconfig"; sourceTree = ""; }; + 7B24DF4D276943460092DEF3 /* FavoritesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewController.swift; sourceTree = ""; }; + 7B24DF4F276943810092DEF3 /* Favorites.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Favorites.storyboard; sourceTree = ""; }; + 7B42E7402767D0C1004D1473 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + 7B42E7422767D0F1004D1473 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; + 7B42E7432767D0FF004D1473 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; + 7B42E7442767D10B004D1473 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = ""; }; + 7B42E7482767D2AA004D1473 /* UserDefaultStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultStorage.swift; sourceTree = ""; }; + 7B42E74A2767D3A7004D1473 /* UserDefaultData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultData.swift; sourceTree = ""; }; + 7B42E74C2767D8FF004D1473 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + 7B42E74F2767D957004D1473 /* LanguageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageManager.swift; sourceTree = ""; }; + 7B42E7522767DA81004D1473 /* Bundle+SetLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+SetLanguage.swift"; sourceTree = ""; }; + 7B42E7542767DBE8004D1473 /* LocalizedStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedStrings.swift; sourceTree = ""; }; + 7B42E7572767DE37004D1473 /* FlowControlViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowControlViewController.swift; sourceTree = ""; }; + 7B42E7592767E238004D1473 /* FlowControl.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = FlowControl.storyboard; sourceTree = ""; }; + 7B42E7622767E9EE004D1473 /* HomeCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCoordinator.swift; sourceTree = ""; }; + 7B42E7662767F34A004D1473 /* HomeMovieCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeMovieCell.swift; sourceTree = ""; }; + 7B42E76A2767F443004D1473 /* HomeMovieCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HomeMovieCell.xib; sourceTree = ""; }; + 7B42E76D2767F4D3004D1473 /* DaMoviesCollectionViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaMoviesCollectionViewDataSource.swift; sourceTree = ""; }; + 7B42E76F2767F51E004D1473 /* DaMoviesCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaMoviesCollectionViewCell.swift; sourceTree = ""; }; + 7B42E7732767F8AB004D1473 /* RequestManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestManagerProtocol.swift; sourceTree = ""; }; + 7B42E7762767F8FD004D1473 /* URLRequestLoggableProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLRequestLoggableProtocol.swift; sourceTree = ""; }; + 7B42E7782767F918004D1473 /* ResponseLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseLog.swift; sourceTree = ""; }; + 7B42E77B2767F958004D1473 /* ResponseValidatableProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseValidatableProtocol.swift; sourceTree = ""; }; + 7B42E77D2767F99B004D1473 /* ResponseValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseValidator.swift; sourceTree = ""; }; + 7B42E7802767F9DD004D1473 /* CacheManagableProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheManagableProtocol.swift; sourceTree = ""; }; + 7B42E7822767FA43004D1473 /* CacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheManager.swift; sourceTree = ""; }; + 7B42E7842767FACC004D1473 /* RequestError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestError.swift; sourceTree = ""; }; + 7B42E7862767FAEB004D1473 /* HTTPMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; + 7B42E7882767FB03004D1473 /* RequestManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestManager.swift; sourceTree = ""; }; + 7B42E78C2767FB90004D1473 /* Bundle+info.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+info.swift"; sourceTree = ""; }; + 7B42E78E276801F7004D1473 /* HomeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeService.swift; sourceTree = ""; }; + 7B42E7902768028F004D1473 /* MovieListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieListModel.swift; sourceTree = ""; }; + 7B42E79227680494004D1473 /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; + 7B42E794276804D2004D1473 /* GeneralTypealias.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralTypealias.swift; sourceTree = ""; }; + 7B4F3F0C276A0ECE00951B4D /* Movies.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Movies.json; sourceTree = ""; }; + 7B4F3F0D276A0F8100951B4D /* URLSessionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionMock.swift; sourceTree = ""; }; + 7B4F3F0F276A0F9D00951B4D /* URLSessionDataTaskMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDataTaskMock.swift; sourceTree = ""; }; + 7B4F3F11276A0FC200951B4D /* RequestManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestManagerMock.swift; sourceTree = ""; }; + 7B4F3F13276A106100951B4D /* ResponseValidatorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseValidatorMock.swift; sourceTree = ""; }; + 7B4F3F16276A10A000951B4D /* Test_HomeServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Test_HomeServices.swift; sourceTree = ""; }; + 7B56CDC82768BCF1009EA164 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + 7B56CDCB2768C713009EA164 /* LoadingViewable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingViewable.swift; sourceTree = ""; }; + 7B56CDCE2768C7CE009EA164 /* UIImageView+LoadImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+LoadImage.swift"; sourceTree = ""; }; + 7B56CDD02768CDB7009EA164 /* ConfigModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigModel.swift; sourceTree = ""; }; + 7B56CDD32768D4EF009EA164 /* RequestCallback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestCallback.swift; sourceTree = ""; }; + 7B56CDD627690FE3009EA164 /* ImageBaseUrlBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageBaseUrlBuilder.swift; sourceTree = ""; }; + 7B56CDD9276922DD009EA164 /* DaMoviesNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaMoviesNavigationController.swift; sourceTree = ""; }; + 7B56CDDB276923C2009EA164 /* UIView+Radius.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Radius.swift"; sourceTree = ""; }; + 7B56CDDF2769319D009EA164 /* FavoriteMoviesHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteMoviesHandler.swift; sourceTree = ""; }; + 7B655AC027675C690045C710 /* DaMovies Dev.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "DaMovies Dev.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 7B655AC327675C690045C710 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7B655AC727675C690045C710 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; + 7B655ACA27675C690045C710 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Home.storyboard; sourceTree = ""; }; + 7B655ACD27675C690045C710 /* Daresay_Movies.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Daresay_Movies.xcdatamodel; sourceTree = ""; }; + 7B655ACF27675C6B0045C710 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 7B655AD227675C6B0045C710 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 7B655AD427675C6B0045C710 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7B655AD927675C6B0045C710 /* Daresay MoviesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Daresay MoviesTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 7B655AE327675C6B0045C710 /* Daresay MoviesUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Daresay MoviesUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 7B655AE727675C6B0045C710 /* Daresay_MoviesUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Daresay_MoviesUITests.swift; sourceTree = ""; }; + 7B655AE927675C6B0045C710 /* Daresay_MoviesUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Daresay_MoviesUITestsLaunchTests.swift; sourceTree = ""; }; + 7B655AF9276778060045C710 /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; }; + 7B655AFC276779040045C710 /* Storyboarded.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storyboarded.swift; sourceTree = ""; }; + 7B93E24C2767B3A900D5DD07 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; + 7B93E24F2767B47700D5DD07 /* Deeplink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Deeplink.swift; sourceTree = ""; }; + 7B93E2522767B5D800D5DD07 /* Dev.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Dev.xcconfig; sourceTree = ""; }; + 7B93E2532767B5E900D5DD07 /* Production.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Production.xcconfig; sourceTree = ""; }; + 7BBA59F62769E7540011ED20 /* MovieDetail.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = MovieDetail.storyboard; sourceTree = ""; }; + 7BBA59F82769E76D0011ED20 /* MovieDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieDetailViewController.swift; sourceTree = ""; }; + 7BBA59FA2769F4500011ED20 /* UIView+Shadow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Shadow.swift"; sourceTree = ""; }; + 7BBA59FD2769F7B10011ED20 /* FavoriteButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteButton.swift; sourceTree = ""; }; + 7BBA5A03276A03970011ED20 /* UnitTestError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitTestError.swift; sourceTree = ""; }; + 7BBA5A05276A05490011ED20 /* Test_AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Test_AppCoordinator.swift; sourceTree = ""; }; + 7D49A58F839CA1265B438AF1 /* Pods-Daresay Movies-Daresay MoviesUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Daresay Movies-Daresay MoviesUITests.debug.xcconfig"; path = "Target Support Files/Pods-Daresay Movies-Daresay MoviesUITests/Pods-Daresay Movies-Daresay MoviesUITests.debug.xcconfig"; sourceTree = ""; }; + 9147351F64134092812D4963 /* Pods-Daresay MoviesTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Daresay MoviesTests.release.xcconfig"; path = "Target Support Files/Pods-Daresay MoviesTests/Pods-Daresay MoviesTests.release.xcconfig"; sourceTree = ""; }; + A7A6FA2DA7E4D0C034F37DED /* Pods-Daresay Movies.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Daresay Movies.release.xcconfig"; path = "Target Support Files/Pods-Daresay Movies/Pods-Daresay Movies.release.xcconfig"; sourceTree = ""; }; + AFEDA65F33F74A422D79DBF0 /* libPods-Daresay Movies.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Daresay Movies.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + C194457ED3A23648A6825E0A /* Pods-Daresay MoviesTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Daresay MoviesTests.debug.xcconfig"; path = "Target Support Files/Pods-Daresay MoviesTests/Pods-Daresay MoviesTests.debug.xcconfig"; sourceTree = ""; }; + E6047E6ECB3D01699AD85F2B /* Pods-Daresay Movies-Daresay MoviesUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Daresay Movies-Daresay MoviesUITests.release.xcconfig"; path = "Target Support Files/Pods-Daresay Movies-Daresay MoviesUITests/Pods-Daresay Movies-Daresay MoviesUITests.release.xcconfig"; sourceTree = ""; }; + FB21DA4A5DE2AC346CC51C29 /* libPods-Daresay MoviesTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Daresay MoviesTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 7B655ABD27675C690045C710 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6531228A1E3EBB90B2155EBB /* libPods-Daresay Movies.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7B655AD627675C6B0045C710 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D7E22CDA3B0D3AB9EE5C584B /* libPods-Daresay MoviesTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7B655AE027675C6B0045C710 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8BC23F020EEEC97A687CBCA6 /* libPods-Daresay Movies-Daresay MoviesUITests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 7B42E73C2767D081004D1473 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 7B56CDC82768BCF1009EA164 /* Constants.swift */, + 7B655AD127675C6B0045C710 /* LaunchScreen.storyboard */, + 7B655ACF27675C6B0045C710 /* Assets.xcassets */, + 7B42E7412767D0C1004D1473 /* Localizable.strings */, + ); + path = "Supporting Files"; + sourceTree = ""; + }; + 7B42E7472767D20C004D1473 /* Data */ = { + isa = PBXGroup; + children = ( + 7B42E7482767D2AA004D1473 /* UserDefaultStorage.swift */, + 7B42E74A2767D3A7004D1473 /* UserDefaultData.swift */, + ); + path = Data; + sourceTree = ""; + }; + 7B42E74E2767D942004D1473 /* LanguageManager */ = { + isa = PBXGroup; + children = ( + 7B42E74F2767D957004D1473 /* LanguageManager.swift */, + 7B42E7542767DBE8004D1473 /* LocalizedStrings.swift */, + ); + path = LanguageManager; + sourceTree = ""; + }; + 7B42E7512767DA69004D1473 /* Extensions */ = { + isa = PBXGroup; + children = ( + 7B56CDCA2768C703009EA164 /* UIKit */, + 7B42E78A2767FB6A004D1473 /* Foundation */, + ); + path = Extensions; + sourceTree = ""; + }; + 7B42E7562767DE07004D1473 /* FlowControl */ = { + isa = PBXGroup; + children = ( + 7B42E7572767DE37004D1473 /* FlowControlViewController.swift */, + 7B42E7592767E238004D1473 /* FlowControl.storyboard */, + ); + path = FlowControl; + sourceTree = ""; + }; + 7B42E75B2767E368004D1473 /* Scenes */ = { + isa = PBXGroup; + children = ( + 7B42E75C2767E371004D1473 /* HomeScene */, + ); + path = Scenes; + sourceTree = ""; + }; + 7B42E75C2767E371004D1473 /* HomeScene */ = { + isa = PBXGroup; + children = ( + 7B42E7602767E3EA004D1473 /* Coordinator */, + 7B42E7612767E3F2004D1473 /* Service */, + 7B42E7642767F273004D1473 /* HomeView */, + 7BBA59F42769E71F0011ED20 /* MovieDetail */, + 7B56CDDE27693175009EA164 /* FavoritesView */, + ); + path = HomeScene; + sourceTree = ""; + }; + 7B42E75D2767E3D1004D1473 /* View */ = { + isa = PBXGroup; + children = ( + 7B42E7652767F31F004D1473 /* Cell */, + 7B655AC727675C690045C710 /* HomeViewController.swift */, + 7B655AC927675C690045C710 /* Home.storyboard */, + ); + path = View; + sourceTree = ""; + }; + 7B42E75E2767E3DC004D1473 /* ViewModel */ = { + isa = PBXGroup; + children = ( + 7B42E79227680494004D1473 /* HomeViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 7B42E75F2767E3E3004D1473 /* Model */ = { + isa = PBXGroup; + children = ( + 7B42E7902768028F004D1473 /* MovieListModel.swift */, + 7B56CDD02768CDB7009EA164 /* ConfigModel.swift */, + ); + path = Model; + sourceTree = ""; + }; + 7B42E7602767E3EA004D1473 /* Coordinator */ = { + isa = PBXGroup; + children = ( + 7B42E7622767E9EE004D1473 /* HomeCoordinator.swift */, + ); + path = Coordinator; + sourceTree = ""; + }; + 7B42E7612767E3F2004D1473 /* Service */ = { + isa = PBXGroup; + children = ( + 7B42E78E276801F7004D1473 /* HomeService.swift */, + ); + path = Service; + sourceTree = ""; + }; + 7B42E7642767F273004D1473 /* HomeView */ = { + isa = PBXGroup; + children = ( + 7B42E75F2767E3E3004D1473 /* Model */, + 7B42E75E2767E3DC004D1473 /* ViewModel */, + 7B42E75D2767E3D1004D1473 /* View */, + ); + path = HomeView; + sourceTree = ""; + }; + 7B42E7652767F31F004D1473 /* Cell */ = { + isa = PBXGroup; + children = ( + 7B42E7662767F34A004D1473 /* HomeMovieCell.swift */, + 7B42E76A2767F443004D1473 /* HomeMovieCell.xib */, + ); + path = Cell; + sourceTree = ""; + }; + 7B42E76C2767F4BA004D1473 /* CollectionView */ = { + isa = PBXGroup; + children = ( + 7B42E76D2767F4D3004D1473 /* DaMoviesCollectionViewDataSource.swift */, + 7B42E76F2767F51E004D1473 /* DaMoviesCollectionViewCell.swift */, + ); + path = CollectionView; + sourceTree = ""; + }; + 7B42E7712767F853004D1473 /* Network */ = { + isa = PBXGroup; + children = ( + 7B56CDD22768D4CB009EA164 /* Model */, + 7B42E7722767F87C004D1473 /* Protocol */, + 7B42E7752767F8D9004D1473 /* APILogger */, + 7B42E77A2767F932004D1473 /* ResponseValidator */, + 7B42E77F2767F9C5004D1473 /* CacheManager */, + 7B42E7882767FB03004D1473 /* RequestManager.swift */, + 7B42E7842767FACC004D1473 /* RequestError.swift */, + 7B42E7862767FAEB004D1473 /* HTTPMethod.swift */, + ); + path = Network; + sourceTree = ""; + }; + 7B42E7722767F87C004D1473 /* Protocol */ = { + isa = PBXGroup; + children = ( + 7B42E7732767F8AB004D1473 /* RequestManagerProtocol.swift */, + ); + path = Protocol; + sourceTree = ""; + }; + 7B42E7752767F8D9004D1473 /* APILogger */ = { + isa = PBXGroup; + children = ( + 7B42E7762767F8FD004D1473 /* URLRequestLoggableProtocol.swift */, + 7B42E7782767F918004D1473 /* ResponseLog.swift */, + ); + path = APILogger; + sourceTree = ""; + }; + 7B42E77A2767F932004D1473 /* ResponseValidator */ = { + isa = PBXGroup; + children = ( + 7B42E77B2767F958004D1473 /* ResponseValidatableProtocol.swift */, + 7B42E77D2767F99B004D1473 /* ResponseValidator.swift */, + ); + path = ResponseValidator; + sourceTree = ""; + }; + 7B42E77F2767F9C5004D1473 /* CacheManager */ = { + isa = PBXGroup; + children = ( + 7B42E7802767F9DD004D1473 /* CacheManagableProtocol.swift */, + 7B42E7822767FA43004D1473 /* CacheManager.swift */, + ); + path = CacheManager; + sourceTree = ""; + }; + 7B42E78A2767FB6A004D1473 /* Foundation */ = { + isa = PBXGroup; + children = ( + 7B42E78B2767FB7B004D1473 /* Bundle */, + ); + path = Foundation; + sourceTree = ""; + }; + 7B42E78B2767FB7B004D1473 /* Bundle */ = { + isa = PBXGroup; + children = ( + 7B42E7522767DA81004D1473 /* Bundle+SetLanguage.swift */, + 7B42E78C2767FB90004D1473 /* Bundle+info.swift */, + ); + path = Bundle; + sourceTree = ""; + }; + 7B4F3F0B276A0EC200951B4D /* Mocks */ = { + isa = PBXGroup; + children = ( + 7B4F3F0C276A0ECE00951B4D /* Movies.json */, + 7B4F3F0D276A0F8100951B4D /* URLSessionMock.swift */, + 7B4F3F0F276A0F9D00951B4D /* URLSessionDataTaskMock.swift */, + 7B4F3F11276A0FC200951B4D /* RequestManagerMock.swift */, + 7B4F3F13276A106100951B4D /* ResponseValidatorMock.swift */, + ); + path = Mocks; + sourceTree = ""; + }; + 7B4F3F15276A108200951B4D /* HomeSceneTest */ = { + isa = PBXGroup; + children = ( + 7B4F3F16276A10A000951B4D /* Test_HomeServices.swift */, + ); + path = HomeSceneTest; + sourceTree = ""; + }; + 7B56CDCA2768C703009EA164 /* UIKit */ = { + isa = PBXGroup; + children = ( + 7B56CDDD27692471009EA164 /* UIView */, + 7B56CDCD2768C7B7009EA164 /* UIImage */, + 7B56CDCB2768C713009EA164 /* LoadingViewable.swift */, + ); + path = UIKit; + sourceTree = ""; + }; + 7B56CDCD2768C7B7009EA164 /* UIImage */ = { + isa = PBXGroup; + children = ( + 7B56CDCE2768C7CE009EA164 /* UIImageView+LoadImage.swift */, + ); + path = UIImage; + sourceTree = ""; + }; + 7B56CDD22768D4CB009EA164 /* Model */ = { + isa = PBXGroup; + children = ( + 7B56CDD32768D4EF009EA164 /* RequestCallback.swift */, + ); + path = Model; + sourceTree = ""; + }; + 7B56CDD527690FCB009EA164 /* Builders */ = { + isa = PBXGroup; + children = ( + 7B56CDD627690FE3009EA164 /* ImageBaseUrlBuilder.swift */, + ); + path = Builders; + sourceTree = ""; + }; + 7B56CDD8276922AE009EA164 /* NavigationController */ = { + isa = PBXGroup; + children = ( + 7B56CDD9276922DD009EA164 /* DaMoviesNavigationController.swift */, + ); + path = NavigationController; + sourceTree = ""; + }; + 7B56CDDD27692471009EA164 /* UIView */ = { + isa = PBXGroup; + children = ( + 7B56CDDB276923C2009EA164 /* UIView+Radius.swift */, + 7BBA59FA2769F4500011ED20 /* UIView+Shadow.swift */, + ); + path = UIView; + sourceTree = ""; + }; + 7B56CDDE27693175009EA164 /* FavoritesView */ = { + isa = PBXGroup; + children = ( + 7B56CDDF2769319D009EA164 /* FavoriteMoviesHandler.swift */, + 7BBA59F32769E6FC0011ED20 /* View */, + ); + path = FavoritesView; + sourceTree = ""; + }; + 7B655AB727675C690045C710 = { + isa = PBXGroup; + children = ( + 7B655AC227675C690045C710 /* Daresay Movies */, + 7B655ADC27675C6B0045C710 /* Daresay MoviesTests */, + 7B655AE627675C6B0045C710 /* Daresay MoviesUITests */, + 7B655AC127675C690045C710 /* Products */, + B5B394CD48A787858C0717D5 /* Pods */, + FF60DEC173AA24BA1646A615 /* Frameworks */, + ); + sourceTree = ""; + }; + 7B655AC127675C690045C710 /* Products */ = { + isa = PBXGroup; + children = ( + 7B655AC027675C690045C710 /* DaMovies Dev.app */, + 7B655AD927675C6B0045C710 /* Daresay MoviesTests.xctest */, + 7B655AE327675C6B0045C710 /* Daresay MoviesUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 7B655AC227675C690045C710 /* Daresay Movies */ = { + isa = PBXGroup; + children = ( + 7B93E2512767B5B100D5DD07 /* XcodeConfig */, + 7B655AFE27677B1D0045C710 /* App */, + 7B655AF8276777E20045C710 /* UIKitUtilities */, + 7B655AF62767776E0045C710 /* Utilities */, + 7BBA59FC2769F78C0011ED20 /* Components */, + 7B42E75B2767E368004D1473 /* Scenes */, + 7B42E7512767DA69004D1473 /* Extensions */, + 7B42E73C2767D081004D1473 /* Supporting Files */, + 7B655AD427675C6B0045C710 /* Info.plist */, + 7B655ACC27675C690045C710 /* Daresay_Movies.xcdatamodeld */, + ); + path = "Daresay Movies"; + sourceTree = ""; + }; + 7B655ADC27675C6B0045C710 /* Daresay MoviesTests */ = { + isa = PBXGroup; + children = ( + 7B4F3F15276A108200951B4D /* HomeSceneTest */, + 7BBA5A02276A03810011ED20 /* UtilitiesTest */, + 7BBA59FF276A02160011ED20 /* Coordinator */, + ); + path = "Daresay MoviesTests"; + sourceTree = ""; + }; + 7B655AE627675C6B0045C710 /* Daresay MoviesUITests */ = { + isa = PBXGroup; + children = ( + 7B655AE727675C6B0045C710 /* Daresay_MoviesUITests.swift */, + 7B655AE927675C6B0045C710 /* Daresay_MoviesUITestsLaunchTests.swift */, + ); + path = "Daresay MoviesUITests"; + sourceTree = ""; + }; + 7B655AF62767776E0045C710 /* Utilities */ = { + isa = PBXGroup; + children = ( + 7B56CDD527690FCB009EA164 /* Builders */, + 7B42E7712767F853004D1473 /* Network */, + 7B42E74E2767D942004D1473 /* LanguageManager */, + 7B42E7472767D20C004D1473 /* Data */, + 7B42E794276804D2004D1473 /* GeneralTypealias.swift */, + ); + path = Utilities; + sourceTree = ""; + }; + 7B655AF72767778F0045C710 /* Coordinator */ = { + isa = PBXGroup; + children = ( + 7B655AFB276778180045C710 /* Protocols */, + ); + path = Coordinator; + sourceTree = ""; + }; + 7B655AF8276777E20045C710 /* UIKitUtilities */ = { + isa = PBXGroup; + children = ( + 7B56CDD8276922AE009EA164 /* NavigationController */, + 7B42E76C2767F4BA004D1473 /* CollectionView */, + 7B655AF72767778F0045C710 /* Coordinator */, + ); + path = UIKitUtilities; + sourceTree = ""; + }; + 7B655AFB276778180045C710 /* Protocols */ = { + isa = PBXGroup; + children = ( + 7B655AF9276778060045C710 /* Coordinator.swift */, + 7B655AFC276779040045C710 /* Storyboarded.swift */, + ); + path = Protocols; + sourceTree = ""; + }; + 7B655AFE27677B1D0045C710 /* App */ = { + isa = PBXGroup; + children = ( + 7B42E7562767DE07004D1473 /* FlowControl */, + 7B655AFF27677B230045C710 /* Coordinator */, + 7B93E24E2767B46900D5DD07 /* Deeplink */, + 7B655AC327675C690045C710 /* AppDelegate.swift */, + 7B42E74C2767D8FF004D1473 /* main.swift */, + ); + path = App; + sourceTree = ""; + }; + 7B655AFF27677B230045C710 /* Coordinator */ = { + isa = PBXGroup; + children = ( + 7B93E24C2767B3A900D5DD07 /* AppCoordinator.swift */, + ); + path = Coordinator; + sourceTree = ""; + }; + 7B93E24E2767B46900D5DD07 /* Deeplink */ = { + isa = PBXGroup; + children = ( + 7B93E24F2767B47700D5DD07 /* Deeplink.swift */, + ); + path = Deeplink; + sourceTree = ""; + }; + 7B93E2512767B5B100D5DD07 /* XcodeConfig */ = { + isa = PBXGroup; + children = ( + 7B93E2532767B5E900D5DD07 /* Production.xcconfig */, + 7B93E2522767B5D800D5DD07 /* Dev.xcconfig */, + ); + path = XcodeConfig; + sourceTree = ""; + }; + 7BBA59F32769E6FC0011ED20 /* View */ = { + isa = PBXGroup; + children = ( + 7B24DF4D276943460092DEF3 /* FavoritesViewController.swift */, + 7B24DF4F276943810092DEF3 /* Favorites.storyboard */, + ); + path = View; + sourceTree = ""; + }; + 7BBA59F42769E71F0011ED20 /* MovieDetail */ = { + isa = PBXGroup; + children = ( + 7BBA59F52769E7400011ED20 /* View */, + ); + path = MovieDetail; + sourceTree = ""; + }; + 7BBA59F52769E7400011ED20 /* View */ = { + isa = PBXGroup; + children = ( + 7BBA59F62769E7540011ED20 /* MovieDetail.storyboard */, + 7BBA59F82769E76D0011ED20 /* MovieDetailViewController.swift */, + ); + path = View; + sourceTree = ""; + }; + 7BBA59FC2769F78C0011ED20 /* Components */ = { + isa = PBXGroup; + children = ( + 7BBA59FD2769F7B10011ED20 /* FavoriteButton.swift */, + ); + path = Components; + sourceTree = ""; + }; + 7BBA59FF276A02160011ED20 /* Coordinator */ = { + isa = PBXGroup; + children = ( + 7BBA5A05276A05490011ED20 /* Test_AppCoordinator.swift */, + ); + path = Coordinator; + sourceTree = ""; + }; + 7BBA5A02276A03810011ED20 /* UtilitiesTest */ = { + isa = PBXGroup; + children = ( + 7B4F3F0B276A0EC200951B4D /* Mocks */, + 7BBA5A03276A03970011ED20 /* UnitTestError.swift */, + ); + path = UtilitiesTest; + sourceTree = ""; + }; + B5B394CD48A787858C0717D5 /* Pods */ = { + isa = PBXGroup; + children = ( + 6A86DAB5A31D669AB61C228D /* Pods-Daresay Movies.debug.xcconfig */, + A7A6FA2DA7E4D0C034F37DED /* Pods-Daresay Movies.release.xcconfig */, + 7D49A58F839CA1265B438AF1 /* Pods-Daresay Movies-Daresay MoviesUITests.debug.xcconfig */, + E6047E6ECB3D01699AD85F2B /* Pods-Daresay Movies-Daresay MoviesUITests.release.xcconfig */, + C194457ED3A23648A6825E0A /* Pods-Daresay MoviesTests.debug.xcconfig */, + 9147351F64134092812D4963 /* Pods-Daresay MoviesTests.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + FF60DEC173AA24BA1646A615 /* Frameworks */ = { + isa = PBXGroup; + children = ( + AFEDA65F33F74A422D79DBF0 /* libPods-Daresay Movies.a */, + 0FC598325C86BD80EB642891 /* libPods-Daresay Movies-Daresay MoviesUITests.a */, + FB21DA4A5DE2AC346CC51C29 /* libPods-Daresay MoviesTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 7B655ABF27675C690045C710 /* Daresay Movies */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7B655AED27675C6B0045C710 /* Build configuration list for PBXNativeTarget "Daresay Movies" */; + buildPhases = ( + BD551B9EAED8D2306024B50C /* [CP] Check Pods Manifest.lock */, + 7B655ABC27675C690045C710 /* Sources */, + 7B42E73B2767CC97004D1473 /* SwiftLint Script */, + 7B655ABD27675C690045C710 /* Frameworks */, + 7B655ABE27675C690045C710 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Daresay Movies"; + productName = "Daresay Movies"; + productReference = 7B655AC027675C690045C710 /* DaMovies Dev.app */; + productType = "com.apple.product-type.application"; + }; + 7B655AD827675C6B0045C710 /* Daresay MoviesTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7B655AF027675C6B0045C710 /* Build configuration list for PBXNativeTarget "Daresay MoviesTests" */; + buildPhases = ( + 77D66F46EF20DE78FB8D8CE6 /* [CP] Check Pods Manifest.lock */, + 7B655AD527675C6B0045C710 /* Sources */, + 7B655AD627675C6B0045C710 /* Frameworks */, + 7B655AD727675C6B0045C710 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7B655ADB27675C6B0045C710 /* PBXTargetDependency */, + ); + name = "Daresay MoviesTests"; + productName = "Daresay MoviesTests"; + productReference = 7B655AD927675C6B0045C710 /* Daresay MoviesTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 7B655AE227675C6B0045C710 /* Daresay MoviesUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7B655AF327675C6B0045C710 /* Build configuration list for PBXNativeTarget "Daresay MoviesUITests" */; + buildPhases = ( + F3F42CD1977498CBAFEE287C /* [CP] Check Pods Manifest.lock */, + 7B655ADF27675C6B0045C710 /* Sources */, + 7B655AE027675C6B0045C710 /* Frameworks */, + 7B655AE127675C6B0045C710 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7B655AE527675C6B0045C710 /* PBXTargetDependency */, + ); + name = "Daresay MoviesUITests"; + productName = "Daresay MoviesUITests"; + productReference = 7B655AE327675C6B0045C710 /* Daresay MoviesUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7B655AB827675C690045C710 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1300; + LastUpgradeCheck = 1300; + TargetAttributes = { + 7B655ABF27675C690045C710 = { + CreatedOnToolsVersion = 13.0; + }; + 7B655AD827675C6B0045C710 = { + CreatedOnToolsVersion = 13.0; + TestTargetID = 7B655ABF27675C690045C710; + }; + 7B655AE227675C6B0045C710 = { + CreatedOnToolsVersion = 13.0; + TestTargetID = 7B655ABF27675C690045C710; + }; + }; + }; + buildConfigurationList = 7B655ABB27675C690045C710 /* Build configuration list for PBXProject "Daresay Movies" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + de, + fi, + sv, + ); + mainGroup = 7B655AB727675C690045C710; + productRefGroup = 7B655AC127675C690045C710 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 7B655ABF27675C690045C710 /* Daresay Movies */, + 7B655AD827675C6B0045C710 /* Daresay MoviesTests */, + 7B655AE227675C6B0045C710 /* Daresay MoviesUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7B655ABE27675C690045C710 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7BBA59F72769E7540011ED20 /* MovieDetail.storyboard in Resources */, + 7B655AD327675C6B0045C710 /* LaunchScreen.storyboard in Resources */, + 7B42E73F2767D0C1004D1473 /* Localizable.strings in Resources */, + 7B24DF50276943810092DEF3 /* Favorites.storyboard in Resources */, + 7B655AD027675C6B0045C710 /* Assets.xcassets in Resources */, + 7B42E76B2767F443004D1473 /* HomeMovieCell.xib in Resources */, + 7B655ACB27675C690045C710 /* Home.storyboard in Resources */, + 7B42E75A2767E238004D1473 /* FlowControl.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7B655AD727675C6B0045C710 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7B4F3F19276A198B00951B4D /* Movies.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7B655AE127675C6B0045C710 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 77D66F46EF20DE78FB8D8CE6 /* [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-Daresay MoviesTests-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; + }; + 7B42E73B2767CC97004D1473 /* SwiftLint Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "SwiftLint 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"; + }; + BD551B9EAED8D2306024B50C /* [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-Daresay Movies-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; + }; + F3F42CD1977498CBAFEE287C /* [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-Daresay Movies-Daresay MoviesUITests-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 */ + 7B655ABC27675C690045C710 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7B42E7832767FA43004D1473 /* CacheManager.swift in Sources */, + 7B56CDC92768BCF1009EA164 /* Constants.swift in Sources */, + 7B93E24D2767B3A900D5DD07 /* AppCoordinator.swift in Sources */, + 7B56CDD42768D4EF009EA164 /* RequestCallback.swift in Sources */, + 7B56CDCC2768C713009EA164 /* LoadingViewable.swift in Sources */, + 7B42E74B2767D3A7004D1473 /* UserDefaultData.swift in Sources */, + 7B24DF4E276943460092DEF3 /* FavoritesViewController.swift in Sources */, + 7B56CDD12768CDB7009EA164 /* ConfigModel.swift in Sources */, + 7B42E79327680494004D1473 /* HomeViewModel.swift in Sources */, + 7B42E77C2767F958004D1473 /* ResponseValidatableProtocol.swift in Sources */, + 7B42E7632767E9EE004D1473 /* HomeCoordinator.swift in Sources */, + 7B42E7812767F9DD004D1473 /* CacheManagableProtocol.swift in Sources */, + 7B655AFA276778060045C710 /* Coordinator.swift in Sources */, + 7B42E795276804D2004D1473 /* GeneralTypealias.swift in Sources */, + 7B56CDD727690FE3009EA164 /* ImageBaseUrlBuilder.swift in Sources */, + 7B93E2502767B47700D5DD07 /* Deeplink.swift in Sources */, + 7BBA59FB2769F4500011ED20 /* UIView+Shadow.swift in Sources */, + 7B42E7852767FACC004D1473 /* RequestError.swift in Sources */, + 7B42E78D2767FB90004D1473 /* Bundle+info.swift in Sources */, + 7B655AC827675C690045C710 /* HomeViewController.swift in Sources */, + 7B42E7702767F51E004D1473 /* DaMoviesCollectionViewCell.swift in Sources */, + 7BBA59F92769E76D0011ED20 /* MovieDetailViewController.swift in Sources */, + 7B42E7492767D2AA004D1473 /* UserDefaultStorage.swift in Sources */, + 7B42E7772767F8FD004D1473 /* URLRequestLoggableProtocol.swift in Sources */, + 7B56CDDA276922DD009EA164 /* DaMoviesNavigationController.swift in Sources */, + 7B42E7892767FB03004D1473 /* RequestManager.swift in Sources */, + 7B42E7672767F34A004D1473 /* HomeMovieCell.swift in Sources */, + 7B42E7742767F8AB004D1473 /* RequestManagerProtocol.swift in Sources */, + 7B42E74D2767D8FF004D1473 /* main.swift in Sources */, + 7B42E7872767FAEB004D1473 /* HTTPMethod.swift in Sources */, + 7B56CDE02769319D009EA164 /* FavoriteMoviesHandler.swift in Sources */, + 7B42E7502767D957004D1473 /* LanguageManager.swift in Sources */, + 7B42E7532767DA81004D1473 /* Bundle+SetLanguage.swift in Sources */, + 7B42E77E2767F99B004D1473 /* ResponseValidator.swift in Sources */, + 7B42E7552767DBE8004D1473 /* LocalizedStrings.swift in Sources */, + 7B42E7792767F918004D1473 /* ResponseLog.swift in Sources */, + 7B56CDCF2768C7CE009EA164 /* UIImageView+LoadImage.swift in Sources */, + 7B42E78F276801F7004D1473 /* HomeService.swift in Sources */, + 7B655AFD276779040045C710 /* Storyboarded.swift in Sources */, + 7B655AC427675C690045C710 /* AppDelegate.swift in Sources */, + 7B42E7582767DE37004D1473 /* FlowControlViewController.swift in Sources */, + 7B42E7912768028F004D1473 /* MovieListModel.swift in Sources */, + 7B56CDDC276923C2009EA164 /* UIView+Radius.swift in Sources */, + 7B42E76E2767F4D3004D1473 /* DaMoviesCollectionViewDataSource.swift in Sources */, + 7B655ACE27675C690045C710 /* Daresay_Movies.xcdatamodeld in Sources */, + 7BBA59FE2769F7B10011ED20 /* FavoriteButton.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7B655AD527675C6B0045C710 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7B4F3F17276A10A000951B4D /* Test_HomeServices.swift in Sources */, + 7B4F3F12276A0FC200951B4D /* RequestManagerMock.swift in Sources */, + 7B4F3F0E276A0F8100951B4D /* URLSessionMock.swift in Sources */, + 7B4F3F10276A0F9D00951B4D /* URLSessionDataTaskMock.swift in Sources */, + 7BBA5A04276A03970011ED20 /* UnitTestError.swift in Sources */, + 7BBA5A06276A05490011ED20 /* Test_AppCoordinator.swift in Sources */, + 7B4F3F14276A106100951B4D /* ResponseValidatorMock.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7B655ADF27675C6B0045C710 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7B655AE827675C6B0045C710 /* Daresay_MoviesUITests.swift in Sources */, + 7B655AEA27675C6B0045C710 /* Daresay_MoviesUITestsLaunchTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 7B655ADB27675C6B0045C710 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7B655ABF27675C690045C710 /* Daresay Movies */; + targetProxy = 7B655ADA27675C6B0045C710 /* PBXContainerItemProxy */; + }; + 7B655AE527675C6B0045C710 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7B655ABF27675C690045C710 /* Daresay Movies */; + targetProxy = 7B655AE427675C6B0045C710 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 7B42E7412767D0C1004D1473 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 7B42E7402767D0C1004D1473 /* en */, + 7B42E7422767D0F1004D1473 /* de */, + 7B42E7432767D0FF004D1473 /* fi */, + 7B42E7442767D10B004D1473 /* sv */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + 7B655AC927675C690045C710 /* Home.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 7B655ACA27675C690045C710 /* Base */, + ); + name = Home.storyboard; + sourceTree = ""; + }; + 7B655AD127675C6B0045C710 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 7B655AD227675C6B0045C710 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 7B655AEB27675C6B0045C710 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7B93E2522767B5D800D5DD07 /* Dev.xcconfig */; + 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++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 = 13.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"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 7B655AEC27675C6B0045C710 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7B93E2532767B5E900D5DD07 /* Production.xcconfig */; + 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++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 = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 7B655AEE27675C6B0045C710 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6A86DAB5A31D669AB61C228D /* Pods-Daresay Movies.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "$(APP_ICON)"; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = BW7XZA6U32; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Daresay Movies/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"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_ID)"; + PRODUCT_NAME = "$(APP_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 7B655AEF27675C6B0045C710 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A7A6FA2DA7E4D0C034F37DED /* Pods-Daresay Movies.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "$(APP_ICON)"; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = BW7XZA6U32; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "Daresay Movies/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"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_ID)"; + PRODUCT_NAME = "$(APP_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; + 7B655AF127675C6B0045C710 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C194457ED3A23648A6825E0A /* Pods-Daresay MoviesTests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = BW7XZA6U32; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.beyrami.Daresay-MoviesTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DaMovies Dev.app/DaMovies Dev"; + }; + name = Debug; + }; + 7B655AF227675C6B0045C710 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9147351F64134092812D4963 /* Pods-Daresay MoviesTests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = BW7XZA6U32; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.beyrami.Daresay-MoviesTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DaMovies Dev.app/DaMovies Dev"; + }; + name = Release; + }; + 7B655AF427675C6B0045C710 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7D49A58F839CA1265B438AF1 /* Pods-Daresay Movies-Daresay MoviesUITests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = BW7XZA6U32; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.beyrami.Daresay-MoviesUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "Daresay Movies"; + }; + name = Debug; + }; + 7B655AF527675C6B0045C710 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E6047E6ECB3D01699AD85F2B /* Pods-Daresay Movies-Daresay MoviesUITests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = BW7XZA6U32; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.beyrami.Daresay-MoviesUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "Daresay Movies"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 7B655ABB27675C690045C710 /* Build configuration list for PBXProject "Daresay Movies" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7B655AEB27675C6B0045C710 /* Debug */, + 7B655AEC27675C6B0045C710 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7B655AED27675C6B0045C710 /* Build configuration list for PBXNativeTarget "Daresay Movies" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7B655AEE27675C6B0045C710 /* Debug */, + 7B655AEF27675C6B0045C710 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7B655AF027675C6B0045C710 /* Build configuration list for PBXNativeTarget "Daresay MoviesTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7B655AF127675C6B0045C710 /* Debug */, + 7B655AF227675C6B0045C710 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7B655AF327675C6B0045C710 /* Build configuration list for PBXNativeTarget "Daresay MoviesUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7B655AF427675C6B0045C710 /* Debug */, + 7B655AF527675C6B0045C710 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCVersionGroup section */ + 7B655ACC27675C690045C710 /* Daresay_Movies.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + 7B655ACD27675C690045C710 /* Daresay_Movies.xcdatamodel */, + ); + currentVersion = 7B655ACD27675C690045C710 /* Daresay_Movies.xcdatamodel */; + path = Daresay_Movies.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ + }; + rootObject = 7B655AB827675C690045C710 /* Project object */; +} diff --git a/Daresay Movies/Daresay Movies.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Daresay Movies/Daresay Movies.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/Daresay Movies/Daresay Movies.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Daresay Movies/Daresay Movies.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Daresay Movies/Daresay Movies.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/Daresay Movies/Daresay Movies.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Daresay Movies/Daresay Movies.xcodeproj/project.xcworkspace/xcuserdata/emadbayramy.xcuserdatad/UserInterfaceState.xcuserstate b/Daresay Movies/Daresay Movies.xcodeproj/project.xcworkspace/xcuserdata/emadbayramy.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 00000000..b40aa762 Binary files /dev/null and b/Daresay Movies/Daresay Movies.xcodeproj/project.xcworkspace/xcuserdata/emadbayramy.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Daresay Movies/Daresay Movies.xcodeproj/xcuserdata/emadbayramy.xcuserdatad/xcschemes/xcschememanagement.plist b/Daresay Movies/Daresay Movies.xcodeproj/xcuserdata/emadbayramy.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 00000000..9ecf36ac --- /dev/null +++ b/Daresay Movies/Daresay Movies.xcodeproj/xcuserdata/emadbayramy.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,32 @@ + + + + + SchemeUserState + + Daresay Movies.xcscheme_^#shared#^_ + + orderHint + 4 + + + SuppressBuildableAutocreation + + 7B655ABF27675C690045C710 + + primary + + + 7B655AD827675C6B0045C710 + + primary + + + 7B655AE227675C6B0045C710 + + primary + + + + + diff --git a/Daresay Movies/Daresay Movies/App/AppDelegate.swift b/Daresay Movies/Daresay Movies/App/AppDelegate.swift new file mode 100644 index 00000000..2ec44ba0 --- /dev/null +++ b/Daresay Movies/Daresay Movies/App/AppDelegate.swift @@ -0,0 +1,32 @@ +// +// AppDelegate.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/13/21. +// + +import UIKit +import CoreData + +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + var appCoordinator: AppCoordinator! + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + + // I Used Coordinator pattern for flow of app. + setupCoordinator() + + return true + } + + fileprivate func setupCoordinator() { + window = UIWindow(frame: UIScreen.main.bounds) + let navController = DaMoviesNavigationController() + + appCoordinator = AppCoordinator(navigationController: navController, window: window) + appCoordinator.start() + } + +} diff --git a/Daresay Movies/Daresay Movies/App/Coordinator/AppCoordinator.swift b/Daresay Movies/Daresay Movies/App/Coordinator/AppCoordinator.swift new file mode 100644 index 00000000..886238d0 --- /dev/null +++ b/Daresay Movies/Daresay Movies/App/Coordinator/AppCoordinator.swift @@ -0,0 +1,87 @@ +// +// AppCoordinator.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/13/21. +// + +import UIKit + +class AppCoordinator: NSObject, Coordinator { + + let window: UIWindow? + + // Since AppCoordinator is top of all coordinators of our app, it has no parent and is nil. + weak var parentCoordinator: Coordinator? + + // ChildCoordinators of AppCoordinator + var childCoordinators: [Coordinator] = [] + + var navigationController: UINavigationController + + init(navigationController: UINavigationController, window: UIWindow?) { + self.navigationController = navigationController + self.window = window + super.init() + navigationController.delegate = self + } + + // Start of the app + func start(animated: Bool) { + guard let window = window else { return } + + window.rootViewController = navigationController + window.makeKeyAndVisible() + + let flowVC = FlowControlViewController.instantiate(coordinator: self) + navigationController.pushViewController(flowVC, animated: true) + } + + // To Home scene + func toHome() { + let homeScene = HomeCoordinator(navigationController: navigationController) + homeScene.parentCoordinator = self + childCoordinators.append(homeScene) + + homeScene.start() + // we dont need FlowVC in our navigation anymore so we remove it + if let flowVC = navigationController.viewControllers.first as? FlowControlViewController { + self.navigationController.viewControllers.removeAll(where: { $0 == flowVC }) + } + } + + // We need to reset app when User changed app's language. + func userChangedLanguage() { + self.reloadApplication() + } + + // Resetting and removing all childcoordinators of AppCoordinator + private func reloadApplication() { + self.childCoordinators.forEach {$0.navigationController.popToRootViewController(animated: false)} + + self.childCoordinators.removeAll() + + window?.rootViewController?.dismiss(animated: false, completion: nil) + self.navigationController.viewControllers.removeAll() + self.start() + } +} + +extension AppCoordinator: UINavigationControllerDelegate { + func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { + // Read the view controller we’re moving from. + guard let fromViewController = navigationController.transitionCoordinator?.viewController(forKey: .from) else { + return + } + + // Check whether our view controller array already contains that view controller. If it does it means we’re pushing a different view controller on top rather than popping it, so exit. + if navigationController.viewControllers.contains(fromViewController) { + return + } + + // We’re still here – it means we’re popping the view controller, so we can check whether it’s a Home view controller + if let homeViewController = fromViewController as? HomeViewController { + childDidFinish(homeViewController.coordinator) + } + } +} diff --git a/Daresay Movies/Daresay Movies/App/Deeplink/Deeplink.swift b/Daresay Movies/Daresay Movies/App/Deeplink/Deeplink.swift new file mode 100644 index 00000000..e9ebf541 --- /dev/null +++ b/Daresay Movies/Daresay Movies/App/Deeplink/Deeplink.swift @@ -0,0 +1,14 @@ +// +// Deeplink.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/13/21. +// + +import Foundation + +// not used in app, but is an enum for controlling and navigating with deeplinks + +enum DeepLink { + case movie(name: String) +} diff --git a/Daresay Movies/Daresay Movies/App/FlowControl/FlowControl.storyboard b/Daresay Movies/Daresay Movies/App/FlowControl/FlowControl.storyboard new file mode 100644 index 00000000..7e4082ca --- /dev/null +++ b/Daresay Movies/Daresay Movies/App/FlowControl/FlowControl.storyboard @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Daresay Movies/Daresay Movies/App/FlowControl/FlowControlViewController.swift b/Daresay Movies/Daresay Movies/App/FlowControl/FlowControlViewController.swift new file mode 100644 index 00000000..3863a78c --- /dev/null +++ b/Daresay Movies/Daresay Movies/App/FlowControl/FlowControlViewController.swift @@ -0,0 +1,96 @@ +// +// FlowControlViewController.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/13/21. +// + +import UIKit + +class FlowControlViewController: UIViewController, Storyboarded { + + // Not using weak IBOultet because: https://stackoverflow.com/a/31395938 + @IBOutlet var toHomeButton: UIButton! + @IBOutlet var changeLanguageButton: UIButton! + @IBOutlet var noteLabel: UILabel! + + weak var coordinator: AppCoordinator? + + lazy var changeLanguageAlertController: UIAlertController = { + + let alert = UIAlertController(title: LocalizedStrings.changeLanguage.value, message: nil, preferredStyle: .actionSheet) + + let englishLangAlert = UIAlertAction(title: SupportedLanguages.english.text, style: .default) { [weak self] _ in + guard let self = self else { return } + LanguageManager.shared.currentLanguage = .english + self.coordinator?.userChangedLanguage() + } + + let finnishLangAlert = UIAlertAction(title: SupportedLanguages.finnish.text, style: .default) { [weak self] _ in + guard let self = self else { return } + LanguageManager.shared.currentLanguage = .finnish + self.coordinator?.userChangedLanguage() + } + + let germanLangAlert = UIAlertAction(title: SupportedLanguages.german.text, style: .default) { [weak self] _ in + guard let self = self else { return } + LanguageManager.shared.currentLanguage = .german + self.coordinator?.userChangedLanguage() + } + + let swedishLangAlert = UIAlertAction(title: SupportedLanguages.swedish.text, style: .default) { [weak self] _ in + guard let self = self else { return } + LanguageManager.shared.currentLanguage = .swedish + self.coordinator?.userChangedLanguage() + } + + let cancelAction = UIAlertAction(title: LocalizedStrings.cancel.value, style: .cancel) + + alert.addAction(englishLangAlert) + alert.addAction(finnishLangAlert) + alert.addAction(germanLangAlert) + alert.addAction(swedishLangAlert) + alert.addAction(cancelAction) + return alert + }() + + override func viewDidLoad() { + setupView() + } + + func setupView() { + + // toHomeButton + toHomeButton.setTitle(LocalizedStrings.toHome.value, for: .normal) + toHomeButton.setTitleColor(.systemGreen, for: .normal) + toHomeButton.addTarget(self, action: #selector(toHome), for: .touchUpInside) + + // changeLanguageButton + changeLanguageButton.backgroundColor = .white + changeLanguageButton.setTitle(LocalizedStrings.changeLanguage.value, for: .normal) + changeLanguageButton.setTitleColor(.systemGreen, for: .normal) + changeLanguageButton.addTarget(self, action: #selector(changeLanguage), for: .touchUpInside) + + // noteLabel + noteLabel.textAlignment = .center + noteLabel.font = UIFont.boldSystemFont(ofSize: 20) + noteLabel.numberOfLines = 0 + noteLabel.text = LocalizedStrings.guideline.value + "\n Translation is copied from google translate :)" + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + // UINavigationController + self.navigationController?.setNavigationBarHidden(true, animated: animated) + } + + // Navigate to home scene + @objc func toHome() { + coordinator?.toHome() + } + + // Present Change language AlertVC + @objc func changeLanguage() { + self.present(changeLanguageAlertController, animated: true, completion: nil) + } +} diff --git a/Daresay Movies/Daresay Movies/App/main.swift b/Daresay Movies/Daresay Movies/App/main.swift new file mode 100644 index 00000000..050176b6 --- /dev/null +++ b/Daresay Movies/Daresay Movies/App/main.swift @@ -0,0 +1,32 @@ +// +// main.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/13/21. +// + +import UIKit + +// If the user's Language is RTL we set all views Layout directions +class Application: UIApplication, UIApplicationDelegate { + override var userInterfaceLayoutDirection: UIUserInterfaceLayoutDirection { + return LanguageManager.shared.currentLanguage.direction == .ltr ? .leftToRight : .rightToLeft + } +} + +/// This function avoids calls for AppDelegate in UnitTest. +private func delegateClassName() -> String? { + return NSClassFromString("XCTestCase") == nil ? NSStringFromClass(AppDelegate.self) : nil +} + +var currentLanguage = UserDefaultData.currentLanguage +if !LanguageManager.isAValidLanguageIdentifier(currentLanguage) { + currentLanguage = SupportedLanguages.english.identifier +} + +UserDefaultData.appleLanguage = [SupportedLanguages.english.identifier] +LanguageManager.shared.currentLanguage = SupportedLanguages(identifier: currentLanguage) + +let argc = CommandLine.argc +let argv = CommandLine.unsafeArgv + _ = UIApplicationMain(argc, argv, NSStringFromClass(Application.self), delegateClassName()) diff --git a/Daresay Movies/Daresay Movies/Components/FavoriteButton.swift b/Daresay Movies/Daresay Movies/Components/FavoriteButton.swift new file mode 100644 index 00000000..470730cb --- /dev/null +++ b/Daresay Movies/Daresay Movies/Components/FavoriteButton.swift @@ -0,0 +1,38 @@ +// +// FavoriteButton.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/15/21. +// + +import UIKit + +class FavoriteButton: UIButton { + + var isFaved: Bool! = false { + didSet { + self.imageView?.image = isFaved ? favedImage : unfavedImage + } + } + + private let favedImage: UIImage = UIImage(systemName: "heart.fill")! + private let unfavedImage: UIImage = UIImage(systemName: "heart")! + + required init?(coder: NSCoder) { + super.init(coder: coder) + + self.setTitle("", for: .normal) + self.imageView?.image = unfavedImage + self.addTarget(self, action: #selector(favButtonPressed), for: .touchUpInside) + } + + @objc private func favButtonPressed(_ button: UIButton) { + isFaved = !isFaved + isFavBinder(isFaved) + } + + private var isFavBinder: DataCompletion = { _ in } + func bind(completion: @escaping DataCompletion) { + self.isFavBinder = completion + } +} diff --git a/Daresay Movies/Daresay Movies/Daresay_Movies.xcdatamodeld/.xccurrentversion b/Daresay Movies/Daresay Movies/Daresay_Movies.xcdatamodeld/.xccurrentversion new file mode 100644 index 00000000..a4bf9bbf --- /dev/null +++ b/Daresay Movies/Daresay Movies/Daresay_Movies.xcdatamodeld/.xccurrentversion @@ -0,0 +1,8 @@ + + + + + _XCCurrentVersionName + Daresay_Movies.xcdatamodel + + diff --git a/Daresay Movies/Daresay Movies/Daresay_Movies.xcdatamodeld/Daresay_Movies.xcdatamodel/contents b/Daresay Movies/Daresay Movies/Daresay_Movies.xcdatamodeld/Daresay_Movies.xcdatamodel/contents new file mode 100644 index 00000000..50d2514e --- /dev/null +++ b/Daresay Movies/Daresay Movies/Daresay_Movies.xcdatamodeld/Daresay_Movies.xcdatamodel/contents @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Daresay Movies/Daresay Movies/Extensions/Foundation/Bundle/Bundle+SetLanguage.swift b/Daresay Movies/Daresay Movies/Extensions/Foundation/Bundle/Bundle+SetLanguage.swift new file mode 100644 index 00000000..8bbd4b98 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Extensions/Foundation/Bundle/Bundle+SetLanguage.swift @@ -0,0 +1,43 @@ +// +// Bundle+SetLanguage.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/13/21. +// + +import Foundation + +/* + + For changing application's language on fly, its using swizzling to change the main bundle. + + */ + +private var associatedLanguageBundle: Character = "0" + +class PrivateBundle: Bundle { + override func localizedString(forKey key: String, value: String?, table tableName: String?) -> String { + let bundle: Bundle? = objc_getAssociatedObject(self, &associatedLanguageBundle) as? Bundle + return (bundle != nil) ? (bundle!.localizedString(forKey: key, value: value, table: tableName)) : (super.localizedString(forKey: key, value: value, table: tableName)) + } +} + +func NSLocalizedString(_ key: String, comment: String) -> String { + return Foundation.NSLocalizedString(key, comment: comment).replacingOccurrences(of: "\\n", with: "\n") +} + +extension Bundle { + class func setLanguage(_ language: String) { + var onceToken: Int = 0 + + if onceToken == 0 { + object_setClass(Bundle.main, PrivateBundle.self) + } + onceToken = 1 + var bundleName = language + if bundleName == "en" { + bundleName = "Base" + } + objc_setAssociatedObject(Bundle.main, &associatedLanguageBundle, Bundle(path: Bundle.main.path(forResource: bundleName, ofType: "lproj") ?? ""), .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } +} diff --git a/Daresay Movies/Daresay Movies/Extensions/Foundation/Bundle/Bundle+info.swift b/Daresay Movies/Daresay Movies/Extensions/Foundation/Bundle/Bundle+info.swift new file mode 100644 index 00000000..c46c9a4d --- /dev/null +++ b/Daresay Movies/Daresay Movies/Extensions/Foundation/Bundle/Bundle+info.swift @@ -0,0 +1,41 @@ +// +// Bundle+info.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import Foundation + +public struct InfoPlistKey { + static let baseAPIURL = "BaseAPIURL" + static let APIAccessToken = "APIAccessToken" +} + +extension Bundle { + public func load(_ nibName: String = String(describing: T.self), owner: Any? = nil) -> T { + return loadNibNamed(nibName, owner: owner, options: nil)?.first as! T + } + + public func info(for key: String) -> String! { + guard let value = infoDictionary?[key] else { + return nil + } + return (value as! String).replacingOccurrences(of: "\\", with: "") + } + + public func read(fileName: String, type: String) -> String? { + autoreleasepool { + if let path = self.path(forResource: fileName, ofType: type) { + do { + let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) + return String(data: data, encoding: .utf8) + } catch { + return nil + } + } else { + return nil + } + } + } +} diff --git a/Daresay Movies/Daresay Movies/Extensions/UIKit/LoadingViewable.swift b/Daresay Movies/Daresay Movies/Extensions/UIKit/LoadingViewable.swift new file mode 100644 index 00000000..2f1cfa7a --- /dev/null +++ b/Daresay Movies/Daresay Movies/Extensions/UIKit/LoadingViewable.swift @@ -0,0 +1,38 @@ +// +// LoadingViewable.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import UIKit + +protocol LoadingViewable: AnyObject { + func animateActivityIndicator() + func removeActivityIndicator() +} + +extension UIView: LoadingViewable { + + func animateActivityIndicator() { + + let indicatorView = UIActivityIndicatorView() + indicatorView.style = .large + + indicatorView.backgroundColor = UIColor.systemGreen.withAlphaComponent(0.7) + + addSubview(indicatorView) + indicatorView.translatesAutoresizingMaskIntoConstraints = false + + indicatorView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true + indicatorView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true + + indicatorView.restorationIdentifier = "loadingView" + + indicatorView.startAnimating() + } + + func removeActivityIndicator() { + self.subviews.first(where: {$0.restorationIdentifier == "loadingView"})?.removeFromSuperview() + } +} diff --git a/Daresay Movies/Daresay Movies/Extensions/UIKit/UIImage/UIImageView+LoadImage.swift b/Daresay Movies/Daresay Movies/Extensions/UIKit/UIImage/UIImageView+LoadImage.swift new file mode 100644 index 00000000..4b5ae8b2 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Extensions/UIKit/UIImage/UIImageView+LoadImage.swift @@ -0,0 +1,36 @@ +// +// UIImageView+LoadImage.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import UIKit + +// MARK: - Load from URL +extension UIImageView { + + func load(url: URL, placeholder: UIImage? = nil, cache: URLCache? = nil) { + defer { self.removeActivityIndicator() } + self.animateActivityIndicator() + let cache = cache ?? URLCache.shared + let request = URLRequest(url: url) + if let data = cache.cachedResponse(for: request)?.data, let image = UIImage(data: data) { + self.contentMode = .scaleAspectFill + self.image = image + } else { + self.contentMode = .scaleAspectFit + self.image = placeholder + URLSession.shared.dataTask(with: request, completionHandler: { (data, response, _) in + if let data = data, let response = response, ((response as? HTTPURLResponse)?.statusCode ?? 500) < 300, let image = UIImage(data: data) { + let cachedData = CachedURLResponse(response: response, data: data) + cache.storeCachedResponse(cachedData, for: request) + DispatchQueue.main.async { + self.contentMode = .scaleAspectFill + self.image = image + } + } + }).resume() + } + } +} diff --git a/Daresay Movies/Daresay Movies/Extensions/UIKit/UIView/UIView+Radius.swift b/Daresay Movies/Daresay Movies/Extensions/UIKit/UIView/UIView+Radius.swift new file mode 100644 index 00000000..6e4631f1 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Extensions/UIKit/UIView/UIView+Radius.swift @@ -0,0 +1,29 @@ +// +// UIView+Radius.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import UIKit + +// MARK: - corner Radius +extension UIView { + + @IBInspectable + var cornerRadius: CGFloat { + get{ + return layer.cornerRadius + } + set { + setCornerRadius(newValue) + } + } + + func setCornerRadius(_ radius: CGFloat) { + self.clipsToBounds = true + self.layer.masksToBounds = true + self.layer.cornerRadius = radius + self.layoutIfNeeded() + } +} diff --git a/Daresay Movies/Daresay Movies/Extensions/UIKit/UIView/UIView+Shadow.swift b/Daresay Movies/Daresay Movies/Extensions/UIKit/UIView/UIView+Shadow.swift new file mode 100644 index 00000000..77a0503b --- /dev/null +++ b/Daresay Movies/Daresay Movies/Extensions/UIKit/UIView/UIView+Shadow.swift @@ -0,0 +1,19 @@ +// +// UIView+Shadow.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/15/21. +// + +import UIKit + +extension UIView { + func addShadow() { + self.clipsToBounds = false + self.layer.shadowColor = UIColor.black.cgColor + self.layer.shadowOpacity = 1 + self.layer.shadowOffset = CGSize.zero + self.layer.shadowRadius = 10 + self.layer.shadowPath = UIBezierPath(roundedRect: self.bounds, cornerRadius: 10).cgPath + } +} diff --git a/Daresay Movies/Daresay Movies/Info.plist b/Daresay Movies/Daresay Movies/Info.plist new file mode 100644 index 00000000..08997099 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Info.plist @@ -0,0 +1,20 @@ + + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + APIAccessToken + $(API_ACCESS_TOKEN) + BaseAPIURL + $(BASE_API_URL) + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + + diff --git a/Daresay Movies/Daresay Movies/Scenes/HomeScene/Coordinator/HomeCoordinator.swift b/Daresay Movies/Daresay Movies/Scenes/HomeScene/Coordinator/HomeCoordinator.swift new file mode 100644 index 00000000..81652820 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Scenes/HomeScene/Coordinator/HomeCoordinator.swift @@ -0,0 +1,44 @@ +// +// HomeCoordinator.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import UIKit + +class HomeCoordinator: Coordinator { + + var rootViewController: UIViewController? + + weak var parentCoordinator: Coordinator? + + var childCoordinators: [Coordinator] = [] + + var navigationController: UINavigationController + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + + func start(animated: Bool) { + let homeViewController = HomeViewController.instantiate(coordinator: self) + navigationController.pushViewController(homeViewController, animated: animated) + } + + func showFavourites() { + let favListVC = FavoritesViewController.instantiate(coordinator: self) + favListVC.favoriteList = UserDefaultData.favoriteList + navigationController.pushViewController(favListVC, animated: true) + } + + func showDetail(model: MovieModel) { + let detailVC = MovieDetailViewController.instantiate(coordinator: self) + detailVC.movie = model + navigationController.pushViewController(detailVC, animated: true) + } + + deinit { + print("Removed \(self) from memory") + } +} diff --git a/Daresay Movies/Daresay Movies/Scenes/HomeScene/FavoritesView/FavoriteMoviesHandler.swift b/Daresay Movies/Daresay Movies/Scenes/HomeScene/FavoritesView/FavoriteMoviesHandler.swift new file mode 100644 index 00000000..48c26cca --- /dev/null +++ b/Daresay Movies/Daresay Movies/Scenes/HomeScene/FavoritesView/FavoriteMoviesHandler.swift @@ -0,0 +1,31 @@ +// +// FavoriteMoviesHandler.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import Foundation + +protocol MovieFavorable: AnyObject { + func fave(_ model: MovieModel) + func unfave(_ model: MovieModel) +} + +class FavoriteMoviesHandler { + + static let shared: MovieFavorable = FavoriteMoviesHandler() + + func fave(_ model: MovieModel) { + if UserDefaultData.favoriteList.contains(where: {$0 == model}) { return } + UserDefaultData.favoriteList.append(model) + } + + func unfave(_ model: MovieModel) { + var favList = UserDefaultData.favoriteList + favList.removeAll(where: {$0 == model}) + UserDefaultData.favoriteList = favList + } +} + +extension FavoriteMoviesHandler: MovieFavorable { } diff --git a/Daresay Movies/Daresay Movies/Scenes/HomeScene/FavoritesView/View/Favorites.storyboard b/Daresay Movies/Daresay Movies/Scenes/HomeScene/FavoritesView/View/Favorites.storyboard new file mode 100644 index 00000000..78131324 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Scenes/HomeScene/FavoritesView/View/Favorites.storyboard @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Daresay Movies/Daresay Movies/Scenes/HomeScene/FavoritesView/View/FavoritesViewController.swift b/Daresay Movies/Daresay Movies/Scenes/HomeScene/FavoritesView/View/FavoritesViewController.swift new file mode 100644 index 00000000..d0964a70 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Scenes/HomeScene/FavoritesView/View/FavoritesViewController.swift @@ -0,0 +1,65 @@ +// +// FavoritesViewController.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/15/21. +// + +import UIKit + +class FavoritesViewController: UIViewController, Storyboarded { + + // MARK: - Coordinator + weak var coordinator: HomeCoordinator? + + // MARK: - IBOutlets + @IBOutlet var collectionView: UICollectionView! + + // MARK: - Properties + var favoriteList: MovieArrayModel! + private var moviesDataSource: DaMoviesCollectionViewDataSource! + + // MARK: - LifeCycle + override func viewDidLoad() { + super.viewDidLoad() + setupView() + } + + // MARK: - View + private func setupView() { + guard favoriteList != nil else { + assertionFailure("Fill FavoriteList") + return + } + + moviesDataSource = DaMoviesCollectionViewDataSource(items: favoriteList, collectionView: collectionView, delegate: self) + collectionView.dataSource = moviesDataSource + collectionView.delegate = moviesDataSource + collectionView.contentInset.top = 10 + } +} + +// MARK: - DaMoviesCollectionViewDelegate +extension FavoritesViewController: DaMoviesCollectionViewDelegate { + + func collection(willDisplay cellIndexPath: IndexPath, cell: UICollectionViewCell) { + guard let cell = cell as? HomeMovieCell else { return } + cell.delegate = self + cell.index = cellIndexPath.item + } + + func colelction(didSelectModelAt model: T) { + guard let model = model as? MovieModel else { return } + coordinator?.showDetail(model: model) + } + +} + +extension FavoritesViewController: MovieCellDelagate { + func isFaved(_ isFaved: Bool, model: MovieModel, cellIndex: Int) { + print("isFAVED: ====", isFaved, model, cellIndex) + isFaved ? FavoriteMoviesHandler.shared.fave(model) : FavoriteMoviesHandler.shared.unfave(model) + moviesDataSource.items[cellIndex].isFaved = isFaved + moviesDataSource.collectionView.reloadData() + } +} diff --git a/Daresay Movies/Daresay Movies/Scenes/HomeScene/HomeView/Model/ConfigModel.swift b/Daresay Movies/Daresay Movies/Scenes/HomeScene/HomeView/Model/ConfigModel.swift new file mode 100644 index 00000000..a3eae78f --- /dev/null +++ b/Daresay Movies/Daresay Movies/Scenes/HomeScene/HomeView/Model/ConfigModel.swift @@ -0,0 +1,90 @@ +// +// ConfigModel.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import Foundation + +// MARK: - Base +struct ConfigModel: Codable { + var images: ImagesConfigModel? + var changeKeys: [String]? + + enum CodingKeys: String, CodingKey { + case images + case changeKeys = "change_keys" + } +} + +// MARK: - Images +struct ImagesConfigModel: Codable { + var baseURL: String? + var secureBaseURL: String? + var backdropSizes: [String]? + var logoSizes: [String]? + var posterSizes: [String]? + var profileSizes: [String]? + var stillSizes: [String]? + + enum CodingKeys: String, CodingKey { + case baseURL = "base_url" + case secureBaseURL = "secure_base_url" + case backdropSizes = "backdrop_sizes" + case logoSizes = "logo_sizes" + case posterSizes = "poster_sizes" + case profileSizes = "profile_sizes" + case stillSizes = "still_sizes" + } +} + +enum ImageTypes { + case backDrop(BDSize) + case Logo(LogoSizes) + case poster(PosterSizes) + case profile(LogoSizes) + + var sizeString: String { + switch self { + case .backDrop(let bDSize): + return bDSize.rawValue + case .Logo(let logoSizes): + return logoSizes.rawValue + case .poster(let posterSizes): + return posterSizes.rawValue + case .profile(let logoSizes): + return logoSizes.rawValue + } + } +} + + enum Size { + case small + case normal + case original + } + +enum BDSize: String, Codable { + case w300 + case w780 + case w1280 + case original +} + +enum LogoSizes: String, Codable { + case w45 + case w92 + case w154 + case w185 + case w300 + case w500 + case original +} + +enum PosterSizes: String, Codable { + case w342 + case w500 + case w780 + case original +} diff --git a/Daresay Movies/Daresay Movies/Scenes/HomeScene/HomeView/Model/MovieListModel.swift b/Daresay Movies/Daresay Movies/Scenes/HomeScene/HomeView/Model/MovieListModel.swift new file mode 100644 index 00000000..7259df0e --- /dev/null +++ b/Daresay Movies/Daresay Movies/Scenes/HomeScene/HomeView/Model/MovieListModel.swift @@ -0,0 +1,64 @@ +// +// MovieListModel.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import Foundation + +// MARK: - MovieArrayModel +typealias MovieArrayModel = [MovieModel] +// MARK: - MovieModel +struct MovieModel: Codable { + let adult: Bool? + let backdropPath: String? + let genreIDS: [Int]? + let id: Int? + let originalLanguage, originalTitle, overview: String? + let popularity: Double? + let posterPath, releaseDate, title: String? + let video: Bool? + let voteAverage: Double? + let voteCount: Int? + var isFaved: Bool = false + + enum CodingKeys: String, CodingKey { + case adult + case backdropPath = "backdrop_path" + case genreIDS = "genre_ids" + case id + case originalLanguage = "original_language" + case originalTitle = "original_title" + case overview, popularity + case posterPath = "poster_path" + case releaseDate = "release_date" + case title, video + case voteAverage = "vote_average" + case voteCount = "vote_count" + } +} + +// MARK: - Equatable (==) +extension MovieModel: Equatable { + + static func == (lhs: Self, rhs: Self) -> Bool { + return lhs.id == rhs.id + } + +} + +// MARK: - Favorite extensions +extension Sequence where Self == MovieArrayModel { + + func fetchFavorites(from FavList: MovieArrayModel) -> MovieArrayModel { + var movieList = self + let favourites = FavList + for (i, movies) in movieList.enumerated() { + for fav in favourites where movies == fav { + movieList[i].isFaved = true + } + } + return movieList + } +} diff --git a/Daresay Movies/Daresay Movies/Scenes/HomeScene/HomeView/View/Base.lproj/Home.storyboard b/Daresay Movies/Daresay Movies/Scenes/HomeScene/HomeView/View/Base.lproj/Home.storyboard new file mode 100644 index 00000000..6fce7757 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Scenes/HomeScene/HomeView/View/Base.lproj/Home.storyboard @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Daresay Movies/Daresay Movies/Scenes/HomeScene/HomeView/View/Cell/HomeMovieCell.swift b/Daresay Movies/Daresay Movies/Scenes/HomeScene/HomeView/View/Cell/HomeMovieCell.swift new file mode 100644 index 00000000..a7cef556 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Scenes/HomeScene/HomeView/View/Cell/HomeMovieCell.swift @@ -0,0 +1,92 @@ +// +// HomeMovieCell.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import UIKit +protocol MovieCellDelagate: AnyObject { + func isFaved(_ isFaved: Bool, model: MovieModel, cellIndex: Int) +} +class HomeMovieCell: UICollectionViewCell { + + // MARK: - IBOutlets + @IBOutlet var thumbnailImageView: UIImageView! + @IBOutlet var FavBtn: UIButton! + @IBOutlet var titleLabel: UILabel! + + // MARK: - Properties + private lazy var placeHolderImage: UIImage = { + let image = UIImage(systemName: "film")!.withTintColor(.systemGreen, renderingMode: .alwaysTemplate) + return image + }() + private var isFaved: Bool! { + didSet { + model.isFaved = isFaved + FavBtn.imageView?.image = isFaved ? favedImage : unfavedImage + } + } + + private let favedImage: UIImage = UIImage(systemName: "heart.fill")! + private let unfavedImage: UIImage = UIImage(systemName: "heart")! + private var model: MovieModel! + var index: Int! + weak var delegate: MovieCellDelagate? + + // MARK: - LifeCycle + override func awakeFromNib() { + super.awakeFromNib() + initialize() + } + + override func prepareForReuse() { + super.prepareForReuse() + thumbnailImageView.image = nil + titleLabel.text = "" + model = nil + index = nil + } + + // MARK: - setupView + private func initialize() { + titleLabel.backgroundColor = UIColor.systemGreen.withAlphaComponent(0.5) + FavBtn.addTarget(self, action: #selector(favButtonPressed), for: .touchUpInside) + setCornerRadius(15) + } + + @objc private func favButtonPressed(_ button: UIButton) { + isFaved = !isFaved + delegate?.isFaved(isFaved, model: model, cellIndex: index) + } + + // MARK: - Setters and Getters + private func fill(_ model: MovieModel) { + print("🔴 isFaved ", model.isFaved) + self.model = model + isFaved = model.isFaved + titleLabel.text = model.title + if let imageURL = imageURL(model.posterPath) { + thumbnailImageView.load(url: imageURL, placeholder: placeHolderImage) + } + } + + private func imageURL(_ url: String?) -> URL? { + guard let url = url else { return nil } + let urlBuilder = ImageBaseUrlBuilder(forTypeAndSize: .poster(.w342)) + let fullUrl = urlBuilder.createURL(filePath: url) + return fullUrl + } +} + +extension HomeMovieCell: DaMoviesCollectionViewCell { + + func configureCellWith(_ item: MovieModel) { + fill(item) + } + + func configCellSize(item: MovieModel) -> CGSize { + let deviceWidth = UIScreen.main.bounds.width + return CGSize(width: (deviceWidth - 12) / 3, height: 180) + } +} diff --git a/Daresay Movies/Daresay Movies/Scenes/HomeScene/HomeView/View/Cell/HomeMovieCell.xib b/Daresay Movies/Daresay Movies/Scenes/HomeScene/HomeView/View/Cell/HomeMovieCell.xib new file mode 100644 index 00000000..4dac5e4a --- /dev/null +++ b/Daresay Movies/Daresay Movies/Scenes/HomeScene/HomeView/View/Cell/HomeMovieCell.xib @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Daresay Movies/Daresay Movies/Scenes/HomeScene/HomeView/View/HomeViewController.swift b/Daresay Movies/Daresay Movies/Scenes/HomeScene/HomeView/View/HomeViewController.swift new file mode 100644 index 00000000..91702c77 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Scenes/HomeScene/HomeView/View/HomeViewController.swift @@ -0,0 +1,131 @@ +// +// HomeViewController.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/13/21. +// + +import UIKit + +class HomeViewController: UIViewController, Storyboarded { + + // MARK: - Coordinator + weak var coordinator: HomeCoordinator? + + // MARK: - ViewModel + private let homeVM: HomeViewModel = HomeViewModel(homeService: HomeService.shared) + + // MARK: - IBOutlets + @IBOutlet var collectionView: UICollectionView! + + // MARK: - Properties + private lazy var favListBtn: UIBarButtonItem = { + let button = UIBarButtonItem(image: UIImage(systemName: "heart"), + style: .plain, + target: self, + action: #selector(openFavList(_:))) + button.image = UIImage(systemName: "heart") + return button + }() + private var moviesDataSource: DaMoviesCollectionViewDataSource! + + // MARK: - LifeCycle + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + setupView() + setupBindings() + callService() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + navigationController?.setNavigationBarHidden(false, animated: true) + } + + // MARK: - View + private func setupView() { + + self.title = LocalizedStrings.titleName.value + + moviesDataSource = DaMoviesCollectionViewDataSource(items: [], collectionView: collectionView, delegate: self) + collectionView.dataSource = moviesDataSource + collectionView.delegate = moviesDataSource + collectionView.contentInset.top = 10 + + // right bar button + self.navigationItem.setRightBarButton(favListBtn, animated: true) + } + + // AlertVeiw + private func showMessage(title: String = "Error", message: String, buttonTitle: String = "Ok") { + let ac = UIAlertController(title: title, + message: message, + preferredStyle: .alert) + ac.addAction(UIAlertAction(title: buttonTitle, style: .default, handler: nil)) + self.present(ac, animated: true, completion: nil) + } + + // MARK: - Bindings + private func setupBindings() { + + // subscribe to loading + homeVM.loading = { [weak self] isLoading in + guard let self = self else { return } + self.view.isUserInteractionEnabled = !isLoading + isLoading ? self.view.animateActivityIndicator() : self.view.removeActivityIndicator() + } + // subscribe to movies Response + homeVM.list = { [weak self] movies in + guard let self = self else { return } + // Show Images to collectionView + DispatchQueue.main.async { + self.moviesDataSource.refreshWithNewItems(movies) + } + self.homeVM.isPaging = false + } + + // subscribe to Errors + homeVM.errorHandler = { [weak self] error in + guard let self = self else { return } + self.showMessage(message: error) + } + } + + @objc private func openFavList(_ button: UIBarButtonItem) { + coordinator?.showFavourites() + } + + // MARK: - service + private func callService() { + homeVM.getMovies() + } + +} + +// MARK: - DaMoviesCollectionViewDelegate +extension HomeViewController: DaMoviesCollectionViewDelegate { + + func collection(willDisplay cellIndexPath: IndexPath, cell: UICollectionViewCell) { + guard let cell = cell as? HomeMovieCell else { return } + cell.delegate = self + cell.index = cellIndexPath.item + } + + func scrollDidEndDragging(_ scrollView: UIScrollView, willDecelerate: Bool) { + if homeVM.isValidForPaging(scrollView: scrollView) { + homeVM.getNextPageMovies() + } + } + + func colelction(didSelectModelAt model: T) { + guard let model = model as? MovieModel else { return } + coordinator?.showDetail(model: model) + } +} + +extension HomeViewController: MovieCellDelagate { + func isFaved(_ isFaved: Bool, model: MovieModel, cellIndex: Int) { + homeVM.isFaved(isFaved, model: model) + } +} diff --git a/Daresay Movies/Daresay Movies/Scenes/HomeScene/HomeView/ViewModel/HomeViewModel.swift b/Daresay Movies/Daresay Movies/Scenes/HomeScene/HomeView/ViewModel/HomeViewModel.swift new file mode 100644 index 00000000..32fed523 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Scenes/HomeScene/HomeView/ViewModel/HomeViewModel.swift @@ -0,0 +1,131 @@ +// +// HomeViewModel.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import Foundation +import UIKit + +class HomeViewModel { + + // MARK: - initializer + private var homeService: HomeServiceProtocol + init(homeService: HomeServiceProtocol) { + self.homeService = homeService + } + + // MARK: - Publishers + var loading: DataCompletion? + + var list: DataCompletion? + var errorHandler: DataCompletion? + + // MARK: - Properties + private var isFinished = false + var isPaging = false + private var currentPage = 1 + private var totalPages = 1 + + private var moviesCache: MovieArrayModel? + private var allMovies: MovieArrayModel = [] + private var configCache: ConfigModel? + + var dispatchApiGroup: DispatchGroup = DispatchGroup() + + // MARK: - Popular Movies + private func getPopularMovies() { + homeService.getMovies(page: currentPage) { [weak self] result in + guard let self = self else { return } + if self.moviesCache == nil { + self.dispatchApiGroup.leave() + } + switch result { + case .success(let data): + self.totalPages = data.totalPages ?? 1 + guard var movies = data.results else { + assertionFailure("result not found") + return + } + movies = self.manipulateData(model: movies) + self.moviesCache = movies + self.allMovies.append(contentsOf: movies) + self.list?(self.allMovies) + case .failure(let error): + self.errorHandler?(error.localizedDescription) + } + } + } + + // MARK: - Configuration + private func getConfig() { + + homeService.getConfiguration { [weak self] result in + guard let self = self else { return } + if self.configCache == nil { + self.dispatchApiGroup.leave() + } + switch result { + case .success(let config): + self.configCache = config + UserDefaultData.configModel = config + case .failure(let error): + self.errorHandler?(error.localizedDescription) + } + } + } + + // MARK: - Initialize Request + func getMovies() { + dispatchApiGroup.enter() + getPopularMovies() + + dispatchApiGroup.enter() + getConfig() + + self.loading?(true) + // Both Apis are called and has response so show data to user + dispatchApiGroup.notify(queue: .main) { [weak self] in + guard let self = self else { return } + self.loading?(false) + + guard let _ = self.configCache, let movies = self.moviesCache else { + self.errorHandler?(RequestError.serverUnavailable.localizedDescription) + return + } + + self.list?(movies) + } + } + + // MARK: - Paginate Logic Validator + func isValidForPaging(scrollView: UIScrollView) -> Bool { + let currentOffset = scrollView.contentOffset.y + let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height + return (maximumOffset - currentOffset <= 20.0) && !isFinished && !isPaging + } + + func getNextPageMovies() { + isPaging = true + self.currentPage += 1 + if self.currentPage == self.totalPages { + self.isFinished = true + } else { + getPopularMovies() + } + } + + // MARK: - Favourites + // MARK: Data Manipulation + // for Offline/Online Favourite List + private func manipulateData(model: MovieArrayModel) -> MovieArrayModel { + return model.fetchFavorites(from: UserDefaultData.favoriteList) + } + + func isFaved(_ isFaved: Bool, model: MovieModel) { + isFaved ? FavoriteMoviesHandler.shared.fave(model) : FavoriteMoviesHandler.shared.unfave(model) + allMovies = allMovies.fetchFavorites(from: UserDefaultData.favoriteList) + list?(allMovies) + } +} diff --git a/Daresay Movies/Daresay Movies/Scenes/HomeScene/MovieDetail/View/MovieDetail.storyboard b/Daresay Movies/Daresay Movies/Scenes/HomeScene/MovieDetail/View/MovieDetail.storyboard new file mode 100644 index 00000000..1d5cf447 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Scenes/HomeScene/MovieDetail/View/MovieDetail.storyboard @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Daresay Movies/Daresay Movies/Scenes/HomeScene/MovieDetail/View/MovieDetailViewController.swift b/Daresay Movies/Daresay Movies/Scenes/HomeScene/MovieDetail/View/MovieDetailViewController.swift new file mode 100644 index 00000000..aaddf452 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Scenes/HomeScene/MovieDetail/View/MovieDetailViewController.swift @@ -0,0 +1,84 @@ +// +// MovieDetailViewController.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/15/21. +// + +import UIKit + +class MovieDetailViewController: UIViewController, Storyboarded { + + // MARK: - Coordinator + weak var coordinator: HomeCoordinator? + + // MARK: - IBOutlets + @IBOutlet var BGImageView: UIImageView! + @IBOutlet var titleLabel: UILabel! + @IBOutlet var posterImgView: UIImageView! + @IBOutlet var movieDescLabel: UILabel! + @IBOutlet var rateLabel: UILabel! + @IBOutlet var favBtn: UIButton! + @IBOutlet var detailStack: UIStackView! + + // MARK: - Properties + var movie: MovieModel! + private var isFaved: Bool! { + didSet { + movie.isFaved = isFaved + favBtn.imageView?.image = isFaved ? favedImage : unfavedImage + } + } + + private let favedImage: UIImage = UIImage(systemName: "heart.fill")! + private let unfavedImage: UIImage = UIImage(systemName: "heart")! + + // MARK: - LifeCycle + override func viewDidLoad() { + super.viewDidLoad() + setupView() + } + + // MARK: - View + private func setupView() { + guard movie != nil else { + assertionFailure("Fill movie model") + return + } + favBtn.addTarget(self, action: #selector(favButtonPressed), for: .touchUpInside) + detailStack.setCornerRadius(15) + posterImgView.addShadow() + posterImgView.setCornerRadius(10) + fillView() + } + + private func fillView() { + self.title = movie.title + self.titleLabel.text = movie.originalTitle + self.movieDescLabel.text = movie.overview + self.isFaved = movie.isFaved + if let rate = movie.voteAverage { + self.rateLabel.text = "\(String(describing: rate*10))%" + } + + if let img = imageURL(movie.backdropPath, typeAndSize: .backDrop(.w780)) { + BGImageView.load(url: img) + } + + if let img = imageURL(movie.posterPath, typeAndSize: .poster(.w342)) { + posterImgView.load(url: img, placeholder: UIImage(systemName: "film")) + } + } + + private func imageURL(_ url: String?, typeAndSize: ImageTypes) -> URL? { + guard let url = url else { return nil } + let urlBuilder = ImageBaseUrlBuilder(forTypeAndSize: typeAndSize) + let fullUrl = urlBuilder.createURL(filePath: url) + return fullUrl + } + + @objc private func favButtonPressed(_ button: UIButton) { + isFaved = !isFaved + isFaved ? FavoriteMoviesHandler.shared.fave(movie) : FavoriteMoviesHandler.shared.unfave(movie) + } +} diff --git a/Daresay Movies/Daresay Movies/Scenes/HomeScene/Service/HomeService.swift b/Daresay Movies/Daresay Movies/Scenes/HomeScene/Service/HomeService.swift new file mode 100644 index 00000000..10b3db17 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Scenes/HomeScene/Service/HomeService.swift @@ -0,0 +1,71 @@ +// +// HomeService.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import Foundation + +/* + + This is Home Service, responsible for making api calls of getting Movies. + + */ + +typealias MoviesCompletionHandler = (Result, RequestError>) -> Void +typealias ConfigCompletionHandler = (Result) -> Void + +protocol HomeServiceProtocol { + func getConfiguration(completionHandler: @escaping ConfigCompletionHandler) + func getMovies(page: Int, completionHandler: @escaping MoviesCompletionHandler) +} + +/* + HomeEndPoints is URLPath of home Movies Api calls + */ + +private enum HomeEndPoints { + + case popular(Int) + case config + + var path: String { + switch self { + case .popular(let page): + return "movie/popular?page=\(page)" + case .config: + return "configuration" + } + } +} + +class HomeService: HomeServiceProtocol { + + private let requestManager: RequestManagerProtocol + + public static let shared: HomeServiceProtocol = HomeService(requestManager: RequestManager.shared) + + // We can also inject requestManager for testing purposes. + init(requestManager: RequestManagerProtocol) { + self.requestManager = requestManager + } + + func getMovies(page: Int, completionHandler: @escaping MoviesCompletionHandler) { + self.requestManager.performRequestWith(url: HomeEndPoints.popular(page).path, httpMethod: .get) { (result: Result, RequestError>) in + // Taking Data to main thread so we can update UI. + DispatchQueue.main.async { + completionHandler(result) + } + } + } + + func getConfiguration(completionHandler: @escaping ConfigCompletionHandler) { + self.requestManager.performRequestWith(url: HomeEndPoints.config.path, httpMethod: .get) { (result: Result) in + // Taking Data to main thread so we can update UI. + DispatchQueue.main.async { + completionHandler(result) + } + } + } +} diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AccentColor.colorset/Contents.json b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/1024.png b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/1024.png new file mode 100644 index 00000000..0d9516ae Binary files /dev/null and b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/1024.png differ diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/114.png b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/114.png new file mode 100644 index 00000000..16894feb Binary files /dev/null and b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/114.png differ diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/120.png b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/120.png new file mode 100644 index 00000000..64e6b0c4 Binary files /dev/null and b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/120.png differ diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/180.png b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/180.png new file mode 100644 index 00000000..9803759f Binary files /dev/null and b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/180.png differ diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/29.png b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/29.png new file mode 100644 index 00000000..599c2211 Binary files /dev/null and b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/29.png differ diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/40.png b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/40.png new file mode 100644 index 00000000..c8a511a0 Binary files /dev/null and b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/40.png differ diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/57.png b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/57.png new file mode 100644 index 00000000..14f4c06b Binary files /dev/null and b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/57.png differ diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/58.png b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/58.png new file mode 100644 index 00000000..94396fd9 Binary files /dev/null and b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/58.png differ diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/60.png b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/60.png new file mode 100644 index 00000000..ce701220 Binary files /dev/null and b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/60.png differ diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/80.png b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/80.png new file mode 100644 index 00000000..5c1cd273 Binary files /dev/null and b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/80.png differ diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/87.png b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/87.png new file mode 100644 index 00000000..cec5fb45 Binary files /dev/null and b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/87.png differ diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/Contents.json b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/Contents.json new file mode 100644 index 00000000..af727e0c --- /dev/null +++ b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon-dev.appiconset/Contents.json @@ -0,0 +1,80 @@ +{ + "images" : [ + { + "filename" : "40.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "60.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "29.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "58.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "87.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "80.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "120.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "57.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "57x57" + }, + { + "filename" : "114.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "57x57" + }, + { + "filename" : "120.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "180.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "1024.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/1024.png b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 00000000..c864a69d Binary files /dev/null and b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/1024.png differ diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/114.png b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/114.png new file mode 100644 index 00000000..22170f64 Binary files /dev/null and b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/114.png differ diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/120.png b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 00000000..bf8f6145 Binary files /dev/null and b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/120.png differ diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/180.png b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 00000000..94dc5b15 Binary files /dev/null and b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/180.png differ diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/29.png b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/29.png new file mode 100644 index 00000000..fdae4f69 Binary files /dev/null and b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/29.png differ diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/40.png b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 00000000..ed80429d Binary files /dev/null and b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/40.png differ diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/57.png b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/57.png new file mode 100644 index 00000000..8b927eb1 Binary files /dev/null and b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/57.png differ diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/58.png b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 00000000..9103cd9d Binary files /dev/null and b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/58.png differ diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/60.png b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 00000000..67a74efd Binary files /dev/null and b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/60.png differ diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/80.png b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 00000000..d12d387b Binary files /dev/null and b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/80.png differ diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/87.png b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/87.png new file mode 100644 index 00000000..68e244bf Binary files /dev/null and b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/87.png differ diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..af727e0c --- /dev/null +++ b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,80 @@ +{ + "images" : [ + { + "filename" : "40.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "60.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "29.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "58.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "87.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "80.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "120.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "57.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "57x57" + }, + { + "filename" : "114.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "57x57" + }, + { + "filename" : "120.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "180.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "1024.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/Contents.json b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Supporting Files/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Base.lproj/LaunchScreen.storyboard b/Daresay Movies/Daresay Movies/Supporting Files/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Supporting Files/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Daresay Movies/Daresay Movies/Supporting Files/Constants.swift b/Daresay Movies/Daresay Movies/Supporting Files/Constants.swift new file mode 100644 index 00000000..c23a370c --- /dev/null +++ b/Daresay Movies/Daresay Movies/Supporting Files/Constants.swift @@ -0,0 +1,12 @@ +// +// Constants.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import Foundation + +class Constants { + static let accessToken = "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIwOGE2Zjk0NjE4NzA5MDE2NWM5YzBiZWQ4ZDdjNDg1ZSIsInN1YiI6IjU5MjU0Y2ZmOTI1MTQxM2I1YjAwZDk0MCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.FEmg_yklYNh83rqbGb6UOJJJkuCp0YENYOlmm2S5WLE" +} diff --git a/Daresay Movies/Daresay Movies/Supporting Files/de.lproj/Localizable.strings b/Daresay Movies/Daresay Movies/Supporting Files/de.lproj/Localizable.strings new file mode 100644 index 00000000..086cb960 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Supporting Files/de.lproj/Localizable.strings @@ -0,0 +1,14 @@ +/* + Localizable.strings + Daresay Movies + + Created by Emad Bayramy on 12/13/21. + +*/ +"titleName" = "DaMovies"; + +"popularMovies" = "Beliebte Filme"; +"Change_Language" = "Sprache ändern"; +"cancel" = "abbrechen"; +"to_home" = "Nach Hause"; +"guideline" = "Auf dieser Seite können Sie entweder die Sprache ändern oder zur Home-Ansicht wechseln"; diff --git a/Daresay Movies/Daresay Movies/Supporting Files/en.lproj/Localizable.strings b/Daresay Movies/Daresay Movies/Supporting Files/en.lproj/Localizable.strings new file mode 100644 index 00000000..4f4355bc --- /dev/null +++ b/Daresay Movies/Daresay Movies/Supporting Files/en.lproj/Localizable.strings @@ -0,0 +1,14 @@ +/* + Localizable.strings + Daresay Movies + + Created by Emad Bayramy on 12/13/21. + +*/ + +"titleName" = "DaMovies"; +"popularMovies" = "Popular Movies"; +"Change_Language" = "Change Language"; +"cancel" = "cancel"; +"to_home" = "to home"; +"guideline" = "In this page, you can either change the language or proceed to the home view"; diff --git a/Daresay Movies/Daresay Movies/Supporting Files/fi.lproj/Localizable.strings b/Daresay Movies/Daresay Movies/Supporting Files/fi.lproj/Localizable.strings new file mode 100644 index 00000000..650ffba2 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Supporting Files/fi.lproj/Localizable.strings @@ -0,0 +1,14 @@ +/* + Localizable.strings + Daresay Movies + + Created by Emad Bayramy on 12/13/21. + +*/ +"titleName" = "DaMovies"; + +"popularMovies" = "suosittuja elokuvia"; +"Change_Language" = "Vaihda kieli"; +"cancel" = "peruuta"; +"to_home" = "Kotiin"; +"guideline" = "Tällä sivulla voit joko vaihtaa kieltä tai siirtyä aloitusnäkymään"; diff --git a/Daresay Movies/Daresay Movies/Supporting Files/sv.lproj/Localizable.strings b/Daresay Movies/Daresay Movies/Supporting Files/sv.lproj/Localizable.strings new file mode 100644 index 00000000..74b70c92 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Supporting Files/sv.lproj/Localizable.strings @@ -0,0 +1,13 @@ +/* + Localizable.strings + Daresay Movies + + Created by Emad Bayramy on 12/13/21. + +*/ +"titleName" = "DaMovies"; +"popularMovies" = "populära filmer"; +"Change_Language" = "Ändra språk"; +"cancel" = "avbryt"; +"to_home" = "Till hem"; +"guideline" = "På den här sidan kan du antingen ändra språk eller fortsätta till startvyn"; diff --git a/Daresay Movies/Daresay Movies/UIKitUtilities/CollectionView/DaMoviesCollectionViewCell.swift b/Daresay Movies/Daresay Movies/UIKitUtilities/CollectionView/DaMoviesCollectionViewCell.swift new file mode 100644 index 00000000..a0d8edec --- /dev/null +++ b/Daresay Movies/Daresay Movies/UIKitUtilities/CollectionView/DaMoviesCollectionViewCell.swift @@ -0,0 +1,14 @@ +// +// DaMoviesCollectionViewCell.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import UIKit + +protocol DaMoviesCollectionViewCell: UICollectionViewCell { + associatedtype CellViewModel + func configureCellWith(_ item: CellViewModel) + func configCellSize(item: CellViewModel) -> CGSize +} diff --git a/Daresay Movies/Daresay Movies/UIKitUtilities/CollectionView/DaMoviesCollectionViewDataSource.swift b/Daresay Movies/Daresay Movies/UIKitUtilities/CollectionView/DaMoviesCollectionViewDataSource.swift new file mode 100644 index 00000000..5f0ab980 --- /dev/null +++ b/Daresay Movies/Daresay Movies/UIKitUtilities/CollectionView/DaMoviesCollectionViewDataSource.swift @@ -0,0 +1,111 @@ +// +// DaMoviesCollectionViewDataSource.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import UIKit + +/* + + This is DaMoviesCollectionViewDataSource responsible for data source of simple collectionViews. + + You can easily add new datas to array by confirming your UICollectionViewCell to DaMoviesCollectionViewCell. + */ + +protocol DaMoviesCollectionViewDelegate: class { + func collection(willDisplay cellIndexPath: IndexPath, cell: UICollectionViewCell) + func collection(_ collectionView: UICollectionView, didSelectItem index: IndexPath) + func collection(_ collectionView: UICollectionView, didDeselectItemAt index: IndexPath) + func colelction(didSelectModelAt model: T) + func scrollDidEndDragging(_ scrollView: UIScrollView, willDecelerate: Bool) +} + +extension DaMoviesCollectionViewDelegate { + func collection(willDisplay cellIndexPath: IndexPath, cell: UICollectionViewCell) { } + func collection(_ collectionView: UICollectionView, didSelectItem index: IndexPath) { } + func collection(_ collectionView: UICollectionView, didDeselectItemAt index: IndexPath) { } + func colelction(didSelectModelAt model: T) { } + func scrollDidEndDragging(_ scrollView: UIScrollView, willDecelerate: Bool) { } +} + +class DaMoviesCollectionViewDataSource: NSObject, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout { + // MARK: - Variables + var items: [T.CellViewModel] = [] + var selectItem: IndexPath? + var collectionView: UICollectionView + + weak var delegate: DaMoviesCollectionViewDelegate? + + // MARK: - Initializer + init(items: [T.CellViewModel], collectionView: UICollectionView, delegate: DaMoviesCollectionViewDelegate) { + self.items = items + self.collectionView = collectionView + // Register cell to collectionView + self.collectionView.register(UINib(nibName: String.init(describing: T.self), bundle: nil), 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 { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String.init(describing: T.self), for: indexPath) as! T + cell.configureCellWith(items[indexPath.row]) + return cell + } + + // MARK: - UICollectionView Delegate + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + delegate?.collection(collectionView, didSelectItem: indexPath) + delegate?.colelction(didSelectModelAt: items[indexPath.row]) + } + + func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + let cell = cell as! T + delegate?.collection(willDisplay: indexPath, cell: cell) + } + + // MARK: - ScrollView Delegate + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + delegate?.scrollDidEndDragging(scrollView, willDecelerate: decelerate) + } + + // MARK: - UICollectionView Delegate FlowLayout + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + let item = items[indexPath.row] + let baseCell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: T.self), for: indexPath) + let cell = baseCell as! T + let size = cell.configCellSize(item: item) + return size + } + + 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() + } +} diff --git a/Daresay Movies/Daresay Movies/UIKitUtilities/Coordinator/Protocols/Coordinator.swift b/Daresay Movies/Daresay Movies/UIKitUtilities/Coordinator/Protocols/Coordinator.swift new file mode 100644 index 00000000..753031c2 --- /dev/null +++ b/Daresay Movies/Daresay Movies/UIKitUtilities/Coordinator/Protocols/Coordinator.swift @@ -0,0 +1,35 @@ +// +// Coordinator.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/13/21. +// + +import UIKit + +protocol Coordinator: AnyObject { + var parentCoordinator: Coordinator? {get set} + var childCoordinators: [Coordinator] { get set } + var navigationController: UINavigationController { get set } + func navigateTo(deepLink: DeepLink) + func start(animated: Bool) + func finish() +} + +extension Coordinator { + + func start() { + start(animated: true) + } + + func childDidFinish(_ child: Coordinator?) { + for (index, coordinator) in childCoordinators.enumerated() where coordinator === child { + childCoordinators.remove(at: index) + break + } + } + + func navigateTo(deepLink: DeepLink) {} + + func finish() {} +} diff --git a/Daresay Movies/Daresay Movies/UIKitUtilities/Coordinator/Protocols/Storyboarded.swift b/Daresay Movies/Daresay Movies/UIKitUtilities/Coordinator/Protocols/Storyboarded.swift new file mode 100644 index 00000000..0bcd4975 --- /dev/null +++ b/Daresay Movies/Daresay Movies/UIKitUtilities/Coordinator/Protocols/Storyboarded.swift @@ -0,0 +1,73 @@ +// +// Storyboarded.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/13/21. +// + +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 vc = storyboard.instantiateViewController(withIdentifier: className) as? Self else { + fatalError("Could not find View Controller named \(className)") + } + return vc + } +} + +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/Daresay Movies/Daresay Movies/UIKitUtilities/NavigationController/DaMoviesNavigationController.swift b/Daresay Movies/Daresay Movies/UIKitUtilities/NavigationController/DaMoviesNavigationController.swift new file mode 100644 index 00000000..a7ea4e39 --- /dev/null +++ b/Daresay Movies/Daresay Movies/UIKitUtilities/NavigationController/DaMoviesNavigationController.swift @@ -0,0 +1,33 @@ +// +// DaMoviesNavigationController.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import UIKit + +class DaMoviesNavigationController: UINavigationController { + + override func viewDidLoad() { + super.viewDidLoad() + setupView() + } + + private func setupView() { + navigationBar.barTintColor = .systemGreen + + if #available(iOS 15, *) { + let appearance = UINavigationBarAppearance() + appearance.configureWithOpaqueBackground() + appearance.backgroundColor = UIColor.systemGreen + appearance.shadowImage = UIImage() + appearance.shadowColor = .clear + navigationController?.navigationBar.scrollEdgeAppearance = appearance + + self.navigationBar.standardAppearance = appearance + UINavigationBar.appearance().scrollEdgeAppearance = appearance + UINavigationBar.appearance().compactScrollEdgeAppearance = appearance + } + } +} diff --git a/Daresay Movies/Daresay Movies/Utilities/Builders/ImageBaseUrlBuilder.swift b/Daresay Movies/Daresay Movies/Utilities/Builders/ImageBaseUrlBuilder.swift new file mode 100644 index 00000000..8e710d2e --- /dev/null +++ b/Daresay Movies/Daresay Movies/Utilities/Builders/ImageBaseUrlBuilder.swift @@ -0,0 +1,29 @@ +// +// ImageBaseUrlBuilder.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import Foundation + +class ImageBaseUrlBuilder { + + private var model: ConfigModel + private var type: ImageTypes + init(config: ConfigModel = UserDefaultData.configModel, forTypeAndSize: ImageTypes) { + model = config + type = forTypeAndSize + } + + func createURL(filePath: String) -> URL? { + let baseConfig = model.images + let baseURL = baseConfig?.secureBaseURL ?? "" + let size = getSizeString() + return URL(string: baseURL + size + filePath) + } + + private func getSizeString() -> String { + return type.sizeString + } +} diff --git a/Daresay Movies/Daresay Movies/Utilities/Data/UserDefaultData.swift b/Daresay Movies/Daresay Movies/Utilities/Data/UserDefaultData.swift new file mode 100644 index 00000000..64fda6c4 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Utilities/Data/UserDefaultData.swift @@ -0,0 +1,46 @@ +// +// UserDefaultData.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/13/21. +// + +import Foundation + +struct UserDefaultData { + + enum UserDefaultsKey: String { + case appleLanguages = "AppleLanguages" + case currentLanguage + case accessToken + case configModel + case favorites + } + + @UserDefaultStorage(.currentLanguage, defaultValue: "en") + static var currentLanguage: String + + @UserDefaultStorage(.appleLanguages, defaultValue: ["en"]) + static var appleLanguage: [String] + + @UserDefaultStorage(.accessToken, defaultValue: Constants.accessToken) + static var accessToken: String + + @UserDefaultStorage(.configModel, defaultValue: ConfigModel()) + static var configModel: ConfigModel + + @UserDefaultStorage(.favorites, defaultValue: MovieArrayModel()) + static var favoriteList: MovieArrayModel + + static func clearUserDefaultFor(_ key: UserDefaultsKey) { + UserDefaults.standard.removeObject(forKey: key.rawValue) + UserDefaults.standard.synchronize() + } + + static func clearAllUserDefault() { + if let bundleID = Bundle.main.bundleIdentifier { + UserDefaults.standard.removePersistentDomain(forName: bundleID) + } + UserDefaults.standard.synchronize() + } +} diff --git a/Daresay Movies/Daresay Movies/Utilities/Data/UserDefaultStorage.swift b/Daresay Movies/Daresay Movies/Utilities/Data/UserDefaultStorage.swift new file mode 100644 index 00000000..466096a3 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Utilities/Data/UserDefaultStorage.swift @@ -0,0 +1,55 @@ +// +// UserDefaultStorage.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/13/21. +// + +import Foundation +/* + I have also uploaded the better version of this anotation and property wrapper as 'SwiftDataManager' on my github + link: https://github.com/EmadBeyrami/SwiftDataManager + I coded a lighter one for this use case. + */ + +// Responsible for UserDefault of App +@propertyWrapper +struct UserDefaultStorage { + + struct Wrapper: Codable where T : Codable { + let wrapper: T + } + + private let key: UserDefaultData.UserDefaultsKey + private let defaultValue: T + private let storage: UserDefaults = .standard + + init(_ key: UserDefaultData.UserDefaultsKey, defaultValue: T) { + self.key = key + self.defaultValue = defaultValue + } + + var wrappedValue: T { + get { + // Read value from UserDefaults + guard let data = storage.object(forKey: key.rawValue) as? Data else { + // Return defaultValue when no data in UserDefaults + return defaultValue + } + + // Convert data to the desire data type + let value = try? JSONDecoder().decode(Wrapper.self, from: data) + return value?.wrapper ?? defaultValue + } + set { + // Convert newValue to data + do { + let data = try JSONEncoder().encode(Wrapper(wrapper: newValue)) + storage.set(data, forKey: key.rawValue) + } catch { + storage.removeObject(forKey: key.rawValue) + print(error) + } + } + } +} diff --git a/Daresay Movies/Daresay Movies/Utilities/GeneralTypealias.swift b/Daresay Movies/Daresay Movies/Utilities/GeneralTypealias.swift new file mode 100644 index 00000000..2688252e --- /dev/null +++ b/Daresay Movies/Daresay Movies/Utilities/GeneralTypealias.swift @@ -0,0 +1,12 @@ +// +// GeneralTypealias.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import Foundation + +// For making Completions readable, clean and more beautiful :) +typealias Completion = (() -> Void) +typealias DataCompletion = ((T) -> Void) diff --git a/Daresay Movies/Daresay Movies/Utilities/LanguageManager/LanguageManager.swift b/Daresay Movies/Daresay Movies/Utilities/LanguageManager/LanguageManager.swift new file mode 100644 index 00000000..d1af58da --- /dev/null +++ b/Daresay Movies/Daresay Movies/Utilities/LanguageManager/LanguageManager.swift @@ -0,0 +1,181 @@ +// +// LanguageManager.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/13/21. +// + +import UIKit + +enum SupportedLanguages: Int, CaseIterable { + case english + case german + case finnish + case swedish + + var languageDirection: LanguageDirection { + switch self { + case .english: + return .ltr + case .german: + return .ltr + case .finnish: + return .ltr + case .swedish: + return .ltr + } + } +} + +@objc enum LanguageDirection: Int { + case rtl + case ltr +} + +// MARK: - Language Managable Protocol +protocol LanguageManagable { + var currentLanguage: SupportedLanguages { get set } + var allSupportedLanguages: [SupportedLanguages] { get } +} + +// MARK: - Language Manager +class LanguageManager: LanguageManagable { + // MARK: - Properties + static let shared: LanguageManager = LanguageManager() + + var allSupportedLanguages: [SupportedLanguages] = SupportedLanguages.allCases + var currentLanguage: SupportedLanguages { + get { + let identifier = UserDefaultData.currentLanguage + return SupportedLanguages(identifier: identifier) + } + set { + UserDefaultData.currentLanguage = newValue.identifier + UIView.appearance().semanticContentAttribute = newValue.direction.semantic + Bundle.setLanguage(newValue.identifier) + } + } + + var languagecalendar: Calendar { + let locale = Locale(identifier: currentLanguage.locale) + return locale.calendar + } + // MARK: - Methods + private init() {} + + class func isAValidLanguageIdentifier(_ identifier: String) -> Bool { + for language in shared.allSupportedLanguages where language.identifier == identifier { + return true + } + return false + } +} + +// MARK: - Extensions +// MARK: Supported Languages +extension SupportedLanguages { + var text: String { + switch self { + case .english: + return "English" + case .finnish: + return "Suomi" + case .german: + return "Deutsch" + case .swedish: + return "Swedish" + } + } + + var identifier: String { + switch self { + case .english: + return "en-US" + case .finnish: + return "fi" + case .german: + return "de" + case .swedish: + return "sv" + } + } + + var direction: LanguageDirection { + switch self { + case .english: + return .ltr + case .finnish: + return .ltr + case .german: + return .ltr + case .swedish: + return .ltr + } + } + + var textAlignment: NSTextAlignment { + switch self { + case .english: + return .left + case .finnish: + return .left + case .german: + return .left + case .swedish: + return .left + } + } + + var oppositeTextAlignment: NSTextAlignment { + switch self { + case .english: + return .left + case .finnish: + return .left + case .german: + return .left + case .swedish: + return .left + } + } + + var locale: String { + switch self { + case .english: + return "en" + case .finnish: + return "fi" + case .german: + return "de" + case .swedish: + return "sv" + } + } + + init(identifier: String?) { + switch identifier ?? "en-US" { + case "en-US": + self = .english + case "fi": + self = .finnish + case "de": + self = .german + case "sv": + self = .swedish + default: + self = .english + } + } +} + +// MARK: LanguageDirection +extension LanguageDirection { + var semantic: UISemanticContentAttribute { + switch self { + case .ltr: + return .forceLeftToRight + case .rtl: + return .forceRightToLeft + } + } +} diff --git a/Daresay Movies/Daresay Movies/Utilities/LanguageManager/LocalizedStrings.swift b/Daresay Movies/Daresay Movies/Utilities/LanguageManager/LocalizedStrings.swift new file mode 100644 index 00000000..b0bebb7e --- /dev/null +++ b/Daresay Movies/Daresay Movies/Utilities/LanguageManager/LocalizedStrings.swift @@ -0,0 +1,27 @@ +// +// LocalizedStrings.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/13/21. +// + +import Foundation + +// We can also using swiftgen for generating string files +enum LocalizedStrings: String { + + case titleName + case popularMovies + case changeLanguage = "Change_Language" + case cancel + case toHome = "to_home" + case guideline + + var value: String { + return localized(key: self.rawValue) + } + + private func localized(key: String) -> String { + return NSLocalizedString(key, comment: "") + } +} diff --git a/Daresay Movies/Daresay Movies/Utilities/Network/APILogger/ResponseLog.swift b/Daresay Movies/Daresay Movies/Utilities/Network/APILogger/ResponseLog.swift new file mode 100644 index 00000000..60a294e5 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Utilities/Network/APILogger/ResponseLog.swift @@ -0,0 +1,60 @@ +// +// ResponseLog.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import Foundation + +struct DaMoviesResponseLog: 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/Daresay Movies/Daresay Movies/Utilities/Network/APILogger/URLRequestLoggableProtocol.swift b/Daresay Movies/Daresay Movies/Utilities/Network/APILogger/URLRequestLoggableProtocol.swift new file mode 100644 index 00000000..cd73b60e --- /dev/null +++ b/Daresay Movies/Daresay Movies/Utilities/Network/APILogger/URLRequestLoggableProtocol.swift @@ -0,0 +1,12 @@ +// +// URLRequestLoggableProtocol.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import Foundation + +protocol URLRequestLoggableProtocol { + func logResponse(_ response: HTTPURLResponse?, data: Data?, error: Error?, HTTPMethod: String?) +} diff --git a/Daresay Movies/Daresay Movies/Utilities/Network/CacheManager/CacheManagableProtocol.swift b/Daresay Movies/Daresay Movies/Utilities/Network/CacheManager/CacheManagableProtocol.swift new file mode 100644 index 00000000..47c66a90 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Utilities/Network/CacheManager/CacheManagableProtocol.swift @@ -0,0 +1,15 @@ +// +// CacheManagableProtocol.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import Foundation + +protocol CacheManagable { + var cacheManager: URLCache { get set } + func cachedResponse(for urlRequest: URLRequest) -> CachedURLResponse? + func clearAllCache() + func cacheConfig() -> URLSessionConfiguration +} diff --git a/Daresay Movies/Daresay Movies/Utilities/Network/CacheManager/CacheManager.swift b/Daresay Movies/Daresay Movies/Utilities/Network/CacheManager/CacheManager.swift new file mode 100644 index 00000000..060343e6 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Utilities/Network/CacheManager/CacheManager.swift @@ -0,0 +1,44 @@ +// +// CacheManager.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import Foundation + +class DaMoviesCacheManager { + + // MARK: - singleton + static let standard = DaMoviesCacheManager(cacheManager: URLCache.shared) + + // MARK: - Initializer + var cacheManager: URLCache + init(cacheManager: URLCache) { + self.cacheManager = cacheManager + } + + func cachedResponse(for urlRequest: URLRequest) -> CachedURLResponse? { + cacheDesciption() + return cacheManager.cachedResponse(for: urlRequest) + } + + internal func cacheConfig() -> URLSessionConfiguration { + let config = URLSessionConfiguration.default + config.requestCachePolicy = .reloadRevalidatingCacheData + config.urlCache?.memoryCapacity = 256 * 1024 * 1024 + return config + } + + func clearAllCache() { + cacheManager.removeAllCachedResponses() + print("🔴 Removed All Cached Response 🔴") + } + + private func cacheDesciption() { + print("💾 Disk usage/capacity: \(cacheManager.currentDiskUsage)/\(cacheManager.diskCapacity), 💾 memory usage/capacity: \(cacheManager.currentMemoryUsage)/\(cacheManager.memoryCapacity) 💾") + } +} + +// MARK: - CacheManagable +extension DaMoviesCacheManager: CacheManagable { } diff --git a/Daresay Movies/Daresay Movies/Utilities/Network/HTTPMethod.swift b/Daresay Movies/Daresay Movies/Utilities/Network/HTTPMethod.swift new file mode 100644 index 00000000..13b1d6db --- /dev/null +++ b/Daresay Movies/Daresay Movies/Utilities/Network/HTTPMethod.swift @@ -0,0 +1,12 @@ +// +// HTTPMethod.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import Foundation + +enum HTTPMethod: String { + case get = "GET" +} diff --git a/Daresay Movies/Daresay Movies/Utilities/Network/Model/RequestCallback.swift b/Daresay Movies/Daresay Movies/Utilities/Network/Model/RequestCallback.swift new file mode 100644 index 00000000..9760a6bf --- /dev/null +++ b/Daresay Movies/Daresay Movies/Utilities/Network/Model/RequestCallback.swift @@ -0,0 +1,20 @@ +// +// RequestCallback.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import Foundation + +struct RequestCallback: Codable { + let page: Int? + let results: T? + let totalPages, totalResults: Int? + + enum CodingKeys: String, CodingKey { + case page, results + case totalPages = "total_pages" + case totalResults = "total_results" + } +} diff --git a/Daresay Movies/Daresay Movies/Utilities/Network/Protocol/RequestManagerProtocol.swift b/Daresay Movies/Daresay Movies/Utilities/Network/Protocol/RequestManagerProtocol.swift new file mode 100644 index 00000000..e8c8f30b --- /dev/null +++ b/Daresay Movies/Daresay Movies/Utilities/Network/Protocol/RequestManagerProtocol.swift @@ -0,0 +1,37 @@ +// +// RequestManagerProtocol.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import Foundation + +protocol RequestManagerProtocol { + /// Provided URL Session + var session: URLSession! { get set } + + /// Access Token + var accessToken: String? { get } + + /// Timeout interval is interval for a request to be timedOut + var timeOutInterval: Double { get } + + var baseApi: String { get set } + + var responseValidator: ResponseValidatableProtocol { get set } + + var reponseLog: URLRequestLoggableProtocol? { get set } + + var cacheManager: CacheManagable! { 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 codable . On failure returns RequestError based on your server RequestError. + */ + func performRequestWith(url: String, httpMethod: HTTPMethod, completionHandler: @escaping CodableResponse) + +} diff --git a/Daresay Movies/Daresay Movies/Utilities/Network/RequestError.swift b/Daresay Movies/Daresay Movies/Utilities/Network/RequestError.swift new file mode 100644 index 00000000..e30e59b8 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Utilities/Network/RequestError.swift @@ -0,0 +1,36 @@ +// +// RequestError.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +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/Daresay Movies/Daresay Movies/Utilities/Network/RequestManager.swift b/Daresay Movies/Daresay Movies/Utilities/Network/RequestManager.swift new file mode 100644 index 00000000..f6436ce4 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Utilities/Network/RequestManager.swift @@ -0,0 +1,111 @@ +// +// RequestManager.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import Foundation + +/* + This is the network layer that I wrote for my apps, it is super duper light and no need for adding alamofire or such to the app :) + */ + +typealias CodableResponse = (Result) -> Void + +final class RequestManager: NSObject, URLSessionDelegate { + + private let baseAPIPrefix = Bundle.main.info(for: InfoPlistKey.baseAPIURL)! + private let baseAPIVersion = "3/" + var baseApi: String + + var session: URLSession! + + var responseValidator: ResponseValidatableProtocol + + var reponseLog: URLRequestLoggableProtocol? + + var cacheManager: CacheManagable! + + typealias Headers = [String: String] + + private override init() { + self.baseApi = baseAPIPrefix + baseAPIVersion + self.reponseLog = DaMoviesResponseLog() + self.responseValidator = DaMoviesResponseValidator() + self.cacheManager = DaMoviesCacheManager.standard + super.init() + self.session = URLSession(configuration: cacheManager.cacheConfig(), delegate: self, delegateQueue: OperationQueue.main) + } + + public init(session: URLSession, validator: ResponseValidatableProtocol) { + self.baseApi = baseAPIPrefix + baseAPIVersion + self.session = session + self.responseValidator = validator + } + + static let shared = RequestManager() + +} + +extension RequestManager: RequestManagerProtocol { + + var timeOutInterval: Double { + return 40 + } + + var accessToken: String? { + return UserDefaultData.accessToken + } + + func performRequestWith(url: String, httpMethod: HTTPMethod, completionHandler: @escaping CodableResponse) { + + let headers = headerBuilder() + + let urlRequest = urlRequestBuilder(url: url, header: headers, httpMethod: httpMethod) + + performURLRequest(urlRequest, completionHandler: completionHandler) + } + + private func headerBuilder() -> Headers { + var headers = [ + "Content-Type": "json;charset=utf-8" + ] + if let token = accessToken { + headers["Authorization"] = "Bearer " + token + } + return headers + } + + private func urlRequestBuilder(url: String, header: Headers, httpMethod: HTTPMethod) -> URLRequest { + var urlRequest = URLRequest(url: URL(string: baseApi + url)!, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: timeOutInterval) + urlRequest.allHTTPHeaderFields = header + + urlRequest.httpMethod = httpMethod.rawValue + return urlRequest + } + + private func performURLRequest(_ request: URLRequest, completionHandler: @escaping CodableResponse) { + + session.dataTask(with: request) { [weak self] (data, response, error) in + guard let self = self else { return } + self.reponseLog?.logResponse(response as? HTTPURLResponse, data: data, error: error, HTTPMethod: request.httpMethod) + if error != nil { + guard let cachedResponse = self.cacheManager.cachedResponse(for: request) else { + return completionHandler(.failure(RequestError.connectionError)) + } + + if let cachedResp = cachedResponse.data as? T { + let validationResult: (Result) = self.responseValidator.validation(response: cachedResp as? HTTPURLResponse, data: data) + return completionHandler(validationResult) + } else { + return completionHandler(Result.failure(.notFound)) + } + } else { + let validationResult: (Result) = self.responseValidator.validation(response: response as? HTTPURLResponse, data: data) + return completionHandler(validationResult) + } + }.resume() + } + +} diff --git a/Daresay Movies/Daresay Movies/Utilities/Network/ResponseValidator/ResponseValidatableProtocol.swift b/Daresay Movies/Daresay Movies/Utilities/Network/ResponseValidator/ResponseValidatableProtocol.swift new file mode 100644 index 00000000..87269f0d --- /dev/null +++ b/Daresay Movies/Daresay Movies/Utilities/Network/ResponseValidator/ResponseValidatableProtocol.swift @@ -0,0 +1,12 @@ +// +// ResponseValidatableProtocol.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import Foundation + +protocol ResponseValidatableProtocol { + func validation(response: HTTPURLResponse?, data: Data?) -> Result +} diff --git a/Daresay Movies/Daresay Movies/Utilities/Network/ResponseValidator/ResponseValidator.swift b/Daresay Movies/Daresay Movies/Utilities/Network/ResponseValidator/ResponseValidator.swift new file mode 100644 index 00000000..bfeece68 --- /dev/null +++ b/Daresay Movies/Daresay Movies/Utilities/Network/ResponseValidator/ResponseValidator.swift @@ -0,0 +1,34 @@ +// +// ResponseValidator.swift +// Daresay Movies +// +// Created by Emad Bayramy on 12/14/21. +// + +import Foundation + +struct DaMoviesResponseValidator: ResponseValidatableProtocol { + + 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/Daresay Movies/Daresay Movies/XcodeConfig/Dev.xcconfig b/Daresay Movies/Daresay Movies/XcodeConfig/Dev.xcconfig new file mode 100644 index 00000000..24f93d3c --- /dev/null +++ b/Daresay Movies/Daresay Movies/XcodeConfig/Dev.xcconfig @@ -0,0 +1,15 @@ +// +// Dev.xcconfig +// Daresay Movies +// +// Created by Emad Bayramy on 12/13/21. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 + +APP_NAME = DaMovies Dev +BASE_BUNDLE_ID = com.beyrami.Daresay-Movies.dev +URL_SCHEME = damovies +APP_ICON = AppIcon-dev +BASE_API_URL = https:\/\/api.themoviedb.org\/ diff --git a/Daresay Movies/Daresay Movies/XcodeConfig/Production.xcconfig b/Daresay Movies/Daresay Movies/XcodeConfig/Production.xcconfig new file mode 100644 index 00000000..d2d130f5 --- /dev/null +++ b/Daresay Movies/Daresay Movies/XcodeConfig/Production.xcconfig @@ -0,0 +1,15 @@ +// +// Production.xcconfig +// Daresay Movies +// +// Created by Emad Bayramy on 12/13/21. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 + +APP_NAME = DaMovies +BASE_BUNDLE_ID = com.beyrami.Daresay-Movies +URL_SCHEME = damovies +APP_ICON = AppIcon +BASE_API_URL = https:\/\/api.themoviedb.org\/ diff --git a/Daresay Movies/Daresay MoviesTests/Coordinator/Test_AppCoordinator.swift b/Daresay Movies/Daresay MoviesTests/Coordinator/Test_AppCoordinator.swift new file mode 100644 index 00000000..ec78720d --- /dev/null +++ b/Daresay Movies/Daresay MoviesTests/Coordinator/Test_AppCoordinator.swift @@ -0,0 +1,75 @@ +// +// Test_AppCoordinator.swift +// Daresay MoviesTests +// +// Created by Emad Bayramy on 12/15/21. +// + +import XCTest +@testable import DaMovies_Dev + +final class AppCoordinatorTests: XCTestCase { + + var sut: AppCoordinator? + var window: UIWindow? + + override func tearDownWithError() throws { + sut = nil + window = nil + try? super.tearDownWithError() + } + + override func setUp() { + let nav = UINavigationController() + window = UIWindow(frame: UIScreen.main.bounds) + sut = AppCoordinator(navigationController: nav, window: window) + } + + override func tearDown() { + sut = nil + window = nil + } + + func test_start() throws { + // given + guard let sut = sut else { + throw UnitTestError() + } + + // when + sut.start(animated: false) + + // then + XCTAssertEqual(sut.navigationController.viewControllers.count, 1) + let rootVC = sut.navigationController.viewControllers[0] as? FlowControlViewController + XCTAssertNotNil(rootVC, "Check if root vsc is FlowControlViewController") + } + + func test_ToHome() throws { + // given + guard let sut = sut else { + throw UnitTestError() + } + // when + sut.toHome() + + // then + XCTAssertTrue(sut.childCoordinators.count == 1) + let visibleVC = sut.navigationController.visibleViewController as? HomeViewController + XCTAssertNotNil(visibleVC, "Check if presented vc is HomeViewController") + } + + func test_ChildDidFinish() throws { + // given + guard let sut = sut else { + throw UnitTestError() + } + // when + let child = HomeCoordinator(navigationController: sut.navigationController) + sut.childCoordinators.append(child) + sut.childDidFinish(child) + + // then + XCTAssertTrue(sut.childCoordinators.count == 0) + } +} diff --git a/Daresay Movies/Daresay MoviesTests/HomeSceneTest/Test_HomeServices.swift b/Daresay Movies/Daresay MoviesTests/HomeSceneTest/Test_HomeServices.swift new file mode 100644 index 00000000..5afeaa77 --- /dev/null +++ b/Daresay Movies/Daresay MoviesTests/HomeSceneTest/Test_HomeServices.swift @@ -0,0 +1,62 @@ +// +// Test_HomeServices.swift +// Daresay MoviesTests +// +// Created by Emad Bayramy on 12/15/21. +// + +import XCTest +@testable import DaMovies_Dev + +final class HomeServicesTest: XCTestCase { + + var sut: HomeService? + var moviesJson: Data? + + override func setUp() { + let bundle = Bundle(for: type(of: self)) + autoreleasepool { + if let path = bundle.path(forResource: "Movies", ofType: "json") { + do { + let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) + self.moviesJson = data + } catch { + + } + } + } + } + + override func tearDown() { + sut = nil + } + + func test_getMovies() { + + // Given + let urlSessionMock = URLSessionMock() + urlSessionMock.data = moviesJson + let mockRequestManager = RequestManagerMock(session: urlSessionMock, validator: MockResponseValidator()) + sut = HomeService(requestManager: mockRequestManager) + let expectation = XCTestExpectation(description: "Async Movies test") + var movies: MovieArrayModel = [] + + // When + sut?.getMovies(page: 0, completionHandler: { (result) in + defer { + expectation.fulfill() + } + switch result { + case .success(let data): + movies = data.results! + case .failure: + XCTFail() + } + }) + + // Then + print(movies.count) + wait(for: [expectation], timeout: 5) + XCTAssertTrue(movies.count == 20) + } +} diff --git a/Daresay Movies/Daresay MoviesTests/UtilitiesTest/Mocks/Movies.json b/Daresay Movies/Daresay MoviesTests/UtilitiesTest/Mocks/Movies.json new file mode 100644 index 00000000..f9bda80d --- /dev/null +++ b/Daresay Movies/Daresay MoviesTests/UtilitiesTest/Mocks/Movies.json @@ -0,0 +1,409 @@ +{ + "results" : [ + { + "genre_ids" : [ + 878, + 28, + 12 + ], + "adult" : false, + "backdrop_path" : "\/eENEf62tMXbhyVvdcXlnQz2wcuT.jpg", + "id" : 580489, + "original_title" : "Venom: Let There Be Carnage", + "vote_average" : 7.2000000000000002, + "popularity" : 6972.0029999999997, + "poster_path" : "\/rjkmN1dniUHVYAtwuV3Tji7FsDO.jpg", + "overview" : "After finding a host body in investigative reporter Eddie Brock, the alien symbiote must face a new enemy, Carnage, the alter ego of serial killer Cletus Kasady.", + "title" : "Venom: Let There Be Carnage", + "original_language" : "en", + "vote_count" : 4500, + "release_date" : "2021-09-30", + "video" : false + }, + { + "genre_ids" : [ + 28, + 35, + 80, + 53 + ], + "adult" : false, + "backdrop_path" : "\/7ajHGIAYNMiIzejy1LJWdPrcAx8.jpg", + "id" : 512195, + "original_title" : "Red Notice", + "vote_average" : 6.7999999999999998, + "popularity" : 4496.7600000000002, + "poster_path" : "\/lAXONuqg41NwUMuzMiFvicDET9Y.jpg", + "overview" : "An Interpol-issued Red Notice is a global alert to hunt and capture the world's most wanted. But when a daring heist brings together the FBI's top profiler and two rival criminals, there's no telling what will happen.", + "title" : "Red Notice", + "original_language" : "en", + "vote_count" : 2016, + "release_date" : "2021-11-04", + "video" : false + }, + { + "genre_ids" : [ + 28, + 12, + 878, + 14 + ], + "adult" : false, + "backdrop_path" : "\/VlHt27nCqOuTnuX6bku8QZapzO.jpg", + "id" : 634649, + "original_title" : "Spider-Man: No Way Home", + "vote_average" : 8.4000000000000004, + "popularity" : 4062.4459999999999, + "poster_path" : "\/1g0dhYtq4irTY1GPXvft6k4YLjm.jpg", + "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.", + "title" : "Spider-Man: No Way Home", + "original_language" : "en", + "vote_count" : 84, + "release_date" : "2021-12-15", + "video" : false + }, + { + "genre_ids" : [ + 16, + 35, + 10751 + ], + "adult" : false, + "backdrop_path" : "\/gSq4IvnsMKJQf0FaKn9bDE2Sggq.jpg", + "id" : 585245, + "original_title" : "Clifford the Big Red Dog", + "vote_average" : 7.5999999999999996, + "popularity" : 2509.5120000000002, + "poster_path" : "\/ygPTrycbMSFDc5zUpy4K5ZZtQSC.jpg", + "overview" : "As Emily struggles to fit in at home and at school, she discovers a small red puppy who is destined to become her best friend. When Clifford magically undergoes one heck of a growth spurt, becomes a gigantic dog and attracts the attention of a genetics company, Emily and her Uncle Casey have to fight the forces of greed as they go on the run across New York City. Along the way, Clifford affects the lives of everyone around him and teaches Emily and her uncle the true meaning of acceptance and unconditional love.", + "title" : "Clifford the Big Red Dog", + "original_language" : "en", + "vote_count" : 568, + "release_date" : "2021-11-10", + "video" : false + }, + { + "genre_ids" : [ + 16, + 35, + 10751, + 14 + ], + "adult" : false, + "backdrop_path" : "\/5RuR7GhOI5fElADXZb0X2sr9w5n.jpg", + "id" : 568124, + "original_title" : "Encanto", + "vote_average" : 7.4000000000000004, + "popularity" : 2579.114, + "poster_path" : "\/4j0PNHkMr5ax3IA8tjtxcmPU3QT.jpg", + "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.", + "title" : "Encanto", + "original_language" : "en", + "vote_count" : 328, + "release_date" : "2021-11-24", + "video" : false + }, + { + "genre_ids" : [ + 28, + 18, + 36 + ], + "adult" : false, + "backdrop_path" : "\/mFbS5TwN95BcSEfiztdchLgTQ0v.jpg", + "id" : 617653, + "original_title" : "The Last Duel", + "vote_average" : 7.5999999999999996, + "popularity" : 2326.27, + "poster_path" : "\/zjrJE0fpzPvX8saJXj8VNfcjBoU.jpg", + "overview" : "King Charles VI declares that Knight Jean de Carrouges settle his dispute with his squire, Jacques Le Gris, by challenging him to a duel.", + "title" : "The Last Duel", + "original_language" : "en", + "vote_count" : 917, + "release_date" : "2021-10-13", + "video" : false + }, + { + "genre_ids" : [ + 28, + 12, + 14 + ], + "adult" : false, + "backdrop_path" : "\/cinER0ESG0eJ49kXlExM0MEWGxW.jpg", + "id" : 566525, + "original_title" : "Shang-Chi and the Legend of the Ten Rings", + "vote_average" : 7.7999999999999998, + "popularity" : 2453.799, + "poster_path" : "\/1BIoJGKbXjdFDAqUEiA2VHqkK1Z.jpg", + "overview" : "Shang-Chi must confront the past he thought he left behind when he is drawn into the web of the mysterious Ten Rings organization.", + "title" : "Shang-Chi and the Legend of the Ten Rings", + "original_language" : "en", + "vote_count" : 4244, + "release_date" : "2021-09-01", + "video" : false + }, + { + "genre_ids" : [ + 18, + 36, + 12 + ], + "adult" : false, + "backdrop_path" : "\/xGrTm3J0FTafmuQ85vF7ZCw94x6.jpg", + "id" : 589761, + "original_title" : "Чернобыль", + "vote_average" : 6.2000000000000002, + "popularity" : 1851.2539999999999, + "poster_path" : "\/kfQJQWFEoWRVBH8FUKnT0HX1yRS.jpg", + "overview" : "The aftermath of a shocking explosion at the Chernobyl nuclear power station made hundreds of people sacrifice their lives to clean up the site of the catastrophe and to successfully prevent an even bigger disaster that could have turned a large part of the European continent into an uninhabitable exclusion zone. This is their story.", + "title" : "Chernobyl: Abyss", + "original_language" : "ru", + "vote_count" : 236, + "release_date" : "2021-04-15", + "video" : false + }, + { + "genre_ids" : [ + 16, + 878, + 10751, + 35 + ], + "adult" : false, + "backdrop_path" : "\/1aHMaBE40pU77XUHqSi7FSCngQ2.jpg", + "id" : 482321, + "original_title" : "Ron's Gone Wrong", + "vote_average" : 8.5, + "popularity" : 1917.981, + "poster_path" : "\/gA9QxSravC2EVEkEKgyEmDrfL0e.jpg", + "overview" : "In a world where walking, talking, digitally connected bots have become children's best friends, an 11-year-old finds that his robot buddy doesn't quite work the same as the others do.", + "title" : "Ron's Gone Wrong", + "original_language" : "en", + "vote_count" : 343, + "release_date" : "2021-10-15", + "video" : false + }, + { + "genre_ids" : [ + 37, + 28, + 53 + ], + "adult" : false, + "backdrop_path" : "\/upOi9aVqPPky7Ba4GsiyFdjc82I.jpg", + "id" : 887767, + "original_title" : "Last Shoot Out", + "vote_average" : 7.0999999999999996, + "popularity" : 1483.7239999999999, + "poster_path" : "\/pvEtPxotI3POlVPvNxgrHJuDXfe.jpg", + "overview" : "Soon after a newlywed learns that her husband had her father shot down, she flees from the Callahan ranch in fear. She's rescued by a gunman who safeguards her at a remote outpost as he staves off her husband's attempts to reclaim his bride.", + "title" : "Last Shoot Out", + "original_language" : "en", + "vote_count" : 27, + "release_date" : "2021-12-03", + "video" : false + }, + { + "genre_ids" : [ + 12, + 28, + 53 + ], + "adult" : false, + "backdrop_path" : "\/r2GAjd4rNOHJh6i6Y0FntmYuPQW.jpg", + "id" : 370172, + "original_title" : "No Time to Die", + "vote_average" : 7.5999999999999996, + "popularity" : 1474.837, + "poster_path" : "\/iUgygt3fscRoKWCV1d0C7FbM9TP.jpg", + "overview" : "Bond has left active service and is enjoying a tranquil life in Jamaica. His peace is short-lived when his old friend Felix Leiter from the CIA turns up asking for help. The mission to rescue a kidnapped scientist turns out to be far more treacherous than expected, leading Bond onto the trail of a mysterious villain armed with dangerous new technology.", + "title" : "No Time to Die", + "original_language" : "en", + "vote_count" : 2543, + "release_date" : "2021-09-29", + "video" : false + }, + { + "genre_ids" : [ + 28, + 12, + 878, + 18, + 14 + ], + "adult" : false, + "backdrop_path" : "\/lyvszvJJqqI8aqBJ70XzdCNoK0y.jpg", + "id" : 524434, + "original_title" : "Eternals", + "vote_average" : 7.0999999999999996, + "popularity" : 1474.011, + "poster_path" : "\/6AdXwFTRTAzggD2QUTt5B7JFGKL.jpg", + "overview" : "The Eternals are a team of ancient aliens who have been living on Earth in secret for thousands of years. When an unexpected tragedy forces them out of the shadows, they are forced to reunite against mankind’s most ancient enemy, the Deviants.", + "title" : "Eternals", + "original_language" : "en", + "vote_count" : 1416, + "release_date" : "2021-11-03", + "video" : false + }, + { + "genre_ids" : [ + 27, + 9648, + 53 + ], + "adult" : false, + "backdrop_path" : "\/39EQTf94Lckdo8yzS1CdWQSgmen.jpg", + "id" : 609972, + "original_title" : "Paranormal Activity: Next of Kin", + "vote_average" : 6.7000000000000002, + "popularity" : 1127.5599999999999, + "poster_path" : "\/bXAVveHiLotZbWdg3PKGhAzxYKP.jpg", + "overview" : "Margot, a documentary filmmaker, heads to a secluded Amish community in the hopes of learning about her long-lost mother and extended family. Following a string of strange occurrences and discoveries, she comes to realize this community may not be what it seems.", + "title" : "Paranormal Activity: Next of Kin", + "original_language" : "en", + "vote_count" : 151, + "release_date" : "2021-10-29", + "video" : false + }, + { + "genre_ids" : [ + 16, + 35, + 10751 + ], + "adult" : false, + "backdrop_path" : "\/5RMqFZdefnDwY7rdD1oJcTkWPdF.jpg", + "id" : 774741, + "original_title" : "Diary of a Wimpy Kid", + "vote_average" : 7.0999999999999996, + "popularity" : 1836.038, + "poster_path" : "\/obg6lWuNaZkoSlwrVG4VVk4SmT.jpg", + "overview" : "Greg Heffley is a scrawny but ambitious kid with an active imagination and big plans to be rich and famous – he just has to survive middle school first.", + "title" : "Diary of a Wimpy Kid", + "original_language" : "en", + "vote_count" : 71, + "release_date" : "2021-12-03", + "video" : false + }, + { + "genre_ids" : [ + 878, + 18, + 12 + ], + "adult" : false, + "backdrop_path" : "\/oE6bhqqVFyIECtBzqIuvh6JdaB5.jpg", + "id" : 522402, + "original_title" : "Finch", + "vote_average" : 8.1999999999999993, + "popularity" : 1039.6120000000001, + "poster_path" : "\/jKuDyqx7jrjiR9cDzB5pxzhJAdv.jpg", + "overview" : "On a post-apocalyptic Earth, a robot, built to protect the life of his dying creator's beloved dog, learns about life, love, friendship, and what it means to be human.", + "title" : "Finch", + "original_language" : "en", + "vote_count" : 1439, + "release_date" : "2021-11-04", + "video" : false + }, + { + "genre_ids" : [ + 53, + 10752 + ], + "adult" : false, + "backdrop_path" : "\/uWGPC7j70LE64nbetxQGSSYJO53.jpg", + "id" : 762433, + "original_title" : "Zeros and Ones", + "vote_average" : 5.5999999999999996, + "popularity" : 1154.7670000000001, + "poster_path" : "\/wrClTreFxBtxo2XhHQf9GtUPa3x.jpg", + "overview" : "Called to Rome to stop an imminent terrorist bombing, a soldier desperately seeks news of his imprisoned brother — a rebel with knowledge that could thwart the attack. Navigating the capital's darkened streets, he races to a series of ominous encounters to keep the Vatican from being blown to bits.", + "title" : "Zeros and Ones", + "original_language" : "en", + "vote_count" : 68, + "release_date" : "2021-11-18", + "video" : false + }, + { + "genre_ids" : [ + 16, + 35, + 14 + ], + "adult" : false, + "backdrop_path" : "\/1zgob2Z8xVE3RZUgOKnHKcVPzOE.jpg", + "id" : 877183, + "original_title" : "The Simpsons in Plusaversary", + "vote_average" : 6.7000000000000002, + "popularity" : 1020.3579999999999, + "poster_path" : "\/9xaAT3V3I9xxqnNiEjCivNFfdlh.jpg", + "overview" : "The Simpsons host a Disney+ Day party and everyone is on the list… except Homer. With friends from across the service and music fit for a Disney Princess, Plusaversary is Springfield's event of the year.", + "title" : "The Simpsons in Plusaversary", + "original_language" : "en", + "vote_count" : 164, + "release_date" : "2021-11-12", + "video" : false + }, + { + "genre_ids" : [ + 27 + ], + "adult" : false, + "backdrop_path" : "\/5WnfgU7myfqgkbl3CSj7LQeHU0e.jpg", + "id" : 895001, + "original_title" : "ปริศนารูหลอน", + "vote_average" : 6.5999999999999996, + "popularity" : 999.52200000000005, + "poster_path" : "\/bPq6x47nEqMZED5PrKelcWWpezM.jpg", + "overview" : "When two siblings stumble on a strange hole in the wall of their grandparents’ house, horrifying incidents reveal sinister secrets about their family.", + "title" : "The Whole Truth", + "original_language" : "th", + "vote_count" : 49, + "release_date" : "2021-12-02", + "video" : false + }, + { + "genre_ids" : [ + 28 + ], + "adult" : false, + "backdrop_path" : "\/3cMfwbF1J9fglSssim4zKG6scIs.jpg", + "id" : 876262, + "original_title" : "Garota da Moto", + "vote_average" : 4.9000000000000004, + "popularity" : 1055.7570000000001, + "poster_path" : "\/sYoWjGSW4XQRWwFDHf3kdMqtCLr.jpg", + "overview" : "", + "title" : "Garota da Moto", + "original_language" : "pt", + "vote_count" : 4, + "release_date" : "2021-09-23", + "video" : false + }, + { + "genre_ids" : [ + 35, + 28, + 12, + 878 + ], + "adult" : false, + "backdrop_path" : "\/8Y43POKjjKDGI9MH89NW0NAzzp8.jpg", + "id" : 550988, + "original_title" : "Free Guy", + "vote_average" : 7.7999999999999998, + "popularity" : 971.66800000000001, + "poster_path" : "\/xmbU4JTUm8rsdtn7Y3Fcm30GpeT.jpg", + "overview" : "A bank teller called Guy realizes he is a background character in an open world video game called Free City that will soon go offline.", + "title" : "Free Guy", + "original_language" : "en", + "vote_count" : 4091, + "release_date" : "2021-08-11", + "video" : false + } + ], + "total_pages" : 31568, + "total_results" : 631355, + "page" : 1 +} diff --git a/Daresay Movies/Daresay MoviesTests/UtilitiesTest/Mocks/RequestManagerMock.swift b/Daresay Movies/Daresay MoviesTests/UtilitiesTest/Mocks/RequestManagerMock.swift new file mode 100644 index 00000000..ac806e72 --- /dev/null +++ b/Daresay Movies/Daresay MoviesTests/UtilitiesTest/Mocks/RequestManagerMock.swift @@ -0,0 +1,51 @@ +// +// RequestManagerMock.swift +// Daresay MoviesTests +// +// Created by Emad Bayramy on 12/15/21. +// + +import Foundation +@testable import DaMovies_Dev + +class RequestManagerMock: RequestManagerProtocol { + + var accessToken: String? + + var cacheManager: CacheManagable! + + var session: URLSession! + + var timeOutInterval: Double { + return 5 + } + + var baseApi: String + + var responseValidator: ResponseValidatableProtocol + + var reponseLog: URLRequestLoggableProtocol? + + public init(session: URLSession, validator: ResponseValidatableProtocol) { + self.baseApi = "" + self.session = session + self.responseValidator = validator + } + + func performRequestWith(url: String, httpMethod: HTTPMethod, completionHandler: @escaping CodableResponse) where T: Codable { + 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/Daresay Movies/Daresay MoviesTests/UtilitiesTest/Mocks/ResponseValidatorMock.swift b/Daresay Movies/Daresay MoviesTests/UtilitiesTest/Mocks/ResponseValidatorMock.swift new file mode 100644 index 00000000..db69a750 --- /dev/null +++ b/Daresay Movies/Daresay MoviesTests/UtilitiesTest/Mocks/ResponseValidatorMock.swift @@ -0,0 +1,26 @@ +// +// ResponseValidatorMock.swift +// Daresay MoviesTests +// +// Created by Emad Bayramy on 12/15/21. +// + +import Foundation +@testable import DaMovies_Dev + +struct MockResponseValidator: ResponseValidatableProtocol { + + 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/Daresay Movies/Daresay MoviesTests/UtilitiesTest/Mocks/URLSessionDataTaskMock.swift b/Daresay Movies/Daresay MoviesTests/UtilitiesTest/Mocks/URLSessionDataTaskMock.swift new file mode 100644 index 00000000..3baa5dc6 --- /dev/null +++ b/Daresay Movies/Daresay MoviesTests/UtilitiesTest/Mocks/URLSessionDataTaskMock.swift @@ -0,0 +1,22 @@ +// +// URLSessionDataTaskMock.swift +// Daresay MoviesTests +// +// Created by Emad Bayramy on 12/15/21. +// + +import Foundation + +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/Daresay Movies/Daresay MoviesTests/UtilitiesTest/Mocks/URLSessionMock.swift b/Daresay Movies/Daresay MoviesTests/UtilitiesTest/Mocks/URLSessionMock.swift new file mode 100644 index 00000000..5658181c --- /dev/null +++ b/Daresay Movies/Daresay MoviesTests/UtilitiesTest/Mocks/URLSessionMock.swift @@ -0,0 +1,38 @@ +// +// URLSessionMock.swift +// Daresay MoviesTests +// +// Created by Emad Bayramy on 12/15/21. +// + +import Foundation + +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/Daresay Movies/Daresay MoviesTests/UtilitiesTest/UnitTestError.swift b/Daresay Movies/Daresay MoviesTests/UtilitiesTest/UnitTestError.swift new file mode 100644 index 00000000..ab6b735f --- /dev/null +++ b/Daresay Movies/Daresay MoviesTests/UtilitiesTest/UnitTestError.swift @@ -0,0 +1,10 @@ +// +// UnitTestError.swift +// Daresay MoviesTests +// +// Created by Emad Bayramy on 12/15/21. +// + +import Foundation + +struct UnitTestError: Error {} diff --git a/Daresay Movies/Daresay MoviesUITests/Daresay_MoviesUITests.swift b/Daresay Movies/Daresay MoviesUITests/Daresay_MoviesUITests.swift new file mode 100644 index 00000000..7c5b9c62 --- /dev/null +++ b/Daresay Movies/Daresay MoviesUITests/Daresay_MoviesUITests.swift @@ -0,0 +1,42 @@ +// +// Daresay_MoviesUITests.swift +// Daresay MoviesUITests +// +// Created by Emad Bayramy on 12/13/21. +// + +import XCTest + +class Daresay_MoviesUITests: 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 + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + 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/Daresay Movies/Daresay MoviesUITests/Daresay_MoviesUITestsLaunchTests.swift b/Daresay Movies/Daresay MoviesUITests/Daresay_MoviesUITestsLaunchTests.swift new file mode 100644 index 00000000..cb0b7d74 --- /dev/null +++ b/Daresay Movies/Daresay MoviesUITests/Daresay_MoviesUITestsLaunchTests.swift @@ -0,0 +1,32 @@ +// +// Daresay_MoviesUITestsLaunchTests.swift +// Daresay MoviesUITests +// +// Created by Emad Bayramy on 12/13/21. +// + +import XCTest + +class Daresay_MoviesUITestsLaunchTests: 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) + } +}