From e0978e2a95e704260e9f892e8b9c862392242c72 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Tue, 10 Jan 2023 19:48:22 -0300 Subject: [PATCH 01/65] Add basic project structure from Xcode --- .DS_Store | Bin 0 -> 6148 bytes .gitignore | 2 + MyMovies.xcodeproj/project.pbxproj | 607 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../UserInterfaceState.xcuserstate | Bin 0 -> 11845 bytes .../xcschemes/xcschememanagement.plist | 14 + MyMovies/.DS_Store | Bin 0 -> 6148 bytes MyMovies/AppDelegate.swift | 36 ++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 13 + MyMovies/Assets.xcassets/Contents.json | 6 + MyMovies/Base.lproj/LaunchScreen.storyboard | 25 + MyMovies/Base.lproj/Main.storyboard | 24 + MyMovies/Info.plist | 25 + MyMovies/SceneDelegate.swift | 52 ++ MyMovies/ViewController.swift | 19 + MyMoviesTests/MyMoviesTests.swift | 36 ++ MyMoviesUITests/MyMoviesUITests.swift | 41 ++ .../MyMoviesUITestsLaunchTests.swift | 32 + 20 files changed, 958 insertions(+) create mode 100644 .DS_Store create mode 100644 .gitignore create mode 100644 MyMovies.xcodeproj/project.pbxproj create mode 100644 MyMovies.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 MyMovies.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 MyMovies.xcodeproj/project.xcworkspace/xcuserdata/caioamb.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 MyMovies.xcodeproj/xcuserdata/caioamb.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 MyMovies/.DS_Store create mode 100644 MyMovies/AppDelegate.swift create mode 100644 MyMovies/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 MyMovies/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 MyMovies/Assets.xcassets/Contents.json create mode 100644 MyMovies/Base.lproj/LaunchScreen.storyboard create mode 100644 MyMovies/Base.lproj/Main.storyboard create mode 100644 MyMovies/Info.plist create mode 100644 MyMovies/SceneDelegate.swift create mode 100644 MyMovies/ViewController.swift create mode 100644 MyMoviesTests/MyMoviesTests.swift create mode 100644 MyMoviesUITests/MyMoviesUITests.swift create mode 100644 MyMoviesUITests/MyMoviesUITestsLaunchTests.swift diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..094ffebb39571cd6aabd4aabc9bd9de7fa598164 GIT binary patch literal 6148 zcmeHKu};H447E#;U@<4Nq5`Ujy30|Qfkph|)$DAh$&AlUK+{0=JvAI9_9s#*e4 z#efjllKn0|JC}PG#W4}_^uC@Djfg0N3JwlnJ`tH0?a9DA`#?_jc%U1)qE)@BnqIU! zej@{Nb{SpL6ExDs`IRxIGd%t{Nn?7X3c)pwx}`geZky}LtcuH4cleAKi+0Le#-b>j zSjt0;FXpF5FK=hN=holOZ5Nx*`-&x9oB?OR8Td~Opk|9?hl*~U0cXG&STi8shX57K z4Lim3>3~Ts0B{I*5{#vnkepzc8+MAAfv~0mHI=Q!U`>ZVSX^${DQY^gH6Ls%vvnw( zR>%EA3MbAL-8uu#z%~PWdO4E%fBtp8i1%p(I5i6zm1eyf?AOm1-*eSvT@gIRigBxeyM;UkrDOyCm literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..45376a3b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +**/xcuserdata +*.DS_Store \ No newline at end of file diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj new file mode 100644 index 00000000..ec0621ec --- /dev/null +++ b/MyMovies.xcodeproj/project.pbxproj @@ -0,0 +1,607 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 7D35A404296E22430054954E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A403296E22430054954E /* AppDelegate.swift */; }; + 7D35A406296E22430054954E /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A405296E22430054954E /* SceneDelegate.swift */; }; + 7D35A408296E22430054954E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A407296E22430054954E /* ViewController.swift */; }; + 7D35A40B296E22430054954E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7D35A409296E22430054954E /* Main.storyboard */; }; + 7D35A40D296E22450054954E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7D35A40C296E22450054954E /* Assets.xcassets */; }; + 7D35A410296E22450054954E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7D35A40E296E22450054954E /* LaunchScreen.storyboard */; }; + 7D35A41B296E22450054954E /* MyMoviesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A41A296E22450054954E /* MyMoviesTests.swift */; }; + 7D35A425296E22450054954E /* MyMoviesUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A424296E22450054954E /* MyMoviesUITests.swift */; }; + 7D35A427296E22450054954E /* MyMoviesUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A426296E22450054954E /* MyMoviesUITestsLaunchTests.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 7D35A417296E22450054954E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7D35A3F8296E22430054954E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7D35A3FF296E22430054954E; + remoteInfo = MyMovies; + }; + 7D35A421296E22450054954E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7D35A3F8296E22430054954E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7D35A3FF296E22430054954E; + remoteInfo = MyMovies; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 7D35A400296E22430054954E /* MyMovies.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MyMovies.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 7D35A403296E22430054954E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7D35A405296E22430054954E /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 7D35A407296E22430054954E /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 7D35A40A296E22430054954E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 7D35A40C296E22450054954E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 7D35A40F296E22450054954E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 7D35A411296E22450054954E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7D35A416296E22450054954E /* MyMoviesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MyMoviesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 7D35A41A296E22450054954E /* MyMoviesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyMoviesTests.swift; sourceTree = ""; }; + 7D35A420296E22450054954E /* MyMoviesUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MyMoviesUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 7D35A424296E22450054954E /* MyMoviesUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyMoviesUITests.swift; sourceTree = ""; }; + 7D35A426296E22450054954E /* MyMoviesUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyMoviesUITestsLaunchTests.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 7D35A3FD296E22430054954E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7D35A413296E22450054954E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7D35A41D296E22450054954E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 7D35A3F7296E22430054954E = { + isa = PBXGroup; + children = ( + 7D35A402296E22430054954E /* MyMovies */, + 7D35A419296E22450054954E /* MyMoviesTests */, + 7D35A423296E22450054954E /* MyMoviesUITests */, + 7D35A401296E22430054954E /* Products */, + ); + sourceTree = ""; + }; + 7D35A401296E22430054954E /* Products */ = { + isa = PBXGroup; + children = ( + 7D35A400296E22430054954E /* MyMovies.app */, + 7D35A416296E22450054954E /* MyMoviesTests.xctest */, + 7D35A420296E22450054954E /* MyMoviesUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 7D35A402296E22430054954E /* MyMovies */ = { + isa = PBXGroup; + children = ( + 7D35A403296E22430054954E /* AppDelegate.swift */, + 7D35A405296E22430054954E /* SceneDelegate.swift */, + 7D35A407296E22430054954E /* ViewController.swift */, + 7D35A409296E22430054954E /* Main.storyboard */, + 7D35A40C296E22450054954E /* Assets.xcassets */, + 7D35A40E296E22450054954E /* LaunchScreen.storyboard */, + 7D35A411296E22450054954E /* Info.plist */, + ); + path = MyMovies; + sourceTree = ""; + }; + 7D35A419296E22450054954E /* MyMoviesTests */ = { + isa = PBXGroup; + children = ( + 7D35A41A296E22450054954E /* MyMoviesTests.swift */, + ); + path = MyMoviesTests; + sourceTree = ""; + }; + 7D35A423296E22450054954E /* MyMoviesUITests */ = { + isa = PBXGroup; + children = ( + 7D35A424296E22450054954E /* MyMoviesUITests.swift */, + 7D35A426296E22450054954E /* MyMoviesUITestsLaunchTests.swift */, + ); + path = MyMoviesUITests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 7D35A3FF296E22430054954E /* MyMovies */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7D35A42A296E22450054954E /* Build configuration list for PBXNativeTarget "MyMovies" */; + buildPhases = ( + 7D35A3FC296E22430054954E /* Sources */, + 7D35A3FD296E22430054954E /* Frameworks */, + 7D35A3FE296E22430054954E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MyMovies; + productName = MyMovies; + productReference = 7D35A400296E22430054954E /* MyMovies.app */; + productType = "com.apple.product-type.application"; + }; + 7D35A415296E22450054954E /* MyMoviesTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7D35A42D296E22450054954E /* Build configuration list for PBXNativeTarget "MyMoviesTests" */; + buildPhases = ( + 7D35A412296E22450054954E /* Sources */, + 7D35A413296E22450054954E /* Frameworks */, + 7D35A414296E22450054954E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7D35A418296E22450054954E /* PBXTargetDependency */, + ); + name = MyMoviesTests; + productName = MyMoviesTests; + productReference = 7D35A416296E22450054954E /* MyMoviesTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 7D35A41F296E22450054954E /* MyMoviesUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7D35A430296E22450054954E /* Build configuration list for PBXNativeTarget "MyMoviesUITests" */; + buildPhases = ( + 7D35A41C296E22450054954E /* Sources */, + 7D35A41D296E22450054954E /* Frameworks */, + 7D35A41E296E22450054954E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7D35A422296E22450054954E /* PBXTargetDependency */, + ); + name = MyMoviesUITests; + productName = MyMoviesUITests; + productReference = 7D35A420296E22450054954E /* MyMoviesUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7D35A3F8296E22430054954E /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1420; + LastUpgradeCheck = 1420; + TargetAttributes = { + 7D35A3FF296E22430054954E = { + CreatedOnToolsVersion = 14.2; + }; + 7D35A415296E22450054954E = { + CreatedOnToolsVersion = 14.2; + TestTargetID = 7D35A3FF296E22430054954E; + }; + 7D35A41F296E22450054954E = { + CreatedOnToolsVersion = 14.2; + TestTargetID = 7D35A3FF296E22430054954E; + }; + }; + }; + buildConfigurationList = 7D35A3FB296E22430054954E /* Build configuration list for PBXProject "MyMovies" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 7D35A3F7296E22430054954E; + productRefGroup = 7D35A401296E22430054954E /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 7D35A3FF296E22430054954E /* MyMovies */, + 7D35A415296E22450054954E /* MyMoviesTests */, + 7D35A41F296E22450054954E /* MyMoviesUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7D35A3FE296E22430054954E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7D35A410296E22450054954E /* LaunchScreen.storyboard in Resources */, + 7D35A40D296E22450054954E /* Assets.xcassets in Resources */, + 7D35A40B296E22430054954E /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7D35A414296E22450054954E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7D35A41E296E22450054954E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 7D35A3FC296E22430054954E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7D35A408296E22430054954E /* ViewController.swift in Sources */, + 7D35A404296E22430054954E /* AppDelegate.swift in Sources */, + 7D35A406296E22430054954E /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7D35A412296E22450054954E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7D35A41B296E22450054954E /* MyMoviesTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7D35A41C296E22450054954E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7D35A427296E22450054954E /* MyMoviesUITestsLaunchTests.swift in Sources */, + 7D35A425296E22450054954E /* MyMoviesUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 7D35A418296E22450054954E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7D35A3FF296E22430054954E /* MyMovies */; + targetProxy = 7D35A417296E22450054954E /* PBXContainerItemProxy */; + }; + 7D35A422296E22450054954E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7D35A3FF296E22430054954E /* MyMovies */; + targetProxy = 7D35A421296E22450054954E /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 7D35A409296E22430054954E /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 7D35A40A296E22430054954E /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 7D35A40E296E22450054954E /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 7D35A40F296E22450054954E /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 7D35A428296E22450054954E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 7D35A429296E22450054954E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 7D35A42B296E22450054954E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = T2QW6534L6; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MyMovies/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.daresay.caioambrosio.MyMovies; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7D35A42C296E22450054954E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = T2QW6534L6; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MyMovies/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.daresay.caioambrosio.MyMovies; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 7D35A42E296E22450054954E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = T2QW6534L6; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.daresay.caioambrosio.MyMoviesTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MyMovies.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MyMovies"; + }; + name = Debug; + }; + 7D35A42F296E22450054954E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = T2QW6534L6; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.daresay.caioambrosio.MyMoviesTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MyMovies.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MyMovies"; + }; + name = Release; + }; + 7D35A431296E22450054954E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = T2QW6534L6; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.daresay.caioambrosio.MyMoviesUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = MyMovies; + }; + name = Debug; + }; + 7D35A432296E22450054954E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = T2QW6534L6; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.daresay.caioambrosio.MyMoviesUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = MyMovies; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 7D35A3FB296E22430054954E /* Build configuration list for PBXProject "MyMovies" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7D35A428296E22450054954E /* Debug */, + 7D35A429296E22450054954E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7D35A42A296E22450054954E /* Build configuration list for PBXNativeTarget "MyMovies" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7D35A42B296E22450054954E /* Debug */, + 7D35A42C296E22450054954E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7D35A42D296E22450054954E /* Build configuration list for PBXNativeTarget "MyMoviesTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7D35A42E296E22450054954E /* Debug */, + 7D35A42F296E22450054954E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7D35A430296E22450054954E /* Build configuration list for PBXNativeTarget "MyMoviesUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7D35A431296E22450054954E /* Debug */, + 7D35A432296E22450054954E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 7D35A3F8296E22430054954E /* Project object */; +} diff --git a/MyMovies.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/MyMovies.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/MyMovies.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/MyMovies.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/MyMovies.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/MyMovies.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/MyMovies.xcodeproj/project.xcworkspace/xcuserdata/caioamb.xcuserdatad/UserInterfaceState.xcuserstate b/MyMovies.xcodeproj/project.xcworkspace/xcuserdata/caioamb.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..2cc6520dc153d7f83e665dde01715fa952ded7ac GIT binary patch literal 11845 zcmd5?34Bw<)}NW1rdgWihBiynrb*gDLFf{?qd=FmP-sh~3#BX}ZExE^nv^6h6a@!2 z1VIHsRM1CTa6v^y#TA!_KA(zPLF57M8;a{g#GUWldy}-qzW4cUzu(v2uSxEmGiT16 zIm>^}OjD=Zd;)&h*}VbT2UKXg8V3eE=4QRDs&m5=t{H}tw$TsMzjrW zN4KHdQ6IV!-G%N(52C&3A@neM1U-%ppy$yG=x^vCdIKFo|3L4c571}mBszt@LZ{L9 z=nPijXspI8*5FY%2FGG8)?qz1;5cl=@i+mSa3VJ2WIPV1;Y^%`^YKNv3eUpT_+mU8 z*Wg-QhwE`WcHzZ%33lTS?7?2#gIC~7@TK?~ycVy+*W&f~2D}k(#W&;a_%_^!`|;g) zH@*iyi1*@0@IL$oeiIMiL-;U$3m?I6W=_!vHpPvCd(yZAl)3H}tH!e{Ug_$>Yr z|Ac=g5)whAL_v%so+J9WUHDoSnBnydyG?O;6gt$p3 z36L(*O;(UqssCc02w4KBPgTD4|RribYx~ zp-x)jU}MYerE|Q#CH_ulGiUcUcXe={fP+oR%gHJ$C@#)R%PuI&P0P+KC`c|^7HMXfsH5yrQeO>Q35idL}W%5WJO6RnMP14jifRvrwXd1QB-v|8jZ%F zR5TWiLl>a&XaY*3(Ns-yshv)vjdUJeNF6j8#47f2&Ly2*mnYyaY7V%%!QfjQtkKT3 zI=kF~(iT_1>l56-U*`4I&93k}Sk3G%Pl?wP;FbqwbImXf|9qh{=c$wrO4NXE9q4ZvKA!U2fWUAq3nAgzkZQ;^|&#Y>nm-qUbuFg)cFOWW*({P#) z%OYe)>3ygemC#XC+lNX~8P(IVH2DxJ2VJ-`UaW<}xaKD!tt< z&Y!-Vm%S5&9zfOTVl*4oG>S&%advPG;!nzHQ zjWC&zLasyCqx2m+cpni%<|cF#ZzDIN&1ef9M=zk``_Wc(GrEONpxHF%|Cx>ag}0F% zG;OGj^rM|<7xK|ennBZP%Kx%22_`kBz;HaIu-)iB-lXn9_fi|3*oXFjNoD;>llr$y zvkyHwd}*FQPa|DFdJ;W__R~C?PYe3dGw50L9Gyfb(?ULXB|`4{L(CV0y&q7MgUuO< zpu*`7K*Vbqo(770#EwG0F(dUGPR|bzQp$_yB|f^Zph>;xWqKh^9zd^x`@V)==ff}W zvw|Lz7yRa=7l~&3CK{**mw~L-hu%cBAx?+UkuayX=@g!m8XZH&(TTah$sh2!JZ*HU zgPl~6Z_m!lE6z+S%gnN+WoH!^r4`un>}k1~nYsA|Ib~&e1*HyGXS>(K`CT1d5cs`5 zhtt=Qn=Ow2E_zP@mqCB75jgjwchUPotQMa`QhkU%=B4_G7WJY}Xff{<|04l-<$R7_ z7nD-mWY`%}$`$C#K0cU)#{C+7i*&u{8(P|nzN2NdWC5&86=WDuyl{vL(f@!k((OiP z(U0gS^fUSe{fd4=zoT;)(do3D&Y&}C1+Ao2bQZ0q7w<-+F$4b*Sc)UD49l?sD*=tn zhW{E`OY3MookJVwTw2oT@|U_@Z7zs>fFc4ysE0J^YUZjzv#nlVhlAA(eO>McAkysP z0ziLW!yFgnVUEuatk|Q*KC|BGb2)j~HH>#C^MK!ol)@$el5(KS$IT6Q4mXPul{q_H zZm@mrPFK)Y@gTJmYY7Y-c`+8 zWuy3I%b74yNPyVF2e(m_p`(M#iBs?x-t6FAFHWWNd1nBai!Z{&lH2fv9TZ6UuGwgDAI?F=bP-@C7-tYD?Gm6!dT8j$xD=(|iKpPHxDXfN zVqAjlw23y;7Ru39+D6;&#ASFI*iJc~%G(fi(ZygjzX1tvJ;@8fQbl}N<^zKTjq)!- zIJ?%_!~<2I*X`zfj)E-P#N7N+TUuUmPF7mB-IkkHR9pbQT#}WWT{h7^F|QyO&_tLc z^e%S#<~aTJE{U`uN-}pk|0XP6ulSQV|pX z3(?rP!BM?lcazgs9vaob8V1`0iH0=kF1aLgf+ch11j{A1f{F0cmYI{Ya%D)pb1*=& zJ8=V^iyQGgJRdK>3+WQ-rXAEny|j}qy%Ut`#7(#vx4^GfP^yn!MX#o7;nzAo0I@K5 zO}lqlm9yK`CPX*kRrs+8_7AWn0IIrx%k2sPYMtY13ABqn%RT;1@FTBpkSyGD2pK;> zxH}(wMBK!83QeUG2LM6frPzo4)K3Gns~>maZoG_k)63}od1EoXvnXW*3c%1aG z&Oy%5o+8`|UOCO@?dlX80BiX}Bd)}kqx7A46}}8pyqYef%V`f?u@hf`uf$j3E9fQk zQo0g8VdU5{mpj0T`=nyP?k${<-ZXp)43EUWhA36%0T4a{u|ed)wbYdhmJ{X^l4k?H z4(R|?m53HGjBkw>R1YXIM7s%ZhKP=Dq^o-I7JAvg?E|R~--2(&+h9Ki+YR1ytNCim zY}j~ke%O-DT>-8`kQkO60C}LOxf#MP*TN^=h5lyV z`KrBsunezffrzlVmU8Ihze^g$j?x(ulLjJXa%F9&GtfR4c81*Y>98B^RfN+Hiw0ED zYUN1mx#gWs4`1F91jyondGW%i*}zbv>{BQPVq&#_zMqCbKYd)E!T?4Qg5`DPwcycy zmJGxvnEpJk*ptA^Z0%E67KAv7eDFYW%IH7OXH>p0Df@<_yZXjn&^K;eG7A3njh~QQ zxKw;dF5K{d@T)f+hRsm+DxiF&fkHt#nhg8XTBt}ZM6mNge$<6lK=o-mU|Akw?u9DT zVW=*Bh(3XI{uBBg>PZ%;8Kq+zgybTKy>p>Pv=pjBE5QlYLOo~;z8&wtyTow`4hoYQ z{0@HMJH?c7JN^p<)Y9dhZZRaz>I%4B9r|~oRS^OOSE4`MkryJ;X^m=*&-AFgl8|h}c1^}30$nZK+;AFBE_5v#W}v^|bLWpl5RM>(?gUJ>i_bP{LB~+b zgN5Vun_M{mS%HTz0}=_?lE~)uyXkIv551S(cL#|f zu|x}>dcc5t=>7Bs_&m-dz~ROsu8fG>f~FWFzMBrq0X_pSo0s?(1Q#1pvMp=;JYKgm zA@vAZ+XQH&%iY3bl4_{>&gPbOK?VRz=IfJ#3r3O%z~+9EOj5{b`T%{9?(HY3;50AL zhrmg;@(VbMU*aNvaA;u#A&Z5q2pR>;0d3iPprGe!4!seTlmSScWYUM}=%XZyWRo0{ zi_|2K;`aFD&qU;e;LhPgz zCRawLk?EwI%pfyK1*rsOonMVwIB3D5$7#dxZnoDO01(BA@1bXh)8!c?2OSCOp4A%S zIoL*@qL0$2>2q`+ebT{ZgyZ%wWr_?3;bb_1i_2BbFW1HNG1%~sT2cqIsmCvnhQ`5B z>V)y><6s<5gwc(VDakxC-@zt;(+uWCaq}&7sdl=2ePjWj9sey&%w^}qze7S@vjbHU zCz=U0%l#CIT0-k_7Oin7k-2Gr|2E5zS-N6?(BrPFP7ZX`I&b?oA8n+s(wBnfeNdFSA`e4p_XvH99_CBC{~HH3!9#+R?IArqPM#WC(Ixw# zj`=oU(H*f|JL1a_s{`bDv4$%;$BX3e0Ey1GN+Hqpk(YS09J!It(!s>%5Dj{GAU;T5 z4I0p!^aJ`%(16|-4d@WB$iwvAK?8bQFrfGT%LeoT`4slq9(m*NB?{#xx zokK+aqk_8z~nHw zZ~z;o?_=^HnGAUmlCFT66m%eFG7L3^naUp^v;abyrHL3fyu=}m9Q)tBMDiZX%H#@V z6jU!*%_yiv==6p-Bh(QR%ob}B)B(n%E~u&*QoaN!YFz=o_^p9bqSzo76geMUGyr&5 z%88@^U+z`Ih@D0qX!fH~` zH38UhRXY7k03U^$p+9Wqrg0w57d*U&Gf7dNFx)D)iy}?)bqX@@;I(Brl!!&Ts;-VE z&POp+nx!&1C33i-AfXBkRkIi_EX1-%qkQsyRy*#3@e|V0Gcxn?r<7GxR$bgMw{gKD z@xle5olHxng|BQ1g-!T9^r;TSos{ooO>lVI1OHysja+c;#*ZwPShyQ;{@Zjorfmi) z{%TGq^1-jg5N08ggr_7*4!1J0iY6B4<(6cp*)naVY1y{o+_d7{qJp%EC8c?`oQXN5 z1(_vcSPkW`!8-FH+r+HwoZR4TkcMS0{<1^49l!yc^#?bFgP4%YSsj(t^HZ^94-`zg z@S>4U_~8t$s%CIcSyx`_;co`O))p!PoTr)VyKwU0(f`y!TTw|VZ_-PK-%#l-rpjJB z+yjX@G;O-Ae8x;bAYcJ-fWmo1i>RwBw~Gi5R5QG(1}uWVTSB8jLH3-yg6!g)Y`%D2 zmX?{FZBNV0&&f-(=N9Btf63>~a**5t#pYizZ3$@O!f zNFK5rQIesA!P-9PKwKe_;*UleH=IBu7COUS5ZGuy@Ey5e{x2Iq?0NIUHZ6|ZH*Ye$ zUg#Kh7T>pU^1{sJa4eXanU&+rbT(xQB{=@d&N#R%mIr4bZYaH74hP}AaM|ktxa4&Z z9fb>BXW{zRIGl|q;|jQ-RUN#XRgdRD>1`eq>=!|Gz6G~J8Ey%bp4Z~L@E&}MAd*U^ z!gi^W)B}X}lBL8C7o#pC>&T7dVYmYIEP0W<4%ePekaywQ(}$2*Pcc%s-eh9TjFp+r z)G>{Wi*Yku%vRm?89fga0PVqVPDVaup7sGB5 zQw(t|bY%geXg^cLl%QJi9F)G3wVU0+R7#$IaSRHXq#JY&}5!Xd*iP#!(OT@N_+aeA|{31<} zrb#oU6Q$YGQt2$|Tq!4Qle(l!q#e?3=@rszr0b;Xr5mL;O1DV2N^h6$l0G3lAU!Dk zQ2M#_2kE&;bL5!FvdH<7Tx3V2FLF)f=EyCPH$~nYxhwMC$UTt{MDC4zJo3rN{gKZ^ zJ{S3|%q*KOyI58$tCuy%T(Tvy4w+ZBRQ9awCD{?#aoIbv_hcW-K9!x6eIff!_M==S zPm*WIGvzjUmb_3tOaHpz*QNO8T zR7TZkRl3Ti%2MU1idChmX{s5j3RRVAj;c{LU$s!xrs`I$R9&rFqgt!lq}r;wMYTR=ThgC;ZM^(pEpQ}!(zEXXo`c8FPbw+ho^;5Jgx;)w! zeOvT1(Z{2IP^;Am>hbC{b%xre&Qj;7^V9|E>FOEk3U!sbT0L7`tFBi!sF$c$sW+*2 zsUKG#R3B4+rv6&}t@@wp@72Gke^Z}hG0U)H*hy>!JC|)@Ti8~%on6fO*)Db&+rwVM zu4Ff}cd^g0C)m%~FW6J;R~nf{t1)Pd8k5GXv1%^TlxmtZEt=MtjWM^z9Ef>2=5%aB z>?N_+#9kY_A@+vYO|hF}pNf4Ub|Chx*tcVk#h!?LS4*@?ZIm`ztJkJzFVs%fPSqA^ zOSGlhY1(q_Ol_rhmiA(8yLPeGt@UU-wLWb?+pS%$U7@{HyGl#7*K42BzNY<$_A6b4 zZoIBY*P>ga+pW7-w@3H5?n&L>b+71N)xDt`&>hyD)P1M>LHDEXXWg%Qxt`UJ(#Ps` zdXwI)x9XGiqxD((LVb;XfxcDm*RRlDs$Zq2`ZfBs`fK$Y^w;Zq^}F>C>G$a$(?6kq zQGZx}M1NF&On*ZEuKs=fSNd=C-|0{5&*;zUf71VAkQ$7JOv4nz48vlB+t6d6hRY3C z8m=~MFkElgXt>d^#jxA3-|)KOsNuNb9m9KZC@vwcFm85SXWW%>H^l9UdmwIa+{1DE z;vS28BJQcUr{ms^`y%cqqsEwGEHoAy?Zz_WbmI(Tg>jW}jd7iEz41EZHsc+}dyRXH z4;c3vpEN#i{K$ARJ~Ez-H^rOdt?|k66XJ{Gr^PRbZ;f9X?~m__e>i@B{7dn##UG3R zC?Pr_CSgiKLxMA5al-0^s}pWZxFg~2gnJV1OW2q2X2PL_w-SCx_%Y${(n?{>bP2)`CP1&YAQ-SG1({$4eQ-!I@G{>~a)M4_PmYVz~YFcAjYr597 z!L-?Qlj&yDt)}g!S4=-ACMQlv%t*8)W+mDa%Mzz2&Pc3C+?2R2@vg-C67NrZFmYev zV~I~BK9%@p;-`tH6TeS9lX%vQ%m%Z`oMWD5USw`Ex0qYa?dHX1x4Fx_+`Phksd=sW zTJr|;_2!$+z2-gU2h4lT51XGezid8ee$D)bdBA+!{EqoO^9SaS%%7OQwn!}+OT1;Q z#b(K}sadrR+}};nq$qg7Fdg|Gp!9)ht+9qwsKat)no0n`m6!#D(f2S zTI)^LTddoxw_A5u`>gw{uUL;-zqFo9!byrGT~d5fYEpJmPEu}Ceo|3Vd1`m+^3>I- V*QeeTOlL&=X6&IvCVr>h_8%~-)-eD8 literal 0 HcmV?d00001 diff --git a/MyMovies.xcodeproj/xcuserdata/caioamb.xcuserdatad/xcschemes/xcschememanagement.plist b/MyMovies.xcodeproj/xcuserdata/caioamb.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 00000000..09d270b7 --- /dev/null +++ b/MyMovies.xcodeproj/xcuserdata/caioamb.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + MyMovies.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/MyMovies/.DS_Store b/MyMovies/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..3a7869248ad4bf94eb5ee9ba54e7c0c4c43dc0c5 GIT binary patch literal 6148 zcmeHKyK2Kg5Zp~v2-3K8dB5O4IE-AT$`1s`DGUY`#({LI{9XC9%svP)CxaU|W)^nm zZg1y=rqF6dMAxU?N@O7-J={>v7Fx4&^TwVsqd++B_>#SR%5iu6I5gGs3FG$iD&O$& zKYu%px85;-GqY5H3Qz$mKn1A4oC4N+Vf{-WBNd Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/MyMovies/Assets.xcassets/AccentColor.colorset/Contents.json b/MyMovies/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/MyMovies/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MyMovies/Assets.xcassets/AppIcon.appiconset/Contents.json b/MyMovies/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..13613e3e --- /dev/null +++ b/MyMovies/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MyMovies/Assets.xcassets/Contents.json b/MyMovies/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/MyMovies/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MyMovies/Base.lproj/LaunchScreen.storyboard b/MyMovies/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..865e9329 --- /dev/null +++ b/MyMovies/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MyMovies/Base.lproj/Main.storyboard b/MyMovies/Base.lproj/Main.storyboard new file mode 100644 index 00000000..25a76385 --- /dev/null +++ b/MyMovies/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MyMovies/Info.plist b/MyMovies/Info.plist new file mode 100644 index 00000000..dd3c9afd --- /dev/null +++ b/MyMovies/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/MyMovies/SceneDelegate.swift b/MyMovies/SceneDelegate.swift new file mode 100644 index 00000000..ba0c0159 --- /dev/null +++ b/MyMovies/SceneDelegate.swift @@ -0,0 +1,52 @@ +// +// SceneDelegate.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/MyMovies/ViewController.swift b/MyMovies/ViewController.swift new file mode 100644 index 00000000..03f1edc7 --- /dev/null +++ b/MyMovies/ViewController.swift @@ -0,0 +1,19 @@ +// +// ViewController.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import UIKit + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + } + + +} + diff --git a/MyMoviesTests/MyMoviesTests.swift b/MyMoviesTests/MyMoviesTests.swift new file mode 100644 index 00000000..813ad845 --- /dev/null +++ b/MyMoviesTests/MyMoviesTests.swift @@ -0,0 +1,36 @@ +// +// MyMoviesTests.swift +// MyMoviesTests +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import XCTest +@testable import MyMovies + +final class MyMoviesTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/MyMoviesUITests/MyMoviesUITests.swift b/MyMoviesUITests/MyMoviesUITests.swift new file mode 100644 index 00000000..d93ba831 --- /dev/null +++ b/MyMoviesUITests/MyMoviesUITests.swift @@ -0,0 +1,41 @@ +// +// MyMoviesUITests.swift +// MyMoviesUITests +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import XCTest + +final class MyMoviesUITests: 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 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/MyMoviesUITests/MyMoviesUITestsLaunchTests.swift b/MyMoviesUITests/MyMoviesUITestsLaunchTests.swift new file mode 100644 index 00000000..3f06c257 --- /dev/null +++ b/MyMoviesUITests/MyMoviesUITestsLaunchTests.swift @@ -0,0 +1,32 @@ +// +// MyMoviesUITestsLaunchTests.swift +// MyMoviesUITests +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import XCTest + +final class MyMoviesUITestsLaunchTests: 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) + } +} From 548193f002b55ca9a1bb49cb82ae6682230426a7 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Tue, 10 Jan 2023 19:55:25 -0300 Subject: [PATCH 02/65] Add basic coordnator structure --- MyMovies.xcodeproj/project.pbxproj | 30 ++++++++++++---------- MyMovies/Base.lproj/Main.storyboard | 24 ----------------- MyMovies/Coordinator/Coordinator.swift | 18 +++++++++++++ MyMovies/Coordinator/MainCoordinator.swift | 18 +++++++++++++ MyMovies/Info.plist | 2 -- MyMovies/SceneDelegate.swift | 17 ++++++++++-- MyMovies/ViewController.swift | 4 +-- 7 files changed, 68 insertions(+), 45 deletions(-) delete mode 100644 MyMovies/Base.lproj/Main.storyboard create mode 100644 MyMovies/Coordinator/Coordinator.swift create mode 100644 MyMovies/Coordinator/MainCoordinator.swift diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index ec0621ec..48f5d2e0 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -10,12 +10,13 @@ 7D35A404296E22430054954E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A403296E22430054954E /* AppDelegate.swift */; }; 7D35A406296E22430054954E /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A405296E22430054954E /* SceneDelegate.swift */; }; 7D35A408296E22430054954E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A407296E22430054954E /* ViewController.swift */; }; - 7D35A40B296E22430054954E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7D35A409296E22430054954E /* Main.storyboard */; }; 7D35A40D296E22450054954E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7D35A40C296E22450054954E /* Assets.xcassets */; }; 7D35A410296E22450054954E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7D35A40E296E22450054954E /* LaunchScreen.storyboard */; }; 7D35A41B296E22450054954E /* MyMoviesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A41A296E22450054954E /* MyMoviesTests.swift */; }; 7D35A425296E22450054954E /* MyMoviesUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A424296E22450054954E /* MyMoviesUITests.swift */; }; 7D35A427296E22450054954E /* MyMoviesUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A426296E22450054954E /* MyMoviesUITestsLaunchTests.swift */; }; + 7D35A435296E233F0054954E /* MainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A434296E233F0054954E /* MainCoordinator.swift */; }; + 7D35A437296E23740054954E /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A436296E23740054954E /* Coordinator.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -40,7 +41,6 @@ 7D35A403296E22430054954E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7D35A405296E22430054954E /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 7D35A407296E22430054954E /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - 7D35A40A296E22430054954E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 7D35A40C296E22450054954E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 7D35A40F296E22450054954E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 7D35A411296E22450054954E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -49,6 +49,8 @@ 7D35A420296E22450054954E /* MyMoviesUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MyMoviesUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 7D35A424296E22450054954E /* MyMoviesUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyMoviesUITests.swift; sourceTree = ""; }; 7D35A426296E22450054954E /* MyMoviesUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyMoviesUITestsLaunchTests.swift; sourceTree = ""; }; + 7D35A434296E233F0054954E /* MainCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCoordinator.swift; sourceTree = ""; }; + 7D35A436296E23740054954E /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -99,10 +101,10 @@ 7D35A402296E22430054954E /* MyMovies */ = { isa = PBXGroup; children = ( + 7D35A433296E232E0054954E /* Coordinator */, 7D35A403296E22430054954E /* AppDelegate.swift */, 7D35A405296E22430054954E /* SceneDelegate.swift */, 7D35A407296E22430054954E /* ViewController.swift */, - 7D35A409296E22430054954E /* Main.storyboard */, 7D35A40C296E22450054954E /* Assets.xcassets */, 7D35A40E296E22450054954E /* LaunchScreen.storyboard */, 7D35A411296E22450054954E /* Info.plist */, @@ -127,6 +129,15 @@ path = MyMoviesUITests; sourceTree = ""; }; + 7D35A433296E232E0054954E /* Coordinator */ = { + isa = PBXGroup; + children = ( + 7D35A434296E233F0054954E /* MainCoordinator.swift */, + 7D35A436296E23740054954E /* Coordinator.swift */, + ); + path = Coordinator; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -233,7 +244,6 @@ files = ( 7D35A410296E22450054954E /* LaunchScreen.storyboard in Resources */, 7D35A40D296E22450054954E /* Assets.xcassets in Resources */, - 7D35A40B296E22430054954E /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -259,6 +269,8 @@ buildActionMask = 2147483647; files = ( 7D35A408296E22430054954E /* ViewController.swift in Sources */, + 7D35A435296E233F0054954E /* MainCoordinator.swift in Sources */, + 7D35A437296E23740054954E /* Coordinator.swift in Sources */, 7D35A404296E22430054954E /* AppDelegate.swift in Sources */, 7D35A406296E22430054954E /* SceneDelegate.swift in Sources */, ); @@ -297,14 +309,6 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ - 7D35A409296E22430054954E /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 7D35A40A296E22430054954E /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; 7D35A40E296E22450054954E /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -442,7 +446,6 @@ INFOPLIST_FILE = MyMovies/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; LD_RUNPATH_SEARCH_PATHS = ( @@ -470,7 +473,6 @@ INFOPLIST_FILE = MyMovies/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/MyMovies/Base.lproj/Main.storyboard b/MyMovies/Base.lproj/Main.storyboard deleted file mode 100644 index 25a76385..00000000 --- a/MyMovies/Base.lproj/Main.storyboard +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/MyMovies/Coordinator/Coordinator.swift b/MyMovies/Coordinator/Coordinator.swift new file mode 100644 index 00000000..de205ad5 --- /dev/null +++ b/MyMovies/Coordinator/Coordinator.swift @@ -0,0 +1,18 @@ +// +// Coordinator.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import UIKit + +protocol Coordinator { + var navigationController: UINavigationController? { get set } + + func start() +} + +protocol Coordinating { + var coordinator: Coordinator? { get set } +} diff --git a/MyMovies/Coordinator/MainCoordinator.swift b/MyMovies/Coordinator/MainCoordinator.swift new file mode 100644 index 00000000..d9559765 --- /dev/null +++ b/MyMovies/Coordinator/MainCoordinator.swift @@ -0,0 +1,18 @@ +// +// MainCoordinator.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import UIKit + +class MainCoordinator: Coordinator { + var navigationController: UINavigationController? + + func start() { + let viewController = ViewController() + + navigationController?.setViewControllers([viewController], animated: false) + } +} diff --git a/MyMovies/Info.plist b/MyMovies/Info.plist index dd3c9afd..0eb786dc 100644 --- a/MyMovies/Info.plist +++ b/MyMovies/Info.plist @@ -15,8 +15,6 @@ Default Configuration UISceneDelegateClassName $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main diff --git a/MyMovies/SceneDelegate.swift b/MyMovies/SceneDelegate.swift index ba0c0159..8e4f780f 100644 --- a/MyMovies/SceneDelegate.swift +++ b/MyMovies/SceneDelegate.swift @@ -11,12 +11,25 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } + guard let windowScene = (scene as? UIWindowScene) else { return } + + let navigationController = UINavigationController() + + let coordinator = MainCoordinator() + coordinator.navigationController = navigationController + + let window = UIWindow(frame: windowScene.coordinateSpace.bounds) + + window.windowScene = windowScene + window.rootViewController = navigationController + window.makeKeyAndVisible() + self.window = window + + coordinator.start() } func sceneDidDisconnect(_ scene: UIScene) { diff --git a/MyMovies/ViewController.swift b/MyMovies/ViewController.swift index 03f1edc7..0c7f070f 100644 --- a/MyMovies/ViewController.swift +++ b/MyMovies/ViewController.swift @@ -11,9 +11,7 @@ class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - // Do any additional setup after loading the view. + self.view.backgroundColor = .red } - - } From 1c2048b39c66e6d1999bdb6a956372439134cafe Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Tue, 10 Jan 2023 19:56:29 -0300 Subject: [PATCH 03/65] Fix .gitignore --- .gitignore | 1 + .../UserInterfaceState.xcuserstate | Bin 11845 -> 0 bytes 2 files changed, 1 insertion(+) delete mode 100644 MyMovies.xcodeproj/project.xcworkspace/xcuserdata/caioamb.xcuserdatad/UserInterfaceState.xcuserstate diff --git a/.gitignore b/.gitignore index 45376a3b..171abf2c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ **/xcuserdata +**.xcuserstate *.DS_Store \ No newline at end of file diff --git a/MyMovies.xcodeproj/project.xcworkspace/xcuserdata/caioamb.xcuserdatad/UserInterfaceState.xcuserstate b/MyMovies.xcodeproj/project.xcworkspace/xcuserdata/caioamb.xcuserdatad/UserInterfaceState.xcuserstate deleted file mode 100644 index 2cc6520dc153d7f83e665dde01715fa952ded7ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11845 zcmd5?34Bw<)}NW1rdgWihBiynrb*gDLFf{?qd=FmP-sh~3#BX}ZExE^nv^6h6a@!2 z1VIHsRM1CTa6v^y#TA!_KA(zPLF57M8;a{g#GUWldy}-qzW4cUzu(v2uSxEmGiT16 zIm>^}OjD=Zd;)&h*}VbT2UKXg8V3eE=4QRDs&m5=t{H}tw$TsMzjrW zN4KHdQ6IV!-G%N(52C&3A@neM1U-%ppy$yG=x^vCdIKFo|3L4c571}mBszt@LZ{L9 z=nPijXspI8*5FY%2FGG8)?qz1;5cl=@i+mSa3VJ2WIPV1;Y^%`^YKNv3eUpT_+mU8 z*Wg-QhwE`WcHzZ%33lTS?7?2#gIC~7@TK?~ycVy+*W&f~2D}k(#W&;a_%_^!`|;g) zH@*iyi1*@0@IL$oeiIMiL-;U$3m?I6W=_!vHpPvCd(yZAl)3H}tH!e{Ug_$>Yr z|Ac=g5)whAL_v%so+J9WUHDoSnBnydyG?O;6gt$p3 z36L(*O;(UqssCc02w4KBPgTD4|RribYx~ zp-x)jU}MYerE|Q#CH_ulGiUcUcXe={fP+oR%gHJ$C@#)R%PuI&P0P+KC`c|^7HMXfsH5yrQeO>Q35idL}W%5WJO6RnMP14jifRvrwXd1QB-v|8jZ%F zR5TWiLl>a&XaY*3(Ns-yshv)vjdUJeNF6j8#47f2&Ly2*mnYyaY7V%%!QfjQtkKT3 zI=kF~(iT_1>l56-U*`4I&93k}Sk3G%Pl?wP;FbqwbImXf|9qh{=c$wrO4NXE9q4ZvKA!U2fWUAq3nAgzkZQ;^|&#Y>nm-qUbuFg)cFOWW*({P#) z%OYe)>3ygemC#XC+lNX~8P(IVH2DxJ2VJ-`UaW<}xaKD!tt< z&Y!-Vm%S5&9zfOTVl*4oG>S&%advPG;!nzHQ zjWC&zLasyCqx2m+cpni%<|cF#ZzDIN&1ef9M=zk``_Wc(GrEONpxHF%|Cx>ag}0F% zG;OGj^rM|<7xK|ennBZP%Kx%22_`kBz;HaIu-)iB-lXn9_fi|3*oXFjNoD;>llr$y zvkyHwd}*FQPa|DFdJ;W__R~C?PYe3dGw50L9Gyfb(?ULXB|`4{L(CV0y&q7MgUuO< zpu*`7K*Vbqo(770#EwG0F(dUGPR|bzQp$_yB|f^Zph>;xWqKh^9zd^x`@V)==ff}W zvw|Lz7yRa=7l~&3CK{**mw~L-hu%cBAx?+UkuayX=@g!m8XZH&(TTah$sh2!JZ*HU zgPl~6Z_m!lE6z+S%gnN+WoH!^r4`un>}k1~nYsA|Ib~&e1*HyGXS>(K`CT1d5cs`5 zhtt=Qn=Ow2E_zP@mqCB75jgjwchUPotQMa`QhkU%=B4_G7WJY}Xff{<|04l-<$R7_ z7nD-mWY`%}$`$C#K0cU)#{C+7i*&u{8(P|nzN2NdWC5&86=WDuyl{vL(f@!k((OiP z(U0gS^fUSe{fd4=zoT;)(do3D&Y&}C1+Ao2bQZ0q7w<-+F$4b*Sc)UD49l?sD*=tn zhW{E`OY3MookJVwTw2oT@|U_@Z7zs>fFc4ysE0J^YUZjzv#nlVhlAA(eO>McAkysP z0ziLW!yFgnVUEuatk|Q*KC|BGb2)j~HH>#C^MK!ol)@$el5(KS$IT6Q4mXPul{q_H zZm@mrPFK)Y@gTJmYY7Y-c`+8 zWuy3I%b74yNPyVF2e(m_p`(M#iBs?x-t6FAFHWWNd1nBai!Z{&lH2fv9TZ6UuGwgDAI?F=bP-@C7-tYD?Gm6!dT8j$xD=(|iKpPHxDXfN zVqAjlw23y;7Ru39+D6;&#ASFI*iJc~%G(fi(ZygjzX1tvJ;@8fQbl}N<^zKTjq)!- zIJ?%_!~<2I*X`zfj)E-P#N7N+TUuUmPF7mB-IkkHR9pbQT#}WWT{h7^F|QyO&_tLc z^e%S#<~aTJE{U`uN-}pk|0XP6ulSQV|pX z3(?rP!BM?lcazgs9vaob8V1`0iH0=kF1aLgf+ch11j{A1f{F0cmYI{Ya%D)pb1*=& zJ8=V^iyQGgJRdK>3+WQ-rXAEny|j}qy%Ut`#7(#vx4^GfP^yn!MX#o7;nzAo0I@K5 zO}lqlm9yK`CPX*kRrs+8_7AWn0IIrx%k2sPYMtY13ABqn%RT;1@FTBpkSyGD2pK;> zxH}(wMBK!83QeUG2LM6frPzo4)K3Gns~>maZoG_k)63}od1EoXvnXW*3c%1aG z&Oy%5o+8`|UOCO@?dlX80BiX}Bd)}kqx7A46}}8pyqYef%V`f?u@hf`uf$j3E9fQk zQo0g8VdU5{mpj0T`=nyP?k${<-ZXp)43EUWhA36%0T4a{u|ed)wbYdhmJ{X^l4k?H z4(R|?m53HGjBkw>R1YXIM7s%ZhKP=Dq^o-I7JAvg?E|R~--2(&+h9Ki+YR1ytNCim zY}j~ke%O-DT>-8`kQkO60C}LOxf#MP*TN^=h5lyV z`KrBsunezffrzlVmU8Ihze^g$j?x(ulLjJXa%F9&GtfR4c81*Y>98B^RfN+Hiw0ED zYUN1mx#gWs4`1F91jyondGW%i*}zbv>{BQPVq&#_zMqCbKYd)E!T?4Qg5`DPwcycy zmJGxvnEpJk*ptA^Z0%E67KAv7eDFYW%IH7OXH>p0Df@<_yZXjn&^K;eG7A3njh~QQ zxKw;dF5K{d@T)f+hRsm+DxiF&fkHt#nhg8XTBt}ZM6mNge$<6lK=o-mU|Akw?u9DT zVW=*Bh(3XI{uBBg>PZ%;8Kq+zgybTKy>p>Pv=pjBE5QlYLOo~;z8&wtyTow`4hoYQ z{0@HMJH?c7JN^p<)Y9dhZZRaz>I%4B9r|~oRS^OOSE4`MkryJ;X^m=*&-AFgl8|h}c1^}30$nZK+;AFBE_5v#W}v^|bLWpl5RM>(?gUJ>i_bP{LB~+b zgN5Vun_M{mS%HTz0}=_?lE~)uyXkIv551S(cL#|f zu|x}>dcc5t=>7Bs_&m-dz~ROsu8fG>f~FWFzMBrq0X_pSo0s?(1Q#1pvMp=;JYKgm zA@vAZ+XQH&%iY3bl4_{>&gPbOK?VRz=IfJ#3r3O%z~+9EOj5{b`T%{9?(HY3;50AL zhrmg;@(VbMU*aNvaA;u#A&Z5q2pR>;0d3iPprGe!4!seTlmSScWYUM}=%XZyWRo0{ zi_|2K;`aFD&qU;e;LhPgz zCRawLk?EwI%pfyK1*rsOonMVwIB3D5$7#dxZnoDO01(BA@1bXh)8!c?2OSCOp4A%S zIoL*@qL0$2>2q`+ebT{ZgyZ%wWr_?3;bb_1i_2BbFW1HNG1%~sT2cqIsmCvnhQ`5B z>V)y><6s<5gwc(VDakxC-@zt;(+uWCaq}&7sdl=2ePjWj9sey&%w^}qze7S@vjbHU zCz=U0%l#CIT0-k_7Oin7k-2Gr|2E5zS-N6?(BrPFP7ZX`I&b?oA8n+s(wBnfeNdFSA`e4p_XvH99_CBC{~HH3!9#+R?IArqPM#WC(Ixw# zj`=oU(H*f|JL1a_s{`bDv4$%;$BX3e0Ey1GN+Hqpk(YS09J!It(!s>%5Dj{GAU;T5 z4I0p!^aJ`%(16|-4d@WB$iwvAK?8bQFrfGT%LeoT`4slq9(m*NB?{#xx zokK+aqk_8z~nHw zZ~z;o?_=^HnGAUmlCFT66m%eFG7L3^naUp^v;abyrHL3fyu=}m9Q)tBMDiZX%H#@V z6jU!*%_yiv==6p-Bh(QR%ob}B)B(n%E~u&*QoaN!YFz=o_^p9bqSzo76geMUGyr&5 z%88@^U+z`Ih@D0qX!fH~` zH38UhRXY7k03U^$p+9Wqrg0w57d*U&Gf7dNFx)D)iy}?)bqX@@;I(Brl!!&Ts;-VE z&POp+nx!&1C33i-AfXBkRkIi_EX1-%qkQsyRy*#3@e|V0Gcxn?r<7GxR$bgMw{gKD z@xle5olHxng|BQ1g-!T9^r;TSos{ooO>lVI1OHysja+c;#*ZwPShyQ;{@Zjorfmi) z{%TGq^1-jg5N08ggr_7*4!1J0iY6B4<(6cp*)naVY1y{o+_d7{qJp%EC8c?`oQXN5 z1(_vcSPkW`!8-FH+r+HwoZR4TkcMS0{<1^49l!yc^#?bFgP4%YSsj(t^HZ^94-`zg z@S>4U_~8t$s%CIcSyx`_;co`O))p!PoTr)VyKwU0(f`y!TTw|VZ_-PK-%#l-rpjJB z+yjX@G;O-Ae8x;bAYcJ-fWmo1i>RwBw~Gi5R5QG(1}uWVTSB8jLH3-yg6!g)Y`%D2 zmX?{FZBNV0&&f-(=N9Btf63>~a**5t#pYizZ3$@O!f zNFK5rQIesA!P-9PKwKe_;*UleH=IBu7COUS5ZGuy@Ey5e{x2Iq?0NIUHZ6|ZH*Ye$ zUg#Kh7T>pU^1{sJa4eXanU&+rbT(xQB{=@d&N#R%mIr4bZYaH74hP}AaM|ktxa4&Z z9fb>BXW{zRIGl|q;|jQ-RUN#XRgdRD>1`eq>=!|Gz6G~J8Ey%bp4Z~L@E&}MAd*U^ z!gi^W)B}X}lBL8C7o#pC>&T7dVYmYIEP0W<4%ePekaywQ(}$2*Pcc%s-eh9TjFp+r z)G>{Wi*Yku%vRm?89fga0PVqVPDVaup7sGB5 zQw(t|bY%geXg^cLl%QJi9F)G3wVU0+R7#$IaSRHXq#JY&}5!Xd*iP#!(OT@N_+aeA|{31<} zrb#oU6Q$YGQt2$|Tq!4Qle(l!q#e?3=@rszr0b;Xr5mL;O1DV2N^h6$l0G3lAU!Dk zQ2M#_2kE&;bL5!FvdH<7Tx3V2FLF)f=EyCPH$~nYxhwMC$UTt{MDC4zJo3rN{gKZ^ zJ{S3|%q*KOyI58$tCuy%T(Tvy4w+ZBRQ9awCD{?#aoIbv_hcW-K9!x6eIff!_M==S zPm*WIGvzjUmb_3tOaHpz*QNO8T zR7TZkRl3Ti%2MU1idChmX{s5j3RRVAj;c{LU$s!xrs`I$R9&rFqgt!lq}r;wMYTR=ThgC;ZM^(pEpQ}!(zEXXo`c8FPbw+ho^;5Jgx;)w! zeOvT1(Z{2IP^;Am>hbC{b%xre&Qj;7^V9|E>FOEk3U!sbT0L7`tFBi!sF$c$sW+*2 zsUKG#R3B4+rv6&}t@@wp@72Gke^Z}hG0U)H*hy>!JC|)@Ti8~%on6fO*)Db&+rwVM zu4Ff}cd^g0C)m%~FW6J;R~nf{t1)Pd8k5GXv1%^TlxmtZEt=MtjWM^z9Ef>2=5%aB z>?N_+#9kY_A@+vYO|hF}pNf4Ub|Chx*tcVk#h!?LS4*@?ZIm`ztJkJzFVs%fPSqA^ zOSGlhY1(q_Ol_rhmiA(8yLPeGt@UU-wLWb?+pS%$U7@{HyGl#7*K42BzNY<$_A6b4 zZoIBY*P>ga+pW7-w@3H5?n&L>b+71N)xDt`&>hyD)P1M>LHDEXXWg%Qxt`UJ(#Ps` zdXwI)x9XGiqxD((LVb;XfxcDm*RRlDs$Zq2`ZfBs`fK$Y^w;Zq^}F>C>G$a$(?6kq zQGZx}M1NF&On*ZEuKs=fSNd=C-|0{5&*;zUf71VAkQ$7JOv4nz48vlB+t6d6hRY3C z8m=~MFkElgXt>d^#jxA3-|)KOsNuNb9m9KZC@vwcFm85SXWW%>H^l9UdmwIa+{1DE z;vS28BJQcUr{ms^`y%cqqsEwGEHoAy?Zz_WbmI(Tg>jW}jd7iEz41EZHsc+}dyRXH z4;c3vpEN#i{K$ARJ~Ez-H^rOdt?|k66XJ{Gr^PRbZ;f9X?~m__e>i@B{7dn##UG3R zC?Pr_CSgiKLxMA5al-0^s}pWZxFg~2gnJV1OW2q2X2PL_w-SCx_%Y${(n?{>bP2)`CP1&YAQ-SG1({$4eQ-!I@G{>~a)M4_PmYVz~YFcAjYr597 z!L-?Qlj&yDt)}g!S4=-ACMQlv%t*8)W+mDa%Mzz2&Pc3C+?2R2@vg-C67NrZFmYev zV~I~BK9%@p;-`tH6TeS9lX%vQ%m%Z`oMWD5USw`Ex0qYa?dHX1x4Fx_+`Phksd=sW zTJr|;_2!$+z2-gU2h4lT51XGezid8ee$D)bdBA+!{EqoO^9SaS%%7OQwn!}+OT1;Q z#b(K}sadrR+}};nq$qg7Fdg|Gp!9)ht+9qwsKat)no0n`m6!#D(f2S zTI)^LTddoxw_A5u`>gw{uUL;-zqFo9!byrGT~d5fYEpJmPEu}Ceo|3Vd1`m+^3>I- V*QeeTOlL&=X6&IvCVr>h_8%~-)-eD8 From 769f1d77433c4ce4a469c3ae7dbb2972e2d709f6 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Tue, 10 Jan 2023 20:14:32 -0300 Subject: [PATCH 04/65] Create basic API structure and environment --- MyMovies.xcodeproj/project.pbxproj | 16 +++++++++++++++ MyMovies/Network/APIEnvironment.swift | 24 ++++++++++++++++++++++ MyMovies/Network/APIPath.swift | 29 +++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 MyMovies/Network/APIEnvironment.swift create mode 100644 MyMovies/Network/APIPath.swift diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index 48f5d2e0..fca426db 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -17,6 +17,8 @@ 7D35A427296E22450054954E /* MyMoviesUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A426296E22450054954E /* MyMoviesUITestsLaunchTests.swift */; }; 7D35A435296E233F0054954E /* MainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A434296E233F0054954E /* MainCoordinator.swift */; }; 7D35A437296E23740054954E /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A436296E23740054954E /* Coordinator.swift */; }; + 7D35A43A296E27AE0054954E /* APIEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A439296E27AE0054954E /* APIEnvironment.swift */; }; + 7D35A43C296E28490054954E /* APIPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A43B296E28490054954E /* APIPath.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -51,6 +53,8 @@ 7D35A426296E22450054954E /* MyMoviesUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyMoviesUITestsLaunchTests.swift; sourceTree = ""; }; 7D35A434296E233F0054954E /* MainCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCoordinator.swift; sourceTree = ""; }; 7D35A436296E23740054954E /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; }; + 7D35A439296E27AE0054954E /* APIEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIEnvironment.swift; sourceTree = ""; }; + 7D35A43B296E28490054954E /* APIPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIPath.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -101,6 +105,7 @@ 7D35A402296E22430054954E /* MyMovies */ = { isa = PBXGroup; children = ( + 7D35A438296E27930054954E /* Network */, 7D35A433296E232E0054954E /* Coordinator */, 7D35A403296E22430054954E /* AppDelegate.swift */, 7D35A405296E22430054954E /* SceneDelegate.swift */, @@ -138,6 +143,15 @@ path = Coordinator; sourceTree = ""; }; + 7D35A438296E27930054954E /* Network */ = { + isa = PBXGroup; + children = ( + 7D35A439296E27AE0054954E /* APIEnvironment.swift */, + 7D35A43B296E28490054954E /* APIPath.swift */, + ); + path = Network; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -270,7 +284,9 @@ files = ( 7D35A408296E22430054954E /* ViewController.swift in Sources */, 7D35A435296E233F0054954E /* MainCoordinator.swift in Sources */, + 7D35A43A296E27AE0054954E /* APIEnvironment.swift in Sources */, 7D35A437296E23740054954E /* Coordinator.swift in Sources */, + 7D35A43C296E28490054954E /* APIPath.swift in Sources */, 7D35A404296E22430054954E /* AppDelegate.swift in Sources */, 7D35A406296E22430054954E /* SceneDelegate.swift in Sources */, ); diff --git a/MyMovies/Network/APIEnvironment.swift b/MyMovies/Network/APIEnvironment.swift new file mode 100644 index 00000000..c4499a1b --- /dev/null +++ b/MyMovies/Network/APIEnvironment.swift @@ -0,0 +1,24 @@ +// +// APIEnvironment.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import Foundation + +enum APIEnvironment { + case development + case production + + func baseURL() -> String { + return "https://\(domain())" + } + + func domain() -> String { + switch self { + case .development, .production: + return "api.themoviedb.org" + } + } +} diff --git a/MyMovies/Network/APIPath.swift b/MyMovies/Network/APIPath.swift new file mode 100644 index 00000000..45162077 --- /dev/null +++ b/MyMovies/Network/APIPath.swift @@ -0,0 +1,29 @@ +// +// APIPath.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import Foundation + +#if DEBUG +let environment = APIEnvironment.development +#else +let environment = APIEnvironment.production +#endif + +let baseURL = environment.baseURL() + +struct APIPath { + func fetchTrending(mediaType: String, timeWindow: String) -> String { + return "\(baseURL)/\(apiVersion())/trending/\(mediaType)/\(timeWindow)" + } + + private func apiVersion() -> String { + return "3" + } +} + + + From 51cbbc143f181bdf37191009c6d5defd828fc860 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Tue, 10 Jan 2023 20:17:20 -0300 Subject: [PATCH 05/65] Create APIHandler, an extension for its decode and ServiceError, to enable API calls --- MyMovies.xcodeproj/project.pbxproj | 12 +++++++++ MyMovies/Network/APIHandler.swift | 20 +++++++++++++++ .../Network/ResponseHandler+Extension.swift | 25 +++++++++++++++++++ MyMovies/Network/ServiceError.swift | 12 +++++++++ 4 files changed, 69 insertions(+) create mode 100644 MyMovies/Network/APIHandler.swift create mode 100644 MyMovies/Network/ResponseHandler+Extension.swift create mode 100644 MyMovies/Network/ServiceError.swift diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index fca426db..9762a611 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -19,6 +19,9 @@ 7D35A437296E23740054954E /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A436296E23740054954E /* Coordinator.swift */; }; 7D35A43A296E27AE0054954E /* APIEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A439296E27AE0054954E /* APIEnvironment.swift */; }; 7D35A43C296E28490054954E /* APIPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A43B296E28490054954E /* APIPath.swift */; }; + 7D35A43E296E29000054954E /* APIHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A43D296E29000054954E /* APIHandler.swift */; }; + 7D35A440296E29280054954E /* ResponseHandler+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A43F296E29280054954E /* ResponseHandler+Extension.swift */; }; + 7D35A442296E293D0054954E /* ServiceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A441296E293D0054954E /* ServiceError.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -55,6 +58,9 @@ 7D35A436296E23740054954E /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; }; 7D35A439296E27AE0054954E /* APIEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIEnvironment.swift; sourceTree = ""; }; 7D35A43B296E28490054954E /* APIPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIPath.swift; sourceTree = ""; }; + 7D35A43D296E29000054954E /* APIHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIHandler.swift; sourceTree = ""; }; + 7D35A43F296E29280054954E /* ResponseHandler+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ResponseHandler+Extension.swift"; sourceTree = ""; }; + 7D35A441296E293D0054954E /* ServiceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceError.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -148,6 +154,9 @@ children = ( 7D35A439296E27AE0054954E /* APIEnvironment.swift */, 7D35A43B296E28490054954E /* APIPath.swift */, + 7D35A43D296E29000054954E /* APIHandler.swift */, + 7D35A43F296E29280054954E /* ResponseHandler+Extension.swift */, + 7D35A441296E293D0054954E /* ServiceError.swift */, ); path = Network; sourceTree = ""; @@ -282,11 +291,14 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7D35A442296E293D0054954E /* ServiceError.swift in Sources */, 7D35A408296E22430054954E /* ViewController.swift in Sources */, 7D35A435296E233F0054954E /* MainCoordinator.swift in Sources */, 7D35A43A296E27AE0054954E /* APIEnvironment.swift in Sources */, + 7D35A43E296E29000054954E /* APIHandler.swift in Sources */, 7D35A437296E23740054954E /* Coordinator.swift in Sources */, 7D35A43C296E28490054954E /* APIPath.swift in Sources */, + 7D35A440296E29280054954E /* ResponseHandler+Extension.swift in Sources */, 7D35A404296E22430054954E /* AppDelegate.swift in Sources */, 7D35A406296E22430054954E /* SceneDelegate.swift in Sources */, ); diff --git a/MyMovies/Network/APIHandler.swift b/MyMovies/Network/APIHandler.swift new file mode 100644 index 00000000..6f965650 --- /dev/null +++ b/MyMovies/Network/APIHandler.swift @@ -0,0 +1,20 @@ +// +// APIHandler.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import Foundation + +protocol RequestHandler { + associatedtype RequestDataType + func makeRequest(from data: RequestDataType) -> URLRequest? +} + +protocol ResponseHandler { + associatedtype ResponseDataType + func parseResponse(data: Data, response: HTTPURLResponse) throws -> ResponseDataType +} + +typealias APIHandler = RequestHandler & ResponseHandler diff --git a/MyMovies/Network/ResponseHandler+Extension.swift b/MyMovies/Network/ResponseHandler+Extension.swift new file mode 100644 index 00000000..ed2dcaac --- /dev/null +++ b/MyMovies/Network/ResponseHandler+Extension.swift @@ -0,0 +1,25 @@ +// +// ResponseHandler+Extension.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import Foundation + +extension ResponseHandler { + func defaultParseResponse(data: Data, response: HTTPURLResponse) throws -> T { + let jsonDecoder = JSONDecoder() + jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase + if response.statusCode == 200 { + do { + let body = try jsonDecoder.decode(T.self, from: data) + return body + } catch { + throw ServiceError(message: "Error when decoding json: \(error.localizedDescription)") + } + } else { + throw ServiceError(message: "Error when communicating with API") + } + } +} diff --git a/MyMovies/Network/ServiceError.swift b/MyMovies/Network/ServiceError.swift new file mode 100644 index 00000000..01933bd9 --- /dev/null +++ b/MyMovies/Network/ServiceError.swift @@ -0,0 +1,12 @@ +// +// ServiceError.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import Foundation + +struct ServiceError: Error { + let message: String +} From a0633e189ce105298b20e526c83dcbe551725c10 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Tue, 10 Jan 2023 20:18:51 -0300 Subject: [PATCH 06/65] Add APILoader with basic structure to call API using URL Session --- MyMovies.xcodeproj/project.pbxproj | 4 +++ MyMovies/Network/APILoader.swift | 51 ++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 MyMovies/Network/APILoader.swift diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index 9762a611..87bff9c4 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 7D35A43E296E29000054954E /* APIHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A43D296E29000054954E /* APIHandler.swift */; }; 7D35A440296E29280054954E /* ResponseHandler+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A43F296E29280054954E /* ResponseHandler+Extension.swift */; }; 7D35A442296E293D0054954E /* ServiceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A441296E293D0054954E /* ServiceError.swift */; }; + 7D35A444296E29950054954E /* APILoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A443296E29950054954E /* APILoader.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -61,6 +62,7 @@ 7D35A43D296E29000054954E /* APIHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIHandler.swift; sourceTree = ""; }; 7D35A43F296E29280054954E /* ResponseHandler+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ResponseHandler+Extension.swift"; sourceTree = ""; }; 7D35A441296E293D0054954E /* ServiceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceError.swift; sourceTree = ""; }; + 7D35A443296E29950054954E /* APILoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APILoader.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -157,6 +159,7 @@ 7D35A43D296E29000054954E /* APIHandler.swift */, 7D35A43F296E29280054954E /* ResponseHandler+Extension.swift */, 7D35A441296E293D0054954E /* ServiceError.swift */, + 7D35A443296E29950054954E /* APILoader.swift */, ); path = Network; sourceTree = ""; @@ -298,6 +301,7 @@ 7D35A43E296E29000054954E /* APIHandler.swift in Sources */, 7D35A437296E23740054954E /* Coordinator.swift in Sources */, 7D35A43C296E28490054954E /* APIPath.swift in Sources */, + 7D35A444296E29950054954E /* APILoader.swift in Sources */, 7D35A440296E29280054954E /* ResponseHandler+Extension.swift in Sources */, 7D35A404296E22430054954E /* AppDelegate.swift in Sources */, 7D35A406296E22430054954E /* SceneDelegate.swift in Sources */, diff --git a/MyMovies/Network/APILoader.swift b/MyMovies/Network/APILoader.swift new file mode 100644 index 00000000..22c3ba76 --- /dev/null +++ b/MyMovies/Network/APILoader.swift @@ -0,0 +1,51 @@ +// +// APILoader.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import Foundation + +struct APILoader { + let apiHandler: T + let urlSession: URLSession + + init(apiHandler: T, urlSession: URLSession = .shared) { + self.apiHandler = apiHandler + self.urlSession = urlSession + } + + func loadAPIRequest(requestData: T.RequestDataType, completion: @escaping (T.ResponseDataType?, ServiceError?) -> ()) { + if let urlRequest = apiHandler.makeRequest(from: requestData) { + urlSession.dataTask(with: urlRequest) { data, response, error in + if let httpResponse = response as? HTTPURLResponse { + guard error == nil, + let responseData = data else { + completion( + nil, + ServiceError(message: "Service Error: \(error?.localizedDescription ?? "Unknow Error")") + ) + return + } + + do { + let parsedResponse = try self.apiHandler.parseResponse(data: responseData, response: httpResponse) + completion(parsedResponse, nil) + } catch { + if let error = error as? ServiceError { + completion(nil, error) + } else { + completion( + nil, + ServiceError(message: "Error when decoding json: \(error.localizedDescription)") + ) + } + } + } else { + completion(nil, ServiceError(message: "Error when performing request")) + } + }.resume() + } + } +} From ce40f6a8432c9c9eafc809debb9fe41238655a2b Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Tue, 10 Jan 2023 20:40:12 -0300 Subject: [PATCH 07/65] Add new TrendingAPI and TrendingDTOs to call API, also add implementation for API Key --- MyMovies.xcodeproj/project.pbxproj | 36 +++++++++++++++++++++++++++ MyMovies/Model/DTO/TrendingDTO.swift | 15 +++++++++++ MyMovies/Model/TrendingResult.swift | 25 +++++++++++++++++++ MyMovies/Network/APIEnvironment.swift | 7 ++++++ MyMovies/Network/APIPath.swift | 8 +++++- MyMovies/Service/TrendingAPI.swift | 36 +++++++++++++++++++++++++++ 6 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 MyMovies/Model/DTO/TrendingDTO.swift create mode 100644 MyMovies/Model/TrendingResult.swift create mode 100644 MyMovies/Service/TrendingAPI.swift diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index 87bff9c4..23977f4c 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -23,6 +23,9 @@ 7D35A440296E29280054954E /* ResponseHandler+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A43F296E29280054954E /* ResponseHandler+Extension.swift */; }; 7D35A442296E293D0054954E /* ServiceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A441296E293D0054954E /* ServiceError.swift */; }; 7D35A444296E29950054954E /* APILoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A443296E29950054954E /* APILoader.swift */; }; + 7D35A447296E2A390054954E /* TrendingAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A446296E2A390054954E /* TrendingAPI.swift */; }; + 7D35A44B296E2C920054954E /* TrendingDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A44A296E2C920054954E /* TrendingDTO.swift */; }; + 7D35A44D296E2CA80054954E /* TrendingResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A44C296E2CA80054954E /* TrendingResult.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -63,6 +66,9 @@ 7D35A43F296E29280054954E /* ResponseHandler+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ResponseHandler+Extension.swift"; sourceTree = ""; }; 7D35A441296E293D0054954E /* ServiceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceError.swift; sourceTree = ""; }; 7D35A443296E29950054954E /* APILoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APILoader.swift; sourceTree = ""; }; + 7D35A446296E2A390054954E /* TrendingAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingAPI.swift; sourceTree = ""; }; + 7D35A44A296E2C920054954E /* TrendingDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingDTO.swift; sourceTree = ""; }; + 7D35A44C296E2CA80054954E /* TrendingResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingResult.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -113,6 +119,8 @@ 7D35A402296E22430054954E /* MyMovies */ = { isa = PBXGroup; children = ( + 7D35A448296E2BDB0054954E /* Model */, + 7D35A445296E2A250054954E /* Service */, 7D35A438296E27930054954E /* Network */, 7D35A433296E232E0054954E /* Coordinator */, 7D35A403296E22430054954E /* AppDelegate.swift */, @@ -164,6 +172,31 @@ path = Network; sourceTree = ""; }; + 7D35A445296E2A250054954E /* Service */ = { + isa = PBXGroup; + children = ( + 7D35A446296E2A390054954E /* TrendingAPI.swift */, + ); + path = Service; + sourceTree = ""; + }; + 7D35A448296E2BDB0054954E /* Model */ = { + isa = PBXGroup; + children = ( + 7D35A449296E2C870054954E /* DTO */, + 7D35A44C296E2CA80054954E /* TrendingResult.swift */, + ); + path = Model; + sourceTree = ""; + }; + 7D35A449296E2C870054954E /* DTO */ = { + isa = PBXGroup; + children = ( + 7D35A44A296E2C920054954E /* TrendingDTO.swift */, + ); + path = DTO; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -295,12 +328,15 @@ buildActionMask = 2147483647; files = ( 7D35A442296E293D0054954E /* ServiceError.swift in Sources */, + 7D35A447296E2A390054954E /* TrendingAPI.swift in Sources */, 7D35A408296E22430054954E /* ViewController.swift in Sources */, 7D35A435296E233F0054954E /* MainCoordinator.swift in Sources */, + 7D35A44B296E2C920054954E /* TrendingDTO.swift in Sources */, 7D35A43A296E27AE0054954E /* APIEnvironment.swift in Sources */, 7D35A43E296E29000054954E /* APIHandler.swift in Sources */, 7D35A437296E23740054954E /* Coordinator.swift in Sources */, 7D35A43C296E28490054954E /* APIPath.swift in Sources */, + 7D35A44D296E2CA80054954E /* TrendingResult.swift in Sources */, 7D35A444296E29950054954E /* APILoader.swift in Sources */, 7D35A440296E29280054954E /* ResponseHandler+Extension.swift in Sources */, 7D35A404296E22430054954E /* AppDelegate.swift in Sources */, diff --git a/MyMovies/Model/DTO/TrendingDTO.swift b/MyMovies/Model/DTO/TrendingDTO.swift new file mode 100644 index 00000000..7575ceba --- /dev/null +++ b/MyMovies/Model/DTO/TrendingDTO.swift @@ -0,0 +1,15 @@ +// +// TrendingDTO.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import Foundation + +struct TrendingDTO: Decodable { + let page: Int? + let results: [TrendingResult]? + let totalPages: Int? + let totalResults: Int? +} diff --git a/MyMovies/Model/TrendingResult.swift b/MyMovies/Model/TrendingResult.swift new file mode 100644 index 00000000..2268bed2 --- /dev/null +++ b/MyMovies/Model/TrendingResult.swift @@ -0,0 +1,25 @@ +// +// TrendingResult.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import Foundation + +struct TrendingResult: Decodable { + let posterPath: String? + let adult: Bool? + let overview: String? + let releaseDate: String? + let genreId: [Int]? + let id: Int64? + let originalTitle: String? + let originalLanguage: String? + let title: String? + let backdropPath: String? + let popularity: Double? + let voteCount: Int64? + let video: Bool? + let voteAverage: Double? +} diff --git a/MyMovies/Network/APIEnvironment.swift b/MyMovies/Network/APIEnvironment.swift index c4499a1b..0e60e83f 100644 --- a/MyMovies/Network/APIEnvironment.swift +++ b/MyMovies/Network/APIEnvironment.swift @@ -21,4 +21,11 @@ enum APIEnvironment { return "api.themoviedb.org" } } + + func apiKey() -> String { + switch self { + case .development, .production: + return "16094d8ca19f9c0407db3d0b5203bd21" + } + } } diff --git a/MyMovies/Network/APIPath.swift b/MyMovies/Network/APIPath.swift index 45162077..f71c727c 100644 --- a/MyMovies/Network/APIPath.swift +++ b/MyMovies/Network/APIPath.swift @@ -17,7 +17,13 @@ let baseURL = environment.baseURL() struct APIPath { func fetchTrending(mediaType: String, timeWindow: String) -> String { - return "\(baseURL)/\(apiVersion())/trending/\(mediaType)/\(timeWindow)" + let url = "\(baseURL)/\(apiVersion())/trending/\(mediaType)/\(timeWindow)" + + return url + } + + private func applyApiKey(_ url: String) -> String { + return "\(url)?api_key=\(environment.apiKey())" } private func apiVersion() -> String { diff --git a/MyMovies/Service/TrendingAPI.swift b/MyMovies/Service/TrendingAPI.swift new file mode 100644 index 00000000..4dcd1ae8 --- /dev/null +++ b/MyMovies/Service/TrendingAPI.swift @@ -0,0 +1,36 @@ +// +// TrendingAPI.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import Foundation + +enum MediaType: String { + case all = "all" + case movie = "movie" + case tv = "tv" + case person = "person" +} + +enum TimeWindow: String { + case day = "day" + case week = "week" +} + +struct TrendingAPI: APIHandler { + func makeRequest(from params: (mediaType: MediaType, timeWindow: TimeWindow)) -> URLRequest? { + let urlString = APIPath().fetchTrending(mediaType: params.mediaType.rawValue, timeWindow: params.timeWindow.rawValue) + if let url = URL(string: urlString) { + var urlRequest = URLRequest(url: url) + urlRequest.httpMethod = "GET" + return urlRequest + } + return nil + } + + func parseResponse(data: Data, response: HTTPURLResponse) throws -> TrendingDTO { + return try defaultParseResponse(data: data, response: response) + } +} From 3738b6589780362a5b0225fd52b24edddaf7d5f7 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Tue, 10 Jan 2023 20:40:41 -0300 Subject: [PATCH 08/65] Add call to method that actually applies API Key --- MyMovies/Network/APIPath.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MyMovies/Network/APIPath.swift b/MyMovies/Network/APIPath.swift index f71c727c..a41bac7f 100644 --- a/MyMovies/Network/APIPath.swift +++ b/MyMovies/Network/APIPath.swift @@ -18,8 +18,7 @@ let baseURL = environment.baseURL() struct APIPath { func fetchTrending(mediaType: String, timeWindow: String) -> String { let url = "\(baseURL)/\(apiVersion())/trending/\(mediaType)/\(timeWindow)" - - return url + return applyApiKey(url) } private func applyApiKey(_ url: String) -> String { From d551fb98ca61e3fcab859d17037e62c1843e4aaf Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Tue, 10 Jan 2023 21:13:28 -0300 Subject: [PATCH 09/65] Create basic HomeViewController structure --- MyMovies.xcodeproj/project.pbxproj | 36 +++++++++++++++++++ MyMovies/Network/APIEnvironment.swift | 4 +++ MyMovies/View/HomeViewController.swift | 36 +++++++++++++++++++ .../TopRatedViewController.swift | 33 +++++++++++++++++ .../TrendingViewController.swift | 33 +++++++++++++++++ MyMovies/ViewController.swift | 7 ++++ 6 files changed, 149 insertions(+) create mode 100644 MyMovies/View/HomeViewController.swift create mode 100644 MyMovies/View/TopRatedViewController/TopRatedViewController.swift create mode 100644 MyMovies/View/TrendingViewController/TrendingViewController.swift diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index 23977f4c..eaf4cc30 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -26,6 +26,9 @@ 7D35A447296E2A390054954E /* TrendingAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A446296E2A390054954E /* TrendingAPI.swift */; }; 7D35A44B296E2C920054954E /* TrendingDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A44A296E2C920054954E /* TrendingDTO.swift */; }; 7D35A44D296E2CA80054954E /* TrendingResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A44C296E2CA80054954E /* TrendingResult.swift */; }; + 7D35A450296E31C30054954E /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A44F296E31C30054954E /* HomeViewController.swift */; }; + 7D35A453296E32EE0054954E /* TrendingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A452296E32EE0054954E /* TrendingViewController.swift */; }; + 7D35A458296E35D70054954E /* TopRatedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A457296E35D70054954E /* TopRatedViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -69,6 +72,9 @@ 7D35A446296E2A390054954E /* TrendingAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingAPI.swift; sourceTree = ""; }; 7D35A44A296E2C920054954E /* TrendingDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingDTO.swift; sourceTree = ""; }; 7D35A44C296E2CA80054954E /* TrendingResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingResult.swift; sourceTree = ""; }; + 7D35A44F296E31C30054954E /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; + 7D35A452296E32EE0054954E /* TrendingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingViewController.swift; sourceTree = ""; }; + 7D35A457296E35D70054954E /* TopRatedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopRatedViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -120,6 +126,7 @@ isa = PBXGroup; children = ( 7D35A448296E2BDB0054954E /* Model */, + 7D35A44E296E318E0054954E /* View */, 7D35A445296E2A250054954E /* Service */, 7D35A438296E27930054954E /* Network */, 7D35A433296E232E0054954E /* Coordinator */, @@ -197,6 +204,32 @@ path = DTO; sourceTree = ""; }; + 7D35A44E296E318E0054954E /* View */ = { + isa = PBXGroup; + children = ( + 7D35A456296E35C60054954E /* TopRatedViewController */, + 7D35A451296E32DC0054954E /* TrendingViewController */, + 7D35A44F296E31C30054954E /* HomeViewController.swift */, + ); + path = View; + sourceTree = ""; + }; + 7D35A451296E32DC0054954E /* TrendingViewController */ = { + isa = PBXGroup; + children = ( + 7D35A452296E32EE0054954E /* TrendingViewController.swift */, + ); + path = TrendingViewController; + sourceTree = ""; + }; + 7D35A456296E35C60054954E /* TopRatedViewController */ = { + isa = PBXGroup; + children = ( + 7D35A457296E35D70054954E /* TopRatedViewController.swift */, + ); + path = TopRatedViewController; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -328,13 +361,16 @@ buildActionMask = 2147483647; files = ( 7D35A442296E293D0054954E /* ServiceError.swift in Sources */, + 7D35A453296E32EE0054954E /* TrendingViewController.swift in Sources */, 7D35A447296E2A390054954E /* TrendingAPI.swift in Sources */, 7D35A408296E22430054954E /* ViewController.swift in Sources */, 7D35A435296E233F0054954E /* MainCoordinator.swift in Sources */, 7D35A44B296E2C920054954E /* TrendingDTO.swift in Sources */, + 7D35A450296E31C30054954E /* HomeViewController.swift in Sources */, 7D35A43A296E27AE0054954E /* APIEnvironment.swift in Sources */, 7D35A43E296E29000054954E /* APIHandler.swift in Sources */, 7D35A437296E23740054954E /* Coordinator.swift in Sources */, + 7D35A458296E35D70054954E /* TopRatedViewController.swift in Sources */, 7D35A43C296E28490054954E /* APIPath.swift in Sources */, 7D35A44D296E2CA80054954E /* TrendingResult.swift in Sources */, 7D35A444296E29950054954E /* APILoader.swift in Sources */, diff --git a/MyMovies/Network/APIEnvironment.swift b/MyMovies/Network/APIEnvironment.swift index 0e60e83f..c65b61e8 100644 --- a/MyMovies/Network/APIEnvironment.swift +++ b/MyMovies/Network/APIEnvironment.swift @@ -15,6 +15,10 @@ enum APIEnvironment { return "https://\(domain())" } + func baseImageURL() -> String { + return "https://image.tmdb.org/t/p" + } + func domain() -> String { switch self { case .development, .production: diff --git a/MyMovies/View/HomeViewController.swift b/MyMovies/View/HomeViewController.swift new file mode 100644 index 00000000..3412c184 --- /dev/null +++ b/MyMovies/View/HomeViewController.swift @@ -0,0 +1,36 @@ +// +// HomeViewController.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import UIKit + +class HomeViewController: UIViewController { + + private var trendingViewController: TrendingViewController + private var topRatedViewController: TopRatedViewController + +// private var homeView: HomeView? = nil + + init(trendingViewController: TrendingViewController, topRatedViewController: TopRatedViewController) { + self.trendingViewController = trendingViewController + self.topRatedViewController = topRatedViewController + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError() + } + + override func loadView() { +// self.homeView = HomeView(trendingViewController, topRatedViewController) +// self.homeView?.delegate = self +// view = homeView + } + + override func viewDidLoad() { + super.viewDidLoad() + } +} diff --git a/MyMovies/View/TopRatedViewController/TopRatedViewController.swift b/MyMovies/View/TopRatedViewController/TopRatedViewController.swift new file mode 100644 index 00000000..c3547730 --- /dev/null +++ b/MyMovies/View/TopRatedViewController/TopRatedViewController.swift @@ -0,0 +1,33 @@ +// +// TopRatedViewController.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import UIKit + +class TopRatedViewController: UIViewController { + +// private var topRatedView: TopRatedView? = nil +// private var topRatedViewModel: TopRatedViewModel + +// init(topRatedViewModel: TopRatedViewModel) { +// self.topRatedViewModel = topRatedViewModel +// super.init(nibName: nil, bundle: nil) +// } + + required init?(coder: NSCoder) { + fatalError() + } + + override func loadView() { +// self.topRatedView = TopRatedView() +// self.topRatedView?.delegate = self +// view = topRatedView + } + + override func viewDidLoad() { + super.viewDidLoad() + } +} diff --git a/MyMovies/View/TrendingViewController/TrendingViewController.swift b/MyMovies/View/TrendingViewController/TrendingViewController.swift new file mode 100644 index 00000000..b03980b4 --- /dev/null +++ b/MyMovies/View/TrendingViewController/TrendingViewController.swift @@ -0,0 +1,33 @@ +// +// TrendingViewController.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import UIKit + +class TrendingViewController: UIViewController { + +// private var trendingView: TrendingView? = nil +// private var trendingViewModel: TrendingViewModel + +// init(trendingViewModel: TrendingViewModel) { +// self.trendingViewModel = trendingViewModel +// super.init(nibName: nil, bundle: nil) +// } + + required init?(coder: NSCoder) { + fatalError() + } + + override func loadView() { +// self.trendingView = TrendingView() +// self.trendingView?.delegate = self +// view = trendingView + } + + override func viewDidLoad() { + super.viewDidLoad() + } +} diff --git a/MyMovies/ViewController.swift b/MyMovies/ViewController.swift index 0c7f070f..4701e68c 100644 --- a/MyMovies/ViewController.swift +++ b/MyMovies/ViewController.swift @@ -12,6 +12,13 @@ class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() self.view.backgroundColor = .red + + let api = TrendingAPI() + let apiLoader = APILoader(apiHandler: api) + apiLoader.loadAPIRequest(requestData: (MediaType.all, TimeWindow.day)) { trending, error in + print(trending?.results?.count) + print(error) + } } } From b083b548f283613df3b64f93853803fab825f20f Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Tue, 10 Jan 2023 21:24:54 -0300 Subject: [PATCH 10/65] Add HomeView and initialize it with Trending and TopRated view controllers --- MyMovies.xcodeproj/project.pbxproj | 18 ++++-- MyMovies/Coordinator/MainCoordinator.swift | 7 ++- MyMovies/View/Home/HomeView.swift | 55 +++++++++++++++++++ .../View/{ => Home}/HomeViewController.swift | 10 ++-- .../TopRatedViewController.swift | 6 +- .../TrendingViewController.swift | 6 +- MyMovies/ViewController.swift | 24 -------- 7 files changed, 86 insertions(+), 40 deletions(-) create mode 100644 MyMovies/View/Home/HomeView.swift rename MyMovies/View/{ => Home}/HomeViewController.swift (76%) delete mode 100644 MyMovies/ViewController.swift diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index eaf4cc30..14dcdb5c 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* Begin PBXBuildFile section */ 7D35A404296E22430054954E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A403296E22430054954E /* AppDelegate.swift */; }; 7D35A406296E22430054954E /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A405296E22430054954E /* SceneDelegate.swift */; }; - 7D35A408296E22430054954E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A407296E22430054954E /* ViewController.swift */; }; 7D35A40D296E22450054954E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7D35A40C296E22450054954E /* Assets.xcassets */; }; 7D35A410296E22450054954E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7D35A40E296E22450054954E /* LaunchScreen.storyboard */; }; 7D35A41B296E22450054954E /* MyMoviesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A41A296E22450054954E /* MyMoviesTests.swift */; }; @@ -29,6 +28,7 @@ 7D35A450296E31C30054954E /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A44F296E31C30054954E /* HomeViewController.swift */; }; 7D35A453296E32EE0054954E /* TrendingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A452296E32EE0054954E /* TrendingViewController.swift */; }; 7D35A458296E35D70054954E /* TopRatedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A457296E35D70054954E /* TopRatedViewController.swift */; }; + 7D35A45B296E36DD0054954E /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A45A296E36DD0054954E /* HomeView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -52,7 +52,6 @@ 7D35A400296E22430054954E /* MyMovies.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MyMovies.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7D35A403296E22430054954E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7D35A405296E22430054954E /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - 7D35A407296E22430054954E /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 7D35A40C296E22450054954E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 7D35A40F296E22450054954E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 7D35A411296E22450054954E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -75,6 +74,7 @@ 7D35A44F296E31C30054954E /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; 7D35A452296E32EE0054954E /* TrendingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingViewController.swift; sourceTree = ""; }; 7D35A457296E35D70054954E /* TopRatedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopRatedViewController.swift; sourceTree = ""; }; + 7D35A45A296E36DD0054954E /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -132,7 +132,6 @@ 7D35A433296E232E0054954E /* Coordinator */, 7D35A403296E22430054954E /* AppDelegate.swift */, 7D35A405296E22430054954E /* SceneDelegate.swift */, - 7D35A407296E22430054954E /* ViewController.swift */, 7D35A40C296E22450054954E /* Assets.xcassets */, 7D35A40E296E22450054954E /* LaunchScreen.storyboard */, 7D35A411296E22450054954E /* Info.plist */, @@ -207,9 +206,9 @@ 7D35A44E296E318E0054954E /* View */ = { isa = PBXGroup; children = ( + 7D35A459296E36C60054954E /* Home */, 7D35A456296E35C60054954E /* TopRatedViewController */, 7D35A451296E32DC0054954E /* TrendingViewController */, - 7D35A44F296E31C30054954E /* HomeViewController.swift */, ); path = View; sourceTree = ""; @@ -230,6 +229,15 @@ path = TopRatedViewController; sourceTree = ""; }; + 7D35A459296E36C60054954E /* Home */ = { + isa = PBXGroup; + children = ( + 7D35A44F296E31C30054954E /* HomeViewController.swift */, + 7D35A45A296E36DD0054954E /* HomeView.swift */, + ); + path = Home; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -363,12 +371,12 @@ 7D35A442296E293D0054954E /* ServiceError.swift in Sources */, 7D35A453296E32EE0054954E /* TrendingViewController.swift in Sources */, 7D35A447296E2A390054954E /* TrendingAPI.swift in Sources */, - 7D35A408296E22430054954E /* ViewController.swift in Sources */, 7D35A435296E233F0054954E /* MainCoordinator.swift in Sources */, 7D35A44B296E2C920054954E /* TrendingDTO.swift in Sources */, 7D35A450296E31C30054954E /* HomeViewController.swift in Sources */, 7D35A43A296E27AE0054954E /* APIEnvironment.swift in Sources */, 7D35A43E296E29000054954E /* APIHandler.swift in Sources */, + 7D35A45B296E36DD0054954E /* HomeView.swift in Sources */, 7D35A437296E23740054954E /* Coordinator.swift in Sources */, 7D35A458296E35D70054954E /* TopRatedViewController.swift in Sources */, 7D35A43C296E28490054954E /* APIPath.swift in Sources */, diff --git a/MyMovies/Coordinator/MainCoordinator.swift b/MyMovies/Coordinator/MainCoordinator.swift index d9559765..0665db48 100644 --- a/MyMovies/Coordinator/MainCoordinator.swift +++ b/MyMovies/Coordinator/MainCoordinator.swift @@ -11,7 +11,12 @@ class MainCoordinator: Coordinator { var navigationController: UINavigationController? func start() { - let viewController = ViewController() + let trendingViewController = TrendingViewController() + let topRatedViewController = TopRatedViewController() + let viewController = HomeViewController( + trendingViewController: trendingViewController, + topRatedViewController: topRatedViewController + ) navigationController?.setViewControllers([viewController], animated: false) } diff --git a/MyMovies/View/Home/HomeView.swift b/MyMovies/View/Home/HomeView.swift new file mode 100644 index 00000000..18b0d8b0 --- /dev/null +++ b/MyMovies/View/Home/HomeView.swift @@ -0,0 +1,55 @@ +// +// HomeView.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import UIKit + +class HomeView: UIView { + + private var trendingView: UIView + private var topRatedView: UIView + + init(trendingViewController: TrendingViewController, topRatedViewController: TopRatedViewController) { + self.trendingView = trendingViewController.view + self.topRatedView = topRatedViewController.view + super.init(frame: CGRect()) + + setup() + } + + required init?(coder: NSCoder) { + fatalError() + } + + private func setup() { + self.backgroundColor = .systemBackground + + setupTrendingView() + } + + private func setupTrendingView() { + addSubview(trendingView) + + NSLayoutConstraint.activate([ + trendingView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 20), + trendingView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 20), + trendingView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -20) + ]) + + setupTopRatedView() + } + + private func setupTopRatedView() { + addSubview(topRatedView) + + NSLayoutConstraint.activate([ + topRatedView.topAnchor.constraint(equalTo: trendingView.bottomAnchor, constant: 20), + topRatedView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 20), + topRatedView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -20), + topRatedView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -20) + ]) + } +} diff --git a/MyMovies/View/HomeViewController.swift b/MyMovies/View/Home/HomeViewController.swift similarity index 76% rename from MyMovies/View/HomeViewController.swift rename to MyMovies/View/Home/HomeViewController.swift index 3412c184..c1c4181a 100644 --- a/MyMovies/View/HomeViewController.swift +++ b/MyMovies/View/Home/HomeViewController.swift @@ -12,7 +12,7 @@ class HomeViewController: UIViewController { private var trendingViewController: TrendingViewController private var topRatedViewController: TopRatedViewController -// private var homeView: HomeView? = nil + private var homeView: HomeView? = nil init(trendingViewController: TrendingViewController, topRatedViewController: TopRatedViewController) { self.trendingViewController = trendingViewController @@ -25,9 +25,11 @@ class HomeViewController: UIViewController { } override func loadView() { -// self.homeView = HomeView(trendingViewController, topRatedViewController) -// self.homeView?.delegate = self -// view = homeView + self.homeView = HomeView( + trendingViewController: trendingViewController, + topRatedViewController: topRatedViewController + ) + view = homeView } override func viewDidLoad() { diff --git a/MyMovies/View/TopRatedViewController/TopRatedViewController.swift b/MyMovies/View/TopRatedViewController/TopRatedViewController.swift index c3547730..970d5719 100644 --- a/MyMovies/View/TopRatedViewController/TopRatedViewController.swift +++ b/MyMovies/View/TopRatedViewController/TopRatedViewController.swift @@ -12,10 +12,10 @@ class TopRatedViewController: UIViewController { // private var topRatedView: TopRatedView? = nil // private var topRatedViewModel: TopRatedViewModel -// init(topRatedViewModel: TopRatedViewModel) { + init(/*topRatedViewModel: TopRatedViewModel*/) { // self.topRatedViewModel = topRatedViewModel -// super.init(nibName: nil, bundle: nil) -// } + super.init(nibName: nil, bundle: nil) + } required init?(coder: NSCoder) { fatalError() diff --git a/MyMovies/View/TrendingViewController/TrendingViewController.swift b/MyMovies/View/TrendingViewController/TrendingViewController.swift index b03980b4..c8f55efc 100644 --- a/MyMovies/View/TrendingViewController/TrendingViewController.swift +++ b/MyMovies/View/TrendingViewController/TrendingViewController.swift @@ -12,10 +12,10 @@ class TrendingViewController: UIViewController { // private var trendingView: TrendingView? = nil // private var trendingViewModel: TrendingViewModel -// init(trendingViewModel: TrendingViewModel) { + init(/*trendingViewModel: TrendingViewModel*/) { // self.trendingViewModel = trendingViewModel -// super.init(nibName: nil, bundle: nil) -// } + super.init(nibName: nil, bundle: nil) + } required init?(coder: NSCoder) { fatalError() diff --git a/MyMovies/ViewController.swift b/MyMovies/ViewController.swift deleted file mode 100644 index 4701e68c..00000000 --- a/MyMovies/ViewController.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// ViewController.swift -// MyMovies -// -// Created by Caio dos Santos Ambrosio on 1/10/23. -// - -import UIKit - -class ViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - self.view.backgroundColor = .red - - let api = TrendingAPI() - let apiLoader = APILoader(apiHandler: api) - apiLoader.loadAPIRequest(requestData: (MediaType.all, TimeWindow.day)) { trending, error in - print(trending?.results?.count) - print(error) - } - } -} - From 429e5d30f89e20891b3f4acc83232b96d074a333 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Tue, 10 Jan 2023 21:45:14 -0300 Subject: [PATCH 11/65] Create TrendingView with label and collection view --- MyMovies.xcodeproj/project.pbxproj | 8 ++ .../TrendingCollectionViewCell.swift | 12 +++ .../TrendingViewController/TrendingView.swift | 83 +++++++++++++++++++ .../TrendingViewController.swift | 9 +- 4 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 MyMovies/View/TrendingViewController/TrendingCollectionViewCell.swift create mode 100644 MyMovies/View/TrendingViewController/TrendingView.swift diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index 14dcdb5c..a1f99326 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -29,6 +29,8 @@ 7D35A453296E32EE0054954E /* TrendingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A452296E32EE0054954E /* TrendingViewController.swift */; }; 7D35A458296E35D70054954E /* TopRatedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A457296E35D70054954E /* TopRatedViewController.swift */; }; 7D35A45B296E36DD0054954E /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A45A296E36DD0054954E /* HomeView.swift */; }; + 7D35A45F296E3D360054954E /* TrendingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A45E296E3D360054954E /* TrendingView.swift */; }; + 7D35A461296E3D5F0054954E /* TrendingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A460296E3D5F0054954E /* TrendingCollectionViewCell.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -75,6 +77,8 @@ 7D35A452296E32EE0054954E /* TrendingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingViewController.swift; sourceTree = ""; }; 7D35A457296E35D70054954E /* TopRatedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopRatedViewController.swift; sourceTree = ""; }; 7D35A45A296E36DD0054954E /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; + 7D35A45E296E3D360054954E /* TrendingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingView.swift; sourceTree = ""; }; + 7D35A460296E3D5F0054954E /* TrendingCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingCollectionViewCell.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -217,6 +221,8 @@ isa = PBXGroup; children = ( 7D35A452296E32EE0054954E /* TrendingViewController.swift */, + 7D35A45E296E3D360054954E /* TrendingView.swift */, + 7D35A460296E3D5F0054954E /* TrendingCollectionViewCell.swift */, ); path = TrendingViewController; sourceTree = ""; @@ -375,6 +381,8 @@ 7D35A44B296E2C920054954E /* TrendingDTO.swift in Sources */, 7D35A450296E31C30054954E /* HomeViewController.swift in Sources */, 7D35A43A296E27AE0054954E /* APIEnvironment.swift in Sources */, + 7D35A461296E3D5F0054954E /* TrendingCollectionViewCell.swift in Sources */, + 7D35A45F296E3D360054954E /* TrendingView.swift in Sources */, 7D35A43E296E29000054954E /* APIHandler.swift in Sources */, 7D35A45B296E36DD0054954E /* HomeView.swift in Sources */, 7D35A437296E23740054954E /* Coordinator.swift in Sources */, diff --git a/MyMovies/View/TrendingViewController/TrendingCollectionViewCell.swift b/MyMovies/View/TrendingViewController/TrendingCollectionViewCell.swift new file mode 100644 index 00000000..65e45d47 --- /dev/null +++ b/MyMovies/View/TrendingViewController/TrendingCollectionViewCell.swift @@ -0,0 +1,12 @@ +// +// TrendingCollectionViewCell.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import UIKit + +class TrendingCollectionViewCell: UICollectionViewCell { + +} diff --git a/MyMovies/View/TrendingViewController/TrendingView.swift b/MyMovies/View/TrendingViewController/TrendingView.swift new file mode 100644 index 00000000..81d0d3bf --- /dev/null +++ b/MyMovies/View/TrendingViewController/TrendingView.swift @@ -0,0 +1,83 @@ +// +// TrendingView.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import UIKit + +class TrendingView: UIView { + + private lazy var trendingLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = .label + label.font = UIFont.boldSystemFont(ofSize: 24) + return label + }() + + private lazy var trendingCollectionView: UICollectionView = { + let layoutConfig = UICollectionLayoutListConfiguration(appearance: UICollectionLayoutListConfiguration.Appearance.insetGrouped) + let listLayout = UICollectionViewCompositionalLayout.list(using: layoutConfig) + let collectionView = UICollectionView(frame: CGRect(), collectionViewLayout: listLayout) + collectionView.translatesAutoresizingMaskIntoConstraints = false + collectionView.register(TrendingCollectionViewCell.self, forCellWithReuseIdentifier: "TrendingCollectionViewCell") + return collectionView + }() + + weak var collectionViewDelegate: UICollectionViewDelegate? { + get { + self.trendingCollectionView.delegate + } + set { + self.trendingCollectionView.delegate = newValue + } + } + + weak var collectionDataSource: UICollectionViewDataSource? { + get { + self.trendingCollectionView.dataSource + } + set { + self.trendingCollectionView.dataSource = newValue + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder: NSCoder) { + fatalError() + } + + private func setup() { + self.backgroundColor = .systemBackground + + setupTrendingLabel() + } + + private func setupTrendingLabel() { + addSubview(trendingLabel) + + NSLayoutConstraint.activate([ + trendingLabel.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), + trendingLabel.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor) + ]) + + setupTrendingCollectionView() + } + + private func setupTrendingCollectionView() { + addSubview(trendingCollectionView) + + NSLayoutConstraint.activate([ + trendingCollectionView.topAnchor.constraint(equalTo: trendingLabel.bottomAnchor, constant: 10), + trendingCollectionView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor), + trendingCollectionView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor), + trendingCollectionView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor) + ]) + } +} diff --git a/MyMovies/View/TrendingViewController/TrendingViewController.swift b/MyMovies/View/TrendingViewController/TrendingViewController.swift index c8f55efc..83e1ecfc 100644 --- a/MyMovies/View/TrendingViewController/TrendingViewController.swift +++ b/MyMovies/View/TrendingViewController/TrendingViewController.swift @@ -9,7 +9,7 @@ import UIKit class TrendingViewController: UIViewController { -// private var trendingView: TrendingView? = nil + private var trendingView: TrendingView? = nil // private var trendingViewModel: TrendingViewModel init(/*trendingViewModel: TrendingViewModel*/) { @@ -22,9 +22,10 @@ class TrendingViewController: UIViewController { } override func loadView() { -// self.trendingView = TrendingView() -// self.trendingView?.delegate = self -// view = trendingView + self.trendingView = TrendingView() +// self.trendingView?.collectionViewDelegate = trendingViewModel +// self.trendingView?.collectionDataSource = trendingViewModel + view = trendingView } override func viewDidLoad() { From c3fbfbb258b29ac091b4f72a13d2cad31cd2deca Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Tue, 10 Jan 2023 21:45:38 -0300 Subject: [PATCH 12/65] Move TrendingResult to DTO folder --- MyMovies.xcodeproj/project.pbxproj | 2 +- MyMovies/Model/{ => DTO}/TrendingResult.swift | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename MyMovies/Model/{ => DTO}/TrendingResult.swift (100%) diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index a1f99326..8f850e06 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -194,7 +194,6 @@ isa = PBXGroup; children = ( 7D35A449296E2C870054954E /* DTO */, - 7D35A44C296E2CA80054954E /* TrendingResult.swift */, ); path = Model; sourceTree = ""; @@ -202,6 +201,7 @@ 7D35A449296E2C870054954E /* DTO */ = { isa = PBXGroup; children = ( + 7D35A44C296E2CA80054954E /* TrendingResult.swift */, 7D35A44A296E2C920054954E /* TrendingDTO.swift */, ); path = DTO; diff --git a/MyMovies/Model/TrendingResult.swift b/MyMovies/Model/DTO/TrendingResult.swift similarity index 100% rename from MyMovies/Model/TrendingResult.swift rename to MyMovies/Model/DTO/TrendingResult.swift From a1c621242bc853f7af28cc08c5d4d0be87613316 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Tue, 10 Jan 2023 22:27:15 -0300 Subject: [PATCH 13/65] Create TrendingListViewModel and TrendingViewModel, also add logic to call api and initialize model --- MyMovies.xcodeproj/project.pbxproj | 8 +++ MyMovies/Coordinator/MainCoordinator.swift | 7 ++- MyMovies/Model/TrendingListViewModel.swift | 63 +++++++++++++++++++ MyMovies/Model/TrendingViewModel.swift | 20 ++++++ MyMovies/Network/APIPath.swift | 9 ++- .../TrendingCollectionViewCell.swift | 5 +- .../TrendingViewController.swift | 25 ++++++-- 7 files changed, 128 insertions(+), 9 deletions(-) create mode 100644 MyMovies/Model/TrendingListViewModel.swift create mode 100644 MyMovies/Model/TrendingViewModel.swift diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index 8f850e06..2fbf6cdb 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -31,6 +31,8 @@ 7D35A45B296E36DD0054954E /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A45A296E36DD0054954E /* HomeView.swift */; }; 7D35A45F296E3D360054954E /* TrendingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A45E296E3D360054954E /* TrendingView.swift */; }; 7D35A461296E3D5F0054954E /* TrendingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A460296E3D5F0054954E /* TrendingCollectionViewCell.swift */; }; + 7D35A463296E3E410054954E /* TrendingListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A462296E3E410054954E /* TrendingListViewModel.swift */; }; + 7D35A465296E45170054954E /* TrendingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A464296E45170054954E /* TrendingViewModel.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -79,6 +81,8 @@ 7D35A45A296E36DD0054954E /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 7D35A45E296E3D360054954E /* TrendingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingView.swift; sourceTree = ""; }; 7D35A460296E3D5F0054954E /* TrendingCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingCollectionViewCell.swift; sourceTree = ""; }; + 7D35A462296E3E410054954E /* TrendingListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingListViewModel.swift; sourceTree = ""; }; + 7D35A464296E45170054954E /* TrendingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingViewModel.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -194,6 +198,8 @@ isa = PBXGroup; children = ( 7D35A449296E2C870054954E /* DTO */, + 7D35A462296E3E410054954E /* TrendingListViewModel.swift */, + 7D35A464296E45170054954E /* TrendingViewModel.swift */, ); path = Model; sourceTree = ""; @@ -384,9 +390,11 @@ 7D35A461296E3D5F0054954E /* TrendingCollectionViewCell.swift in Sources */, 7D35A45F296E3D360054954E /* TrendingView.swift in Sources */, 7D35A43E296E29000054954E /* APIHandler.swift in Sources */, + 7D35A465296E45170054954E /* TrendingViewModel.swift in Sources */, 7D35A45B296E36DD0054954E /* HomeView.swift in Sources */, 7D35A437296E23740054954E /* Coordinator.swift in Sources */, 7D35A458296E35D70054954E /* TopRatedViewController.swift in Sources */, + 7D35A463296E3E410054954E /* TrendingListViewModel.swift in Sources */, 7D35A43C296E28490054954E /* APIPath.swift in Sources */, 7D35A44D296E2CA80054954E /* TrendingResult.swift in Sources */, 7D35A444296E29950054954E /* APILoader.swift in Sources */, diff --git a/MyMovies/Coordinator/MainCoordinator.swift b/MyMovies/Coordinator/MainCoordinator.swift index 0665db48..938e6606 100644 --- a/MyMovies/Coordinator/MainCoordinator.swift +++ b/MyMovies/Coordinator/MainCoordinator.swift @@ -11,8 +11,13 @@ class MainCoordinator: Coordinator { var navigationController: UINavigationController? func start() { - let trendingViewController = TrendingViewController() + let api = TrendingAPI() + let apiLoader = APILoader(apiHandler: api) + let trendingViewModel = TrendingListViewModel(apiLoader: apiLoader) + let trendingViewController = TrendingViewController(trendingListViewModel: trendingViewModel) + let topRatedViewController = TopRatedViewController() + let viewController = HomeViewController( trendingViewController: trendingViewController, topRatedViewController: topRatedViewController diff --git a/MyMovies/Model/TrendingListViewModel.swift b/MyMovies/Model/TrendingListViewModel.swift new file mode 100644 index 00000000..22c2c83c --- /dev/null +++ b/MyMovies/Model/TrendingListViewModel.swift @@ -0,0 +1,63 @@ +// +// TrendingListViewModel.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import UIKit + +class TrendingListViewModel { + + // MARK: - Properties + + private var trendings: [TrendingViewModel] = [] + private var selected = 0 + + private let apiLoader: APILoader + + var numberOfRowsInSection: Int { + self.trendings.count + } + + // MARK: - Initializer + + init(apiLoader: APILoader) { + self.apiLoader = apiLoader + } + + // MARK: - Functions + + func fetchTrendings( + of param: (mediaType: MediaType, timeWindow: TimeWindow), + completion: @escaping (TrendingListViewModel?, ServiceError?) -> () + ) { + apiLoader.loadAPIRequest(requestData: param) { [weak self] trendingResponse, error in + guard let self = self else { + return + } + + if let _ = error { + DispatchQueue.main.async { + completion(nil, error) + } + } else if let trendingResponse = trendingResponse { + self.trendings = trendingResponse.results?.map({ trending in + TrendingViewModel(trending: trending) + }) ?? [] + + DispatchQueue.main.async { + completion(self, nil) + } + } else { + DispatchQueue.main.async { + completion(nil, ServiceError(message: "Service ErrorL Unknow Error")) + } + } + } + } + + func getTrending(_ index: Int) -> TrendingViewModel { + return self.trendings[index] + } +} diff --git a/MyMovies/Model/TrendingViewModel.swift b/MyMovies/Model/TrendingViewModel.swift new file mode 100644 index 00000000..c0475ebf --- /dev/null +++ b/MyMovies/Model/TrendingViewModel.swift @@ -0,0 +1,20 @@ +// +// TrendingViewModel.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import Foundation + +class TrendingViewModel { + private var trending: TrendingResult + + var posterImageURL: String { + APIPath().fetchImage(width: "w154", imagePath: trending.posterPath ?? "") + } + + init(trending: TrendingResult) { + self.trending = trending + } +} diff --git a/MyMovies/Network/APIPath.swift b/MyMovies/Network/APIPath.swift index a41bac7f..ff38ae9e 100644 --- a/MyMovies/Network/APIPath.swift +++ b/MyMovies/Network/APIPath.swift @@ -14,6 +14,8 @@ let environment = APIEnvironment.production #endif let baseURL = environment.baseURL() +let baseImageURL = environment.baseImageURL() +let apiKey = environment.apiKey() struct APIPath { func fetchTrending(mediaType: String, timeWindow: String) -> String { @@ -21,8 +23,13 @@ struct APIPath { return applyApiKey(url) } + func fetchImage(width: String, imagePath: String) -> String { + let url = "\(baseImageURL)/\(width)/\(imagePath)" + return applyApiKey(url) + } + private func applyApiKey(_ url: String) -> String { - return "\(url)?api_key=\(environment.apiKey())" + return "\(url)?api_key=\(apiKey)" } private func apiVersion() -> String { diff --git a/MyMovies/View/TrendingViewController/TrendingCollectionViewCell.swift b/MyMovies/View/TrendingViewController/TrendingCollectionViewCell.swift index 65e45d47..e47b5e85 100644 --- a/MyMovies/View/TrendingViewController/TrendingCollectionViewCell.swift +++ b/MyMovies/View/TrendingViewController/TrendingCollectionViewCell.swift @@ -8,5 +8,8 @@ import UIKit class TrendingCollectionViewCell: UICollectionViewCell { - + + func setupData(model: TrendingViewModel) { + + } } diff --git a/MyMovies/View/TrendingViewController/TrendingViewController.swift b/MyMovies/View/TrendingViewController/TrendingViewController.swift index 83e1ecfc..975b8479 100644 --- a/MyMovies/View/TrendingViewController/TrendingViewController.swift +++ b/MyMovies/View/TrendingViewController/TrendingViewController.swift @@ -7,13 +7,13 @@ import UIKit -class TrendingViewController: UIViewController { +class TrendingViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource { private var trendingView: TrendingView? = nil -// private var trendingViewModel: TrendingViewModel + private var trendingListViewModel: TrendingListViewModel - init(/*trendingViewModel: TrendingViewModel*/) { -// self.trendingViewModel = trendingViewModel + init(trendingListViewModel: TrendingListViewModel) { + self.trendingListViewModel = trendingListViewModel super.init(nibName: nil, bundle: nil) } @@ -23,12 +23,25 @@ class TrendingViewController: UIViewController { override func loadView() { self.trendingView = TrendingView() -// self.trendingView?.collectionViewDelegate = trendingViewModel -// self.trendingView?.collectionDataSource = trendingViewModel + self.trendingView?.collectionViewDelegate = self + self.trendingView?.collectionDataSource = self view = trendingView } override func viewDidLoad() { super.viewDidLoad() } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return self.trendingListViewModel.numberOfRowsInSection + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let trending = trendingListViewModel.getTrending(indexPath.row) + + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "TrendingCollectionViewCell", for: indexPath) as? TrendingCollectionViewCell + cell?.setupData(model: trending) + + return cell ?? UICollectionViewCell() + } } From c49779cb858fa81925b817e4e6d8d7c9f72b827a Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Wed, 11 Jan 2023 16:34:56 -0300 Subject: [PATCH 14/65] Add TrendingCollectionViewCell and update Collection View with its information --- MyMovies.xcodeproj/project.pbxproj | 20 ++++++ MyMovies/Model/TrendingViewModel.swift | 8 ++- .../Service/ImageLoader/ImageLoader.swift | 51 ++++++++++++++ .../Service/ImageLoader/UIImageLoader.swift | 42 ++++++++++++ .../ImageLoader/UIImageView+ImageLoader.swift | 18 +++++ MyMovies/View/Home/HomeView.swift | 31 +++++---- .../TrendingCollectionViewCell.swift | 64 +++++++++++++++++ .../TrendingViewController/TrendingView.swift | 68 ++++++++++++++++++- .../TrendingViewController.swift | 17 ++++- 9 files changed, 300 insertions(+), 19 deletions(-) create mode 100644 MyMovies/Service/ImageLoader/ImageLoader.swift create mode 100644 MyMovies/Service/ImageLoader/UIImageLoader.swift create mode 100644 MyMovies/Service/ImageLoader/UIImageView+ImageLoader.swift diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index 2fbf6cdb..9becdc56 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -33,6 +33,9 @@ 7D35A461296E3D5F0054954E /* TrendingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A460296E3D5F0054954E /* TrendingCollectionViewCell.swift */; }; 7D35A463296E3E410054954E /* TrendingListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A462296E3E410054954E /* TrendingListViewModel.swift */; }; 7D35A465296E45170054954E /* TrendingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A464296E45170054954E /* TrendingViewModel.swift */; }; + 7D35A467296E4C170054954E /* UIImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A466296E4C170054954E /* UIImageLoader.swift */; }; + 7D35A46A296E4C820054954E /* ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A469296E4C820054954E /* ImageLoader.swift */; }; + 7D35A46C296E4D090054954E /* UIImageView+ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A46B296E4D090054954E /* UIImageView+ImageLoader.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -83,6 +86,9 @@ 7D35A460296E3D5F0054954E /* TrendingCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingCollectionViewCell.swift; sourceTree = ""; }; 7D35A462296E3E410054954E /* TrendingListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingListViewModel.swift; sourceTree = ""; }; 7D35A464296E45170054954E /* TrendingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingViewModel.swift; sourceTree = ""; }; + 7D35A466296E4C170054954E /* UIImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageLoader.swift; sourceTree = ""; }; + 7D35A469296E4C820054954E /* ImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageLoader.swift; sourceTree = ""; }; + 7D35A46B296E4D090054954E /* UIImageView+ImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+ImageLoader.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -189,6 +195,7 @@ 7D35A445296E2A250054954E /* Service */ = { isa = PBXGroup; children = ( + 7D35A468296E4C1E0054954E /* ImageLoader */, 7D35A446296E2A390054954E /* TrendingAPI.swift */, ); path = Service; @@ -250,6 +257,16 @@ path = Home; sourceTree = ""; }; + 7D35A468296E4C1E0054954E /* ImageLoader */ = { + isa = PBXGroup; + children = ( + 7D35A466296E4C170054954E /* UIImageLoader.swift */, + 7D35A469296E4C820054954E /* ImageLoader.swift */, + 7D35A46B296E4D090054954E /* UIImageView+ImageLoader.swift */, + ); + path = ImageLoader; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -395,7 +412,10 @@ 7D35A437296E23740054954E /* Coordinator.swift in Sources */, 7D35A458296E35D70054954E /* TopRatedViewController.swift in Sources */, 7D35A463296E3E410054954E /* TrendingListViewModel.swift in Sources */, + 7D35A46A296E4C820054954E /* ImageLoader.swift in Sources */, 7D35A43C296E28490054954E /* APIPath.swift in Sources */, + 7D35A46C296E4D090054954E /* UIImageView+ImageLoader.swift in Sources */, + 7D35A467296E4C170054954E /* UIImageLoader.swift in Sources */, 7D35A44D296E2CA80054954E /* TrendingResult.swift in Sources */, 7D35A444296E29950054954E /* APILoader.swift in Sources */, 7D35A440296E29280054954E /* ResponseHandler+Extension.swift in Sources */, diff --git a/MyMovies/Model/TrendingViewModel.swift b/MyMovies/Model/TrendingViewModel.swift index c0475ebf..0e3dc6c3 100644 --- a/MyMovies/Model/TrendingViewModel.swift +++ b/MyMovies/Model/TrendingViewModel.swift @@ -10,8 +10,12 @@ import Foundation class TrendingViewModel { private var trending: TrendingResult - var posterImageURL: String { - APIPath().fetchImage(width: "w154", imagePath: trending.posterPath ?? "") + var title: String { + trending.title ?? "" + } + + var posterImageURL: URL? { + URL(string: APIPath().fetchImage(width: "w154", imagePath: trending.posterPath ?? "")) } init(trending: TrendingResult) { diff --git a/MyMovies/Service/ImageLoader/ImageLoader.swift b/MyMovies/Service/ImageLoader/ImageLoader.swift new file mode 100644 index 00000000..c61db14f --- /dev/null +++ b/MyMovies/Service/ImageLoader/ImageLoader.swift @@ -0,0 +1,51 @@ +// +// ImageLoader.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import UIKit + +class ImageLoader { + private var loadedImages = [URL: UIImage]() + private var runningRequests = [UUID: URLSessionDataTask]() + + func loadImage(_ url: URL, _ completion: @escaping (Result) -> Void) -> UUID? { + if let image = loadedImages[url] { + completion(.success(image)) + return nil + } + + let uuid = UUID() + + let task = URLSession.shared.dataTask(with: url) { data, response, error in + defer {self.runningRequests.removeValue(forKey: uuid) } + + if let data = data, let image = UIImage(data: data) { + self.loadedImages[url] = image + completion(.success(image)) + return + } + + guard let error = error else { + return + } + + guard (error as NSError).code == NSURLErrorCancelled else { + completion(.failure(error)) + return + } + } + task.resume() + + // 6 + runningRequests[uuid] = task + return uuid + } + + func cancelLoad(_ uuid: UUID) { + runningRequests[uuid]?.cancel() + runningRequests.removeValue(forKey: uuid) + } +} diff --git a/MyMovies/Service/ImageLoader/UIImageLoader.swift b/MyMovies/Service/ImageLoader/UIImageLoader.swift new file mode 100644 index 00000000..7f536902 --- /dev/null +++ b/MyMovies/Service/ImageLoader/UIImageLoader.swift @@ -0,0 +1,42 @@ +// +// ImageLoader.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import UIKit + +class UIImageLoader { + static let loader = UIImageLoader() + + private let imageLoader = ImageLoader() + private var uuidMap = [UIImageView: UUID]() + + private init() {} + + func load(_ url: URL, for imageView: UIImageView) { + let token = imageLoader.loadImage(url) { result in + defer { self.uuidMap.removeValue(forKey: imageView) } + do { + let image = try result.get() + DispatchQueue.main.async { + imageView.image = image + } + } catch { + // handle the error + } + } + + if let token = token { + uuidMap[imageView] = token + } + } + + func cancel(for imageView: UIImageView) { + if let uuid = uuidMap[imageView] { + imageLoader.cancelLoad(uuid) + uuidMap.removeValue(forKey: imageView) + } + } +} diff --git a/MyMovies/Service/ImageLoader/UIImageView+ImageLoader.swift b/MyMovies/Service/ImageLoader/UIImageView+ImageLoader.swift new file mode 100644 index 00000000..a053495b --- /dev/null +++ b/MyMovies/Service/ImageLoader/UIImageView+ImageLoader.swift @@ -0,0 +1,18 @@ +// +// UIImageView+ImageLoader.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/10/23. +// + +import UIKit + +extension UIImageView { + func loadImage(at url: URL) { + UIImageLoader.loader.load(url, for: self) + } + + func cancelImageLoad() { + UIImageLoader.loader.cancel(for: self) + } +} diff --git a/MyMovies/View/Home/HomeView.swift b/MyMovies/View/Home/HomeView.swift index 18b0d8b0..58a6d648 100644 --- a/MyMovies/View/Home/HomeView.swift +++ b/MyMovies/View/Home/HomeView.swift @@ -10,11 +10,11 @@ import UIKit class HomeView: UIView { private var trendingView: UIView - private var topRatedView: UIView +// private var topRatedView: UIView init(trendingViewController: TrendingViewController, topRatedViewController: TopRatedViewController) { self.trendingView = trendingViewController.view - self.topRatedView = topRatedViewController.view +// self.topRatedView = topRatedViewController.view super.init(frame: CGRect()) setup() @@ -33,23 +33,26 @@ class HomeView: UIView { private func setupTrendingView() { addSubview(trendingView) + trendingView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ trendingView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 20), trendingView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 20), - trendingView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -20) + trendingView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -20), + trendingView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -20) ]) - setupTopRatedView() +// setupTopRatedView() } - private func setupTopRatedView() { - addSubview(topRatedView) - - NSLayoutConstraint.activate([ - topRatedView.topAnchor.constraint(equalTo: trendingView.bottomAnchor, constant: 20), - topRatedView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 20), - topRatedView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -20), - topRatedView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -20) - ]) - } +// private func setupTopRatedView() { +// addSubview(topRatedView) +// +// NSLayoutConstraint.activate([ +// topRatedView.topAnchor.constraint(equalTo: trendingView.bottomAnchor, constant: 20), +// topRatedView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 20), +// topRatedView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -20), +// topRatedView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -20) +// ]) +// } } diff --git a/MyMovies/View/TrendingViewController/TrendingCollectionViewCell.swift b/MyMovies/View/TrendingViewController/TrendingCollectionViewCell.swift index e47b5e85..8f8e8656 100644 --- a/MyMovies/View/TrendingViewController/TrendingCollectionViewCell.swift +++ b/MyMovies/View/TrendingViewController/TrendingCollectionViewCell.swift @@ -9,7 +9,71 @@ import UIKit class TrendingCollectionViewCell: UICollectionViewCell { + private lazy var posterImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.isUserInteractionEnabled = false + return imageView + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = .label + label.font = UIFont.systemFont(ofSize: 15) + return label + }() + + override init(frame: CGRect) { + super.init(frame: CGRect()) + + setup() + } + + required init?(coder: NSCoder) { + fatalError() + } + + private func setup() { + self.backgroundColor = .systemBackground + + setupPosterImage() + } + + func setupPosterImage() { + addSubview(posterImageView) + + NSLayoutConstraint.activate([ + posterImageView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), + posterImageView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor), + posterImageView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor), + posterImageView.widthAnchor.constraint(equalToConstant: 154), + posterImageView.heightAnchor.constraint(equalToConstant: 231) + ]) + + setupTitleLabel() + } + + func setupTitleLabel() { + addSubview(titleLabel) + + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: posterImageView.bottomAnchor, constant: 10), + titleLabel.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 5), + titleLabel.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -5), + titleLabel.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor), + ]) + } + func setupData(model: TrendingViewModel) { + titleLabel.text = model.title + if let url = model.posterImageURL { + posterImageView.loadImage(at: url) + } + } + override func prepareForReuse() { + posterImageView.image = nil + posterImageView.cancelImageLoad() } } diff --git a/MyMovies/View/TrendingViewController/TrendingView.swift b/MyMovies/View/TrendingViewController/TrendingView.swift index 81d0d3bf..152ba1e0 100644 --- a/MyMovies/View/TrendingViewController/TrendingView.swift +++ b/MyMovies/View/TrendingViewController/TrendingView.swift @@ -12,6 +12,7 @@ class TrendingView: UIView { private lazy var trendingLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false + label.text = "Trending" label.textColor = .label label.font = UIFont.boldSystemFont(ofSize: 24) return label @@ -22,10 +23,29 @@ class TrendingView: UIView { let listLayout = UICollectionViewCompositionalLayout.list(using: layoutConfig) let collectionView = UICollectionView(frame: CGRect(), collectionViewLayout: listLayout) collectionView.translatesAutoresizingMaskIntoConstraints = false + collectionView.backgroundColor = .red collectionView.register(TrendingCollectionViewCell.self, forCellWithReuseIdentifier: "TrendingCollectionViewCell") return collectionView }() + private lazy var activityIndicator: UIActivityIndicatorView = { + let activityIndicator = UIActivityIndicatorView(style: .large) + activityIndicator.translatesAutoresizingMaskIntoConstraints = false + activityIndicator.hidesWhenStopped = true + return activityIndicator + }() + + private lazy var errorLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = .label + label.font = UIFont.systemFont(ofSize: 28) + label.text = "Error when loading Trending information" + label.numberOfLines = 0 + label.textAlignment = .center + return label + }() + weak var collectionViewDelegate: UICollectionViewDelegate? { get { self.trendingCollectionView.delegate @@ -63,8 +83,8 @@ class TrendingView: UIView { addSubview(trendingLabel) NSLayoutConstraint.activate([ - trendingLabel.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), - trendingLabel.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor) + trendingLabel.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 10), + trendingLabel.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 10) ]) setupTrendingCollectionView() @@ -79,5 +99,49 @@ class TrendingView: UIView { trendingCollectionView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor), trendingCollectionView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor) ]) + + setupActivityIndicator() + } + + private func setupActivityIndicator() { + addSubview(activityIndicator) + + NSLayoutConstraint.activate([ + activityIndicator.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor), + activityIndicator.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor) + ]) + + setupErrorLabel() + } + + private func setupErrorLabel() { + addSubview(errorLabel) + + NSLayoutConstraint.activate([ + errorLabel.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 20), + errorLabel.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -20), + errorLabel.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor), + errorLabel.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor) + ]) + } + + func renderLoadingState() { + self.trendingCollectionView.isHidden = true + self.errorLabel.isHidden = true + self.activityIndicator.startAnimating() + } + + func renderErrorState() { + self.activityIndicator.stopAnimating() + self.trendingCollectionView.isHidden = true + self.trendingLabel.isHidden = true + self.errorLabel.isHidden = false + } + + func renderSuccessState() { + self.activityIndicator.stopAnimating() + self.errorLabel.isHidden = true + self.trendingCollectionView.isHidden = false + self.trendingCollectionView.reloadData() } } diff --git a/MyMovies/View/TrendingViewController/TrendingViewController.swift b/MyMovies/View/TrendingViewController/TrendingViewController.swift index 975b8479..0a889c78 100644 --- a/MyMovies/View/TrendingViewController/TrendingViewController.swift +++ b/MyMovies/View/TrendingViewController/TrendingViewController.swift @@ -23,13 +23,28 @@ class TrendingViewController: UIViewController, UICollectionViewDelegate, UIColl override func loadView() { self.trendingView = TrendingView() - self.trendingView?.collectionViewDelegate = self self.trendingView?.collectionDataSource = self + self.trendingView?.collectionViewDelegate = self view = trendingView } override func viewDidLoad() { super.viewDidLoad() + + trendingView?.renderLoadingState() + + trendingListViewModel.fetchTrendings(of: (MediaType.all, TimeWindow.day)) { [weak self] trendingListViewModel, error in + guard let self = self else { + return + } + + if error != nil { + self.trendingView?.renderErrorState() + } else if let trendingListViewModel = trendingListViewModel { + self.trendingListViewModel = trendingListViewModel + self.trendingView?.renderSuccessState() + } + } } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { From 46cd58c427856db990b4ef79ee98ef69a461d9ad Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Wed, 11 Jan 2023 21:07:26 -0300 Subject: [PATCH 15/65] Remove .DS_Store files --- .DS_Store | Bin 6148 -> 0 bytes MyMovies/.DS_Store | Bin 6148 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store delete mode 100644 MyMovies/.DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 094ffebb39571cd6aabd4aabc9bd9de7fa598164..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKu};H447E#;U@<4Nq5`Ujy30|Qfkph|)$DAh$&AlUK+{0=JvAI9_9s#*e4 z#efjllKn0|JC}PG#W4}_^uC@Djfg0N3JwlnJ`tH0?a9DA`#?_jc%U1)qE)@BnqIU! zej@{Nb{SpL6ExDs`IRxIGd%t{Nn?7X3c)pwx}`geZky}LtcuH4cleAKi+0Le#-b>j zSjt0;FXpF5FK=hN=holOZ5Nx*`-&x9oB?OR8Td~Opk|9?hl*~U0cXG&STi8shX57K z4Lim3>3~Ts0B{I*5{#vnkepzc8+MAAfv~0mHI=Q!U`>ZVSX^${DQY^gH6Ls%vvnw( zR>%EA3MbAL-8uu#z%~PWdO4E%fBtp8i1%p(I5i6zm1eyf?AOm1-*eSvT@gIRigBxeyM;UkrDOyCm diff --git a/MyMovies/.DS_Store b/MyMovies/.DS_Store deleted file mode 100644 index 3a7869248ad4bf94eb5ee9ba54e7c0c4c43dc0c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKyK2Kg5Zp~v2-3K8dB5O4IE-AT$`1s`DGUY`#({LI{9XC9%svP)CxaU|W)^nm zZg1y=rqF6dMAxU?N@O7-J={>v7Fx4&^TwVsqd++B_>#SR%5iu6I5gGs3FG$iD&O$& zKYu%px85;-GqY5H3Qz$mKn1A4oC4N+Vf{-WBNd Date: Wed, 11 Jan 2023 21:08:21 -0300 Subject: [PATCH 16/65] Remove xcuserdatad --- .../xcschemes/xcschememanagement.plist | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 MyMovies.xcodeproj/xcuserdata/caioamb.xcuserdatad/xcschemes/xcschememanagement.plist diff --git a/MyMovies.xcodeproj/xcuserdata/caioamb.xcuserdatad/xcschemes/xcschememanagement.plist b/MyMovies.xcodeproj/xcuserdata/caioamb.xcuserdatad/xcschemes/xcschememanagement.plist deleted file mode 100644 index 09d270b7..00000000 --- a/MyMovies.xcodeproj/xcuserdata/caioamb.xcuserdatad/xcschemes/xcschememanagement.plist +++ /dev/null @@ -1,14 +0,0 @@ - - - - - SchemeUserState - - MyMovies.xcscheme_^#shared#^_ - - orderHint - 0 - - - - From 419ff8b3dc661f2b8f475cb0f4c5daf119fd2f6d Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Wed, 11 Jan 2023 21:11:23 -0300 Subject: [PATCH 17/65] Fix navigationController in Coordinator pattern --- MyMovies/Coordinator/Coordinator.swift | 2 +- MyMovies/Coordinator/MainCoordinator.swift | 9 +++++++-- MyMovies/SceneDelegate.swift | 3 +-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/MyMovies/Coordinator/Coordinator.swift b/MyMovies/Coordinator/Coordinator.swift index de205ad5..42200d43 100644 --- a/MyMovies/Coordinator/Coordinator.swift +++ b/MyMovies/Coordinator/Coordinator.swift @@ -8,7 +8,7 @@ import UIKit protocol Coordinator { - var navigationController: UINavigationController? { get set } + var navigationController: UINavigationController { get } func start() } diff --git a/MyMovies/Coordinator/MainCoordinator.swift b/MyMovies/Coordinator/MainCoordinator.swift index 938e6606..f162dae3 100644 --- a/MyMovies/Coordinator/MainCoordinator.swift +++ b/MyMovies/Coordinator/MainCoordinator.swift @@ -8,7 +8,12 @@ import UIKit class MainCoordinator: Coordinator { - var navigationController: UINavigationController? + + let navigationController: UINavigationController + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } func start() { let api = TrendingAPI() @@ -23,6 +28,6 @@ class MainCoordinator: Coordinator { topRatedViewController: topRatedViewController ) - navigationController?.setViewControllers([viewController], animated: false) + navigationController.setViewControllers([viewController], animated: false) } } diff --git a/MyMovies/SceneDelegate.swift b/MyMovies/SceneDelegate.swift index 8e4f780f..31406738 100644 --- a/MyMovies/SceneDelegate.swift +++ b/MyMovies/SceneDelegate.swift @@ -19,8 +19,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let navigationController = UINavigationController() - let coordinator = MainCoordinator() - coordinator.navigationController = navigationController + let coordinator = MainCoordinator(navigationController: navigationController) let window = UIWindow(frame: windowScene.coordinateSpace.bounds) From cd89954ec9b96f238f13d4f83fd04018eb223d41 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Wed, 11 Jan 2023 21:24:38 -0300 Subject: [PATCH 18/65] Make backend configuration more developer friendly --- MyMovies.xcodeproj/project.pbxproj | 6 ++++++ MyMovies/Info.plist | 6 ++++++ MyMovies/Network/APIEnvironment.swift | 13 ++++++------- MyMovies/Network/APIPath.swift | 26 +++++++++++++++++--------- 4 files changed, 35 insertions(+), 16 deletions(-) diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index 9becdc56..ebfb5dc8 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -587,6 +587,9 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + BASE_API_KEY = 16094d8ca19f9c0407db3d0b5203bd21; + BASE_IMAGE_URL = "https://image.tmdb.org/t/p"; + BASE_URL = "https://api.themoviedb.org"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = T2QW6534L6; @@ -614,6 +617,9 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + BASE_API_KEY = 16094d8ca19f9c0407db3d0b5203bd21; + BASE_IMAGE_URL = "https://image.tmdb.org/t/p"; + BASE_URL = "https://api.themoviedb.org"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = T2QW6534L6; diff --git a/MyMovies/Info.plist b/MyMovies/Info.plist index 0eb786dc..6dc4eac8 100644 --- a/MyMovies/Info.plist +++ b/MyMovies/Info.plist @@ -2,6 +2,12 @@ + BASE_API_KEY + $(BASE_API_KEY) + BASE_IMAGE_URL + $(BASE_IMAGE_URL) + BASE_URL + $(BASE_URL) UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/MyMovies/Network/APIEnvironment.swift b/MyMovies/Network/APIEnvironment.swift index c65b61e8..584e6eb4 100644 --- a/MyMovies/Network/APIEnvironment.swift +++ b/MyMovies/Network/APIEnvironment.swift @@ -12,24 +12,23 @@ enum APIEnvironment { case production func baseURL() -> String { - return "https://\(domain())" + switch self { + case .development, .production: + return Bundle.main.infoDictionary?["BASE_URL"] as? String ?? "" + } } func baseImageURL() -> String { - return "https://image.tmdb.org/t/p" - } - - func domain() -> String { switch self { case .development, .production: - return "api.themoviedb.org" + return Bundle.main.infoDictionary?["BASE_IMAGE_URL"] as? String ?? "" } } func apiKey() -> String { switch self { case .development, .production: - return "16094d8ca19f9c0407db3d0b5203bd21" + return Bundle.main.infoDictionary?["BASE_API_KEY"] as? String ?? "" } } } diff --git a/MyMovies/Network/APIPath.swift b/MyMovies/Network/APIPath.swift index ff38ae9e..36715d51 100644 --- a/MyMovies/Network/APIPath.swift +++ b/MyMovies/Network/APIPath.swift @@ -7,17 +7,25 @@ import Foundation -#if DEBUG -let environment = APIEnvironment.development -#else -let environment = APIEnvironment.production -#endif +struct APIPath { -let baseURL = environment.baseURL() -let baseImageURL = environment.baseImageURL() -let apiKey = environment.apiKey() + let environment: APIEnvironment + let baseURL: String + let baseImageURL: String + let apiKey: String + + init() { + #if DEBUG + environment = APIEnvironment.development + #else + environment = APIEnvironment.production + #endif + + baseURL = environment.baseURL() + baseImageURL = environment.baseImageURL() + apiKey = environment.apiKey() + } -struct APIPath { func fetchTrending(mediaType: String, timeWindow: String) -> String { let url = "\(baseURL)/\(apiVersion())/trending/\(mediaType)/\(timeWindow)" return applyApiKey(url) From ff9a002055794cf323653e6299d327a44de2cd5c Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Wed, 11 Jan 2023 21:31:29 -0300 Subject: [PATCH 19/65] Improve APILoader with a more understandable code --- MyMovies/Network/APILoader.swift | 58 +++++++++++++++++--------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/MyMovies/Network/APILoader.swift b/MyMovies/Network/APILoader.swift index 22c3ba76..4cee4ceb 100644 --- a/MyMovies/Network/APILoader.swift +++ b/MyMovies/Network/APILoader.swift @@ -17,35 +17,39 @@ struct APILoader { } func loadAPIRequest(requestData: T.RequestDataType, completion: @escaping (T.ResponseDataType?, ServiceError?) -> ()) { - if let urlRequest = apiHandler.makeRequest(from: requestData) { - urlSession.dataTask(with: urlRequest) { data, response, error in - if let httpResponse = response as? HTTPURLResponse { - guard error == nil, - let responseData = data else { - completion( - nil, - ServiceError(message: "Service Error: \(error?.localizedDescription ?? "Unknow Error")") - ) - return - } + guard let urlRequest = apiHandler.makeRequest(from: requestData) else { + completion(nil, ServiceError(message: "Error when performing request")) + return + } + + urlSession.dataTask(with: urlRequest) { data, response, error in + guard let httpResponse = response as? HTTPURLResponse else { + completion(nil, ServiceError(message: "Error when performing request")) + return + } - do { - let parsedResponse = try self.apiHandler.parseResponse(data: responseData, response: httpResponse) - completion(parsedResponse, nil) - } catch { - if let error = error as? ServiceError { - completion(nil, error) - } else { - completion( - nil, - ServiceError(message: "Error when decoding json: \(error.localizedDescription)") - ) - } - } + guard error == nil, + let responseData = data else { + completion( + nil, + ServiceError(message: "Service Error: \(error?.localizedDescription ?? "Unknow Error")") + ) + return + } + + do { + let parsedResponse = try self.apiHandler.parseResponse(data: responseData, response: httpResponse) + completion(parsedResponse, nil) + } catch { + if error is ServiceError { + completion(nil, error as? ServiceError) } else { - completion(nil, ServiceError(message: "Error when performing request")) + completion( + nil, + ServiceError(message: "Error when decoding json: \(error.localizedDescription)") + ) } - }.resume() - } + } + }.resume() } } From 3fb9f5b6c716eb5d5fc6ddeaa3282837ffbd8ccf Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Wed, 11 Jan 2023 21:34:43 -0300 Subject: [PATCH 20/65] Create struct for TrendingParams --- MyMovies/Model/TrendingListViewModel.swift | 2 +- MyMovies/Network/APIPath.swift | 4 ++-- MyMovies/Service/TrendingAPI.swift | 9 +++++++-- .../TrendingViewController/TrendingViewController.swift | 4 +++- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/MyMovies/Model/TrendingListViewModel.swift b/MyMovies/Model/TrendingListViewModel.swift index 22c2c83c..db445e38 100644 --- a/MyMovies/Model/TrendingListViewModel.swift +++ b/MyMovies/Model/TrendingListViewModel.swift @@ -29,7 +29,7 @@ class TrendingListViewModel { // MARK: - Functions func fetchTrendings( - of param: (mediaType: MediaType, timeWindow: TimeWindow), + of param: TrendingParams, completion: @escaping (TrendingListViewModel?, ServiceError?) -> () ) { apiLoader.loadAPIRequest(requestData: param) { [weak self] trendingResponse, error in diff --git a/MyMovies/Network/APIPath.swift b/MyMovies/Network/APIPath.swift index 36715d51..246ce07d 100644 --- a/MyMovies/Network/APIPath.swift +++ b/MyMovies/Network/APIPath.swift @@ -26,8 +26,8 @@ struct APIPath { apiKey = environment.apiKey() } - func fetchTrending(mediaType: String, timeWindow: String) -> String { - let url = "\(baseURL)/\(apiVersion())/trending/\(mediaType)/\(timeWindow)" + func fetchTrending(with params: TrendingParams) -> String { + let url = "\(baseURL)/\(apiVersion())/trending/\(params.mediaType.rawValue)/\(params.timeWindow.rawValue)" return applyApiKey(url) } diff --git a/MyMovies/Service/TrendingAPI.swift b/MyMovies/Service/TrendingAPI.swift index 4dcd1ae8..e56c6c60 100644 --- a/MyMovies/Service/TrendingAPI.swift +++ b/MyMovies/Service/TrendingAPI.swift @@ -19,9 +19,14 @@ enum TimeWindow: String { case week = "week" } +struct TrendingParams { + let mediaType: MediaType + let timeWindow: TimeWindow +} + struct TrendingAPI: APIHandler { - func makeRequest(from params: (mediaType: MediaType, timeWindow: TimeWindow)) -> URLRequest? { - let urlString = APIPath().fetchTrending(mediaType: params.mediaType.rawValue, timeWindow: params.timeWindow.rawValue) + func makeRequest(from params: TrendingParams) -> URLRequest? { + let urlString = APIPath().fetchTrending(with: params) if let url = URL(string: urlString) { var urlRequest = URLRequest(url: url) urlRequest.httpMethod = "GET" diff --git a/MyMovies/View/TrendingViewController/TrendingViewController.swift b/MyMovies/View/TrendingViewController/TrendingViewController.swift index 0a889c78..19f22903 100644 --- a/MyMovies/View/TrendingViewController/TrendingViewController.swift +++ b/MyMovies/View/TrendingViewController/TrendingViewController.swift @@ -33,7 +33,9 @@ class TrendingViewController: UIViewController, UICollectionViewDelegate, UIColl trendingView?.renderLoadingState() - trendingListViewModel.fetchTrendings(of: (MediaType.all, TimeWindow.day)) { [weak self] trendingListViewModel, error in + trendingListViewModel.fetchTrendings( + of: TrendingParams(mediaType: .all, timeWindow: .day) + ) { [weak self] trendingListViewModel, error in guard let self = self else { return } From 3c3e9f726169083289539ce29d0a45aa0cbb97af Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Wed, 11 Jan 2023 21:36:52 -0300 Subject: [PATCH 21/65] Create extension for collection view delegate and datasource --- .../View/TrendingViewController/TrendingViewController.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MyMovies/View/TrendingViewController/TrendingViewController.swift b/MyMovies/View/TrendingViewController/TrendingViewController.swift index 19f22903..5faf690b 100644 --- a/MyMovies/View/TrendingViewController/TrendingViewController.swift +++ b/MyMovies/View/TrendingViewController/TrendingViewController.swift @@ -7,7 +7,7 @@ import UIKit -class TrendingViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource { +class TrendingViewController: UIViewController { private var trendingView: TrendingView? = nil private var trendingListViewModel: TrendingListViewModel @@ -48,7 +48,9 @@ class TrendingViewController: UIViewController, UICollectionViewDelegate, UIColl } } } +} +extension TrendingViewController: UICollectionViewDelegate, UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return self.trendingListViewModel.numberOfRowsInSection } From bf175dbc721bcee953fb659dc5c3e54cdb16b18a Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Wed, 11 Jan 2023 22:10:47 -0300 Subject: [PATCH 22/65] Minor refactorings to make CollectionView horizontal --- MyMovies/View/Home/HomeView.swift | 2 +- .../TrendingCollectionViewCell.swift | 19 +++++++++++-------- .../TrendingViewController/TrendingView.swift | 11 ++++++----- .../TrendingViewController.swift | 2 +- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/MyMovies/View/Home/HomeView.swift b/MyMovies/View/Home/HomeView.swift index 58a6d648..4cafa347 100644 --- a/MyMovies/View/Home/HomeView.swift +++ b/MyMovies/View/Home/HomeView.swift @@ -39,7 +39,7 @@ class HomeView: UIView { trendingView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 20), trendingView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 20), trendingView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -20), - trendingView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -20) + trendingView.heightAnchor.constraint(equalToConstant: 300) ]) // setupTopRatedView() diff --git a/MyMovies/View/TrendingViewController/TrendingCollectionViewCell.swift b/MyMovies/View/TrendingViewController/TrendingCollectionViewCell.swift index 8f8e8656..4f749cce 100644 --- a/MyMovies/View/TrendingViewController/TrendingCollectionViewCell.swift +++ b/MyMovies/View/TrendingViewController/TrendingCollectionViewCell.swift @@ -9,6 +9,8 @@ import UIKit class TrendingCollectionViewCell: UICollectionViewCell { + static let identifier = "TrendingCollectionViewCell" + private lazy var posterImageView: UIImageView = { let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false @@ -20,7 +22,8 @@ class TrendingCollectionViewCell: UICollectionViewCell { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.textColor = .label - label.font = UIFont.systemFont(ofSize: 15) + label.font = UIFont.systemFont(ofSize: 18) + label.numberOfLines = 3 return label }() @@ -44,9 +47,9 @@ class TrendingCollectionViewCell: UICollectionViewCell { addSubview(posterImageView) NSLayoutConstraint.activate([ - posterImageView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), - posterImageView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor), - posterImageView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor), + posterImageView.topAnchor.constraint(equalTo: contentView.topAnchor), + posterImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + posterImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), posterImageView.widthAnchor.constraint(equalToConstant: 154), posterImageView.heightAnchor.constraint(equalToConstant: 231) ]) @@ -58,10 +61,10 @@ class TrendingCollectionViewCell: UICollectionViewCell { addSubview(titleLabel) NSLayoutConstraint.activate([ - titleLabel.topAnchor.constraint(equalTo: posterImageView.bottomAnchor, constant: 10), - titleLabel.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 5), - titleLabel.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -5), - titleLabel.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor), + titleLabel.topAnchor.constraint(equalTo: posterImageView.bottomAnchor, constant: 5), + titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 2), + titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -2), + titleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), ]) } diff --git a/MyMovies/View/TrendingViewController/TrendingView.swift b/MyMovies/View/TrendingViewController/TrendingView.swift index 152ba1e0..69eacbb1 100644 --- a/MyMovies/View/TrendingViewController/TrendingView.swift +++ b/MyMovies/View/TrendingViewController/TrendingView.swift @@ -19,12 +19,13 @@ class TrendingView: UIView { }() private lazy var trendingCollectionView: UICollectionView = { - let layoutConfig = UICollectionLayoutListConfiguration(appearance: UICollectionLayoutListConfiguration.Appearance.insetGrouped) - let listLayout = UICollectionViewCompositionalLayout.list(using: layoutConfig) - let collectionView = UICollectionView(frame: CGRect(), collectionViewLayout: listLayout) + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.itemSize = CGSize(width: 154, height: 310) + let collectionView = UICollectionView(frame: CGRect(), collectionViewLayout: layout) + collectionView.showsHorizontalScrollIndicator = false collectionView.translatesAutoresizingMaskIntoConstraints = false - collectionView.backgroundColor = .red - collectionView.register(TrendingCollectionViewCell.self, forCellWithReuseIdentifier: "TrendingCollectionViewCell") + collectionView.register(TrendingCollectionViewCell.self, forCellWithReuseIdentifier: TrendingCollectionViewCell.identifier) return collectionView }() diff --git a/MyMovies/View/TrendingViewController/TrendingViewController.swift b/MyMovies/View/TrendingViewController/TrendingViewController.swift index 5faf690b..9dd177da 100644 --- a/MyMovies/View/TrendingViewController/TrendingViewController.swift +++ b/MyMovies/View/TrendingViewController/TrendingViewController.swift @@ -58,7 +58,7 @@ extension TrendingViewController: UICollectionViewDelegate, UICollectionViewData func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let trending = trendingListViewModel.getTrending(indexPath.row) - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "TrendingCollectionViewCell", for: indexPath) as? TrendingCollectionViewCell + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TrendingCollectionViewCell.identifier, for: indexPath) as? TrendingCollectionViewCell cell?.setupData(model: trending) return cell ?? UICollectionViewCell() From 0dc35257671265415d6552bbf8938bdad5009af4 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Wed, 11 Jan 2023 22:22:04 -0300 Subject: [PATCH 23/65] Fix spaces between cells and cells that was with empty title --- MyMovies/Model/DTO/TrendingResult.swift | 2 ++ MyMovies/Model/TrendingViewModel.swift | 8 +++++++- MyMovies/View/Home/HomeView.swift | 4 ++-- MyMovies/View/TrendingViewController/TrendingView.swift | 2 ++ 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/MyMovies/Model/DTO/TrendingResult.swift b/MyMovies/Model/DTO/TrendingResult.swift index 2268bed2..1b1c80ec 100644 --- a/MyMovies/Model/DTO/TrendingResult.swift +++ b/MyMovies/Model/DTO/TrendingResult.swift @@ -15,8 +15,10 @@ struct TrendingResult: Decodable { let genreId: [Int]? let id: Int64? let originalTitle: String? + let originalName: String? let originalLanguage: String? let title: String? + let name: String? let backdropPath: String? let popularity: Double? let voteCount: Int64? diff --git a/MyMovies/Model/TrendingViewModel.swift b/MyMovies/Model/TrendingViewModel.swift index 0e3dc6c3..07196a73 100644 --- a/MyMovies/Model/TrendingViewModel.swift +++ b/MyMovies/Model/TrendingViewModel.swift @@ -11,7 +11,13 @@ class TrendingViewModel { private var trending: TrendingResult var title: String { - trending.title ?? "" + if let title = trending.originalTitle { + return title + } else if let name = trending.name { + return name + } else { + return "" + } } var posterImageURL: URL? { diff --git a/MyMovies/View/Home/HomeView.swift b/MyMovies/View/Home/HomeView.swift index 4cafa347..18be9f58 100644 --- a/MyMovies/View/Home/HomeView.swift +++ b/MyMovies/View/Home/HomeView.swift @@ -37,8 +37,8 @@ class HomeView: UIView { NSLayoutConstraint.activate([ trendingView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 20), - trendingView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 20), - trendingView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -20), + trendingView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 4), + trendingView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -4), trendingView.heightAnchor.constraint(equalToConstant: 300) ]) diff --git a/MyMovies/View/TrendingViewController/TrendingView.swift b/MyMovies/View/TrendingViewController/TrendingView.swift index 69eacbb1..f30f2e07 100644 --- a/MyMovies/View/TrendingViewController/TrendingView.swift +++ b/MyMovies/View/TrendingViewController/TrendingView.swift @@ -22,6 +22,8 @@ class TrendingView: UIView { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .horizontal layout.itemSize = CGSize(width: 154, height: 310) + layout.minimumLineSpacing = 4 + layout.minimumInteritemSpacing = 4 let collectionView = UICollectionView(frame: CGRect(), collectionViewLayout: layout) collectionView.showsHorizontalScrollIndicator = false collectionView.translatesAutoresizingMaskIntoConstraints = false From b626ea871357616f6a8e87027c16858b8278ce18 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Fri, 13 Jan 2023 18:12:26 -0300 Subject: [PATCH 24/65] Add TopRatedAPI and path for it in API Path --- MyMovies/Network/APIPath.swift | 5 +++++ MyMovies/Service/TopRatedAPI.swift | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 MyMovies/Service/TopRatedAPI.swift diff --git a/MyMovies/Network/APIPath.swift b/MyMovies/Network/APIPath.swift index 246ce07d..89566b6c 100644 --- a/MyMovies/Network/APIPath.swift +++ b/MyMovies/Network/APIPath.swift @@ -36,6 +36,11 @@ struct APIPath { return applyApiKey(url) } + func fetchTopRated() -> String { + let url = "\(baseURL)/\(apiVersion())/movie/top_rated" + return applyApiKey(url) + } + private func applyApiKey(_ url: String) -> String { return "\(url)?api_key=\(apiKey)" } diff --git a/MyMovies/Service/TopRatedAPI.swift b/MyMovies/Service/TopRatedAPI.swift new file mode 100644 index 00000000..32b8b6b5 --- /dev/null +++ b/MyMovies/Service/TopRatedAPI.swift @@ -0,0 +1,24 @@ +// +// TopRatedAPI.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/11/23. +// + +import Foundation + +struct TopRatedAPI: APIHandler { + func makeRequest(from params: [String : Any]?) -> URLRequest? { + let urlString = APIPath().fetchTopRated() + if let url = URL(string: urlString) { + var urlRequest = URLRequest(url: url) + urlRequest.httpMethod = "GET" + return urlRequest + } + return nil + } + + func parseResponse(data: Data, response: HTTPURLResponse) throws -> TrendingDTO { + return try defaultParseResponse(data: data, response: response) + } +} From 73db253be09dd88c13720b93ee3169ff468d7010 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Fri, 13 Jan 2023 18:16:27 -0300 Subject: [PATCH 25/65] Create TopRatedListViewModel and also delete TopRatedViewController --- MyMovies.xcodeproj/project.pbxproj | 20 +++--- MyMovies/Model/TopRatedListViewModel.swift | 64 +++++++++++++++++++ .../TopRatedViewController.swift | 33 ---------- 3 files changed, 72 insertions(+), 45 deletions(-) create mode 100644 MyMovies/Model/TopRatedListViewModel.swift delete mode 100644 MyMovies/View/TopRatedViewController/TopRatedViewController.swift diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index ebfb5dc8..90bb9700 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -27,7 +27,6 @@ 7D35A44D296E2CA80054954E /* TrendingResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A44C296E2CA80054954E /* TrendingResult.swift */; }; 7D35A450296E31C30054954E /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A44F296E31C30054954E /* HomeViewController.swift */; }; 7D35A453296E32EE0054954E /* TrendingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A452296E32EE0054954E /* TrendingViewController.swift */; }; - 7D35A458296E35D70054954E /* TopRatedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A457296E35D70054954E /* TopRatedViewController.swift */; }; 7D35A45B296E36DD0054954E /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A45A296E36DD0054954E /* HomeView.swift */; }; 7D35A45F296E3D360054954E /* TrendingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A45E296E3D360054954E /* TrendingView.swift */; }; 7D35A461296E3D5F0054954E /* TrendingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A460296E3D5F0054954E /* TrendingCollectionViewCell.swift */; }; @@ -36,6 +35,8 @@ 7D35A467296E4C170054954E /* UIImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A466296E4C170054954E /* UIImageLoader.swift */; }; 7D35A46A296E4C820054954E /* ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A469296E4C820054954E /* ImageLoader.swift */; }; 7D35A46C296E4D090054954E /* UIImageView+ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A46B296E4D090054954E /* UIImageView+ImageLoader.swift */; }; + 7D35A46E296F98C00054954E /* TopRatedListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A46D296F98C00054954E /* TopRatedListViewModel.swift */; }; + 7D35A470296F99430054954E /* TopRatedAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A46F296F99430054954E /* TopRatedAPI.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -80,7 +81,6 @@ 7D35A44C296E2CA80054954E /* TrendingResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingResult.swift; sourceTree = ""; }; 7D35A44F296E31C30054954E /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; 7D35A452296E32EE0054954E /* TrendingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingViewController.swift; sourceTree = ""; }; - 7D35A457296E35D70054954E /* TopRatedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopRatedViewController.swift; sourceTree = ""; }; 7D35A45A296E36DD0054954E /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 7D35A45E296E3D360054954E /* TrendingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingView.swift; sourceTree = ""; }; 7D35A460296E3D5F0054954E /* TrendingCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingCollectionViewCell.swift; sourceTree = ""; }; @@ -89,6 +89,8 @@ 7D35A466296E4C170054954E /* UIImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageLoader.swift; sourceTree = ""; }; 7D35A469296E4C820054954E /* ImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageLoader.swift; sourceTree = ""; }; 7D35A46B296E4D090054954E /* UIImageView+ImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+ImageLoader.swift"; sourceTree = ""; }; + 7D35A46D296F98C00054954E /* TopRatedListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopRatedListViewModel.swift; sourceTree = ""; }; + 7D35A46F296F99430054954E /* TopRatedAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopRatedAPI.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -197,6 +199,7 @@ children = ( 7D35A468296E4C1E0054954E /* ImageLoader */, 7D35A446296E2A390054954E /* TrendingAPI.swift */, + 7D35A46F296F99430054954E /* TopRatedAPI.swift */, ); path = Service; sourceTree = ""; @@ -207,6 +210,7 @@ 7D35A449296E2C870054954E /* DTO */, 7D35A462296E3E410054954E /* TrendingListViewModel.swift */, 7D35A464296E45170054954E /* TrendingViewModel.swift */, + 7D35A46D296F98C00054954E /* TopRatedListViewModel.swift */, ); path = Model; sourceTree = ""; @@ -224,7 +228,6 @@ isa = PBXGroup; children = ( 7D35A459296E36C60054954E /* Home */, - 7D35A456296E35C60054954E /* TopRatedViewController */, 7D35A451296E32DC0054954E /* TrendingViewController */, ); path = View; @@ -240,14 +243,6 @@ path = TrendingViewController; sourceTree = ""; }; - 7D35A456296E35C60054954E /* TopRatedViewController */ = { - isa = PBXGroup; - children = ( - 7D35A457296E35D70054954E /* TopRatedViewController.swift */, - ); - path = TopRatedViewController; - sourceTree = ""; - }; 7D35A459296E36C60054954E /* Home */ = { isa = PBXGroup; children = ( @@ -410,7 +405,6 @@ 7D35A465296E45170054954E /* TrendingViewModel.swift in Sources */, 7D35A45B296E36DD0054954E /* HomeView.swift in Sources */, 7D35A437296E23740054954E /* Coordinator.swift in Sources */, - 7D35A458296E35D70054954E /* TopRatedViewController.swift in Sources */, 7D35A463296E3E410054954E /* TrendingListViewModel.swift in Sources */, 7D35A46A296E4C820054954E /* ImageLoader.swift in Sources */, 7D35A43C296E28490054954E /* APIPath.swift in Sources */, @@ -421,6 +415,8 @@ 7D35A440296E29280054954E /* ResponseHandler+Extension.swift in Sources */, 7D35A404296E22430054954E /* AppDelegate.swift in Sources */, 7D35A406296E22430054954E /* SceneDelegate.swift in Sources */, + 7D35A46E296F98C00054954E /* TopRatedListViewModel.swift in Sources */, + 7D35A470296F99430054954E /* TopRatedAPI.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MyMovies/Model/TopRatedListViewModel.swift b/MyMovies/Model/TopRatedListViewModel.swift new file mode 100644 index 00000000..1d69e8d9 --- /dev/null +++ b/MyMovies/Model/TopRatedListViewModel.swift @@ -0,0 +1,64 @@ +// +// TopRatedListViewModel.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/11/23. +// + +import UIKit + +class TopRatedListViewModel { + + // MARK: - Properties + + private var topRated: [TrendingViewModel] = [] + private var selected = 0 + + private let apiLoader: APILoader + + var numberOfRowsInSection: Int { + self.topRated.count + } + + var titlePage: String = "Top Rated" + + // MARK: - Initializer + + init(apiLoader: APILoader) { + self.apiLoader = apiLoader + } + + // MARK: - Functions + + func fetchTopRated( + completion: @escaping (TopRatedListViewModel?, ServiceError?) -> () + ) { + apiLoader.loadAPIRequest(requestData: nil) { [weak self] topRatedResponse, error in + guard let self = self else { + return + } + + if let _ = error { + DispatchQueue.main.async { + completion(nil, error) + } + } else if let topRatedResponse = topRatedResponse { + self.topRated = topRatedResponse.results?.map({ topRated in + TrendingViewModel(trending: topRated) + }) ?? [] + + DispatchQueue.main.async { + completion(self, nil) + } + } else { + DispatchQueue.main.async { + completion(nil, ServiceError(message: "Service Error: Unknow Error")) + } + } + } + } + + func getTrending(_ index: Int) -> TrendingViewModel { + return self.topRated[index] + } +} diff --git a/MyMovies/View/TopRatedViewController/TopRatedViewController.swift b/MyMovies/View/TopRatedViewController/TopRatedViewController.swift deleted file mode 100644 index 970d5719..00000000 --- a/MyMovies/View/TopRatedViewController/TopRatedViewController.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// TopRatedViewController.swift -// MyMovies -// -// Created by Caio dos Santos Ambrosio on 1/10/23. -// - -import UIKit - -class TopRatedViewController: UIViewController { - -// private var topRatedView: TopRatedView? = nil -// private var topRatedViewModel: TopRatedViewModel - - init(/*topRatedViewModel: TopRatedViewModel*/) { -// self.topRatedViewModel = topRatedViewModel - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError() - } - - override func loadView() { -// self.topRatedView = TopRatedView() -// self.topRatedView?.delegate = self -// view = topRatedView - } - - override func viewDidLoad() { - super.viewDidLoad() - } -} From ab036625d66dff8084ad725180ac59ab4d188a76 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Fri, 13 Jan 2023 18:16:53 -0300 Subject: [PATCH 26/65] Add title to TrendingListViewModel --- MyMovies/Model/TrendingListViewModel.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MyMovies/Model/TrendingListViewModel.swift b/MyMovies/Model/TrendingListViewModel.swift index db445e38..69013929 100644 --- a/MyMovies/Model/TrendingListViewModel.swift +++ b/MyMovies/Model/TrendingListViewModel.swift @@ -20,6 +20,8 @@ class TrendingListViewModel { self.trendings.count } + var titlePage: String = "Trending" + // MARK: - Initializer init(apiLoader: APILoader) { From 3a907d06151bb4ead3499ae5f0ac0444d4458fbb Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Fri, 13 Jan 2023 18:17:12 -0300 Subject: [PATCH 27/65] Fix typo --- MyMovies/Model/TrendingListViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MyMovies/Model/TrendingListViewModel.swift b/MyMovies/Model/TrendingListViewModel.swift index 69013929..6a4c70fe 100644 --- a/MyMovies/Model/TrendingListViewModel.swift +++ b/MyMovies/Model/TrendingListViewModel.swift @@ -53,7 +53,7 @@ class TrendingListViewModel { } } else { DispatchQueue.main.async { - completion(nil, ServiceError(message: "Service ErrorL Unknow Error")) + completion(nil, ServiceError(message: "Service Error: Unknow Error")) } } } From bf6152e13c0e35263aeb7e8e4c216ac29fbf62c3 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Fri, 13 Jan 2023 18:18:10 -0300 Subject: [PATCH 28/65] Make TrendingView and TrendingViewController accept TopRated logic --- .../TrendingViewController/TrendingView.swift | 5 ++-- .../TrendingViewController.swift | 29 +++++++++++++++---- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/MyMovies/View/TrendingViewController/TrendingView.swift b/MyMovies/View/TrendingViewController/TrendingView.swift index f30f2e07..40d5182f 100644 --- a/MyMovies/View/TrendingViewController/TrendingView.swift +++ b/MyMovies/View/TrendingViewController/TrendingView.swift @@ -12,7 +12,6 @@ class TrendingView: UIView { private lazy var trendingLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false - label.text = "Trending" label.textColor = .label label.font = UIFont.boldSystemFont(ofSize: 24) return label @@ -141,9 +140,11 @@ class TrendingView: UIView { self.errorLabel.isHidden = false } - func renderSuccessState() { + func renderSuccessState(with title: String) { self.activityIndicator.stopAnimating() self.errorLabel.isHidden = true + self.trendingLabel.isHidden = false + self.trendingLabel.text = title self.trendingCollectionView.isHidden = false self.trendingCollectionView.reloadData() } diff --git a/MyMovies/View/TrendingViewController/TrendingViewController.swift b/MyMovies/View/TrendingViewController/TrendingViewController.swift index 9dd177da..b1f8483d 100644 --- a/MyMovies/View/TrendingViewController/TrendingViewController.swift +++ b/MyMovies/View/TrendingViewController/TrendingViewController.swift @@ -10,10 +10,12 @@ import UIKit class TrendingViewController: UIViewController { private var trendingView: TrendingView? = nil - private var trendingListViewModel: TrendingListViewModel + private var trendingListViewModel: TrendingListViewModel? + private var topRatedListViewModel: TopRatedListViewModel? - init(trendingListViewModel: TrendingListViewModel) { + init(trendingListViewModel: TrendingListViewModel? = nil, topRatedListViewModel: TopRatedListViewModel? = nil) { self.trendingListViewModel = trendingListViewModel + self.topRatedListViewModel = topRatedListViewModel super.init(nibName: nil, bundle: nil) } @@ -33,7 +35,7 @@ class TrendingViewController: UIViewController { trendingView?.renderLoadingState() - trendingListViewModel.fetchTrendings( + trendingListViewModel?.fetchTrendings( of: TrendingParams(mediaType: .all, timeWindow: .day) ) { [weak self] trendingListViewModel, error in guard let self = self else { @@ -44,7 +46,20 @@ class TrendingViewController: UIViewController { self.trendingView?.renderErrorState() } else if let trendingListViewModel = trendingListViewModel { self.trendingListViewModel = trendingListViewModel - self.trendingView?.renderSuccessState() + self.trendingView?.renderSuccessState(with: trendingListViewModel.titlePage) + } + } + + topRatedListViewModel?.fetchTopRated { [weak self] topRatedListViewModel, error in + guard let self = self else { + return + } + + if error != nil { + self.trendingView?.renderErrorState() + } else if let topRatedListViewModel = topRatedListViewModel { + self.topRatedListViewModel = topRatedListViewModel + self.trendingView?.renderSuccessState(with: topRatedListViewModel.titlePage) } } } @@ -52,11 +67,13 @@ class TrendingViewController: UIViewController { extension TrendingViewController: UICollectionViewDelegate, UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return self.trendingListViewModel.numberOfRowsInSection + return self.trendingListViewModel?.numberOfRowsInSection ?? + self.topRatedListViewModel?.numberOfRowsInSection ?? 0 } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let trending = trendingListViewModel.getTrending(indexPath.row) + guard let trending = trendingListViewModel?.getTrending(indexPath.row) ?? + topRatedListViewModel?.getTrending(indexPath.row) else { return UICollectionViewCell() } let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TrendingCollectionViewCell.identifier, for: indexPath) as? TrendingCollectionViewCell cell?.setupData(model: trending) From ef6f77de4507faeb720b247bdf30c3429282c44e Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Fri, 13 Jan 2023 18:18:46 -0300 Subject: [PATCH 29/65] Refactor HomeView, HomeViewController and MainCoordinator to add TopRated as well --- MyMovies/Coordinator/MainCoordinator.swift | 11 ++++--- MyMovies/View/Home/HomeView.swift | 33 ++++++++++++--------- MyMovies/View/Home/HomeViewController.swift | 7 +++-- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/MyMovies/Coordinator/MainCoordinator.swift b/MyMovies/Coordinator/MainCoordinator.swift index f162dae3..4a59438c 100644 --- a/MyMovies/Coordinator/MainCoordinator.swift +++ b/MyMovies/Coordinator/MainCoordinator.swift @@ -16,12 +16,15 @@ class MainCoordinator: Coordinator { } func start() { - let api = TrendingAPI() - let apiLoader = APILoader(apiHandler: api) - let trendingViewModel = TrendingListViewModel(apiLoader: apiLoader) + let apiTrending = TrendingAPI() + let apiLoaderTrending = APILoader(apiHandler: apiTrending) + let trendingViewModel = TrendingListViewModel(apiLoader: apiLoaderTrending) let trendingViewController = TrendingViewController(trendingListViewModel: trendingViewModel) - let topRatedViewController = TopRatedViewController() + let apiTopRated = TopRatedAPI() + let apiLoaderTopRated = APILoader(apiHandler: apiTopRated) + let topRatedListViewModel = TopRatedListViewModel(apiLoader: apiLoaderTopRated) + let topRatedViewController = TrendingViewController(topRatedListViewModel: topRatedListViewModel) let viewController = HomeViewController( trendingViewController: trendingViewController, diff --git a/MyMovies/View/Home/HomeView.swift b/MyMovies/View/Home/HomeView.swift index 18be9f58..2f674023 100644 --- a/MyMovies/View/Home/HomeView.swift +++ b/MyMovies/View/Home/HomeView.swift @@ -10,11 +10,14 @@ import UIKit class HomeView: UIView { private var trendingView: UIView -// private var topRatedView: UIView + private var topRatedView: UIView - init(trendingViewController: TrendingViewController, topRatedViewController: TopRatedViewController) { + init( + trendingViewController: TrendingViewController, + topRatedViewController: TrendingViewController + ) { self.trendingView = trendingViewController.view -// self.topRatedView = topRatedViewController.view + self.topRatedView = topRatedViewController.view super.init(frame: CGRect()) setup() @@ -42,17 +45,19 @@ class HomeView: UIView { trendingView.heightAnchor.constraint(equalToConstant: 300) ]) -// setupTopRatedView() + setupTopRatedView() } -// private func setupTopRatedView() { -// addSubview(topRatedView) -// -// NSLayoutConstraint.activate([ -// topRatedView.topAnchor.constraint(equalTo: trendingView.bottomAnchor, constant: 20), -// topRatedView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 20), -// topRatedView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -20), -// topRatedView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -20) -// ]) -// } + private func setupTopRatedView() { + addSubview(topRatedView) + + topRatedView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + topRatedView.topAnchor.constraint(equalTo: trendingView.bottomAnchor, constant: 20), + topRatedView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 4), + topRatedView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -4), + topRatedView.heightAnchor.constraint(equalToConstant: 300) + ]) + } } diff --git a/MyMovies/View/Home/HomeViewController.swift b/MyMovies/View/Home/HomeViewController.swift index c1c4181a..49fe8d9f 100644 --- a/MyMovies/View/Home/HomeViewController.swift +++ b/MyMovies/View/Home/HomeViewController.swift @@ -10,11 +10,14 @@ import UIKit class HomeViewController: UIViewController { private var trendingViewController: TrendingViewController - private var topRatedViewController: TopRatedViewController + private var topRatedViewController: TrendingViewController private var homeView: HomeView? = nil - init(trendingViewController: TrendingViewController, topRatedViewController: TopRatedViewController) { + init( + trendingViewController: TrendingViewController, + topRatedViewController: TrendingViewController + ) { self.trendingViewController = trendingViewController self.topRatedViewController = topRatedViewController super.init(nibName: nil, bundle: nil) From cafc1bf51ffc389fb4a1c66b5a989fabace23820 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Fri, 13 Jan 2023 18:28:31 -0300 Subject: [PATCH 30/65] Hide navigation bar --- MyMovies/View/Home/HomeViewController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/MyMovies/View/Home/HomeViewController.swift b/MyMovies/View/Home/HomeViewController.swift index 49fe8d9f..eb18bebc 100644 --- a/MyMovies/View/Home/HomeViewController.swift +++ b/MyMovies/View/Home/HomeViewController.swift @@ -37,5 +37,6 @@ class HomeViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + self.navigationController?.isNavigationBarHidden = true } } From 0d31ed1e510f56f8ad1006834bcae7ba347fe79a Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Fri, 13 Jan 2023 18:28:41 -0300 Subject: [PATCH 31/65] Change API parameter to .movie --- .../View/TrendingViewController/TrendingViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MyMovies/View/TrendingViewController/TrendingViewController.swift b/MyMovies/View/TrendingViewController/TrendingViewController.swift index b1f8483d..6393a7bf 100644 --- a/MyMovies/View/TrendingViewController/TrendingViewController.swift +++ b/MyMovies/View/TrendingViewController/TrendingViewController.swift @@ -36,7 +36,7 @@ class TrendingViewController: UIViewController { trendingView?.renderLoadingState() trendingListViewModel?.fetchTrendings( - of: TrendingParams(mediaType: .all, timeWindow: .day) + of: TrendingParams(mediaType: .movie, timeWindow: .day) ) { [weak self] trendingListViewModel, error in guard let self = self else { return From c17ad130f0115829bde0323e014362fe389ba120 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Sat, 14 Jan 2023 09:13:59 -0300 Subject: [PATCH 32/65] Create Movie and MovieAPI to get movie details --- MyMovies.xcodeproj/project.pbxproj | 8 ++++++ MyMovies/Model/DTO/Movie.swift | 41 ++++++++++++++++++++++++++++++ MyMovies/Network/APIPath.swift | 5 ++++ MyMovies/Service/MovieAPI.swift | 24 +++++++++++++++++ 4 files changed, 78 insertions(+) create mode 100644 MyMovies/Model/DTO/Movie.swift create mode 100644 MyMovies/Service/MovieAPI.swift diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index 90bb9700..e554e576 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -37,6 +37,8 @@ 7D35A46C296E4D090054954E /* UIImageView+ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A46B296E4D090054954E /* UIImageView+ImageLoader.swift */; }; 7D35A46E296F98C00054954E /* TopRatedListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A46D296F98C00054954E /* TopRatedListViewModel.swift */; }; 7D35A470296F99430054954E /* TopRatedAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A46F296F99430054954E /* TopRatedAPI.swift */; }; + 7D35A476297207C20054954E /* Movie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A475297207C20054954E /* Movie.swift */; }; + 7D35A47A29720A810054954E /* MovieAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A47929720A810054954E /* MovieAPI.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -91,6 +93,8 @@ 7D35A46B296E4D090054954E /* UIImageView+ImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+ImageLoader.swift"; sourceTree = ""; }; 7D35A46D296F98C00054954E /* TopRatedListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopRatedListViewModel.swift; sourceTree = ""; }; 7D35A46F296F99430054954E /* TopRatedAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopRatedAPI.swift; sourceTree = ""; }; + 7D35A475297207C20054954E /* Movie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Movie.swift; sourceTree = ""; }; + 7D35A47929720A810054954E /* MovieAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieAPI.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -200,6 +204,7 @@ 7D35A468296E4C1E0054954E /* ImageLoader */, 7D35A446296E2A390054954E /* TrendingAPI.swift */, 7D35A46F296F99430054954E /* TopRatedAPI.swift */, + 7D35A47929720A810054954E /* MovieAPI.swift */, ); path = Service; sourceTree = ""; @@ -220,6 +225,7 @@ children = ( 7D35A44C296E2CA80054954E /* TrendingResult.swift */, 7D35A44A296E2C920054954E /* TrendingDTO.swift */, + 7D35A475297207C20054954E /* Movie.swift */, ); path = DTO; sourceTree = ""; @@ -400,11 +406,13 @@ 7D35A450296E31C30054954E /* HomeViewController.swift in Sources */, 7D35A43A296E27AE0054954E /* APIEnvironment.swift in Sources */, 7D35A461296E3D5F0054954E /* TrendingCollectionViewCell.swift in Sources */, + 7D35A476297207C20054954E /* Movie.swift in Sources */, 7D35A45F296E3D360054954E /* TrendingView.swift in Sources */, 7D35A43E296E29000054954E /* APIHandler.swift in Sources */, 7D35A465296E45170054954E /* TrendingViewModel.swift in Sources */, 7D35A45B296E36DD0054954E /* HomeView.swift in Sources */, 7D35A437296E23740054954E /* Coordinator.swift in Sources */, + 7D35A47A29720A810054954E /* MovieAPI.swift in Sources */, 7D35A463296E3E410054954E /* TrendingListViewModel.swift in Sources */, 7D35A46A296E4C820054954E /* ImageLoader.swift in Sources */, 7D35A43C296E28490054954E /* APIPath.swift in Sources */, diff --git a/MyMovies/Model/DTO/Movie.swift b/MyMovies/Model/DTO/Movie.swift new file mode 100644 index 00000000..625e6255 --- /dev/null +++ b/MyMovies/Model/DTO/Movie.swift @@ -0,0 +1,41 @@ +// +// Movie.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/13/23. +// + +import Foundation + +struct Movie: Decodable { + let adult: Bool + let backdropPath: String? + let belongsToCollection: MovieCollection? + let budget: Int64 + let genres: [Genre] + let homepage: String? + let id: Int64 + let originalTitle: String? + let overview: String? + let popularity: Double + let posterPath: String? + let releaseDate: String? + let revenue: Int64 + let runtime: Int? + let status: String + let title: String? + let voteAverage: Double + let voteCount: Int64 +} + +struct MovieCollection: Decodable { + let id: Int64? + let name: String? + let posterPath: String? + let backdropPath: String? +} + +struct Genre: Decodable { + let id: Int + let name: String +} diff --git a/MyMovies/Network/APIPath.swift b/MyMovies/Network/APIPath.swift index 89566b6c..dc17da29 100644 --- a/MyMovies/Network/APIPath.swift +++ b/MyMovies/Network/APIPath.swift @@ -41,6 +41,11 @@ struct APIPath { return applyApiKey(url) } + func fetchMovieDetails(for movieId: Int64) -> String { + let url = "\(baseURL)/\(apiVersion())/movie/\(movieId)" + return applyApiKey(url) + } + private func applyApiKey(_ url: String) -> String { return "\(url)?api_key=\(apiKey)" } diff --git a/MyMovies/Service/MovieAPI.swift b/MyMovies/Service/MovieAPI.swift new file mode 100644 index 00000000..78c5b594 --- /dev/null +++ b/MyMovies/Service/MovieAPI.swift @@ -0,0 +1,24 @@ +// +// MovieAPI.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/13/23. +// + +import Foundation + +struct MovieAPI: APIHandler { + func makeRequest(from movieId: Int64) -> URLRequest? { + let urlString = APIPath().fetchMovieDetails(for: movieId) + if let url = URL(string: urlString) { + var urlRequest = URLRequest(url: url) + urlRequest.httpMethod = "GET" + return urlRequest + } + return nil + } + + func parseResponse(data: Data, response: HTTPURLResponse) throws -> Movie { + return try defaultParseResponse(data: data, response: response) + } +} From d374c1e4bfb9cdb19c4e50951c3de4ddf39ed059 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Sat, 14 Jan 2023 09:15:42 -0300 Subject: [PATCH 33/65] Create MovieViewModel that calls API and have all information needed for movie details view --- MyMovies.xcodeproj/project.pbxproj | 4 ++ MyMovies/Model/MovieViewModel.swift | 86 +++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 MyMovies/Model/MovieViewModel.swift diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index e554e576..c02392b0 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -38,6 +38,7 @@ 7D35A46E296F98C00054954E /* TopRatedListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A46D296F98C00054954E /* TopRatedListViewModel.swift */; }; 7D35A470296F99430054954E /* TopRatedAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A46F296F99430054954E /* TopRatedAPI.swift */; }; 7D35A476297207C20054954E /* Movie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A475297207C20054954E /* Movie.swift */; }; + 7D35A478297209250054954E /* MovieViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A477297209250054954E /* MovieViewModel.swift */; }; 7D35A47A29720A810054954E /* MovieAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A47929720A810054954E /* MovieAPI.swift */; }; /* End PBXBuildFile section */ @@ -94,6 +95,7 @@ 7D35A46D296F98C00054954E /* TopRatedListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopRatedListViewModel.swift; sourceTree = ""; }; 7D35A46F296F99430054954E /* TopRatedAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopRatedAPI.swift; sourceTree = ""; }; 7D35A475297207C20054954E /* Movie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Movie.swift; sourceTree = ""; }; + 7D35A477297209250054954E /* MovieViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieViewModel.swift; sourceTree = ""; }; 7D35A47929720A810054954E /* MovieAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieAPI.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -216,6 +218,7 @@ 7D35A462296E3E410054954E /* TrendingListViewModel.swift */, 7D35A464296E45170054954E /* TrendingViewModel.swift */, 7D35A46D296F98C00054954E /* TopRatedListViewModel.swift */, + 7D35A477297209250054954E /* MovieViewModel.swift */, ); path = Model; sourceTree = ""; @@ -412,6 +415,7 @@ 7D35A465296E45170054954E /* TrendingViewModel.swift in Sources */, 7D35A45B296E36DD0054954E /* HomeView.swift in Sources */, 7D35A437296E23740054954E /* Coordinator.swift in Sources */, + 7D35A478297209250054954E /* MovieViewModel.swift in Sources */, 7D35A47A29720A810054954E /* MovieAPI.swift in Sources */, 7D35A463296E3E410054954E /* TrendingListViewModel.swift in Sources */, 7D35A46A296E4C820054954E /* ImageLoader.swift in Sources */, diff --git a/MyMovies/Model/MovieViewModel.swift b/MyMovies/Model/MovieViewModel.swift new file mode 100644 index 00000000..836194e7 --- /dev/null +++ b/MyMovies/Model/MovieViewModel.swift @@ -0,0 +1,86 @@ +// +// MovieViewModel.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/13/23. +// + +import Foundation + +class MovieViewModel { + + // MARK: - Properties + + private var movie: Movie? = nil + + private let apiLoader: APILoader + + var title: String { + self.movie?.title ?? self.movie?.originalTitle ?? "" + } + + var imageUrl: URL? { + URL(string: APIPath().fetchImage(width: "w185", imagePath: movie?.posterPath ?? "")) + } + + var genres: [String] { + movie?.genres.map { $0.name } ?? [] + } + + var overview: String { + movie?.overview ?? "" + } + + var rate: String { + guard let rate = movie?.voteAverage else { return "" } + return String(format: "%.2f", rate) + } + + var totalVoteCount: String { + guard let voteCount = movie?.voteCount else { return "" } + return String(voteCount) + } + + var releaseDate: String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dddd" + guard let date = dateFormatter.date(from: movie?.releaseDate ?? "") else { return "" } + dateFormatter.dateFormat = "MM/dd/yyyy" + return dateFormatter.string(from: date) + } + + // MARK: - Initializer + + init(apiLoader: APILoader) { + self.apiLoader = apiLoader + } + + // MARK: - Functions + + func fetchMovie( + for movieId: Int64, + completion: @escaping (MovieViewModel?, ServiceError?) -> () + ) { + apiLoader.loadAPIRequest(requestData: movieId) { [weak self] movieResponse, error in + guard let self = self else { + return + } + + if let _ = error { + DispatchQueue.main.async { + completion(nil, error) + } + } else if let movieResponse = movieResponse { + self.movie = movieResponse + + DispatchQueue.main.async { + completion(self, nil) + } + } else { + DispatchQueue.main.async { + completion(nil, ServiceError(message: "Service Error: Unknow Error")) + } + } + } + } +} From a53b4457e47388425b9170244543bab3e8c18685 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Sat, 14 Jan 2023 09:17:11 -0300 Subject: [PATCH 34/65] Create MovieView and MovieViewController --- MyMovies.xcodeproj/project.pbxproj | 16 ++ .../View/MovieViewController/MovieView.swift | 246 ++++++++++++++++++ .../MovieViewController.swift | 52 ++++ 3 files changed, 314 insertions(+) create mode 100644 MyMovies/View/MovieViewController/MovieView.swift create mode 100644 MyMovies/View/MovieViewController/MovieViewController.swift diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index c02392b0..263f0395 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -40,6 +40,8 @@ 7D35A476297207C20054954E /* Movie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A475297207C20054954E /* Movie.swift */; }; 7D35A478297209250054954E /* MovieViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A477297209250054954E /* MovieViewModel.swift */; }; 7D35A47A29720A810054954E /* MovieAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A47929720A810054954E /* MovieAPI.swift */; }; + 7D35A47F29720DB50054954E /* MovieViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A47E29720DB50054954E /* MovieViewController.swift */; }; + 7D35A48129720F0E0054954E /* MovieView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A48029720F0E0054954E /* MovieView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -97,6 +99,8 @@ 7D35A475297207C20054954E /* Movie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Movie.swift; sourceTree = ""; }; 7D35A477297209250054954E /* MovieViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieViewModel.swift; sourceTree = ""; }; 7D35A47929720A810054954E /* MovieAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieAPI.swift; sourceTree = ""; }; + 7D35A47E29720DB50054954E /* MovieViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieViewController.swift; sourceTree = ""; }; + 7D35A48029720F0E0054954E /* MovieView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -237,6 +241,7 @@ isa = PBXGroup; children = ( 7D35A459296E36C60054954E /* Home */, + 7D35A47D29720D900054954E /* MovieViewController */, 7D35A451296E32DC0054954E /* TrendingViewController */, ); path = View; @@ -271,6 +276,15 @@ path = ImageLoader; sourceTree = ""; }; + 7D35A47D29720D900054954E /* MovieViewController */ = { + isa = PBXGroup; + children = ( + 7D35A47E29720DB50054954E /* MovieViewController.swift */, + 7D35A48029720F0E0054954E /* MovieView.swift */, + ); + path = MovieViewController; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -411,7 +425,9 @@ 7D35A461296E3D5F0054954E /* TrendingCollectionViewCell.swift in Sources */, 7D35A476297207C20054954E /* Movie.swift in Sources */, 7D35A45F296E3D360054954E /* TrendingView.swift in Sources */, + 7D35A48129720F0E0054954E /* MovieView.swift in Sources */, 7D35A43E296E29000054954E /* APIHandler.swift in Sources */, + 7D35A47F29720DB50054954E /* MovieViewController.swift in Sources */, 7D35A465296E45170054954E /* TrendingViewModel.swift in Sources */, 7D35A45B296E36DD0054954E /* HomeView.swift in Sources */, 7D35A437296E23740054954E /* Coordinator.swift in Sources */, diff --git a/MyMovies/View/MovieViewController/MovieView.swift b/MyMovies/View/MovieViewController/MovieView.swift new file mode 100644 index 00000000..049dee98 --- /dev/null +++ b/MyMovies/View/MovieViewController/MovieView.swift @@ -0,0 +1,246 @@ +// +// MovieView.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/13/23. +// + +import UIKit + +class MovieView: UIView { + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = .label + label.font = UIFont.boldSystemFont(ofSize: 30) + label.numberOfLines = 0 + label.textAlignment = .center + return label + }() + + private lazy var posterImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.isUserInteractionEnabled = false + return imageView + }() + + private lazy var genresStackView: UIStackView = { + let stackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .horizontal + stackView.spacing = 8 + stackView.alignment = .center + return stackView + }() + + private lazy var overviewTextView: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = .label + label.font = UIFont.systemFont(ofSize: 17) + label.numberOfLines = 0 + return label + }() + + private lazy var rateAndReleaseDateStackView: UIStackView = { + let stackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .horizontal + stackView.spacing = 8 + stackView.alignment = .center + stackView.distribution = .fill + return stackView + }() + + private lazy var rateLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = .label + label.font = UIFont.boldSystemFont(ofSize: 20) + return label + }() + + private lazy var totalVoteCountLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = .label + label.font = UIFont.systemFont(ofSize: 20) + return label + }() + + private lazy var releaseDateLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = .label + label.font = UIFont.systemFont(ofSize: 20) + return label + }() + + private lazy var activityIndicator: UIActivityIndicatorView = { + let activityIndicator = UIActivityIndicatorView(style: .large) + activityIndicator.translatesAutoresizingMaskIntoConstraints = false + activityIndicator.hidesWhenStopped = true + return activityIndicator + }() + + private lazy var errorLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = .label + label.font = UIFont.systemFont(ofSize: 28) + label.text = "Error when loading Movie details" + label.numberOfLines = 0 + label.textAlignment = .center + return label + }() + + override init(frame: CGRect) { + super.init(frame: CGRect()) + + setup() + } + + required init?(coder: NSCoder) { + fatalError() + } + + private func setup() { + self.backgroundColor = .systemBackground + + setupTitleLabel() + } + + private func setupTitleLabel() { + addSubview(titleLabel) + + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 20), + titleLabel.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 10), + titleLabel.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -10) + ]) + + setupPosterImage() + } + + private func setupPosterImage() { + addSubview(posterImageView) + + NSLayoutConstraint.activate([ + posterImageView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20), + posterImageView.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor), + posterImageView.widthAnchor.constraint(equalToConstant: 185), + posterImageView.heightAnchor.constraint(equalToConstant: 278) + ]) + + setupGenresStackView() + } + + private func setupGenresStackView() { + addSubview(genresStackView) + + NSLayoutConstraint.activate([ + genresStackView.topAnchor.constraint(equalTo: posterImageView.bottomAnchor, constant: 20), + genresStackView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 10), + genresStackView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -10) + ]) + + setupOverviewTextView() + } + + private func setupOverviewTextView() { + addSubview(overviewTextView) + + NSLayoutConstraint.activate([ + overviewTextView.topAnchor.constraint(equalTo: genresStackView.bottomAnchor, constant: 20), + overviewTextView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 10), + overviewTextView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -10), + overviewTextView.heightAnchor.constraint(lessThanOrEqualToConstant: 200) + ]) + + setupRateAndReleaseDateStackView() + } + + private func setupRateAndReleaseDateStackView() { + addSubview(rateAndReleaseDateStackView) + rateAndReleaseDateStackView.addArrangedSubview(rateLabel) + rateAndReleaseDateStackView.addArrangedSubview(totalVoteCountLabel) + rateAndReleaseDateStackView.addArrangedSubview(releaseDateLabel) + + NSLayoutConstraint.activate([ + rateAndReleaseDateStackView.topAnchor.constraint(equalTo: overviewTextView.bottomAnchor, constant: 20), + rateAndReleaseDateStackView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 10), + rateAndReleaseDateStackView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -10) + ]) + + setupActivityIndicator() + } + + private func setupActivityIndicator() { + addSubview(activityIndicator) + + NSLayoutConstraint.activate([ + activityIndicator.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor), + activityIndicator.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor) + ]) + + setupErrorLabel() + } + + private func setupErrorLabel() { + addSubview(errorLabel) + + NSLayoutConstraint.activate([ + errorLabel.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor), + errorLabel.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor) + ]) + } + + func renderLoadingState() { + self.titleLabel.isHidden = true + self.posterImageView.isHidden = true + self.genresStackView.isHidden = true + self.overviewTextView.isHidden = true + self.rateAndReleaseDateStackView.isHidden = true + self.errorLabel.isHidden = true + self.activityIndicator.startAnimating() + } + + func renderErrorState() { + self.activityIndicator.stopAnimating() + self.titleLabel.isHidden = true + self.posterImageView.isHidden = true + self.genresStackView.isHidden = true + self.overviewTextView.isHidden = true + self.rateAndReleaseDateStackView.isHidden = true + self.errorLabel.isHidden = false + } + + func renderSuccessState(with viewModel: MovieViewModel) { + self.activityIndicator.stopAnimating() + self.errorLabel.isHidden = true + self.titleLabel.isHidden = false + self.posterImageView.isHidden = false + self.genresStackView.isHidden = false + self.overviewTextView.isHidden = false + self.rateAndReleaseDateStackView.isHidden = false + + self.titleLabel.text = viewModel.title + if let url = viewModel.imageUrl { + self.posterImageView.loadImage(at: url) + } + for genre in viewModel.genres { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = .label + label.font = UIFont.systemFont(ofSize: 15) + label.text = genre + genresStackView.addArrangedSubview(label) + } + self.overviewTextView.text = viewModel.overview + self.rateLabel.text = "\(viewModel.rate)/10" + self.totalVoteCountLabel.text = "Total Vote Count: \(viewModel.totalVoteCount)" + self.releaseDateLabel.text = viewModel.releaseDate + } +} diff --git a/MyMovies/View/MovieViewController/MovieViewController.swift b/MyMovies/View/MovieViewController/MovieViewController.swift new file mode 100644 index 00000000..dbd88707 --- /dev/null +++ b/MyMovies/View/MovieViewController/MovieViewController.swift @@ -0,0 +1,52 @@ +// +// MovieViewController.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/13/23. +// + +import UIKit + +class MovieViewController: UIViewController { + + private var movieView: MovieView? = nil + private var movieViewModel: MovieViewModel + private var movieId: Int64 + + init( + movieId: Int64, + movieViewModel: MovieViewModel + ) { + self.movieId = movieId + self.movieViewModel = movieViewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError() + } + + override func loadView() { + self.movieView = MovieView() + view = movieView + } + + override func viewDidLoad() { + super.viewDidLoad() + + movieView?.renderLoadingState() + + movieViewModel.fetchMovie(for: self.movieId) { [weak self] movieViewModel, error in + guard let self = self else { + return + } + + if error != nil { + self.movieView?.renderErrorState() + } else if let movieViewModel = movieViewModel { + self.movieViewModel = movieViewModel + self.movieView?.renderSuccessState(with: movieViewModel) + } + } + } +} From 85a1bdd7d5c9f268a3b33814a84784a24aae8da3 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Sat, 14 Jan 2023 09:18:48 -0300 Subject: [PATCH 35/65] Detect click in cell and redirects to MovieViewController --- MyMovies.xcodeproj/project.pbxproj | 4 ++++ MyMovies/Coordinator/Coordinator.swift | 1 + MyMovies/Coordinator/Event.swift | 12 ++++++++++ MyMovies/Coordinator/MainCoordinator.swift | 24 +++++++++++++++++-- MyMovies/Model/TrendingViewModel.swift | 12 ++++------ MyMovies/View/Home/HomeViewController.swift | 5 ++++ .../TrendingViewController/TrendingView.swift | 2 -- .../TrendingViewController.swift | 19 +++++++++++++-- 8 files changed, 66 insertions(+), 13 deletions(-) create mode 100644 MyMovies/Coordinator/Event.swift diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index 263f0395..43a1ee0e 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -40,6 +40,7 @@ 7D35A476297207C20054954E /* Movie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A475297207C20054954E /* Movie.swift */; }; 7D35A478297209250054954E /* MovieViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A477297209250054954E /* MovieViewModel.swift */; }; 7D35A47A29720A810054954E /* MovieAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A47929720A810054954E /* MovieAPI.swift */; }; + 7D35A47C29720CD40054954E /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A47B29720CD40054954E /* Event.swift */; }; 7D35A47F29720DB50054954E /* MovieViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A47E29720DB50054954E /* MovieViewController.swift */; }; 7D35A48129720F0E0054954E /* MovieView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A48029720F0E0054954E /* MovieView.swift */; }; /* End PBXBuildFile section */ @@ -99,6 +100,7 @@ 7D35A475297207C20054954E /* Movie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Movie.swift; sourceTree = ""; }; 7D35A477297209250054954E /* MovieViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieViewModel.swift; sourceTree = ""; }; 7D35A47929720A810054954E /* MovieAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieAPI.swift; sourceTree = ""; }; + 7D35A47B29720CD40054954E /* Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; }; 7D35A47E29720DB50054954E /* MovieViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieViewController.swift; sourceTree = ""; }; 7D35A48029720F0E0054954E /* MovieView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -187,6 +189,7 @@ children = ( 7D35A434296E233F0054954E /* MainCoordinator.swift */, 7D35A436296E23740054954E /* Coordinator.swift */, + 7D35A47B29720CD40054954E /* Event.swift */, ); path = Coordinator; sourceTree = ""; @@ -436,6 +439,7 @@ 7D35A463296E3E410054954E /* TrendingListViewModel.swift in Sources */, 7D35A46A296E4C820054954E /* ImageLoader.swift in Sources */, 7D35A43C296E28490054954E /* APIPath.swift in Sources */, + 7D35A47C29720CD40054954E /* Event.swift in Sources */, 7D35A46C296E4D090054954E /* UIImageView+ImageLoader.swift in Sources */, 7D35A467296E4C170054954E /* UIImageLoader.swift in Sources */, 7D35A44D296E2CA80054954E /* TrendingResult.swift in Sources */, diff --git a/MyMovies/Coordinator/Coordinator.swift b/MyMovies/Coordinator/Coordinator.swift index 42200d43..3889f03a 100644 --- a/MyMovies/Coordinator/Coordinator.swift +++ b/MyMovies/Coordinator/Coordinator.swift @@ -10,6 +10,7 @@ import UIKit protocol Coordinator { var navigationController: UINavigationController { get } + func eventOccurred(with type: Event, parameters params: Any?) func start() } diff --git a/MyMovies/Coordinator/Event.swift b/MyMovies/Coordinator/Event.swift new file mode 100644 index 00000000..6717995e --- /dev/null +++ b/MyMovies/Coordinator/Event.swift @@ -0,0 +1,12 @@ +// +// Event.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/13/23. +// + +import Foundation + +enum Event { + case movieClicked +} diff --git a/MyMovies/Coordinator/MainCoordinator.swift b/MyMovies/Coordinator/MainCoordinator.swift index 4a59438c..38f11b62 100644 --- a/MyMovies/Coordinator/MainCoordinator.swift +++ b/MyMovies/Coordinator/MainCoordinator.swift @@ -15,16 +15,36 @@ class MainCoordinator: Coordinator { self.navigationController = navigationController } + func eventOccurred(with type: Event, parameters params: Any?) { + switch type { + case .movieClicked: + guard let movieId = params as? Int64 else { return } + + let api = MovieAPI() + let apiLoader = APILoader(apiHandler: api) + let viewModel = MovieViewModel(apiLoader: apiLoader) + let viewController = MovieViewController(movieId: movieId, movieViewModel: viewModel) + + navigationController.pushViewController(viewController, animated: true) + } + } + func start() { let apiTrending = TrendingAPI() let apiLoaderTrending = APILoader(apiHandler: apiTrending) let trendingViewModel = TrendingListViewModel(apiLoader: apiLoaderTrending) - let trendingViewController = TrendingViewController(trendingListViewModel: trendingViewModel) + let trendingViewController = TrendingViewController( + trendingListViewModel: trendingViewModel, + coordinator: self + ) let apiTopRated = TopRatedAPI() let apiLoaderTopRated = APILoader(apiHandler: apiTopRated) let topRatedListViewModel = TopRatedListViewModel(apiLoader: apiLoaderTopRated) - let topRatedViewController = TrendingViewController(topRatedListViewModel: topRatedListViewModel) + let topRatedViewController = TrendingViewController( + topRatedListViewModel: topRatedListViewModel, + coordinator: self + ) let viewController = HomeViewController( trendingViewController: trendingViewController, diff --git a/MyMovies/Model/TrendingViewModel.swift b/MyMovies/Model/TrendingViewModel.swift index 07196a73..e7f7fd44 100644 --- a/MyMovies/Model/TrendingViewModel.swift +++ b/MyMovies/Model/TrendingViewModel.swift @@ -11,19 +11,17 @@ class TrendingViewModel { private var trending: TrendingResult var title: String { - if let title = trending.originalTitle { - return title - } else if let name = trending.name { - return name - } else { - return "" - } + self.trending.originalTitle ?? self.trending.name ?? "" } var posterImageURL: URL? { URL(string: APIPath().fetchImage(width: "w154", imagePath: trending.posterPath ?? "")) } + var movieId: Int64? { + self.trending.id + } + init(trending: TrendingResult) { self.trending = trending } diff --git a/MyMovies/View/Home/HomeViewController.swift b/MyMovies/View/Home/HomeViewController.swift index eb18bebc..bf2a0698 100644 --- a/MyMovies/View/Home/HomeViewController.swift +++ b/MyMovies/View/Home/HomeViewController.swift @@ -39,4 +39,9 @@ class HomeViewController: UIViewController { super.viewDidLoad() self.navigationController?.isNavigationBarHidden = true } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.navigationController?.isNavigationBarHidden = false + } } diff --git a/MyMovies/View/TrendingViewController/TrendingView.swift b/MyMovies/View/TrendingViewController/TrendingView.swift index 40d5182f..9958052a 100644 --- a/MyMovies/View/TrendingViewController/TrendingView.swift +++ b/MyMovies/View/TrendingViewController/TrendingView.swift @@ -120,8 +120,6 @@ class TrendingView: UIView { addSubview(errorLabel) NSLayoutConstraint.activate([ - errorLabel.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 20), - errorLabel.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -20), errorLabel.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor), errorLabel.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor) ]) diff --git a/MyMovies/View/TrendingViewController/TrendingViewController.swift b/MyMovies/View/TrendingViewController/TrendingViewController.swift index 6393a7bf..6c1a43b6 100644 --- a/MyMovies/View/TrendingViewController/TrendingViewController.swift +++ b/MyMovies/View/TrendingViewController/TrendingViewController.swift @@ -7,15 +7,22 @@ import UIKit -class TrendingViewController: UIViewController { +class TrendingViewController: UIViewController, Coordinating { + + var coordinator: Coordinator? private var trendingView: TrendingView? = nil private var trendingListViewModel: TrendingListViewModel? private var topRatedListViewModel: TopRatedListViewModel? - init(trendingListViewModel: TrendingListViewModel? = nil, topRatedListViewModel: TopRatedListViewModel? = nil) { + init( + trendingListViewModel: TrendingListViewModel? = nil, + topRatedListViewModel: TopRatedListViewModel? = nil, + coordinator: Coordinator + ) { self.trendingListViewModel = trendingListViewModel self.topRatedListViewModel = topRatedListViewModel + self.coordinator = coordinator super.init(nibName: nil, bundle: nil) } @@ -80,4 +87,12 @@ extension TrendingViewController: UICollectionViewDelegate, UICollectionViewData return cell ?? UICollectionViewCell() } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let trending = trendingListViewModel?.getTrending(indexPath.row) ?? + topRatedListViewModel?.getTrending(indexPath.row), + let movieId = trending.movieId else { return } + + coordinator?.eventOccurred(with: .movieClicked, parameters: movieId) + } } From f2218f48a779bc5037f44097d6cb60fb1cdb03d1 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Sat, 14 Jan 2023 09:20:52 -0300 Subject: [PATCH 36/65] Fix Xcode warning' --- .../View/TrendingViewController/TrendingViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MyMovies/View/TrendingViewController/TrendingViewController.swift b/MyMovies/View/TrendingViewController/TrendingViewController.swift index 6c1a43b6..8e58a5c7 100644 --- a/MyMovies/View/TrendingViewController/TrendingViewController.swift +++ b/MyMovies/View/TrendingViewController/TrendingViewController.swift @@ -93,6 +93,6 @@ extension TrendingViewController: UICollectionViewDelegate, UICollectionViewData topRatedListViewModel?.getTrending(indexPath.row), let movieId = trending.movieId else { return } - coordinator?.eventOccurred(with: .movieClicked, parameters: movieId) + coordinator?.eventOccurred(with: Event.movieClicked, parameters: movieId) } } From 22d729f79d3a1bd2e2aa15ae1a64b8c5d15efc4f Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Sat, 14 Jan 2023 09:39:22 -0300 Subject: [PATCH 37/65] Create MockURLProtocol and tests for TrendingListViewModel --- MyMovies.xcodeproj/project.pbxproj | 28 ++++++++++++ MyMoviesTests/Mocks/MockURLProtocol.swift | 44 ++++++++++++++++++ MyMoviesTests/TrendingTests.swift | 55 +++++++++++++++++++++++ 3 files changed, 127 insertions(+) create mode 100644 MyMoviesTests/Mocks/MockURLProtocol.swift create mode 100644 MyMoviesTests/TrendingTests.swift diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index 43a1ee0e..4c46a126 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -43,6 +43,9 @@ 7D35A47C29720CD40054954E /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A47B29720CD40054954E /* Event.swift */; }; 7D35A47F29720DB50054954E /* MovieViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A47E29720DB50054954E /* MovieViewController.swift */; }; 7D35A48129720F0E0054954E /* MovieView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A48029720F0E0054954E /* MovieView.swift */; }; + 7D35A4852972D6A00054954E /* trending-success.json in Resources */ = {isa = PBXBuildFile; fileRef = 7D35A4842972D6A00054954E /* trending-success.json */; }; + 7D35A4872972D6D40054954E /* MockURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A4862972D6D40054954E /* MockURLProtocol.swift */; }; + 7D35A4892972D70E0054954E /* TrendingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A4882972D70E0054954E /* TrendingTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -103,6 +106,9 @@ 7D35A47B29720CD40054954E /* Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; }; 7D35A47E29720DB50054954E /* MovieViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieViewController.swift; sourceTree = ""; }; 7D35A48029720F0E0054954E /* MovieView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieView.swift; sourceTree = ""; }; + 7D35A4842972D6A00054954E /* trending-success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = "trending-success.json"; path = "../../../../../Documentos/trending-success.json"; sourceTree = ""; }; + 7D35A4862972D6D40054954E /* MockURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLProtocol.swift; sourceTree = ""; }; + 7D35A4882972D70E0054954E /* TrendingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -170,7 +176,9 @@ 7D35A419296E22450054954E /* MyMoviesTests */ = { isa = PBXGroup; children = ( + 7D35A4822972D5B00054954E /* Mocks */, 7D35A41A296E22450054954E /* MyMoviesTests.swift */, + 7D35A4882972D70E0054954E /* TrendingTests.swift */, ); path = MyMoviesTests; sourceTree = ""; @@ -288,6 +296,23 @@ path = MovieViewController; sourceTree = ""; }; + 7D35A4822972D5B00054954E /* Mocks */ = { + isa = PBXGroup; + children = ( + 7D35A4832972D5BE0054954E /* Trending */, + 7D35A4862972D6D40054954E /* MockURLProtocol.swift */, + ); + path = Mocks; + sourceTree = ""; + }; + 7D35A4832972D5BE0054954E /* Trending */ = { + isa = PBXGroup; + children = ( + 7D35A4842972D6A00054954E /* trending-success.json */, + ); + path = Trending; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -401,6 +426,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7D35A4852972D6A00054954E /* trending-success.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -456,7 +482,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7D35A4892972D70E0054954E /* TrendingTests.swift in Sources */, 7D35A41B296E22450054954E /* MyMoviesTests.swift in Sources */, + 7D35A4872972D6D40054954E /* MockURLProtocol.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MyMoviesTests/Mocks/MockURLProtocol.swift b/MyMoviesTests/Mocks/MockURLProtocol.swift new file mode 100644 index 00000000..05b138f9 --- /dev/null +++ b/MyMoviesTests/Mocks/MockURLProtocol.swift @@ -0,0 +1,44 @@ +// +// MockURLProtocol.swift +// MyMoviesTests +// +// Created by Caio dos Santos Ambrosio on 1/14/23. +// + +import Foundation + +class MockURLProtocol: URLProtocol { + + static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data?))? + + override class func canInit(with request: URLRequest) -> Bool { + return true + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + override func startLoading() { + guard let handler = MockURLProtocol.requestHandler else { + fatalError("Handler is unavailable.") + } + + do { + let (response, data) = try handler(request) + + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + + if let data = data { + client?.urlProtocol(self, didLoad: data) + } + + client?.urlProtocolDidFinishLoading(self) + } catch { + client?.urlProtocol(self, didFailWithError: error) + } + } + + override func stopLoading() { + } +} diff --git a/MyMoviesTests/TrendingTests.swift b/MyMoviesTests/TrendingTests.swift new file mode 100644 index 00000000..024fe110 --- /dev/null +++ b/MyMoviesTests/TrendingTests.swift @@ -0,0 +1,55 @@ +// +// TrendingTests.swift +// MyMoviesTests +// +// Created by Caio dos Santos Ambrosio on 1/14/23. +// + +import XCTest +@testable import MyMovies + +final class TrendingTests: XCTestCase { + var apiLoader: APILoader! + var expectation: XCTestExpectation! + let apiURL = URL(string: APIPath().fetchTrending(with: TrendingParams(mediaType: .movie, timeWindow: .day)))! + + override func setUp() { + let configuration = URLSessionConfiguration.default + configuration.protocolClasses = [MockURLProtocol.self] + let urlSession = URLSession.init(configuration: configuration) + + apiLoader = APILoader(apiHandler: TrendingAPI(), urlSession: urlSession) + expectation = expectation(description: "Expectation") + } + + func testSuccessCall() { + guard let pathString = Bundle(for: type(of: self)).path(forResource: "trending-success", ofType: "json") else { + fatalError("Mock json not found") + } + + guard let jsonString = try? String(contentsOfFile: pathString, encoding: .utf8) else { + fatalError("Mock json can not be converted to String") + } + + let data = jsonString.data(using: .utf8) + + MockURLProtocol.requestHandler = { request in + guard let url = request.url, url == self.apiURL else { + fatalError("Trying to test a different URL") + } + + let response = HTTPURLResponse(url: self.apiURL, statusCode: 200, httpVersion: nil, headerFields: nil)! + return (response, data) + } + + let viewModel = TrendingListViewModel(apiLoader: self.apiLoader) + + viewModel.fetchTrendings(of: TrendingParams(mediaType: .movie, timeWindow: .day)) { viewModel, error in + XCTAssertEqual(viewModel?.numberOfRowsInSection, 20) + XCTAssertEqual(viewModel?.titlePage, "Trending") + XCTAssertEqual(viewModel?.getTrending(2).title, "Avatar: The Way of Water") + self.expectation.fulfill() + } + wait(for: [expectation], timeout: 100.0) + } +} From f4b5f737ed88b9b3bfb00f4e15e1f2f1fd8d1cea Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Sat, 14 Jan 2023 09:44:57 -0300 Subject: [PATCH 38/65] Rename TrendingTests to TrendingListViewModelSpec --- MyMovies.xcodeproj/project.pbxproj | 8 ++++---- ...rendingTests.swift => TrendingListViewModelSpec.swift} | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) rename MyMoviesTests/{TrendingTests.swift => TrendingListViewModelSpec.swift} (97%) diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index 4c46a126..6d9986bc 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -45,7 +45,7 @@ 7D35A48129720F0E0054954E /* MovieView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A48029720F0E0054954E /* MovieView.swift */; }; 7D35A4852972D6A00054954E /* trending-success.json in Resources */ = {isa = PBXBuildFile; fileRef = 7D35A4842972D6A00054954E /* trending-success.json */; }; 7D35A4872972D6D40054954E /* MockURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A4862972D6D40054954E /* MockURLProtocol.swift */; }; - 7D35A4892972D70E0054954E /* TrendingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A4882972D70E0054954E /* TrendingTests.swift */; }; + 7D35A4892972D70E0054954E /* TrendingListViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A4882972D70E0054954E /* TrendingListViewModelSpec.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -108,7 +108,7 @@ 7D35A48029720F0E0054954E /* MovieView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieView.swift; sourceTree = ""; }; 7D35A4842972D6A00054954E /* trending-success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = "trending-success.json"; path = "../../../../../Documentos/trending-success.json"; sourceTree = ""; }; 7D35A4862972D6D40054954E /* MockURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLProtocol.swift; sourceTree = ""; }; - 7D35A4882972D70E0054954E /* TrendingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingTests.swift; sourceTree = ""; }; + 7D35A4882972D70E0054954E /* TrendingListViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingListViewModelSpec.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -178,7 +178,7 @@ children = ( 7D35A4822972D5B00054954E /* Mocks */, 7D35A41A296E22450054954E /* MyMoviesTests.swift */, - 7D35A4882972D70E0054954E /* TrendingTests.swift */, + 7D35A4882972D70E0054954E /* TrendingListViewModelSpec.swift */, ); path = MyMoviesTests; sourceTree = ""; @@ -482,7 +482,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 7D35A4892972D70E0054954E /* TrendingTests.swift in Sources */, + 7D35A4892972D70E0054954E /* TrendingListViewModelSpec.swift in Sources */, 7D35A41B296E22450054954E /* MyMoviesTests.swift in Sources */, 7D35A4872972D6D40054954E /* MockURLProtocol.swift in Sources */, ); diff --git a/MyMoviesTests/TrendingTests.swift b/MyMoviesTests/TrendingListViewModelSpec.swift similarity index 97% rename from MyMoviesTests/TrendingTests.swift rename to MyMoviesTests/TrendingListViewModelSpec.swift index 024fe110..d6499055 100644 --- a/MyMoviesTests/TrendingTests.swift +++ b/MyMoviesTests/TrendingListViewModelSpec.swift @@ -8,7 +8,7 @@ import XCTest @testable import MyMovies -final class TrendingTests: XCTestCase { +final class TrendingListViewModelSpec: XCTestCase { var apiLoader: APILoader! var expectation: XCTestExpectation! let apiURL = URL(string: APIPath().fetchTrending(with: TrendingParams(mediaType: .movie, timeWindow: .day)))! From 9a3bba420ca1f611de74214aa8227edfdc48db80 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Sat, 14 Jan 2023 09:46:21 -0300 Subject: [PATCH 39/65] Move TrendingListViewModelSpec to new folder and delete base test file --- MyMovies.xcodeproj/project.pbxproj | 14 +++++--- .../TrendingListViewModelSpec.swift | 0 MyMoviesTests/MyMoviesTests.swift | 36 ------------------- 3 files changed, 9 insertions(+), 41 deletions(-) rename MyMoviesTests/{ => Model}/TrendingListViewModelSpec.swift (100%) delete mode 100644 MyMoviesTests/MyMoviesTests.swift diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index 6d9986bc..55cff076 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -11,7 +11,6 @@ 7D35A406296E22430054954E /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A405296E22430054954E /* SceneDelegate.swift */; }; 7D35A40D296E22450054954E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7D35A40C296E22450054954E /* Assets.xcassets */; }; 7D35A410296E22450054954E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7D35A40E296E22450054954E /* LaunchScreen.storyboard */; }; - 7D35A41B296E22450054954E /* MyMoviesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A41A296E22450054954E /* MyMoviesTests.swift */; }; 7D35A425296E22450054954E /* MyMoviesUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A424296E22450054954E /* MyMoviesUITests.swift */; }; 7D35A427296E22450054954E /* MyMoviesUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A426296E22450054954E /* MyMoviesUITestsLaunchTests.swift */; }; 7D35A435296E233F0054954E /* MainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A434296E233F0054954E /* MainCoordinator.swift */; }; @@ -73,7 +72,6 @@ 7D35A40F296E22450054954E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 7D35A411296E22450054954E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7D35A416296E22450054954E /* MyMoviesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MyMoviesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 7D35A41A296E22450054954E /* MyMoviesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyMoviesTests.swift; sourceTree = ""; }; 7D35A420296E22450054954E /* MyMoviesUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MyMoviesUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 7D35A424296E22450054954E /* MyMoviesUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyMoviesUITests.swift; sourceTree = ""; }; 7D35A426296E22450054954E /* MyMoviesUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyMoviesUITestsLaunchTests.swift; sourceTree = ""; }; @@ -176,9 +174,8 @@ 7D35A419296E22450054954E /* MyMoviesTests */ = { isa = PBXGroup; children = ( + 7DF3DD012972DB63007BF9A3 /* Model */, 7D35A4822972D5B00054954E /* Mocks */, - 7D35A41A296E22450054954E /* MyMoviesTests.swift */, - 7D35A4882972D70E0054954E /* TrendingListViewModelSpec.swift */, ); path = MyMoviesTests; sourceTree = ""; @@ -313,6 +310,14 @@ path = Trending; sourceTree = ""; }; + 7DF3DD012972DB63007BF9A3 /* Model */ = { + isa = PBXGroup; + children = ( + 7D35A4882972D70E0054954E /* TrendingListViewModelSpec.swift */, + ); + path = Model; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -483,7 +488,6 @@ buildActionMask = 2147483647; files = ( 7D35A4892972D70E0054954E /* TrendingListViewModelSpec.swift in Sources */, - 7D35A41B296E22450054954E /* MyMoviesTests.swift in Sources */, 7D35A4872972D6D40054954E /* MockURLProtocol.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/MyMoviesTests/TrendingListViewModelSpec.swift b/MyMoviesTests/Model/TrendingListViewModelSpec.swift similarity index 100% rename from MyMoviesTests/TrendingListViewModelSpec.swift rename to MyMoviesTests/Model/TrendingListViewModelSpec.swift diff --git a/MyMoviesTests/MyMoviesTests.swift b/MyMoviesTests/MyMoviesTests.swift deleted file mode 100644 index 813ad845..00000000 --- a/MyMoviesTests/MyMoviesTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// MyMoviesTests.swift -// MyMoviesTests -// -// Created by Caio dos Santos Ambrosio on 1/10/23. -// - -import XCTest -@testable import MyMovies - -final class MyMoviesTests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - // Any test you write for XCTest can be annotated as throws and async. - // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. - // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - -} From 6d34b14d26fba8e79e2e3c530d29ca1896621d19 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Sat, 14 Jan 2023 09:49:27 -0300 Subject: [PATCH 40/65] Mock files that was inserted in the wrong way --- MyMovies.xcodeproj/project.pbxproj | 12 +- .../Mocks/Trending/trending-error.json | 5 + .../Mocks/Trending/trending-success.json | 423 ++++++++++++++++++ 3 files changed, 436 insertions(+), 4 deletions(-) create mode 100644 MyMoviesTests/Mocks/Trending/trending-error.json create mode 100644 MyMoviesTests/Mocks/Trending/trending-success.json diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index 55cff076..a3c83953 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -42,9 +42,10 @@ 7D35A47C29720CD40054954E /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A47B29720CD40054954E /* Event.swift */; }; 7D35A47F29720DB50054954E /* MovieViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A47E29720DB50054954E /* MovieViewController.swift */; }; 7D35A48129720F0E0054954E /* MovieView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A48029720F0E0054954E /* MovieView.swift */; }; - 7D35A4852972D6A00054954E /* trending-success.json in Resources */ = {isa = PBXBuildFile; fileRef = 7D35A4842972D6A00054954E /* trending-success.json */; }; 7D35A4872972D6D40054954E /* MockURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A4862972D6D40054954E /* MockURLProtocol.swift */; }; 7D35A4892972D70E0054954E /* TrendingListViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A4882972D70E0054954E /* TrendingListViewModelSpec.swift */; }; + 7DF3DD092972DC32007BF9A3 /* trending-error.json in Resources */ = {isa = PBXBuildFile; fileRef = 7DF3DD082972DC32007BF9A3 /* trending-error.json */; }; + 7DF3DD0B2972DC3B007BF9A3 /* trending-success.json in Resources */ = {isa = PBXBuildFile; fileRef = 7DF3DD0A2972DC3B007BF9A3 /* trending-success.json */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -104,9 +105,10 @@ 7D35A47B29720CD40054954E /* Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; }; 7D35A47E29720DB50054954E /* MovieViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieViewController.swift; sourceTree = ""; }; 7D35A48029720F0E0054954E /* MovieView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieView.swift; sourceTree = ""; }; - 7D35A4842972D6A00054954E /* trending-success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = "trending-success.json"; path = "../../../../../Documentos/trending-success.json"; sourceTree = ""; }; 7D35A4862972D6D40054954E /* MockURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLProtocol.swift; sourceTree = ""; }; 7D35A4882972D70E0054954E /* TrendingListViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingListViewModelSpec.swift; sourceTree = ""; }; + 7DF3DD082972DC32007BF9A3 /* trending-error.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "trending-error.json"; sourceTree = ""; }; + 7DF3DD0A2972DC3B007BF9A3 /* trending-success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "trending-success.json"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -305,7 +307,8 @@ 7D35A4832972D5BE0054954E /* Trending */ = { isa = PBXGroup; children = ( - 7D35A4842972D6A00054954E /* trending-success.json */, + 7DF3DD082972DC32007BF9A3 /* trending-error.json */, + 7DF3DD0A2972DC3B007BF9A3 /* trending-success.json */, ); path = Trending; sourceTree = ""; @@ -431,7 +434,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 7D35A4852972D6A00054954E /* trending-success.json in Resources */, + 7DF3DD0B2972DC3B007BF9A3 /* trending-success.json in Resources */, + 7DF3DD092972DC32007BF9A3 /* trending-error.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MyMoviesTests/Mocks/Trending/trending-error.json b/MyMoviesTests/Mocks/Trending/trending-error.json new file mode 100644 index 00000000..827cc820 --- /dev/null +++ b/MyMoviesTests/Mocks/Trending/trending-error.json @@ -0,0 +1,5 @@ +{ + "success": false, + "status_code": 5, + "status_message": "Invalid parameters: Your request parameters are incorrect." +} \ No newline at end of file diff --git a/MyMoviesTests/Mocks/Trending/trending-success.json b/MyMoviesTests/Mocks/Trending/trending-success.json new file mode 100644 index 00000000..74f26df5 --- /dev/null +++ b/MyMoviesTests/Mocks/Trending/trending-success.json @@ -0,0 +1,423 @@ +{ + "page": 1, + "results": [ + { + "adult": false, + "backdrop_path": "/r9PkFnRUIthgBp2JZZzD380MWZy.jpg", + "id": 315162, + "title": "Puss in Boots: The Last Wish", + "original_language": "en", + "original_title": "Puss in Boots: The Last Wish", + "overview": "Puss in Boots discovers that his passion for adventure has taken its toll: He has burned through eight of his nine lives, leaving him with only one life left. Puss sets out on an epic journey to find the mythical Last Wish and restore his nine lives.", + "poster_path": "/1NqwE6LP9IEdOZ57NCT51ftHtWT.jpg", + "media_type": "movie", + "genre_ids": [ + 16, + 28, + 12, + 35, + 10751, + 14 + ], + "popularity": 9724.995, + "release_date": "2022-12-21", + "video": false, + "vote_average": 8.6, + "vote_count": 1400 + }, + { + "adult": false, + "backdrop_path": "/ng9a2oVoZuohU0Mzls9GAVH0MCn.jpg", + "id": 858408, + "title": "Dog Gone", + "original_language": "en", + "original_title": "Dog Gone", + "overview": "When his beloved dog goes missing, a young man embarks on an incredible search with his parents to find him and give him life-saving medication.", + "poster_path": "/eYWdLZCS9W1Xli9bynzFSddLx02.jpg", + "media_type": "movie", + "genre_ids": [ + 10751, + 12, + 18 + ], + "popularity": 51.754, + "release_date": "2023-01-13", + "video": false, + "vote_average": 6.7, + "vote_count": 6 + }, + { + "adult": false, + "backdrop_path": "/s16H6tpK2utvwDtzZ8Qy4qm5Emw.jpg", + "id": 76600, + "title": "Avatar: The Way of Water", + "original_language": "en", + "original_title": "Avatar: The Way of Water", + "overview": "Set more than a decade after the events of the first film, learn the story of the Sully family (Jake, Neytiri, and their kids), the trouble that follows them, the lengths they go to keep each other safe, the battles they fight to stay alive, and the tragedies they endure.", + "poster_path": "/t6HIqrRAclMCA60NsSmeqe9RmNV.jpg", + "media_type": "movie", + "genre_ids": [ + 878, + 12, + 28 + ], + "popularity": 3003.545, + "release_date": "2022-12-16", + "video": false, + "vote_average": 7.7, + "vote_count": 4094 + }, + { + "adult": false, + "backdrop_path": "/lacnOoTHyS8g0eSu7cyQ8TgdGtv.jpg", + "id": 872954, + "title": "The Old Way", + "original_language": "en", + "original_title": "The Old Way", + "overview": "An old gunslinger and his daughter must face the consequences of his past, when the son of a man he murdered years ago arrives to take his revenge.", + "poster_path": "/8HCCYAIocXxMKn7J9yQfDX1vBM5.jpg", + "media_type": "movie", + "genre_ids": [ + 28, + 37 + ], + "popularity": 24.751, + "release_date": "2023-01-06", + "video": false, + "vote_average": 5.375, + "vote_count": 8 + }, + { + "adult": false, + "backdrop_path": "/5kAGbi9MFAobQTVfK4kWPnIfnP0.jpg", + "id": 536554, + "title": "M3GAN", + "original_language": "en", + "original_title": "M3GAN", + "overview": "A brilliant toy company roboticist uses artificial intelligence to develop M3GAN, a life-like doll programmed to emotionally bond with her newly orphaned niece. But when the doll's programming works too well, she becomes overprotective of her new friend with terrifying results.", + "poster_path": "/7CNCv9uhqdwK7Fv4bR4nmDysnd9.jpg", + "media_type": "movie", + "genre_ids": [ + 878, + 27, + 35 + ], + "popularity": 2067, + "release_date": "2023-01-06", + "video": false, + "vote_average": 7.208, + "vote_count": 209 + }, + { + "adult": false, + "backdrop_path": "/b08BDQPq42AoLMfhi7DtTOoYqVu.jpg", + "id": 800815, + "title": "The Pale Blue Eye", + "original_language": "en", + "original_title": "The Pale Blue Eye", + "overview": "West Point, New York, 1830. When a cadet at the burgeoning military academy is found hanged with his heart cut out, the top brass summons former New York City constable Augustus Landor to investigate. While attempting to solve this grisly mystery, the reluctant detective engages the help of one of the cadets: a strange but brilliant young fellow by the name of Edgar Allan Poe", + "poster_path": "/9xkGlFRqrN8btTLU0KQvOfn2PHr.jpg", + "media_type": "movie", + "genre_ids": [ + 9648, + 53, + 27, + 80 + ], + "popularity": 637.882, + "release_date": "2022-12-23", + "video": false, + "vote_average": 7.138, + "vote_count": 508 + }, + { + "adult": false, + "backdrop_path": "/mSyQoValhBsJdq3JNGXJww2Q5yL.jpg", + "id": 593643, + "title": "The Menu", + "original_language": "en", + "original_title": "The Menu", + "overview": "A young couple travels to a remote island to eat at an exclusive restaurant where the chef has prepared a lavish menu, with some shocking surprises.", + "poster_path": "/fPtUgMcLIboqlTlPrq0bQpKK8eq.jpg", + "media_type": "movie", + "genre_ids": [ + 35, + 27, + 53 + ], + "popularity": 500.984, + "release_date": "2022-11-18", + "video": false, + "vote_average": 7.306, + "vote_count": 1335 + }, + { + "adult": false, + "backdrop_path": "/e1cC9muSRtAHVtF5GJtKAfATYIT.jpg", + "id": 1063242, + "title": "Disconnect: The Wedding Planner", + "original_language": "fr", + "original_title": "Disconnect: The Wedding Planner", + "overview": "After falling victim to a scam, a desperate man races the clock as he attempts to plan a luxurious destination wedding for an important investor.", + "poster_path": "/tGmCxGkVMOqig2TrbXAsE9dOVvX.jpg", + "media_type": "movie", + "genre_ids": [ + 10749, + 35 + ], + "popularity": 40.16, + "release_date": "2023-01-13", + "video": false, + "vote_average": 2, + "vote_count": 1 + }, + { + "adult": false, + "backdrop_path": "/bTqmsKOr9cSdYptFhbFEOmP3qCm.jpg", + "id": 640146, + "title": "Ant-Man and the Wasp: Quantumania", + "original_language": "en", + "original_title": "Ant-Man and the Wasp: Quantumania", + "overview": "Super-Hero partners Scott Lang and Hope van Dyne, along with with Hope's parents Janet van Dyne and Hank Pym, and Scott's daughter Cassie Lang, find themselves exploring the Quantum Realm, interacting with strange new creatures and embarking on an adventure that will push them beyond the limits of what they thought possible.", + "poster_path": "/ynWwsafy7ib7hitxImQ8Lso3lvI.jpg", + "media_type": "movie", + "genre_ids": [ + 12, + 878, + 35 + ], + "popularity": 156.416, + "release_date": "2023-02-17", + "video": false, + "vote_average": 0, + "vote_count": 0 + }, + { + "adult": false, + "backdrop_path": "/5pMy5LF2JAleBNBtuzizfCMWM7k.jpg", + "id": 653851, + "title": "Devotion", + "original_language": "en", + "original_title": "Devotion", + "overview": "The harrowing true story of two elite US Navy fighter pilots during the Korean War. Their heroic sacrifices would ultimately make them the Navy's most celebrated wingmen.", + "poster_path": "/26yQPXymbWeCLKwcmyL8dRjAzth.jpg", + "media_type": "movie", + "genre_ids": [ + 10752, + 36, + 18 + ], + "popularity": 207.761, + "release_date": "2022-11-23", + "video": false, + "vote_average": 6.653, + "vote_count": 49 + }, + { + "adult": false, + "backdrop_path": "/aROq5hpE1WgDMjwIxxmk42kFFQZ.jpg", + "id": 829410, + "title": "Sick", + "original_language": "en", + "original_title": "Sick", + "overview": "In the midst of the pandemic, college student Parker and her best friend Miri decide to self-quarantine at her family's lake house, where they will be alone — or so they think...", + "poster_path": "/yisnA4KdZOfJeDve3DekgUpeCe9.jpg", + "media_type": "movie", + "genre_ids": [ + 27, + 53 + ], + "popularity": 15.085, + "release_date": "2023-01-13", + "video": false, + "vote_average": 6.4, + "vote_count": 9 + }, + { + "adult": false, + "backdrop_path": "/dKqa850uvbNSCaQCV4Im1XlzEtQ.jpg", + "id": 661374, + "title": "Glass Onion: A Knives Out Mystery", + "original_language": "en", + "original_title": "Glass Onion: A Knives Out Mystery", + "overview": "World-famous detective Benoit Blanc heads to Greece to peel back the layers of a mystery surrounding a tech billionaire and his eclectic crew of friends.", + "poster_path": "/vDGr1YdrlfbU9wxTOdpf3zChmv9.jpg", + "media_type": "movie", + "genre_ids": [ + 9648, + 53, + 35 + ], + "popularity": 1656.736, + "release_date": "2022-11-23", + "video": false, + "vote_average": 7.072, + "vote_count": 2576 + }, + { + "adult": false, + "backdrop_path": "/tt79dbOPd9Z9ykEOpvckttgYXwH.jpg", + "id": 545611, + "title": "Everything Everywhere All at Once", + "original_language": "en", + "original_title": "Everything Everywhere All at Once", + "overview": "An aging Chinese immigrant is swept up in an insane adventure, where she alone can save what's important to her by connecting with the lives she could have led in other universes.", + "poster_path": "/w3LxiVYdWWRvEVdn5RYq6jIqkb1.jpg", + "media_type": "movie", + "genre_ids": [ + 28, + 12, + 878 + ], + "popularity": 224.437, + "release_date": "2022-03-24", + "video": false, + "vote_average": 8.067, + "vote_count": 2648 + }, + { + "adult": false, + "backdrop_path": "/bQXAqRx2Fgc46uCVWgoPz5L5Dtr.jpg", + "id": 436270, + "title": "Black Adam", + "original_language": "en", + "original_title": "Black Adam", + "overview": "Nearly 5,000 years after he was bestowed with the almighty powers of the Egyptian gods—and imprisoned just as quickly—Black Adam is freed from his earthly tomb, ready to unleash his unique form of justice on the modern world.", + "poster_path": "/pFlaoHTZeyNkG83vxsAJiGzfSsa.jpg", + "media_type": "movie", + "genre_ids": [ + 14, + 28, + 878 + ], + "popularity": 1876.042, + "release_date": "2022-10-21", + "video": false, + "vote_average": 7.218, + "vote_count": 3656 + }, + { + "adult": false, + "backdrop_path": "/iBEs4PnQLkL4BbsAGvsuBgeTCM3.jpg", + "id": 960519, + "title": "Noise", + "original_language": "es", + "original_title": "Ruido", + "overview": "Julia is a mother, or rather, one of many mothers, sisters, daughters, colleagues, who have had their lives torn apart by the widespread violence in a country waging a war against its women. Julia is searching for Ger, her daughter. And in her search, she will weave through the stories and struggles of the different women she will meet.", + "poster_path": "/6GemI78Ge0ABs5qM6CnvCPduKEU.jpg", + "media_type": "movie", + "genre_ids": [ + 18 + ], + "popularity": 122.966, + "release_date": "2022-10-13", + "video": false, + "vote_average": 7.429, + "vote_count": 7 + }, + { + "adult": false, + "backdrop_path": "/8I37NtDffNV7AZlDa7uDvvqhovU.jpg", + "id": 19995, + "title": "Avatar", + "original_language": "en", + "original_title": "Avatar", + "overview": "In the 22nd century, a paraplegic Marine is dispatched to the moon Pandora on a unique mission, but becomes torn between following orders and protecting an alien civilization.", + "poster_path": "/jRXYjXNq0Cs2TcJjLkki24MLp7u.jpg", + "media_type": "movie", + "genre_ids": [ + 28, + 12, + 14, + 878 + ], + "popularity": 1552.689, + "release_date": "2009-12-15", + "video": false, + "vote_average": 7.561, + "vote_count": 27754 + }, + { + "adult": false, + "backdrop_path": "/AaV1YIdWKnjAIAOe8UUKBFm327v.jpg", + "id": 361743, + "title": "Top Gun: Maverick", + "original_language": "en", + "original_title": "Top Gun: Maverick", + "overview": "After more than thirty years of service as one of the Navy’s top aviators, and dodging the advancement in rank that would ground him, Pete “Maverick” Mitchell finds himself training a detachment of TOP GUN graduates for a specialized mission the likes of which no living pilot has ever seen.", + "poster_path": "/62HCnUTziyWcpDaBO2i1DX17ljH.jpg", + "media_type": "movie", + "genre_ids": [ + 28, + 18 + ], + "popularity": 519.672, + "release_date": "2022-05-27", + "video": false, + "vote_average": 8.3, + "vote_count": 5597 + }, + { + "adult": false, + "backdrop_path": "/yYrvN5WFeGYjJnRzhY0QXuo4Isw.jpg", + "id": 505642, + "title": "Black Panther: Wakanda Forever", + "original_language": "en", + "original_title": "Black Panther: Wakanda Forever", + "overview": "Queen Ramonda, Shuri, M’Baku, Okoye and the Dora Milaje fight to protect their nation from intervening world powers in the wake of King T’Challa’s death. As the Wakandans strive to embrace their next chapter, the heroes must band together with the help of War Dog Nakia and Everett Ross and forge a new path for the kingdom of Wakanda.", + "poster_path": "/sv1xJUazXeYqALzczSZ3O6nkH75.jpg", + "media_type": "movie", + "genre_ids": [ + 28, + 12, + 878 + ], + "popularity": 1180.588, + "release_date": "2022-11-11", + "video": false, + "vote_average": 7.47, + "vote_count": 1604 + }, + { + "adult": false, + "backdrop_path": "/9Md4CqzUGDtK5oEkRRvozLkGc9d.jpg", + "id": 674324, + "title": "The Banshees of Inisherin", + "original_language": "en", + "original_title": "The Banshees of Inisherin", + "overview": "Two lifelong friends find themselves at an impasse when one abruptly ends their relationship, with alarming consequences for both of them.", + "poster_path": "/dpeR3ue0Q93Rk8jhE2DzVPMRGIm.jpg", + "media_type": "movie", + "genre_ids": [ + 18, + 35 + ], + "popularity": 203.388, + "release_date": "2022-10-21", + "video": false, + "vote_average": 7.474, + "vote_count": 463 + }, + { + "adult": false, + "backdrop_path": "/6RCf9jzKxyjblYV4CseayK6bcJo.jpg", + "id": 804095, + "title": "The Fabelmans", + "original_language": "en", + "original_title": "The Fabelmans", + "overview": "Growing up in post-World War II era Arizona, young Sammy Fabelman aspires to become a filmmaker as he reaches adolescence, but soon discovers a shattering family secret and explores how the power of films can help him see the truth.", + "poster_path": "/xId9zoiv5WPQOuxqykUDrlpmrUz.jpg", + "media_type": "movie", + "genre_ids": [ + 18 + ], + "popularity": 161.117, + "release_date": "2022-11-11", + "video": false, + "vote_average": 8.018, + "vote_count": 420 + } + ], + "total_pages": 1000, + "total_results": 20000 +} \ No newline at end of file From 28f00729ba7dfe159999e6aa8f1728e1dcfc7902 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Sat, 14 Jan 2023 09:56:49 -0300 Subject: [PATCH 41/65] Minor fixes in TrendingListViewModelSpec and add test for error call --- .../Model/TrendingListViewModelSpec.swift | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/MyMoviesTests/Model/TrendingListViewModelSpec.swift b/MyMoviesTests/Model/TrendingListViewModelSpec.swift index d6499055..c40b88e3 100644 --- a/MyMoviesTests/Model/TrendingListViewModelSpec.swift +++ b/MyMoviesTests/Model/TrendingListViewModelSpec.swift @@ -22,8 +22,8 @@ final class TrendingListViewModelSpec: XCTestCase { expectation = expectation(description: "Expectation") } - func testSuccessCall() { - guard let pathString = Bundle(for: type(of: self)).path(forResource: "trending-success", ofType: "json") else { + private func getMockData(for path: String) -> Data? { + guard let pathString = Bundle(for: type(of: self)).path(forResource: path, ofType: "json") else { fatalError("Mock json not found") } @@ -31,7 +31,11 @@ final class TrendingListViewModelSpec: XCTestCase { fatalError("Mock json can not be converted to String") } - let data = jsonString.data(using: .utf8) + return jsonString.data(using: .utf8) + } + + func testSuccessCall() { + let data = getMockData(for: "trending-success") MockURLProtocol.requestHandler = { request in guard let url = request.url, url == self.apiURL else { @@ -45,6 +49,7 @@ final class TrendingListViewModelSpec: XCTestCase { let viewModel = TrendingListViewModel(apiLoader: self.apiLoader) viewModel.fetchTrendings(of: TrendingParams(mediaType: .movie, timeWindow: .day)) { viewModel, error in + XCTAssertNotNil(viewModel) XCTAssertEqual(viewModel?.numberOfRowsInSection, 20) XCTAssertEqual(viewModel?.titlePage, "Trending") XCTAssertEqual(viewModel?.getTrending(2).title, "Avatar: The Way of Water") @@ -52,4 +57,26 @@ final class TrendingListViewModelSpec: XCTestCase { } wait(for: [expectation], timeout: 100.0) } + + func testErrorCall() { + let data = getMockData(for: "trending-error") + + MockURLProtocol.requestHandler = { request in + guard let url = request.url, url == self.apiURL else { + fatalError("Trying to test a different URL") + } + + let response = HTTPURLResponse(url: self.apiURL, statusCode: 400, httpVersion: nil, headerFields: nil)! + return (response, data) + } + + let viewModel = TrendingListViewModel(apiLoader: self.apiLoader) + + viewModel.fetchTrendings(of: TrendingParams(mediaType: .movie, timeWindow: .day)) { viewModel, error in + XCTAssertNotNil(error) + XCTAssertEqual(error?.message, "Error when communicating with API") + self.expectation.fulfill() + } + wait(for: [expectation], timeout: 100.0) + } } From 003359390d77b7412dd0d0acd42820205c1de054 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Sat, 14 Jan 2023 10:02:11 -0300 Subject: [PATCH 42/65] Create tests for TopRatedListViewModel --- MyMovies.xcodeproj/project.pbxproj | 20 + .../Mocks/TopRated/top-rated-error.json | 5 + .../Mocks/TopRated/top-rated-success.json | 397 ++++++++++++++++++ .../Model/TopRatedListViewModelSpec.swift | 82 ++++ 4 files changed, 504 insertions(+) create mode 100644 MyMoviesTests/Mocks/TopRated/top-rated-error.json create mode 100644 MyMoviesTests/Mocks/TopRated/top-rated-success.json create mode 100644 MyMoviesTests/Model/TopRatedListViewModelSpec.swift diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index a3c83953..c779da6c 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -46,6 +46,9 @@ 7D35A4892972D70E0054954E /* TrendingListViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A4882972D70E0054954E /* TrendingListViewModelSpec.swift */; }; 7DF3DD092972DC32007BF9A3 /* trending-error.json in Resources */ = {isa = PBXBuildFile; fileRef = 7DF3DD082972DC32007BF9A3 /* trending-error.json */; }; 7DF3DD0B2972DC3B007BF9A3 /* trending-success.json in Resources */ = {isa = PBXBuildFile; fileRef = 7DF3DD0A2972DC3B007BF9A3 /* trending-success.json */; }; + 7DF3DD0D2972DE25007BF9A3 /* TopRatedListViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD0C2972DE25007BF9A3 /* TopRatedListViewModelSpec.swift */; }; + 7DF3DD102972DEED007BF9A3 /* top-rated-success.json in Resources */ = {isa = PBXBuildFile; fileRef = 7DF3DD0F2972DEED007BF9A3 /* top-rated-success.json */; }; + 7DF3DD122972DF22007BF9A3 /* top-rated-error.json in Resources */ = {isa = PBXBuildFile; fileRef = 7DF3DD112972DF22007BF9A3 /* top-rated-error.json */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -109,6 +112,9 @@ 7D35A4882972D70E0054954E /* TrendingListViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingListViewModelSpec.swift; sourceTree = ""; }; 7DF3DD082972DC32007BF9A3 /* trending-error.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "trending-error.json"; sourceTree = ""; }; 7DF3DD0A2972DC3B007BF9A3 /* trending-success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "trending-success.json"; sourceTree = ""; }; + 7DF3DD0C2972DE25007BF9A3 /* TopRatedListViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopRatedListViewModelSpec.swift; sourceTree = ""; }; + 7DF3DD0F2972DEED007BF9A3 /* top-rated-success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "top-rated-success.json"; sourceTree = ""; }; + 7DF3DD112972DF22007BF9A3 /* top-rated-error.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "top-rated-error.json"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -298,6 +304,7 @@ 7D35A4822972D5B00054954E /* Mocks */ = { isa = PBXGroup; children = ( + 7DF3DD0E2972DEDE007BF9A3 /* TopRated */, 7D35A4832972D5BE0054954E /* Trending */, 7D35A4862972D6D40054954E /* MockURLProtocol.swift */, ); @@ -317,10 +324,20 @@ isa = PBXGroup; children = ( 7D35A4882972D70E0054954E /* TrendingListViewModelSpec.swift */, + 7DF3DD0C2972DE25007BF9A3 /* TopRatedListViewModelSpec.swift */, ); path = Model; sourceTree = ""; }; + 7DF3DD0E2972DEDE007BF9A3 /* TopRated */ = { + isa = PBXGroup; + children = ( + 7DF3DD0F2972DEED007BF9A3 /* top-rated-success.json */, + 7DF3DD112972DF22007BF9A3 /* top-rated-error.json */, + ); + path = TopRated; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -435,7 +452,9 @@ buildActionMask = 2147483647; files = ( 7DF3DD0B2972DC3B007BF9A3 /* trending-success.json in Resources */, + 7DF3DD102972DEED007BF9A3 /* top-rated-success.json in Resources */, 7DF3DD092972DC32007BF9A3 /* trending-error.json in Resources */, + 7DF3DD122972DF22007BF9A3 /* top-rated-error.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -492,6 +511,7 @@ buildActionMask = 2147483647; files = ( 7D35A4892972D70E0054954E /* TrendingListViewModelSpec.swift in Sources */, + 7DF3DD0D2972DE25007BF9A3 /* TopRatedListViewModelSpec.swift in Sources */, 7D35A4872972D6D40054954E /* MockURLProtocol.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/MyMoviesTests/Mocks/TopRated/top-rated-error.json b/MyMoviesTests/Mocks/TopRated/top-rated-error.json new file mode 100644 index 00000000..22c9812f --- /dev/null +++ b/MyMoviesTests/Mocks/TopRated/top-rated-error.json @@ -0,0 +1,5 @@ +{ + "success": false, + "status_code": 34, + "status_message": "The resource you requested could not be found." +} \ No newline at end of file diff --git a/MyMoviesTests/Mocks/TopRated/top-rated-success.json b/MyMoviesTests/Mocks/TopRated/top-rated-success.json new file mode 100644 index 00000000..c8966f8d --- /dev/null +++ b/MyMoviesTests/Mocks/TopRated/top-rated-success.json @@ -0,0 +1,397 @@ +{ + "page": 1, + "results": [ + { + "adult": false, + "backdrop_path": "/tmU7GeKVybMWFButWEGl2M4GeiP.jpg", + "genre_ids": [ + 18, + 80 + ], + "id": 238, + "original_language": "en", + "original_title": "The Godfather", + "overview": "Spanning the years 1945 to 1955, a chronicle of the fictional Italian-American Corleone crime family. When organized crime family patriarch, Vito Corleone barely survives an attempt on his life, his youngest son, Michael steps in to take care of the would-be killers, launching a campaign of bloody revenge.", + "popularity": 104.512, + "poster_path": "/3bhkrj58Vtu7enYsRolD1fZdja1.jpg", + "release_date": "1972-03-14", + "title": "The Godfather", + "video": false, + "vote_average": 8.7, + "vote_count": 17262 + }, + { + "adult": false, + "backdrop_path": "/wPU78OPN4BYEgWYdXyg0phMee64.jpg", + "genre_ids": [ + 18, + 80 + ], + "id": 278, + "original_language": "en", + "original_title": "The Shawshank Redemption", + "overview": "Framed in the 1940s for the double murder of his wife and her lover, upstanding banker Andy Dufresne begins a new life at the Shawshank prison, where he puts his accounting skills to work for an amoral warden. During his long stretch in prison, Dufresne comes to be admired by the other inmates -- including an older prisoner named Red -- for his integrity and unquenchable sense of hope.", + "popularity": 89.766, + "poster_path": "/hBcY0fE9pfXzvVaY4GKarweriG2.jpg", + "release_date": "1994-09-23", + "title": "The Shawshank Redemption", + "video": false, + "vote_average": 8.7, + "vote_count": 23060 + }, + { + "adult": false, + "backdrop_path": "/kGzFbGhp99zva6oZODW5atUtnqi.jpg", + "genre_ids": [ + 18, + 80 + ], + "id": 240, + "original_language": "en", + "original_title": "The Godfather Part II", + "overview": "In the continuing saga of the Corleone crime family, a young Vito Corleone grows up in Sicily and in 1910s New York. In the 1950s, Michael Corleone attempts to expand the family business into Las Vegas, Hollywood and Cuba.", + "popularity": 55.967, + "poster_path": "/hek3koDUyRQk7FIhPXsa6mT2Zc3.jpg", + "release_date": "1974-12-20", + "title": "The Godfather Part II", + "video": false, + "vote_average": 8.6, + "vote_count": 10450 + }, + { + "adult": false, + "backdrop_path": "/r9PkFnRUIthgBp2JZZzD380MWZy.jpg", + "genre_ids": [ + 16, + 28, + 12, + 35, + 10751, + 14 + ], + "id": 315162, + "original_language": "en", + "original_title": "Puss in Boots: The Last Wish", + "overview": "Puss in Boots discovers that his passion for adventure has taken its toll: He has burned through eight of his nine lives, leaving him with only one life left. Puss sets out on an epic journey to find the mythical Last Wish and restore his nine lives.", + "popularity": 10563.563, + "poster_path": "/1NqwE6LP9IEdOZ57NCT51ftHtWT.jpg", + "release_date": "2022-12-21", + "title": "Puss in Boots: The Last Wish", + "video": false, + "vote_average": 8.6, + "vote_count": 1067 + }, + { + "adult": false, + "backdrop_path": "/zb6fM1CX41D9rF9hdgclu0peUmy.jpg", + "genre_ids": [ + 18, + 36, + 10752 + ], + "id": 424, + "original_language": "en", + "original_title": "Schindler's List", + "overview": "The true story of how businessman Oskar Schindler saved over a thousand Jewish lives from the Nazis while they worked as slaves in his factory during World War II.", + "popularity": 50.3, + "poster_path": "/sF1U4EUQS8YHUYjNl3pMGNIQyr0.jpg", + "release_date": "1993-12-15", + "title": "Schindler's List", + "video": false, + "vote_average": 8.6, + "vote_count": 13646 + }, + { + "adult": false, + "backdrop_path": "/vI3aUGTuRRdM7J78KIdW98LdxE5.jpg", + "genre_ids": [ + 35, + 18, + 10749 + ], + "id": 19404, + "original_language": "hi", + "original_title": "दिलवाले दुल्हनिया ले जायेंगे", + "overview": "Raj is a rich, carefree, happy-go-lucky second generation NRI. Simran is the daughter of Chaudhary Baldev Singh, who in spite of being an NRI is very strict about adherence to Indian values. Simran has left for India to be married to her childhood fiancé. Raj leaves for India with a mission at his hands, to claim his lady love under the noses of her whole family. Thus begins a saga.", + "popularity": 31.212, + "poster_path": "/2CAL2433ZeIihfX1Hb2139CX0pW.jpg", + "release_date": "1995-10-19", + "title": "Dilwale Dulhania Le Jayenge", + "video": false, + "vote_average": 8.6, + "vote_count": 4019 + }, + { + "adult": false, + "backdrop_path": "/Ab8mkHmkYADjU7wQiOkia9BzGvS.jpg", + "genre_ids": [ + 16, + 10751, + 14 + ], + "id": 129, + "original_language": "ja", + "original_title": "千と千尋の神隠し", + "overview": "A young girl, Chihiro, becomes trapped in a strange new world of spirits. When her parents undergo a mysterious transformation, she must call upon the courage she never knew she had to free her family.", + "popularity": 75.192, + "poster_path": "/39wmItIWsg5sZMyRUHLkWBcuVCM.jpg", + "release_date": "2001-07-20", + "title": "Spirited Away", + "video": false, + "vote_average": 8.5, + "vote_count": 13793 + }, + { + "adult": false, + "backdrop_path": "/qqHQsStV6exghCM7zbObuYBiYxw.jpg", + "genre_ids": [ + 18 + ], + "id": 389, + "original_language": "en", + "original_title": "12 Angry Men", + "overview": "The defense and the prosecution have rested and the jury is filing into the jury room to decide if a young Spanish-American is guilty or innocent of murdering his father. What begins as an open and shut case soon becomes a mini-drama of each of the jurors' prejudices and preconceptions about the trial, the accused, and each other.", + "popularity": 30.865, + "poster_path": "/ppd84D2i9W8jXmsyInGyihiSyqz.jpg", + "release_date": "1957-04-10", + "title": "12 Angry Men", + "video": false, + "vote_average": 8.5, + "vote_count": 6949 + }, + { + "adult": false, + "backdrop_path": "/dIWwZW7dJJtqC6CgWzYkNVKIUm8.jpg", + "genre_ids": [ + 10749, + 16, + 18 + ], + "id": 372058, + "original_language": "ja", + "original_title": "君の名は。", + "overview": "High schoolers Mitsuha and Taki are complete strangers living separate lives. But one night, they suddenly switch places. Mitsuha wakes up in Taki’s body, and he in hers. This bizarre occurrence continues to happen randomly, and the two must adjust their lives around each other.", + "popularity": 105.614, + "poster_path": "/q719jXXEzOoYaps6babgKnONONX.jpg", + "release_date": "2016-08-26", + "title": "Your Name.", + "video": false, + "vote_average": 8.5, + "vote_count": 9397 + }, + { + "adult": false, + "backdrop_path": "/hiKmpZMGZsrkA3cdce8a7Dpos1j.jpg", + "genre_ids": [ + 35, + 53, + 18 + ], + "id": 496243, + "original_language": "ko", + "original_title": "기생충", + "overview": "All unemployed, Ki-taek's family takes peculiar interest in the wealthy and glamorous Parks for their livelihood until they get entangled in an unexpected incident.", + "popularity": 64.296, + "poster_path": "/7IiTTgloJzvGI1TAYymCfbfl3vT.jpg", + "release_date": "2019-05-30", + "title": "Parasite", + "video": false, + "vote_average": 8.5, + "vote_count": 15043 + }, + { + "adult": false, + "backdrop_path": "/3RMLbSEXOn1CzLoNT7xFeLfdxhq.jpg", + "genre_ids": [ + 10749, + 16 + ], + "id": 372754, + "original_language": "ja", + "original_title": "同級生", + "overview": "Rihito Sajo, an honor student with a perfect score on the entrance exam and Hikaru Kusakabe, in a band and popular among girls, would have never crossed paths. Until one day they started talking at the practice for their school’s upcoming chorus festival. After school, the two meet regularly, as Hikaru helps Rihito to improve his singing skills. While they listen to each other’s voice and harmonize, their hearts start to beat together.", + "popularity": 11.746, + "poster_path": "/cIfRCA5wEvj9tApca4UDUagQEiM.jpg", + "release_date": "2016-02-20", + "title": "Dou kyu sei – Classmates", + "video": false, + "vote_average": 8.5, + "vote_count": 284 + }, + { + "adult": false, + "backdrop_path": "/l6hQWH9eDksNJNiXWYRkWqikOdu.jpg", + "genre_ids": [ + 14, + 18, + 80 + ], + "id": 497, + "original_language": "en", + "original_title": "The Green Mile", + "overview": "A supernatural tale set on death row in a Southern prison, where gentle giant John Coffey possesses the mysterious power to heal people's ailments. When the cell block's head guard, Paul Edgecomb, recognizes Coffey's miraculous gift, he tries desperately to help stave off the condemned man's execution.", + "popularity": 116.567, + "poster_path": "/velWPhVMQeQKcxggNEU8YmIo52R.jpg", + "release_date": "1999-12-10", + "title": "The Green Mile", + "video": false, + "vote_average": 8.5, + "vote_count": 14848 + }, + { + "adult": false, + "backdrop_path": "/pbEkjhdfP7yuDcMB78YEZwgD4IN.jpg", + "genre_ids": [ + 18, + 28, + 80, + 53 + ], + "id": 155, + "original_language": "en", + "original_title": "The Dark Knight", + "overview": "Batman raises the stakes in his war on crime. With the help of Lt. Jim Gordon and District Attorney Harvey Dent, Batman sets out to dismantle the remaining criminal organizations that plague the streets. The partnership proves to be effective, but they soon find themselves prey to a reign of chaos unleashed by a rising criminal mastermind known to the terrified citizens of Gotham as the Joker.", + "popularity": 107.305, + "poster_path": "/qJ2tW6WMUDux911r6m7haRef0WH.jpg", + "release_date": "2008-07-14", + "title": "The Dark Knight", + "video": false, + "vote_average": 8.5, + "vote_count": 28945 + }, + { + "adult": false, + "backdrop_path": "/suaEOtk1N1sgg2MTM7oZd2cfVp3.jpg", + "genre_ids": [ + 53, + 80 + ], + "id": 680, + "original_language": "en", + "original_title": "Pulp Fiction", + "overview": "A burger-loving hit man, his philosophical partner, a drug-addled gangster's moll and a washed-up boxer converge in this sprawling, comedic crime caper. Their adventures unfurl in three stories that ingeniously trip back and forth in time.", + "popularity": 66.873, + "poster_path": "/fIE3lAGcZDV1G6XM5KmuWnNsPp1.jpg", + "release_date": "1994-09-10", + "title": "Pulp Fiction", + "video": false, + "vote_average": 8.5, + "vote_count": 24419 + }, + { + "adult": false, + "backdrop_path": "/eoCSp75lxatmIa6aGqfnzwtbttd.jpg", + "genre_ids": [ + 37 + ], + "id": 429, + "original_language": "it", + "original_title": "Il buono, il brutto, il cattivo", + "overview": "While the Civil War rages on between the Union and the Confederacy, three men – a quiet loner, a ruthless hitman, and a Mexican bandit – comb the American Southwest in search of a strongbox containing $200,000 in stolen gold.", + "popularity": 49.075, + "poster_path": "/bX2xnavhMYjWDoZp1VM6VnU1xwe.jpg", + "release_date": "1966-12-23", + "title": "The Good, the Bad and the Ugly", + "video": false, + "vote_average": 8.5, + "vote_count": 7119 + }, + { + "adult": false, + "backdrop_path": "/w2uGvCpMtvRqZg6waC1hvLyZoJa.jpg", + "genre_ids": [ + 10749 + ], + "id": 696374, + "original_language": "en", + "original_title": "Gabriel's Inferno", + "overview": "An intriguing and sinful exploration of seduction, forbidden love, and redemption, Gabriel's Inferno is a captivating and wildly passionate tale of one man's escape from his own personal hell as he tries to earn the impossible--forgiveness and love.", + "popularity": 11.33, + "poster_path": "/oyG9TL7FcRP4EZ9Vid6uKzwdndz.jpg", + "release_date": "2020-05-29", + "title": "Gabriel's Inferno", + "video": false, + "vote_average": 8.5, + "vote_count": 2315 + }, + { + "adult": false, + "backdrop_path": "/3h1JZGDhZ8nzxdgvkxha0qBqi05.jpg", + "genre_ids": [ + 35, + 18, + 10749 + ], + "id": 13, + "original_language": "en", + "original_title": "Forrest Gump", + "overview": "A man with a low IQ has accomplished great things in his life and been present during significant historic events—in each case, far exceeding what anyone imagined he could do. But despite all he has achieved, his one true love eludes him.", + "popularity": 112.207, + "poster_path": "/arw2vcBveWOVZr6pxd9XTd1TdQa.jpg", + "release_date": "1994-06-23", + "title": "Forrest Gump", + "video": false, + "vote_average": 8.5, + "vote_count": 23926 + }, + { + "adult": false, + "backdrop_path": "/lXhgCODAbBXL5buk9yEmTpOoOgR.jpg", + "genre_ids": [ + 12, + 14, + 28 + ], + "id": 122, + "original_language": "en", + "original_title": "The Lord of the Rings: The Return of the King", + "overview": "Aragorn is revealed as the heir to the ancient kings as he, Gandalf and the other members of the broken fellowship struggle to save Gondor from Sauron's forces. Meanwhile, Frodo and Sam take the ring closer to the heart of Mordor, the dark lord's realm.", + "popularity": 86.447, + "poster_path": "/rCzpDGLbOoPwLjy3OAm5NUPOTrC.jpg", + "release_date": "2003-12-01", + "title": "The Lord of the Rings: The Return of the King", + "video": false, + "vote_average": 8.5, + "vote_count": 20900 + }, + { + "adult": false, + "backdrop_path": "/sw7mordbZxgITU877yTpZCud90M.jpg", + "genre_ids": [ + 18, + 80 + ], + "id": 769, + "original_language": "en", + "original_title": "GoodFellas", + "overview": "The true story of Henry Hill, a half-Irish, half-Sicilian Brooklyn kid who is adopted by neighbourhood gangsters at an early age and climbs the ranks of a Mafia family under the guidance of Jimmy Conway.", + "popularity": 51.359, + "poster_path": "/aKuFiU82s5ISJpGZp7YkIr3kCUd.jpg", + "release_date": "1990-09-12", + "title": "GoodFellas", + "video": false, + "vote_average": 8.5, + "vote_count": 10863 + }, + { + "adult": false, + "backdrop_path": "/jtAI6OJIWLWiRItNSZoWjrsUtmi.jpg", + "genre_ids": [ + 10749 + ], + "id": 724089, + "original_language": "en", + "original_title": "Gabriel's Inferno: Part II", + "overview": "Professor Gabriel Emerson finally learns the truth about Julia Mitchell's identity, but his realization comes a moment too late. Julia is done waiting for the well-respected Dante specialist to remember her and wants nothing more to do with him. Can Gabriel win back her heart before she finds love in another's arms?", + "popularity": 11.951, + "poster_path": "/x5o8cLZfEXMoZczTYWLrUo1P7UJ.jpg", + "release_date": "2020-07-31", + "title": "Gabriel's Inferno: Part II", + "video": false, + "vote_average": 8.5, + "vote_count": 1455 + } + ], + "total_pages": 535, + "total_results": 10687 +} \ No newline at end of file diff --git a/MyMoviesTests/Model/TopRatedListViewModelSpec.swift b/MyMoviesTests/Model/TopRatedListViewModelSpec.swift new file mode 100644 index 00000000..33c671b5 --- /dev/null +++ b/MyMoviesTests/Model/TopRatedListViewModelSpec.swift @@ -0,0 +1,82 @@ +// +// TopRatedListViewModelSpec.swift +// MyMoviesTests +// +// Created by Caio dos Santos Ambrosio on 1/14/23. +// + +import XCTest +@testable import MyMovies + +final class TopRatedListViewModelSpec: XCTestCase { + var apiLoader: APILoader! + var expectation: XCTestExpectation! + let apiURL = URL(string: APIPath().fetchTopRated())! + + override func setUp() { + let configuration = URLSessionConfiguration.default + configuration.protocolClasses = [MockURLProtocol.self] + let urlSession = URLSession.init(configuration: configuration) + + apiLoader = APILoader(apiHandler: TopRatedAPI(), urlSession: urlSession) + expectation = expectation(description: "Expectation") + } + + private func getMockData(for path: String) -> Data? { + guard let pathString = Bundle(for: type(of: self)).path(forResource: path, ofType: "json") else { + fatalError("Mock json not found") + } + + guard let jsonString = try? String(contentsOfFile: pathString, encoding: .utf8) else { + fatalError("Mock json can not be converted to String") + } + + return jsonString.data(using: .utf8) + } + + func testSuccessCall() { + let data = getMockData(for: "top-rated-success") + + MockURLProtocol.requestHandler = { request in + guard let url = request.url, url == self.apiURL else { + fatalError("Trying to test a different URL") + } + + let response = HTTPURLResponse(url: self.apiURL, statusCode: 200, httpVersion: nil, headerFields: nil)! + return (response, data) + } + + let viewModel = TopRatedListViewModel(apiLoader: self.apiLoader) + + viewModel.fetchTopRated { viewModel, error in + XCTAssertNotNil(viewModel) + XCTAssertEqual(viewModel?.numberOfRowsInSection, 20) + XCTAssertEqual(viewModel?.titlePage, "Top Rated") + XCTAssertEqual(viewModel?.getTrending(2).title, "The Godfather Part II") + self.expectation.fulfill() + } + wait(for: [expectation], timeout: 100.0) + } + + func testErrorCall() { + let data = getMockData(for: "top-rated-error") + + MockURLProtocol.requestHandler = { request in + guard let url = request.url, url == self.apiURL else { + fatalError("Trying to test a different URL") + } + + let response = HTTPURLResponse(url: self.apiURL, statusCode: 400, httpVersion: nil, headerFields: nil)! + return (response, data) + } + + let viewModel = TopRatedListViewModel(apiLoader: self.apiLoader) + + viewModel.fetchTopRated { viewModel, error in + XCTAssertNotNil(error) + XCTAssertEqual(error?.message, "Error when communicating with API") + self.expectation.fulfill() + } + wait(for: [expectation], timeout: 100.0) + } +} From a0328b9b15082f8a545eb1ee2474fefbdacb787f Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Sat, 14 Jan 2023 10:13:44 -0300 Subject: [PATCH 43/65] Create tests for MovieViewModel --- MyMovies.xcodeproj/project.pbxproj | 20 +++++ MyMoviesTests/Mocks/Movie/movie-error.json | 5 ++ MyMoviesTests/Mocks/Movie/movie-success.json | 75 +++++++++++++++++ MyMoviesTests/Model/MovieViewModelSpec.swift | 87 ++++++++++++++++++++ 4 files changed, 187 insertions(+) create mode 100644 MyMoviesTests/Mocks/Movie/movie-error.json create mode 100644 MyMoviesTests/Mocks/Movie/movie-success.json create mode 100644 MyMoviesTests/Model/MovieViewModelSpec.swift diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index c779da6c..418c1c12 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -49,6 +49,9 @@ 7DF3DD0D2972DE25007BF9A3 /* TopRatedListViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD0C2972DE25007BF9A3 /* TopRatedListViewModelSpec.swift */; }; 7DF3DD102972DEED007BF9A3 /* top-rated-success.json in Resources */ = {isa = PBXBuildFile; fileRef = 7DF3DD0F2972DEED007BF9A3 /* top-rated-success.json */; }; 7DF3DD122972DF22007BF9A3 /* top-rated-error.json in Resources */ = {isa = PBXBuildFile; fileRef = 7DF3DD112972DF22007BF9A3 /* top-rated-error.json */; }; + 7DF3DD142972DF75007BF9A3 /* MovieViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD132972DF75007BF9A3 /* MovieViewModelSpec.swift */; }; + 7DF3DD182972E12C007BF9A3 /* movie-success.json in Resources */ = {isa = PBXBuildFile; fileRef = 7DF3DD162972E12C007BF9A3 /* movie-success.json */; }; + 7DF3DD192972E12C007BF9A3 /* movie-error.json in Resources */ = {isa = PBXBuildFile; fileRef = 7DF3DD172972E12C007BF9A3 /* movie-error.json */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -115,6 +118,9 @@ 7DF3DD0C2972DE25007BF9A3 /* TopRatedListViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopRatedListViewModelSpec.swift; sourceTree = ""; }; 7DF3DD0F2972DEED007BF9A3 /* top-rated-success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "top-rated-success.json"; sourceTree = ""; }; 7DF3DD112972DF22007BF9A3 /* top-rated-error.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "top-rated-error.json"; sourceTree = ""; }; + 7DF3DD132972DF75007BF9A3 /* MovieViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieViewModelSpec.swift; sourceTree = ""; }; + 7DF3DD162972E12C007BF9A3 /* movie-success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "movie-success.json"; sourceTree = ""; }; + 7DF3DD172972E12C007BF9A3 /* movie-error.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "movie-error.json"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -304,6 +310,7 @@ 7D35A4822972D5B00054954E /* Mocks */ = { isa = PBXGroup; children = ( + 7DF3DD152972E0F6007BF9A3 /* Movie */, 7DF3DD0E2972DEDE007BF9A3 /* TopRated */, 7D35A4832972D5BE0054954E /* Trending */, 7D35A4862972D6D40054954E /* MockURLProtocol.swift */, @@ -325,6 +332,7 @@ children = ( 7D35A4882972D70E0054954E /* TrendingListViewModelSpec.swift */, 7DF3DD0C2972DE25007BF9A3 /* TopRatedListViewModelSpec.swift */, + 7DF3DD132972DF75007BF9A3 /* MovieViewModelSpec.swift */, ); path = Model; sourceTree = ""; @@ -338,6 +346,15 @@ path = TopRated; sourceTree = ""; }; + 7DF3DD152972E0F6007BF9A3 /* Movie */ = { + isa = PBXGroup; + children = ( + 7DF3DD172972E12C007BF9A3 /* movie-error.json */, + 7DF3DD162972E12C007BF9A3 /* movie-success.json */, + ); + path = Movie; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -453,6 +470,8 @@ files = ( 7DF3DD0B2972DC3B007BF9A3 /* trending-success.json in Resources */, 7DF3DD102972DEED007BF9A3 /* top-rated-success.json in Resources */, + 7DF3DD182972E12C007BF9A3 /* movie-success.json in Resources */, + 7DF3DD192972E12C007BF9A3 /* movie-error.json in Resources */, 7DF3DD092972DC32007BF9A3 /* trending-error.json in Resources */, 7DF3DD122972DF22007BF9A3 /* top-rated-error.json in Resources */, ); @@ -512,6 +531,7 @@ files = ( 7D35A4892972D70E0054954E /* TrendingListViewModelSpec.swift in Sources */, 7DF3DD0D2972DE25007BF9A3 /* TopRatedListViewModelSpec.swift in Sources */, + 7DF3DD142972DF75007BF9A3 /* MovieViewModelSpec.swift in Sources */, 7D35A4872972D6D40054954E /* MockURLProtocol.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/MyMoviesTests/Mocks/Movie/movie-error.json b/MyMoviesTests/Mocks/Movie/movie-error.json new file mode 100644 index 00000000..22c9812f --- /dev/null +++ b/MyMoviesTests/Mocks/Movie/movie-error.json @@ -0,0 +1,5 @@ +{ + "success": false, + "status_code": 34, + "status_message": "The resource you requested could not be found." +} \ No newline at end of file diff --git a/MyMoviesTests/Mocks/Movie/movie-success.json b/MyMoviesTests/Mocks/Movie/movie-success.json new file mode 100644 index 00000000..dac1295d --- /dev/null +++ b/MyMoviesTests/Mocks/Movie/movie-success.json @@ -0,0 +1,75 @@ +{ + "adult": false, + "backdrop_path": "/tmU7GeKVybMWFButWEGl2M4GeiP.jpg", + "belongs_to_collection": { + "id": 230, + "name": "The Godfather Collection", + "poster_path": "/9Baumh5J9N1nJUYzNkm0xsgjpwY.jpg", + "backdrop_path": "/3WZTxpgscsmoUk81TuECXdFOD0R.jpg" + }, + "budget": 6000000, + "genres": [ + { + "id": 18, + "name": "Drama" + }, + { + "id": 80, + "name": "Crime" + } + ], + "homepage": "http://www.thegodfather.com/", + "id": 238, + "imdb_id": "tt0068646", + "original_language": "en", + "original_title": "The Godfather", + "overview": "Spanning the years 1945 to 1955, a chronicle of the fictional Italian-American Corleone crime family. When organized crime family patriarch, Vito Corleone barely survives an attempt on his life, his youngest son, Michael steps in to take care of the would-be killers, launching a campaign of bloody revenge.", + "popularity": 107.877, + "poster_path": "/3bhkrj58Vtu7enYsRolD1fZdja1.jpg", + "production_companies": [ + { + "id": 4, + "logo_path": "/gz66EfNoYPqHTYI4q9UEN4CbHRc.png", + "name": "Paramount", + "origin_country": "US" + }, + { + "id": 10211, + "logo_path": null, + "name": "Alfran Productions", + "origin_country": "US" + } + ], + "production_countries": [ + { + "iso_3166_1": "US", + "name": "United States of America" + } + ], + "release_date": "1972-03-14", + "revenue": 245066411, + "runtime": 175, + "spoken_languages": [ + { + "english_name": "English", + "iso_639_1": "en", + "name": "English" + }, + { + "english_name": "Italian", + "iso_639_1": "it", + "name": "Italiano" + }, + { + "english_name": "Latin", + "iso_639_1": "la", + "name": "Latin" + } + ], + "status": "Released", + "tagline": "An offer you can't refuse.", + "title": "The Godfather", + "video": false, + "vote_average": 8.713, + "vote_count": 17270 +} \ No newline at end of file diff --git a/MyMoviesTests/Model/MovieViewModelSpec.swift b/MyMoviesTests/Model/MovieViewModelSpec.swift new file mode 100644 index 00000000..9d79a43f --- /dev/null +++ b/MyMoviesTests/Model/MovieViewModelSpec.swift @@ -0,0 +1,87 @@ +// +// MovieViewModelSpec.swift +// MyMoviesTests +// +// Created by Caio dos Santos Ambrosio on 1/14/23. +// + +import XCTest +@testable import MyMovies + +final class MovieViewModelSpec: XCTestCase { + var apiLoader: APILoader! + var expectation: XCTestExpectation! + let movieId: Int64 = 238 + let apiURL = URL(string: APIPath().fetchMovieDetails(for: 238))! + + override func setUp() { + let configuration = URLSessionConfiguration.default + configuration.protocolClasses = [MockURLProtocol.self] + let urlSession = URLSession.init(configuration: configuration) + + apiLoader = APILoader(apiHandler: MovieAPI(), urlSession: urlSession) + expectation = expectation(description: "Expectation") + } + + private func getMockData(for path: String) -> Data? { + guard let pathString = Bundle(for: type(of: self)).path(forResource: path, ofType: "json") else { + fatalError("Mock json not found") + } + + guard let jsonString = try? String(contentsOfFile: pathString, encoding: .utf8) else { + fatalError("Mock json can not be converted to String") + } + + return jsonString.data(using: .utf8) + } + + func testSuccessCall() { + let data = getMockData(for: "movie-success") + + MockURLProtocol.requestHandler = { request in + guard let url = request.url, url == self.apiURL else { + fatalError("Trying to test a different URL") + } + + let response = HTTPURLResponse(url: self.apiURL, statusCode: 200, httpVersion: nil, headerFields: nil)! + return (response, data) + } + + let viewModel = MovieViewModel(apiLoader: self.apiLoader) + + viewModel.fetchMovie(for: movieId) { viewModel, error in + XCTAssertNotNil(viewModel) + XCTAssertEqual(viewModel?.title, "The Godfather") + XCTAssertEqual(viewModel?.imageUrl, URL(string: "https://image.tmdb.org/t/p/w185//3bhkrj58Vtu7enYsRolD1fZdja1.jpg?api_key=16094d8ca19f9c0407db3d0b5203bd21")) + XCTAssertEqual(viewModel?.genres, ["Drama", "Crime"]) + XCTAssertEqual(viewModel?.overview, "Spanning the years 1945 to 1955, a chronicle of the fictional Italian-American Corleone crime family. When organized crime family patriarch, Vito Corleone barely survives an attempt on his life, his youngest son, Michael steps in to take care of the would-be killers, launching a campaign of bloody revenge.") + XCTAssertEqual(viewModel?.rate, "8.71") + XCTAssertEqual(viewModel?.totalVoteCount, "17270") + XCTAssertEqual(viewModel?.releaseDate, "03/14/1972") + self.expectation.fulfill() + } + wait(for: [expectation], timeout: 100.0) + } + + func testErrorCall() { + let data = getMockData(for: "movie-error") + + MockURLProtocol.requestHandler = { request in + guard let url = request.url, url == self.apiURL else { + fatalError("Trying to test a different URL") + } + + let response = HTTPURLResponse(url: self.apiURL, statusCode: 400, httpVersion: nil, headerFields: nil)! + return (response, data) + } + + let viewModel = MovieViewModel(apiLoader: self.apiLoader) + + viewModel.fetchMovie(for: movieId) { viewModel, error in + XCTAssertNotNil(error) + XCTAssertEqual(error?.message, "Error when communicating with API") + self.expectation.fulfill() + } + wait(for: [expectation], timeout: 100.0) + } +} From 66e6a15025970cb618125ab9ae350189f0b3b2a6 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Sat, 14 Jan 2023 10:32:22 -0300 Subject: [PATCH 44/65] Create tests for TrendingViewModel --- MyMovies.xcodeproj/project.pbxproj | 20 ++++++ MyMovies/Network/APIPath.swift | 2 +- .../trending-result-nullable.json | 17 ++++++ .../Mocks/TrendingResult/trending-result.json | 24 ++++++++ .../Model/TrendingViewModelSpec.swift | 61 +++++++++++++++++++ 5 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 MyMoviesTests/Mocks/TrendingResult/trending-result-nullable.json create mode 100644 MyMoviesTests/Mocks/TrendingResult/trending-result.json create mode 100644 MyMoviesTests/Model/TrendingViewModelSpec.swift diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index 418c1c12..df2f0e56 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -52,6 +52,9 @@ 7DF3DD142972DF75007BF9A3 /* MovieViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD132972DF75007BF9A3 /* MovieViewModelSpec.swift */; }; 7DF3DD182972E12C007BF9A3 /* movie-success.json in Resources */ = {isa = PBXBuildFile; fileRef = 7DF3DD162972E12C007BF9A3 /* movie-success.json */; }; 7DF3DD192972E12C007BF9A3 /* movie-error.json in Resources */ = {isa = PBXBuildFile; fileRef = 7DF3DD172972E12C007BF9A3 /* movie-error.json */; }; + 7DF3DD1B2972E23B007BF9A3 /* TrendingViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD1A2972E23B007BF9A3 /* TrendingViewModelSpec.swift */; }; + 7DF3DD1E2972E3A7007BF9A3 /* trending-result.json in Resources */ = {isa = PBXBuildFile; fileRef = 7DF3DD1D2972E3A7007BF9A3 /* trending-result.json */; }; + 7DF3DD202972E641007BF9A3 /* trending-result-nullable.json in Resources */ = {isa = PBXBuildFile; fileRef = 7DF3DD1F2972E641007BF9A3 /* trending-result-nullable.json */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -121,6 +124,9 @@ 7DF3DD132972DF75007BF9A3 /* MovieViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieViewModelSpec.swift; sourceTree = ""; }; 7DF3DD162972E12C007BF9A3 /* movie-success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "movie-success.json"; sourceTree = ""; }; 7DF3DD172972E12C007BF9A3 /* movie-error.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "movie-error.json"; sourceTree = ""; }; + 7DF3DD1A2972E23B007BF9A3 /* TrendingViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingViewModelSpec.swift; sourceTree = ""; }; + 7DF3DD1D2972E3A7007BF9A3 /* trending-result.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "trending-result.json"; sourceTree = ""; }; + 7DF3DD1F2972E641007BF9A3 /* trending-result-nullable.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "trending-result-nullable.json"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -310,6 +316,7 @@ 7D35A4822972D5B00054954E /* Mocks */ = { isa = PBXGroup; children = ( + 7DF3DD1C2972E36E007BF9A3 /* TrendingResult */, 7DF3DD152972E0F6007BF9A3 /* Movie */, 7DF3DD0E2972DEDE007BF9A3 /* TopRated */, 7D35A4832972D5BE0054954E /* Trending */, @@ -333,6 +340,7 @@ 7D35A4882972D70E0054954E /* TrendingListViewModelSpec.swift */, 7DF3DD0C2972DE25007BF9A3 /* TopRatedListViewModelSpec.swift */, 7DF3DD132972DF75007BF9A3 /* MovieViewModelSpec.swift */, + 7DF3DD1A2972E23B007BF9A3 /* TrendingViewModelSpec.swift */, ); path = Model; sourceTree = ""; @@ -355,6 +363,15 @@ path = Movie; sourceTree = ""; }; + 7DF3DD1C2972E36E007BF9A3 /* TrendingResult */ = { + isa = PBXGroup; + children = ( + 7DF3DD1D2972E3A7007BF9A3 /* trending-result.json */, + 7DF3DD1F2972E641007BF9A3 /* trending-result-nullable.json */, + ); + path = TrendingResult; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -468,8 +485,10 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7DF3DD202972E641007BF9A3 /* trending-result-nullable.json in Resources */, 7DF3DD0B2972DC3B007BF9A3 /* trending-success.json in Resources */, 7DF3DD102972DEED007BF9A3 /* top-rated-success.json in Resources */, + 7DF3DD1E2972E3A7007BF9A3 /* trending-result.json in Resources */, 7DF3DD182972E12C007BF9A3 /* movie-success.json in Resources */, 7DF3DD192972E12C007BF9A3 /* movie-error.json in Resources */, 7DF3DD092972DC32007BF9A3 /* trending-error.json in Resources */, @@ -531,6 +550,7 @@ files = ( 7D35A4892972D70E0054954E /* TrendingListViewModelSpec.swift in Sources */, 7DF3DD0D2972DE25007BF9A3 /* TopRatedListViewModelSpec.swift in Sources */, + 7DF3DD1B2972E23B007BF9A3 /* TrendingViewModelSpec.swift in Sources */, 7DF3DD142972DF75007BF9A3 /* MovieViewModelSpec.swift in Sources */, 7D35A4872972D6D40054954E /* MockURLProtocol.swift in Sources */, ); diff --git a/MyMovies/Network/APIPath.swift b/MyMovies/Network/APIPath.swift index dc17da29..7dd41c08 100644 --- a/MyMovies/Network/APIPath.swift +++ b/MyMovies/Network/APIPath.swift @@ -32,7 +32,7 @@ struct APIPath { } func fetchImage(width: String, imagePath: String) -> String { - let url = "\(baseImageURL)/\(width)/\(imagePath)" + let url = "\(baseImageURL)/\(width)\(imagePath)" return applyApiKey(url) } diff --git a/MyMoviesTests/Mocks/TrendingResult/trending-result-nullable.json b/MyMoviesTests/Mocks/TrendingResult/trending-result-nullable.json new file mode 100644 index 00000000..7fe4e6b9 --- /dev/null +++ b/MyMoviesTests/Mocks/TrendingResult/trending-result-nullable.json @@ -0,0 +1,17 @@ +{ + "adult": null, + "backdrop_path": null, + "id": null, + "title": null, + "original_language": null, + "original_title": null, + "overview": null, + "poster_path": null, + "media_type": null, + "genre_ids": null, + "popularity": null, + "release_date": null, + "video": null, + "vote_average": null, + "vote_count": null +} \ No newline at end of file diff --git a/MyMoviesTests/Mocks/TrendingResult/trending-result.json b/MyMoviesTests/Mocks/TrendingResult/trending-result.json new file mode 100644 index 00000000..e8275568 --- /dev/null +++ b/MyMoviesTests/Mocks/TrendingResult/trending-result.json @@ -0,0 +1,24 @@ +{ + "adult": false, + "backdrop_path": "/r9PkFnRUIthgBp2JZZzD380MWZy.jpg", + "id": 315162, + "title": "Puss in Boots: The Last Wish", + "original_language": "en", + "original_title": "Puss in Boots: The Last Wish", + "overview": "Puss in Boots discovers that his passion for adventure has taken its toll: He has burned through eight of his nine lives, leaving him with only one life left. Puss sets out on an epic journey to find the mythical Last Wish and restore his nine lives.", + "poster_path": "/1NqwE6LP9IEdOZ57NCT51ftHtWT.jpg", + "media_type": "movie", + "genre_ids": [ + 16, + 28, + 12, + 35, + 10751, + 14 + ], + "popularity": 9724.995, + "release_date": "2022-12-21", + "video": false, + "vote_average": 8.6, + "vote_count": 1400 +} \ No newline at end of file diff --git a/MyMoviesTests/Model/TrendingViewModelSpec.swift b/MyMoviesTests/Model/TrendingViewModelSpec.swift new file mode 100644 index 00000000..a75cefab --- /dev/null +++ b/MyMoviesTests/Model/TrendingViewModelSpec.swift @@ -0,0 +1,61 @@ +// +// TrendingViewModel.swift +// MyMoviesTests +// +// Created by Caio dos Santos Ambrosio on 1/14/23. +// + +import XCTest +@testable import MyMovies + +final class TrendingViewModelSpec: XCTestCase { + + private func getMockData(for path: String) -> Data? { + guard let pathString = Bundle(for: type(of: self)).path(forResource: path, ofType: "json") else { + fatalError("Mock json not found") + } + + guard let jsonString = try? String(contentsOfFile: pathString, encoding: .utf8) else { + fatalError("Mock json can not be converted to String") + } + + return jsonString.data(using: .utf8) + } + + private func getMockJson(for data: Data) -> TrendingResult { + let jsonDecoder = JSONDecoder() + jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase + do { + let body = try jsonDecoder.decode(TrendingResult.self, from: data) + return body + } catch { + fatalError("Error when decoding json") + } + } + + func testSuccessJson() { + guard let data = getMockData(for: "trending-result") else { + fatalError("Mock json can not be converted to Data") + } + + let trending = getMockJson(for: data) + let viewModel = TrendingViewModel(trending: trending) + + XCTAssertEqual(viewModel.title, trending.originalTitle) + XCTAssertEqual(viewModel.posterImageURL, URL(string: "https://image.tmdb.org/t/p/w154/1NqwE6LP9IEdOZ57NCT51ftHtWT.jpg?api_key=16094d8ca19f9c0407db3d0b5203bd21")) + XCTAssertEqual(viewModel.movieId, 315162) + } + + func testNullableJson() { + guard let data = getMockData(for: "trending-result-nullable") else { + fatalError("Mock json can not be converted to Data") + } + + let trending = getMockJson(for: data) + let viewModel = TrendingViewModel(trending: trending) + + XCTAssertEqual(viewModel.title, "") + XCTAssertEqual(viewModel.posterImageURL, URL(string: "https://image.tmdb.org/t/p/w154?api_key=16094d8ca19f9c0407db3d0b5203bd21")) + XCTAssertEqual(viewModel.movieId, nil) + } +} From 79c7de5774b5d94cb3b562e64f70affa937129e1 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Sat, 14 Jan 2023 10:46:53 -0300 Subject: [PATCH 45/65] Create tests for MainCoordinator --- MyMovies.xcodeproj/project.pbxproj | 16 ++++++++++ .../Coordinator/MainCoordinatorSpec.swift | 32 +++++++++++++++++++ MyMoviesTests/Mocks/MainCoordinatorMock.swift | 28 ++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 MyMoviesTests/Coordinator/MainCoordinatorSpec.swift create mode 100644 MyMoviesTests/Mocks/MainCoordinatorMock.swift diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index df2f0e56..8f52cc63 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -55,6 +55,8 @@ 7DF3DD1B2972E23B007BF9A3 /* TrendingViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD1A2972E23B007BF9A3 /* TrendingViewModelSpec.swift */; }; 7DF3DD1E2972E3A7007BF9A3 /* trending-result.json in Resources */ = {isa = PBXBuildFile; fileRef = 7DF3DD1D2972E3A7007BF9A3 /* trending-result.json */; }; 7DF3DD202972E641007BF9A3 /* trending-result-nullable.json in Resources */ = {isa = PBXBuildFile; fileRef = 7DF3DD1F2972E641007BF9A3 /* trending-result-nullable.json */; }; + 7DF3DD232972E725007BF9A3 /* MainCoordinatorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD222972E725007BF9A3 /* MainCoordinatorSpec.swift */; }; + 7DF3DD252972E7AC007BF9A3 /* MainCoordinatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD242972E7AC007BF9A3 /* MainCoordinatorMock.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -127,6 +129,8 @@ 7DF3DD1A2972E23B007BF9A3 /* TrendingViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingViewModelSpec.swift; sourceTree = ""; }; 7DF3DD1D2972E3A7007BF9A3 /* trending-result.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "trending-result.json"; sourceTree = ""; }; 7DF3DD1F2972E641007BF9A3 /* trending-result-nullable.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "trending-result-nullable.json"; sourceTree = ""; }; + 7DF3DD222972E725007BF9A3 /* MainCoordinatorSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCoordinatorSpec.swift; sourceTree = ""; }; + 7DF3DD242972E7AC007BF9A3 /* MainCoordinatorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCoordinatorMock.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -194,6 +198,7 @@ 7D35A419296E22450054954E /* MyMoviesTests */ = { isa = PBXGroup; children = ( + 7DF3DD212972E6EE007BF9A3 /* Coordinator */, 7DF3DD012972DB63007BF9A3 /* Model */, 7D35A4822972D5B00054954E /* Mocks */, ); @@ -321,6 +326,7 @@ 7DF3DD0E2972DEDE007BF9A3 /* TopRated */, 7D35A4832972D5BE0054954E /* Trending */, 7D35A4862972D6D40054954E /* MockURLProtocol.swift */, + 7DF3DD242972E7AC007BF9A3 /* MainCoordinatorMock.swift */, ); path = Mocks; sourceTree = ""; @@ -372,6 +378,14 @@ path = TrendingResult; sourceTree = ""; }; + 7DF3DD212972E6EE007BF9A3 /* Coordinator */ = { + isa = PBXGroup; + children = ( + 7DF3DD222972E725007BF9A3 /* MainCoordinatorSpec.swift */, + ); + path = Coordinator; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -552,7 +566,9 @@ 7DF3DD0D2972DE25007BF9A3 /* TopRatedListViewModelSpec.swift in Sources */, 7DF3DD1B2972E23B007BF9A3 /* TrendingViewModelSpec.swift in Sources */, 7DF3DD142972DF75007BF9A3 /* MovieViewModelSpec.swift in Sources */, + 7DF3DD232972E725007BF9A3 /* MainCoordinatorSpec.swift in Sources */, 7D35A4872972D6D40054954E /* MockURLProtocol.swift in Sources */, + 7DF3DD252972E7AC007BF9A3 /* MainCoordinatorMock.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MyMoviesTests/Coordinator/MainCoordinatorSpec.swift b/MyMoviesTests/Coordinator/MainCoordinatorSpec.swift new file mode 100644 index 00000000..03af68a2 --- /dev/null +++ b/MyMoviesTests/Coordinator/MainCoordinatorSpec.swift @@ -0,0 +1,32 @@ +// +// MainCoordinatorSpec.swift +// MyMoviesTests +// +// Created by Caio dos Santos Ambrosio on 1/14/23. +// + +import XCTest +@testable import MyMovies + +final class MainCoordinatorSpec: XCTestCase { + private var mainCoordinator: MainCoordinatorMock! + + override func setUp() { + super.setUp() + + let navigationController = UINavigationController(rootViewController: UIViewController()) + mainCoordinator = MainCoordinatorMock(navigationController: navigationController) + } + + func testMainCoordinatorStart() { + mainCoordinator.start() + + XCTAssertTrue(mainCoordinator.startCalled) + } + + func testEventOccurred() { + mainCoordinator.eventOccurred(with: .movieClicked, parameters: nil) + + XCTAssertTrue(mainCoordinator.eventOccurredCalled) + } +} diff --git a/MyMoviesTests/Mocks/MainCoordinatorMock.swift b/MyMoviesTests/Mocks/MainCoordinatorMock.swift new file mode 100644 index 00000000..75044c89 --- /dev/null +++ b/MyMoviesTests/Mocks/MainCoordinatorMock.swift @@ -0,0 +1,28 @@ +// +// UINavigationControllerMock.swift +// MyMoviesTests +// +// Created by Caio dos Santos Ambrosio on 1/14/23. +// + +import UIKit +@testable import MyMovies + +final class MainCoordinatorMock: Coordinator { + var startCalled = false + var eventOccurredCalled = false + + var navigationController: UINavigationController + + init(navigationController: UINavigationController) { + self.navigationController = navigationController + } + + func eventOccurred(with type: Event, parameters params: Any?) { + self.eventOccurredCalled = true + } + + func start() { + self.startCalled = true + } +} From 10ee06aa0833e5798d82cf1ea6c703ecc75856a9 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Sat, 14 Jan 2023 10:47:03 -0300 Subject: [PATCH 46/65] Fix broken test --- MyMoviesTests/Model/MovieViewModelSpec.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MyMoviesTests/Model/MovieViewModelSpec.swift b/MyMoviesTests/Model/MovieViewModelSpec.swift index 9d79a43f..d25da8a4 100644 --- a/MyMoviesTests/Model/MovieViewModelSpec.swift +++ b/MyMoviesTests/Model/MovieViewModelSpec.swift @@ -52,7 +52,7 @@ final class MovieViewModelSpec: XCTestCase { viewModel.fetchMovie(for: movieId) { viewModel, error in XCTAssertNotNil(viewModel) XCTAssertEqual(viewModel?.title, "The Godfather") - XCTAssertEqual(viewModel?.imageUrl, URL(string: "https://image.tmdb.org/t/p/w185//3bhkrj58Vtu7enYsRolD1fZdja1.jpg?api_key=16094d8ca19f9c0407db3d0b5203bd21")) + XCTAssertEqual(viewModel?.imageUrl, URL(string: "https://image.tmdb.org/t/p/w185/3bhkrj58Vtu7enYsRolD1fZdja1.jpg?api_key=16094d8ca19f9c0407db3d0b5203bd21")) XCTAssertEqual(viewModel?.genres, ["Drama", "Crime"]) XCTAssertEqual(viewModel?.overview, "Spanning the years 1945 to 1955, a chronicle of the fictional Italian-American Corleone crime family. When organized crime family patriarch, Vito Corleone barely survives an attempt on his life, his youngest son, Michael steps in to take care of the would-be killers, launching a campaign of bloody revenge.") XCTAssertEqual(viewModel?.rate, "8.71") From bba184679d5d9441dbe2c144e45d4d91080132d3 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Sat, 14 Jan 2023 14:12:47 -0300 Subject: [PATCH 47/65] Add favorite swipe action and favorite repository to store favorites --- MyMovies.xcodeproj/project.pbxproj | 16 +++ MyMovies/Model/DTO/TrendingResult.swift | 2 +- .../Model/Favorite/FavoriteRepository.swift | 101 ++++++++++++++++++ .../Model/Favorite/FavoriteViewModel.swift | 39 +++++++ MyMovies/Model/TopRatedListViewModel.swift | 9 +- MyMovies/Model/TrendingListViewModel.swift | 10 +- MyMovies/Model/TrendingViewModel.swift | 4 + .../TrendingCollectionViewCell.swift | 95 ++++++++++++++-- .../TrendingViewController/TrendingView.swift | 9 ++ .../TrendingViewController.swift | 15 +++ 10 files changed, 287 insertions(+), 13 deletions(-) create mode 100644 MyMovies/Model/Favorite/FavoriteRepository.swift create mode 100644 MyMovies/Model/Favorite/FavoriteViewModel.swift diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index 8f52cc63..3197a5f6 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -57,6 +57,8 @@ 7DF3DD202972E641007BF9A3 /* trending-result-nullable.json in Resources */ = {isa = PBXBuildFile; fileRef = 7DF3DD1F2972E641007BF9A3 /* trending-result-nullable.json */; }; 7DF3DD232972E725007BF9A3 /* MainCoordinatorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD222972E725007BF9A3 /* MainCoordinatorSpec.swift */; }; 7DF3DD252972E7AC007BF9A3 /* MainCoordinatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD242972E7AC007BF9A3 /* MainCoordinatorMock.swift */; }; + 7DF3DD282972F5A4007BF9A3 /* FavoriteRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD272972F5A4007BF9A3 /* FavoriteRepository.swift */; }; + 7DF3DD2B29730F28007BF9A3 /* FavoriteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD2A29730F28007BF9A3 /* FavoriteViewModel.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -131,6 +133,8 @@ 7DF3DD1F2972E641007BF9A3 /* trending-result-nullable.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "trending-result-nullable.json"; sourceTree = ""; }; 7DF3DD222972E725007BF9A3 /* MainCoordinatorSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCoordinatorSpec.swift; sourceTree = ""; }; 7DF3DD242972E7AC007BF9A3 /* MainCoordinatorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCoordinatorMock.swift; sourceTree = ""; }; + 7DF3DD272972F5A4007BF9A3 /* FavoriteRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteRepository.swift; sourceTree = ""; }; + 7DF3DD2A29730F28007BF9A3 /* FavoriteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteViewModel.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -251,6 +255,7 @@ 7D35A448296E2BDB0054954E /* Model */ = { isa = PBXGroup; children = ( + 7DF3DD262972F596007BF9A3 /* Favorite */, 7D35A449296E2C870054954E /* DTO */, 7D35A462296E3E410054954E /* TrendingListViewModel.swift */, 7D35A464296E45170054954E /* TrendingViewModel.swift */, @@ -386,6 +391,15 @@ path = Coordinator; sourceTree = ""; }; + 7DF3DD262972F596007BF9A3 /* Favorite */ = { + isa = PBXGroup; + children = ( + 7DF3DD272972F5A4007BF9A3 /* FavoriteRepository.swift */, + 7DF3DD2A29730F28007BF9A3 /* FavoriteViewModel.swift */, + ); + path = Favorite; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -543,7 +557,9 @@ 7D35A478297209250054954E /* MovieViewModel.swift in Sources */, 7D35A47A29720A810054954E /* MovieAPI.swift in Sources */, 7D35A463296E3E410054954E /* TrendingListViewModel.swift in Sources */, + 7DF3DD282972F5A4007BF9A3 /* FavoriteRepository.swift in Sources */, 7D35A46A296E4C820054954E /* ImageLoader.swift in Sources */, + 7DF3DD2B29730F28007BF9A3 /* FavoriteViewModel.swift in Sources */, 7D35A43C296E28490054954E /* APIPath.swift in Sources */, 7D35A47C29720CD40054954E /* Event.swift in Sources */, 7D35A46C296E4D090054954E /* UIImageView+ImageLoader.swift in Sources */, diff --git a/MyMovies/Model/DTO/TrendingResult.swift b/MyMovies/Model/DTO/TrendingResult.swift index 1b1c80ec..091d2879 100644 --- a/MyMovies/Model/DTO/TrendingResult.swift +++ b/MyMovies/Model/DTO/TrendingResult.swift @@ -7,7 +7,7 @@ import Foundation -struct TrendingResult: Decodable { +struct TrendingResult: Codable { let posterPath: String? let adult: Bool? let overview: String? diff --git a/MyMovies/Model/Favorite/FavoriteRepository.swift b/MyMovies/Model/Favorite/FavoriteRepository.swift new file mode 100644 index 00000000..67138b8b --- /dev/null +++ b/MyMovies/Model/Favorite/FavoriteRepository.swift @@ -0,0 +1,101 @@ +// +// FavoriteRepository.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/14/23. +// + +import Foundation + +class FavoriteRepository { + enum Key: String { + case keys + } + + let userDefaults: UserDefaults + + init(userDefaults: UserDefaults = .standard) { + self.userDefaults = userDefaults + } + + func insetOrUpdateMovie(for movieId: Int64, _ trending: TrendingViewModel) { + let encoded = encode(trending) + let movieIdString = String(movieId) + + var keys: [String]? = readValue(for: Key.keys.rawValue) + if keys == nil { + keys = [movieIdString] + } else { + keys?.append(movieIdString) + } + + saveValue(for: movieIdString, value: encoded) + saveValue(for: Key.keys.rawValue, value: keys!) + } + + func deleteMovie(for movieId: Int64) { + let movieIdString = String(movieId) + + var keys: [String]? = readValue(for: Key.keys.rawValue) + if var keys = keys, + keys.contains(movieIdString) { + keys.removeAll(where: { $0 == movieIdString }) + } + + removeValue(for: String(movieId)) + } + + func getAllFavorites() -> [TrendingViewModel] { + let favorites: [String]? = readValue(for: Key.keys.rawValue) + + guard let favorites = favorites else { + return [] + } + + var result: [TrendingViewModel] = [] + + + favorites.forEach { movieId in + guard let data: String = readValue(for: String(movieId)) else { return } + do { + let trendingViewModel = try decode(Data(data.utf8)) + result.append(trendingViewModel) + } catch { + return + } + } + + return result + } + + private func encode(_ value: TrendingViewModel) -> String { + let jsonDecoder = JSONEncoder() + do { + let data = try jsonDecoder.encode(value.getTrendingResult()) + return String(data: data, encoding: .utf8) ?? "" + } catch { + return "" + } + } + + private func decode(_ value: Data) throws -> TrendingViewModel { + let jsonDecoder = JSONDecoder() + do { + let data = try jsonDecoder.decode(TrendingResult.self, from: value) + return TrendingViewModel(trending: data) + } + } + + private func saveValue(for key: String, value: Any) { + userDefaults.set(value, forKey: key) + } + + private func readValue(for key: String) -> T? { + let value = userDefaults.value(forKey: key) as? T + return value + } + + private func removeValue(for key: String) { + userDefaults.removeObject(forKey: key) + } +} diff --git a/MyMovies/Model/Favorite/FavoriteViewModel.swift b/MyMovies/Model/Favorite/FavoriteViewModel.swift new file mode 100644 index 00000000..83197d02 --- /dev/null +++ b/MyMovies/Model/Favorite/FavoriteViewModel.swift @@ -0,0 +1,39 @@ +// +// FavoriteViewModel.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/14/23. +// + +import Foundation + +class FavoriteViewModel { + + // MARK: - Properties + + private let favoriteRepository: FavoriteRepository + private var favorites: [TrendingViewModel] = [] + + var numberOfRows: Int { + self.favorites.count + } + + // MARK: - Initializer + + init(favoriteRepository: FavoriteRepository) { + self.favoriteRepository = favoriteRepository + } + + // MARK: - Functions + + func fetchFavorites(completion: @escaping (FavoriteViewModel?) -> ()) { + DispatchQueue.global().async { + self.favorites.removeAll() + self.favorites = self.favoriteRepository.getAllFavorites() + + DispatchQueue.main.async { + completion(self) + } + } + } +} diff --git a/MyMovies/Model/TopRatedListViewModel.swift b/MyMovies/Model/TopRatedListViewModel.swift index 1d69e8d9..2a54c8c0 100644 --- a/MyMovies/Model/TopRatedListViewModel.swift +++ b/MyMovies/Model/TopRatedListViewModel.swift @@ -14,6 +14,7 @@ class TopRatedListViewModel { private var topRated: [TrendingViewModel] = [] private var selected = 0 + private let favoriteRepository: FavoriteRepository private let apiLoader: APILoader var numberOfRowsInSection: Int { @@ -24,8 +25,9 @@ class TopRatedListViewModel { // MARK: - Initializer - init(apiLoader: APILoader) { + init(apiLoader: APILoader, favoriteRepository: FavoriteRepository = FavoriteRepository()) { self.apiLoader = apiLoader + self.favoriteRepository = favoriteRepository } // MARK: - Functions @@ -61,4 +63,9 @@ class TopRatedListViewModel { func getTrending(_ index: Int) -> TrendingViewModel { return self.topRated[index] } + + func insertFavorite(for movieId: Int64) { + guard let trending = self.topRated.first(where: { $0.movieId == movieId }) else { return } + favoriteRepository.insetOrUpdateMovie(for: movieId, trending) + } } diff --git a/MyMovies/Model/TrendingListViewModel.swift b/MyMovies/Model/TrendingListViewModel.swift index 6a4c70fe..5fd5ea47 100644 --- a/MyMovies/Model/TrendingListViewModel.swift +++ b/MyMovies/Model/TrendingListViewModel.swift @@ -14,6 +14,7 @@ class TrendingListViewModel { private var trendings: [TrendingViewModel] = [] private var selected = 0 + private let favoriteRepository: FavoriteRepository private let apiLoader: APILoader var numberOfRowsInSection: Int { @@ -24,8 +25,9 @@ class TrendingListViewModel { // MARK: - Initializer - init(apiLoader: APILoader) { + init(apiLoader: APILoader, favoriteRepository: FavoriteRepository = FavoriteRepository()) { self.apiLoader = apiLoader + self.favoriteRepository = favoriteRepository } // MARK: - Functions @@ -62,4 +64,10 @@ class TrendingListViewModel { func getTrending(_ index: Int) -> TrendingViewModel { return self.trendings[index] } + + func insertFavorite(for movieId: Int64) { + guard let trending = self.trendings.first(where: { $0.movieId == movieId }) else { return } + favoriteRepository.insetOrUpdateMovie(for: movieId, trending) + print(favoriteRepository.getAllFavorites()) + } } diff --git a/MyMovies/Model/TrendingViewModel.swift b/MyMovies/Model/TrendingViewModel.swift index e7f7fd44..d40f082c 100644 --- a/MyMovies/Model/TrendingViewModel.swift +++ b/MyMovies/Model/TrendingViewModel.swift @@ -25,4 +25,8 @@ class TrendingViewModel { init(trending: TrendingResult) { self.trending = trending } + + func getTrendingResult() -> TrendingResult { + return trending + } } diff --git a/MyMovies/View/TrendingViewController/TrendingCollectionViewCell.swift b/MyMovies/View/TrendingViewController/TrendingCollectionViewCell.swift index 4f749cce..99ceee3f 100644 --- a/MyMovies/View/TrendingViewController/TrendingCollectionViewCell.swift +++ b/MyMovies/View/TrendingViewController/TrendingCollectionViewCell.swift @@ -11,6 +11,29 @@ class TrendingCollectionViewCell: UICollectionViewCell { static let identifier = "TrendingCollectionViewCell" + private var movieId: Int64? + + private lazy var favoriteImageView: UIImageView = { + let image = UIImage(systemName: "star.fill") + let imageView = UIImageView(image: image) + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.tintColor = .systemYellow + return imageView + }() + + private lazy var favoriteBackground: UIView = { + let view = UIView() + view.backgroundColor = .systemRed + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private lazy var containerView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + private lazy var posterImageView: UIImageView = { let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false @@ -40,16 +63,29 @@ class TrendingCollectionViewCell: UICollectionViewCell { private func setup() { self.backgroundColor = .systemBackground + setupContainerView() + } + + private func setupContainerView() { + addSubview(containerView) + + NSLayoutConstraint.activate([ + containerView.topAnchor.constraint(equalTo: contentView.topAnchor), + containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + ]) + setupPosterImage() } - func setupPosterImage() { - addSubview(posterImageView) + private func setupPosterImage() { + containerView.addSubview(posterImageView) NSLayoutConstraint.activate([ - posterImageView.topAnchor.constraint(equalTo: contentView.topAnchor), - posterImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - posterImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + posterImageView.topAnchor.constraint(equalTo: containerView.topAnchor), + posterImageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + posterImageView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), posterImageView.widthAnchor.constraint(equalToConstant: 154), posterImageView.heightAnchor.constraint(equalToConstant: 231) ]) @@ -57,24 +93,63 @@ class TrendingCollectionViewCell: UICollectionViewCell { setupTitleLabel() } - func setupTitleLabel() { - addSubview(titleLabel) + private func setupTitleLabel() { + containerView.addSubview(titleLabel) NSLayoutConstraint.activate([ titleLabel.topAnchor.constraint(equalTo: posterImageView.bottomAnchor, constant: 5), - titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 2), - titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -2), - titleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + titleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 2), + titleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -2) + ]) + + setupFavoriteButton() + } + + private func setupFavoriteButton() { + favoriteBackground.addSubview(favoriteImageView) + addSubview(favoriteBackground) + + NSLayoutConstraint.activate([ + favoriteBackground.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), + favoriteBackground.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor), + favoriteBackground.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor), + favoriteImageView.centerXAnchor.constraint(equalTo: favoriteBackground.centerXAnchor), + favoriteImageView.centerYAnchor.constraint(equalTo: favoriteBackground.centerYAnchor) ]) } func setupData(model: TrendingViewModel) { titleLabel.text = model.title + movieId = model.movieId if let url = model.posterImageURL { posterImageView.loadImage(at: url) } } + func getMovieId() -> Int64 { + return movieId ?? 0 + } + + func animateFavorite() { + var frame = containerView.frame + + frame.origin.y += 75.0 + + UIView.animate(withDuration: 0.30) { + self.containerView.frame = frame + self.favoriteBackground.frame.size.height = 75 + self.favoriteImageView.frame.origin.y = 50 + } + + frame.origin.y -= 75.0 + + UIView.animate(withDuration: 0.30, delay: 0.30) { + self.containerView.frame = frame + self.favoriteBackground.frame.size.height = 0 + self.favoriteImageView.frame.origin.y = 0 + } + } + override func prepareForReuse() { posterImageView.image = nil posterImageView.cancelImageLoad() diff --git a/MyMovies/View/TrendingViewController/TrendingView.swift b/MyMovies/View/TrendingViewController/TrendingView.swift index 9958052a..87bf3685 100644 --- a/MyMovies/View/TrendingViewController/TrendingView.swift +++ b/MyMovies/View/TrendingViewController/TrendingView.swift @@ -7,6 +7,10 @@ import UIKit +protocol FavoriteDelegate: AnyObject { + func favorite(_ row: Int) +} + class TrendingView: UIView { private lazy var trendingLabel: UILabel = { @@ -146,4 +150,9 @@ class TrendingView: UIView { self.trendingCollectionView.isHidden = false self.trendingCollectionView.reloadData() } + + func favoriteTrending(for row: Int) { + print("Yaaay!") +// self.delegate.favorite(row) + } } diff --git a/MyMovies/View/TrendingViewController/TrendingViewController.swift b/MyMovies/View/TrendingViewController/TrendingViewController.swift index 8e58a5c7..4a5570e7 100644 --- a/MyMovies/View/TrendingViewController/TrendingViewController.swift +++ b/MyMovies/View/TrendingViewController/TrendingViewController.swift @@ -85,6 +85,10 @@ extension TrendingViewController: UICollectionViewDelegate, UICollectionViewData let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TrendingCollectionViewCell.identifier, for: indexPath) as? TrendingCollectionViewCell cell?.setupData(model: trending) + let swipeUpsideDown = UISwipeGestureRecognizer(target: self, action: #selector(favorite(_:))) + swipeUpsideDown.direction = .down + cell?.addGestureRecognizer(swipeUpsideDown) + return cell ?? UICollectionViewCell() } @@ -95,4 +99,15 @@ extension TrendingViewController: UICollectionViewDelegate, UICollectionViewData coordinator?.eventOccurred(with: Event.movieClicked, parameters: movieId) } + + @objc func favorite(_ sender: UISwipeGestureRecognizer) { + guard let cell = sender.view as? TrendingCollectionViewCell else { + return + } + + cell.animateFavorite() + + self.trendingListViewModel?.insertFavorite(for: cell.getMovieId()) + self.topRatedListViewModel?.insertFavorite(for: cell.getMovieId()) + } } From b8d90c8c872826ceade4e2ca54afb67f932ef5b8 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Sat, 14 Jan 2023 14:13:55 -0300 Subject: [PATCH 48/65] Fix navigation bar hide and show --- MyMovies/View/Home/HomeViewController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MyMovies/View/Home/HomeViewController.swift b/MyMovies/View/Home/HomeViewController.swift index bf2a0698..e71539fb 100644 --- a/MyMovies/View/Home/HomeViewController.swift +++ b/MyMovies/View/Home/HomeViewController.swift @@ -35,8 +35,8 @@ class HomeViewController: UIViewController { view = homeView } - override func viewDidLoad() { - super.viewDidLoad() + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) self.navigationController?.isNavigationBarHidden = true } From abfcecf4a0d55b5c899854f87bb9db4ca5c21007 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Sat, 14 Jan 2023 15:25:26 -0300 Subject: [PATCH 49/65] Create favorite movies screen --- MyMovies.xcodeproj/project.pbxproj | 36 +++++++++++++--- MyMovies/Coordinator/Event.swift | 2 +- MyMovies/Coordinator/MainCoordinator.swift | 10 ++++- .../Model/Favorite/FavoriteRepository.swift | 6 ++- .../Model/Favorite/FavoriteViewModel.swift | 8 +++- MyMovies/View/Button/Button.swift | 42 +++++++++++++++++++ .../Favorite/FavoriteViewController.swift | 29 +++++++++++++ MyMovies/View/Home/HomeView.swift | 32 +++++++++++++- MyMovies/View/Home/HomeViewController.swift | 15 ++++++- .../MovieView.swift | 0 .../MovieViewController.swift | 0 .../TrendingCollectionViewCell.swift | 0 .../TrendingView.swift | 33 ++++++++++++++- .../TrendingViewController.swift | 21 +++++++++- 14 files changed, 217 insertions(+), 17 deletions(-) create mode 100644 MyMovies/View/Button/Button.swift create mode 100644 MyMovies/View/Favorite/FavoriteViewController.swift rename MyMovies/View/{MovieViewController => Movie}/MovieView.swift (100%) rename MyMovies/View/{MovieViewController => Movie}/MovieViewController.swift (100%) rename MyMovies/View/{TrendingViewController => Trending}/TrendingCollectionViewCell.swift (100%) rename MyMovies/View/{TrendingViewController => Trending}/TrendingView.swift (77%) rename MyMovies/View/{TrendingViewController => Trending}/TrendingViewController.swift (83%) diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index 3197a5f6..f57c5163 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -59,6 +59,8 @@ 7DF3DD252972E7AC007BF9A3 /* MainCoordinatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD242972E7AC007BF9A3 /* MainCoordinatorMock.swift */; }; 7DF3DD282972F5A4007BF9A3 /* FavoriteRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD272972F5A4007BF9A3 /* FavoriteRepository.swift */; }; 7DF3DD2B29730F28007BF9A3 /* FavoriteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD2A29730F28007BF9A3 /* FavoriteViewModel.swift */; }; + 7DF3DD2E29731AAD007BF9A3 /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD2D29731AAD007BF9A3 /* Button.swift */; }; + 7DF3DD3129731FFE007BF9A3 /* FavoriteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD3029731FFE007BF9A3 /* FavoriteViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -135,6 +137,8 @@ 7DF3DD242972E7AC007BF9A3 /* MainCoordinatorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCoordinatorMock.swift; sourceTree = ""; }; 7DF3DD272972F5A4007BF9A3 /* FavoriteRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteRepository.swift; sourceTree = ""; }; 7DF3DD2A29730F28007BF9A3 /* FavoriteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteViewModel.swift; sourceTree = ""; }; + 7DF3DD2D29731AAD007BF9A3 /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = ""; }; + 7DF3DD3029731FFE007BF9A3 /* FavoriteViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -278,21 +282,23 @@ 7D35A44E296E318E0054954E /* View */ = { isa = PBXGroup; children = ( + 7DF3DD2F29731FE1007BF9A3 /* Favorite */, + 7DF3DD2C29731AA3007BF9A3 /* Button */, 7D35A459296E36C60054954E /* Home */, - 7D35A47D29720D900054954E /* MovieViewController */, - 7D35A451296E32DC0054954E /* TrendingViewController */, + 7D35A47D29720D900054954E /* Movie */, + 7D35A451296E32DC0054954E /* Trending */, ); path = View; sourceTree = ""; }; - 7D35A451296E32DC0054954E /* TrendingViewController */ = { + 7D35A451296E32DC0054954E /* Trending */ = { isa = PBXGroup; children = ( 7D35A452296E32EE0054954E /* TrendingViewController.swift */, 7D35A45E296E3D360054954E /* TrendingView.swift */, 7D35A460296E3D5F0054954E /* TrendingCollectionViewCell.swift */, ); - path = TrendingViewController; + path = Trending; sourceTree = ""; }; 7D35A459296E36C60054954E /* Home */ = { @@ -314,13 +320,13 @@ path = ImageLoader; sourceTree = ""; }; - 7D35A47D29720D900054954E /* MovieViewController */ = { + 7D35A47D29720D900054954E /* Movie */ = { isa = PBXGroup; children = ( 7D35A47E29720DB50054954E /* MovieViewController.swift */, 7D35A48029720F0E0054954E /* MovieView.swift */, ); - path = MovieViewController; + path = Movie; sourceTree = ""; }; 7D35A4822972D5B00054954E /* Mocks */ = { @@ -400,6 +406,22 @@ path = Favorite; sourceTree = ""; }; + 7DF3DD2C29731AA3007BF9A3 /* Button */ = { + isa = PBXGroup; + children = ( + 7DF3DD2D29731AAD007BF9A3 /* Button.swift */, + ); + path = Button; + sourceTree = ""; + }; + 7DF3DD2F29731FE1007BF9A3 /* Favorite */ = { + isa = PBXGroup; + children = ( + 7DF3DD3029731FFE007BF9A3 /* FavoriteViewController.swift */, + ); + path = Favorite; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -562,6 +584,7 @@ 7DF3DD2B29730F28007BF9A3 /* FavoriteViewModel.swift in Sources */, 7D35A43C296E28490054954E /* APIPath.swift in Sources */, 7D35A47C29720CD40054954E /* Event.swift in Sources */, + 7DF3DD3129731FFE007BF9A3 /* FavoriteViewController.swift in Sources */, 7D35A46C296E4D090054954E /* UIImageView+ImageLoader.swift in Sources */, 7D35A467296E4C170054954E /* UIImageLoader.swift in Sources */, 7D35A44D296E2CA80054954E /* TrendingResult.swift in Sources */, @@ -571,6 +594,7 @@ 7D35A406296E22430054954E /* SceneDelegate.swift in Sources */, 7D35A46E296F98C00054954E /* TopRatedListViewModel.swift in Sources */, 7D35A470296F99430054954E /* TopRatedAPI.swift in Sources */, + 7DF3DD2E29731AAD007BF9A3 /* Button.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MyMovies/Coordinator/Event.swift b/MyMovies/Coordinator/Event.swift index 6717995e..8fc0049d 100644 --- a/MyMovies/Coordinator/Event.swift +++ b/MyMovies/Coordinator/Event.swift @@ -8,5 +8,5 @@ import Foundation enum Event { - case movieClicked + case movieClicked, favoriteClicked } diff --git a/MyMovies/Coordinator/MainCoordinator.swift b/MyMovies/Coordinator/MainCoordinator.swift index 38f11b62..ce0ed1f5 100644 --- a/MyMovies/Coordinator/MainCoordinator.swift +++ b/MyMovies/Coordinator/MainCoordinator.swift @@ -25,6 +25,13 @@ class MainCoordinator: Coordinator { let viewModel = MovieViewModel(apiLoader: apiLoader) let viewController = MovieViewController(movieId: movieId, movieViewModel: viewModel) + navigationController.pushViewController(viewController, animated: true) + + case .favoriteClicked: + let repository = FavoriteRepository() + let viewModel = FavoriteViewModel(favoriteRepository: repository) + let viewController = TrendingViewController(favoriteViewModel: viewModel, coordinator: self) + navigationController.pushViewController(viewController, animated: true) } } @@ -48,7 +55,8 @@ class MainCoordinator: Coordinator { let viewController = HomeViewController( trendingViewController: trendingViewController, - topRatedViewController: topRatedViewController + topRatedViewController: topRatedViewController, + coordinator: self ) navigationController.setViewControllers([viewController], animated: false) diff --git a/MyMovies/Model/Favorite/FavoriteRepository.swift b/MyMovies/Model/Favorite/FavoriteRepository.swift index 67138b8b..ffcf133a 100644 --- a/MyMovies/Model/Favorite/FavoriteRepository.swift +++ b/MyMovies/Model/Favorite/FavoriteRepository.swift @@ -26,7 +26,9 @@ class FavoriteRepository { if keys == nil { keys = [movieIdString] } else { - keys?.append(movieIdString) + if !keys!.contains(movieIdString) { + keys?.append(movieIdString) + } } saveValue(for: movieIdString, value: encoded) @@ -36,7 +38,7 @@ class FavoriteRepository { func deleteMovie(for movieId: Int64) { let movieIdString = String(movieId) - var keys: [String]? = readValue(for: Key.keys.rawValue) + let keys: [String]? = readValue(for: Key.keys.rawValue) if var keys = keys, keys.contains(movieIdString) { keys.removeAll(where: { $0 == movieIdString }) diff --git a/MyMovies/Model/Favorite/FavoriteViewModel.swift b/MyMovies/Model/Favorite/FavoriteViewModel.swift index 83197d02..c2bb78c2 100644 --- a/MyMovies/Model/Favorite/FavoriteViewModel.swift +++ b/MyMovies/Model/Favorite/FavoriteViewModel.swift @@ -14,10 +14,12 @@ class FavoriteViewModel { private let favoriteRepository: FavoriteRepository private var favorites: [TrendingViewModel] = [] - var numberOfRows: Int { + var numberOfRowsInSection: Int { self.favorites.count } + let titlePage: String = "Favorites" + // MARK: - Initializer init(favoriteRepository: FavoriteRepository) { @@ -36,4 +38,8 @@ class FavoriteViewModel { } } } + + func getTrending(_ index: Int) -> TrendingViewModel { + return self.favorites[index] + } } diff --git a/MyMovies/View/Button/Button.swift b/MyMovies/View/Button/Button.swift new file mode 100644 index 00000000..d2c5ae83 --- /dev/null +++ b/MyMovies/View/Button/Button.swift @@ -0,0 +1,42 @@ +// +// Button.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/14/23. +// + +import UIKit + +class Button: UIButton { + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + private func commonInit() { + updateBackgroundColor() + layer.cornerRadius = 15 + setTitleColor(.white, for: .normal) + } + + override var isHighlighted: Bool { + didSet { + updateBackgroundColor() + } + } + + private func updateBackgroundColor() { + if isEnabled { + if isHighlighted { + backgroundColor = UIColor(red: 0.10, green: 0.21, blue: 0.56, alpha: 1.00) + } else { + backgroundColor = .systemBlue + } + } + } +} diff --git a/MyMovies/View/Favorite/FavoriteViewController.swift b/MyMovies/View/Favorite/FavoriteViewController.swift new file mode 100644 index 00000000..fa68601d --- /dev/null +++ b/MyMovies/View/Favorite/FavoriteViewController.swift @@ -0,0 +1,29 @@ +// +// FavoriteViewController.swift +// MyMovies +// +// Created by Caio dos Santos Ambrosio on 1/14/23. +// + +import UIKit + +class FavoriteViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + } + + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ + +} diff --git a/MyMovies/View/Home/HomeView.swift b/MyMovies/View/Home/HomeView.swift index 2f674023..bce6f3fc 100644 --- a/MyMovies/View/Home/HomeView.swift +++ b/MyMovies/View/Home/HomeView.swift @@ -7,11 +7,25 @@ import UIKit +protocol FavoriteButtonDelegate: AnyObject { + func favoriteMoviesButtonClicked() +} + class HomeView: UIView { private var trendingView: UIView private var topRatedView: UIView + private lazy var favoritesButton: Button = { + let button = Button() + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle("Favorite Movies", for: .normal) + button.addTarget(self, action: #selector(favoriteMoviesButtonClicked), for: .touchUpInside) + return button + }() + + weak var delegate: FavoriteButtonDelegate? + init( trendingViewController: TrendingViewController, topRatedViewController: TrendingViewController @@ -30,6 +44,18 @@ class HomeView: UIView { private func setup() { self.backgroundColor = .systemBackground + setupFavoriteButton() + } + + private func setupFavoriteButton() { + addSubview(favoritesButton) + + NSLayoutConstraint.activate([ + favoritesButton.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 20), + favoritesButton.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -20), + favoritesButton.widthAnchor.constraint(equalToConstant: 160) + ]) + setupTrendingView() } @@ -39,7 +65,7 @@ class HomeView: UIView { trendingView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - trendingView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 20), + trendingView.topAnchor.constraint(equalTo: favoritesButton.bottomAnchor, constant: 20), trendingView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 4), trendingView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -4), trendingView.heightAnchor.constraint(equalToConstant: 300) @@ -60,4 +86,8 @@ class HomeView: UIView { topRatedView.heightAnchor.constraint(equalToConstant: 300) ]) } + + @objc func favoriteMoviesButtonClicked() { + delegate?.favoriteMoviesButtonClicked() + } } diff --git a/MyMovies/View/Home/HomeViewController.swift b/MyMovies/View/Home/HomeViewController.swift index e71539fb..4c0f211e 100644 --- a/MyMovies/View/Home/HomeViewController.swift +++ b/MyMovies/View/Home/HomeViewController.swift @@ -7,7 +7,9 @@ import UIKit -class HomeViewController: UIViewController { +class HomeViewController: UIViewController, Coordinating { + + var coordinator: Coordinator? private var trendingViewController: TrendingViewController private var topRatedViewController: TrendingViewController @@ -16,10 +18,12 @@ class HomeViewController: UIViewController { init( trendingViewController: TrendingViewController, - topRatedViewController: TrendingViewController + topRatedViewController: TrendingViewController, + coordinator: Coordinator ) { self.trendingViewController = trendingViewController self.topRatedViewController = topRatedViewController + self.coordinator = coordinator super.init(nibName: nil, bundle: nil) } @@ -32,6 +36,7 @@ class HomeViewController: UIViewController { trendingViewController: trendingViewController, topRatedViewController: topRatedViewController ) + self.homeView?.delegate = self view = homeView } @@ -45,3 +50,9 @@ class HomeViewController: UIViewController { self.navigationController?.isNavigationBarHidden = false } } + +extension HomeViewController: FavoriteButtonDelegate { + func favoriteMoviesButtonClicked() { + self.coordinator?.eventOccurred(with: .favoriteClicked, parameters: nil) + } +} diff --git a/MyMovies/View/MovieViewController/MovieView.swift b/MyMovies/View/Movie/MovieView.swift similarity index 100% rename from MyMovies/View/MovieViewController/MovieView.swift rename to MyMovies/View/Movie/MovieView.swift diff --git a/MyMovies/View/MovieViewController/MovieViewController.swift b/MyMovies/View/Movie/MovieViewController.swift similarity index 100% rename from MyMovies/View/MovieViewController/MovieViewController.swift rename to MyMovies/View/Movie/MovieViewController.swift diff --git a/MyMovies/View/TrendingViewController/TrendingCollectionViewCell.swift b/MyMovies/View/Trending/TrendingCollectionViewCell.swift similarity index 100% rename from MyMovies/View/TrendingViewController/TrendingCollectionViewCell.swift rename to MyMovies/View/Trending/TrendingCollectionViewCell.swift diff --git a/MyMovies/View/TrendingViewController/TrendingView.swift b/MyMovies/View/Trending/TrendingView.swift similarity index 77% rename from MyMovies/View/TrendingViewController/TrendingView.swift rename to MyMovies/View/Trending/TrendingView.swift index 87bf3685..edb85d79 100644 --- a/MyMovies/View/TrendingViewController/TrendingView.swift +++ b/MyMovies/View/Trending/TrendingView.swift @@ -125,7 +125,9 @@ class TrendingView: UIView { NSLayoutConstraint.activate([ errorLabel.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor), - errorLabel.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor) + errorLabel.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor), + errorLabel.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 20), + errorLabel.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -20) ]) } @@ -151,6 +153,35 @@ class TrendingView: UIView { self.trendingCollectionView.reloadData() } + func renderFavoriteState(with viewModel: FavoriteViewModel) { + self.activityIndicator.stopAnimating() + if viewModel.numberOfRowsInSection == 0 { + self.errorLabel.text = "You do not have any favorite, to select one just swipe down in a movie" + + self.trendingCollectionView.isHidden = true + self.trendingLabel.isHidden = true + self.errorLabel.isHidden = false + } else { + self.errorLabel.isHidden = true + self.trendingLabel.isHidden = false + self.trendingLabel.text = viewModel.titlePage + self.trendingCollectionView.isHidden = false + self.trendingCollectionView.reloadData() + + NSLayoutConstraint.activate([ + self.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 20), + self.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -20) + ]) + + if let layout = self.trendingCollectionView.collectionViewLayout as? UICollectionViewFlowLayout { + layout.scrollDirection = .vertical + self.trendingCollectionView.collectionViewLayout = layout + } + + self.layoutIfNeeded() + } + } + func favoriteTrending(for row: Int) { print("Yaaay!") // self.delegate.favorite(row) diff --git a/MyMovies/View/TrendingViewController/TrendingViewController.swift b/MyMovies/View/Trending/TrendingViewController.swift similarity index 83% rename from MyMovies/View/TrendingViewController/TrendingViewController.swift rename to MyMovies/View/Trending/TrendingViewController.swift index 4a5570e7..f95a558b 100644 --- a/MyMovies/View/TrendingViewController/TrendingViewController.swift +++ b/MyMovies/View/Trending/TrendingViewController.swift @@ -14,14 +14,17 @@ class TrendingViewController: UIViewController, Coordinating { private var trendingView: TrendingView? = nil private var trendingListViewModel: TrendingListViewModel? private var topRatedListViewModel: TopRatedListViewModel? + private var favoriteViewModel: FavoriteViewModel? init( trendingListViewModel: TrendingListViewModel? = nil, topRatedListViewModel: TopRatedListViewModel? = nil, + favoriteViewModel: FavoriteViewModel? = nil, coordinator: Coordinator ) { self.trendingListViewModel = trendingListViewModel self.topRatedListViewModel = topRatedListViewModel + self.favoriteViewModel = favoriteViewModel self.coordinator = coordinator super.init(nibName: nil, bundle: nil) } @@ -69,18 +72,31 @@ class TrendingViewController: UIViewController, Coordinating { self.trendingView?.renderSuccessState(with: topRatedListViewModel.titlePage) } } + + favoriteViewModel?.fetchFavorites { [weak self] favoriteViewModel in + guard let self = self else { + return + } + + if let favoriteViewModel = favoriteViewModel { + self.favoriteViewModel = favoriteViewModel + self.trendingView?.renderFavoriteState(with: favoriteViewModel) + } + } } } extension TrendingViewController: UICollectionViewDelegate, UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return self.trendingListViewModel?.numberOfRowsInSection ?? - self.topRatedListViewModel?.numberOfRowsInSection ?? 0 + self.topRatedListViewModel?.numberOfRowsInSection ?? + self.favoriteViewModel?.numberOfRowsInSection ?? 0 } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let trending = trendingListViewModel?.getTrending(indexPath.row) ?? - topRatedListViewModel?.getTrending(indexPath.row) else { return UICollectionViewCell() } + topRatedListViewModel?.getTrending(indexPath.row) ?? + favoriteViewModel?.getTrending(indexPath.row) else { return UICollectionViewCell() } let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TrendingCollectionViewCell.identifier, for: indexPath) as? TrendingCollectionViewCell cell?.setupData(model: trending) @@ -109,5 +125,6 @@ extension TrendingViewController: UICollectionViewDelegate, UICollectionViewData self.trendingListViewModel?.insertFavorite(for: cell.getMovieId()) self.topRatedListViewModel?.insertFavorite(for: cell.getMovieId()) +// self.favoriteViewModel?.removeFavorite(for: cell.getMovieId()) } } From 5e577924c8ef447dc459f0f9def965178c4898e2 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Mon, 16 Jan 2023 20:03:28 -0300 Subject: [PATCH 50/65] Fix MovieView --- MyMovies/View/Movie/MovieView.swift | 44 +++++++++++++++-------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/MyMovies/View/Movie/MovieView.swift b/MyMovies/View/Movie/MovieView.swift index 049dee98..3c3eb4f5 100644 --- a/MyMovies/View/Movie/MovieView.swift +++ b/MyMovies/View/Movie/MovieView.swift @@ -29,9 +29,10 @@ class MovieView: UIView { private lazy var genresStackView: UIStackView = { let stackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.axis = .horizontal + stackView.axis = .vertical stackView.spacing = 8 stackView.alignment = .center + stackView.distribution = .fillEqually return stackView }() @@ -47,10 +48,10 @@ class MovieView: UIView { private lazy var rateAndReleaseDateStackView: UIStackView = { let stackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.axis = .horizontal + stackView.axis = .vertical stackView.spacing = 8 stackView.alignment = .center - stackView.distribution = .fill + stackView.distribution = .fillEqually return stackView }() @@ -143,20 +144,7 @@ class MovieView: UIView { NSLayoutConstraint.activate([ genresStackView.topAnchor.constraint(equalTo: posterImageView.bottomAnchor, constant: 20), genresStackView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 10), - genresStackView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -10) - ]) - - setupOverviewTextView() - } - - private func setupOverviewTextView() { - addSubview(overviewTextView) - - NSLayoutConstraint.activate([ - overviewTextView.topAnchor.constraint(equalTo: genresStackView.bottomAnchor, constant: 20), - overviewTextView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 10), - overviewTextView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -10), - overviewTextView.heightAnchor.constraint(lessThanOrEqualToConstant: 200) + genresStackView.widthAnchor.constraint(equalToConstant: 120) ]) setupRateAndReleaseDateStackView() @@ -169,11 +157,25 @@ class MovieView: UIView { rateAndReleaseDateStackView.addArrangedSubview(releaseDateLabel) NSLayoutConstraint.activate([ - rateAndReleaseDateStackView.topAnchor.constraint(equalTo: overviewTextView.bottomAnchor, constant: 20), - rateAndReleaseDateStackView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 10), + rateAndReleaseDateStackView.topAnchor.constraint(equalTo: posterImageView.bottomAnchor, constant: 20), + rateAndReleaseDateStackView.leadingAnchor.constraint(equalTo: genresStackView.trailingAnchor, constant: 10), rateAndReleaseDateStackView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -10) ]) + setupOverviewTextView() + } + + private func setupOverviewTextView() { + addSubview(overviewTextView) + + NSLayoutConstraint.activate([ + overviewTextView.topAnchor.constraint(equalTo: genresStackView.bottomAnchor, constant: 20), + overviewTextView.topAnchor.constraint(equalTo: rateAndReleaseDateStackView.bottomAnchor, constant: 20), + overviewTextView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 10), + overviewTextView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -10), + overviewTextView.heightAnchor.constraint(lessThanOrEqualToConstant: 200) + ]) + setupActivityIndicator() } @@ -240,7 +242,7 @@ class MovieView: UIView { } self.overviewTextView.text = viewModel.overview self.rateLabel.text = "\(viewModel.rate)/10" - self.totalVoteCountLabel.text = "Total Vote Count: \(viewModel.totalVoteCount)" - self.releaseDateLabel.text = viewModel.releaseDate + self.totalVoteCountLabel.text = "\(viewModel.totalVoteCount) votes" + self.releaseDateLabel.text = "Released in \(viewModel.releaseDate)" } } From ae01fac3517b89e1c64d73aeb1658348a0379fb4 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Mon, 16 Jan 2023 20:30:57 -0300 Subject: [PATCH 51/65] Minor fixes in favorites view --- MyMovies/View/Home/HomeView.swift | 8 +++---- .../Trending/TrendingCollectionViewCell.swift | 6 ++++++ MyMovies/View/Trending/TrendingView.swift | 21 ++++++++++++------- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/MyMovies/View/Home/HomeView.swift b/MyMovies/View/Home/HomeView.swift index bce6f3fc..eb663970 100644 --- a/MyMovies/View/Home/HomeView.swift +++ b/MyMovies/View/Home/HomeView.swift @@ -65,10 +65,10 @@ class HomeView: UIView { trendingView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - trendingView.topAnchor.constraint(equalTo: favoritesButton.bottomAnchor, constant: 20), + trendingView.topAnchor.constraint(equalTo: favoritesButton.bottomAnchor, constant: 10), trendingView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 4), trendingView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -4), - trendingView.heightAnchor.constraint(equalToConstant: 300) + trendingView.heightAnchor.constraint(equalToConstant: 350) ]) setupTopRatedView() @@ -80,10 +80,10 @@ class HomeView: UIView { topRatedView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - topRatedView.topAnchor.constraint(equalTo: trendingView.bottomAnchor, constant: 20), + topRatedView.topAnchor.constraint(equalTo: trendingView.bottomAnchor), topRatedView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 4), topRatedView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -4), - topRatedView.heightAnchor.constraint(equalToConstant: 300) + topRatedView.heightAnchor.constraint(equalToConstant: 350) ]) } diff --git a/MyMovies/View/Trending/TrendingCollectionViewCell.swift b/MyMovies/View/Trending/TrendingCollectionViewCell.swift index 99ceee3f..a1652a1c 100644 --- a/MyMovies/View/Trending/TrendingCollectionViewCell.swift +++ b/MyMovies/View/Trending/TrendingCollectionViewCell.swift @@ -18,6 +18,7 @@ class TrendingCollectionViewCell: UICollectionViewCell { let imageView = UIImageView(image: image) imageView.translatesAutoresizingMaskIntoConstraints = false imageView.tintColor = .systemYellow + imageView.isHidden = true return imageView }() @@ -134,6 +135,7 @@ class TrendingCollectionViewCell: UICollectionViewCell { var frame = containerView.frame frame.origin.y += 75.0 + self.favoriteImageView.isHidden = false UIView.animate(withDuration: 0.30) { self.containerView.frame = frame @@ -148,6 +150,10 @@ class TrendingCollectionViewCell: UICollectionViewCell { self.favoriteBackground.frame.size.height = 0 self.favoriteImageView.frame.origin.y = 0 } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { + self.favoriteImageView.isHidden = true + } } override func prepareForReuse() { diff --git a/MyMovies/View/Trending/TrendingView.swift b/MyMovies/View/Trending/TrendingView.swift index edb85d79..7c06b80d 100644 --- a/MyMovies/View/Trending/TrendingView.swift +++ b/MyMovies/View/Trending/TrendingView.swift @@ -96,14 +96,22 @@ class TrendingView: UIView { setupTrendingCollectionView() } + var collectionViewLeadingContraint: NSLayoutConstraint? = nil + var collectionViewTrailingContraint: NSLayoutConstraint? = nil + var collectionViewBottomContraint: NSLayoutConstraint? = nil + private func setupTrendingCollectionView() { addSubview(trendingCollectionView) + collectionViewLeadingContraint = trendingCollectionView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor) + collectionViewTrailingContraint = trendingCollectionView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor) + collectionViewBottomContraint = trendingCollectionView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor) + NSLayoutConstraint.activate([ trendingCollectionView.topAnchor.constraint(equalTo: trendingLabel.bottomAnchor, constant: 10), - trendingCollectionView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor), - trendingCollectionView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor), - trendingCollectionView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor) + collectionViewLeadingContraint!, + collectionViewTrailingContraint!, + collectionViewBottomContraint! ]) setupActivityIndicator() @@ -168,10 +176,9 @@ class TrendingView: UIView { self.trendingCollectionView.isHidden = false self.trendingCollectionView.reloadData() - NSLayoutConstraint.activate([ - self.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 20), - self.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -20) - ]) + collectionViewLeadingContraint?.constant = 10 + collectionViewTrailingContraint?.constant = -10 + collectionViewBottomContraint?.constant = -20 if let layout = self.trendingCollectionView.collectionViewLayout as? UICollectionViewFlowLayout { layout.scrollDirection = .vertical From 7ea1affe1d5905954c3bc449f53dbe105c3c969a Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Mon, 16 Jan 2023 20:31:24 -0300 Subject: [PATCH 52/65] Remove unused class --- MyMovies.xcodeproj/project.pbxproj | 12 -------- .../Favorite/FavoriteViewController.swift | 29 ------------------- 2 files changed, 41 deletions(-) delete mode 100644 MyMovies/View/Favorite/FavoriteViewController.swift diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index f57c5163..3f7ccb67 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -60,7 +60,6 @@ 7DF3DD282972F5A4007BF9A3 /* FavoriteRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD272972F5A4007BF9A3 /* FavoriteRepository.swift */; }; 7DF3DD2B29730F28007BF9A3 /* FavoriteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD2A29730F28007BF9A3 /* FavoriteViewModel.swift */; }; 7DF3DD2E29731AAD007BF9A3 /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD2D29731AAD007BF9A3 /* Button.swift */; }; - 7DF3DD3129731FFE007BF9A3 /* FavoriteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD3029731FFE007BF9A3 /* FavoriteViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -138,7 +137,6 @@ 7DF3DD272972F5A4007BF9A3 /* FavoriteRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteRepository.swift; sourceTree = ""; }; 7DF3DD2A29730F28007BF9A3 /* FavoriteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteViewModel.swift; sourceTree = ""; }; 7DF3DD2D29731AAD007BF9A3 /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = ""; }; - 7DF3DD3029731FFE007BF9A3 /* FavoriteViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -282,7 +280,6 @@ 7D35A44E296E318E0054954E /* View */ = { isa = PBXGroup; children = ( - 7DF3DD2F29731FE1007BF9A3 /* Favorite */, 7DF3DD2C29731AA3007BF9A3 /* Button */, 7D35A459296E36C60054954E /* Home */, 7D35A47D29720D900054954E /* Movie */, @@ -414,14 +411,6 @@ path = Button; sourceTree = ""; }; - 7DF3DD2F29731FE1007BF9A3 /* Favorite */ = { - isa = PBXGroup; - children = ( - 7DF3DD3029731FFE007BF9A3 /* FavoriteViewController.swift */, - ); - path = Favorite; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -584,7 +573,6 @@ 7DF3DD2B29730F28007BF9A3 /* FavoriteViewModel.swift in Sources */, 7D35A43C296E28490054954E /* APIPath.swift in Sources */, 7D35A47C29720CD40054954E /* Event.swift in Sources */, - 7DF3DD3129731FFE007BF9A3 /* FavoriteViewController.swift in Sources */, 7D35A46C296E4D090054954E /* UIImageView+ImageLoader.swift in Sources */, 7D35A467296E4C170054954E /* UIImageLoader.swift in Sources */, 7D35A44D296E2CA80054954E /* TrendingResult.swift in Sources */, diff --git a/MyMovies/View/Favorite/FavoriteViewController.swift b/MyMovies/View/Favorite/FavoriteViewController.swift deleted file mode 100644 index fa68601d..00000000 --- a/MyMovies/View/Favorite/FavoriteViewController.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// FavoriteViewController.swift -// MyMovies -// -// Created by Caio dos Santos Ambrosio on 1/14/23. -// - -import UIKit - -class FavoriteViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - } - - - /* - // MARK: - Navigation - - // In a storyboard-based application, you will often want to do a little preparation before navigation - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - // Get the new view controller using segue.destination. - // Pass the selected object to the new view controller. - } - */ - -} From be3aae04363d6458b32c9fb517f83769619154f2 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Mon, 16 Jan 2023 20:46:29 -0300 Subject: [PATCH 53/65] Add animation for delete a movie in favorites repository --- .../Model/Favorite/FavoriteViewModel.swift | 4 + .../Trending/TrendingCollectionViewCell.swift | 89 +++++++++++++++---- .../Trending/TrendingViewController.swift | 8 +- 3 files changed, 83 insertions(+), 18 deletions(-) diff --git a/MyMovies/Model/Favorite/FavoriteViewModel.swift b/MyMovies/Model/Favorite/FavoriteViewModel.swift index c2bb78c2..7fcaaade 100644 --- a/MyMovies/Model/Favorite/FavoriteViewModel.swift +++ b/MyMovies/Model/Favorite/FavoriteViewModel.swift @@ -42,4 +42,8 @@ class FavoriteViewModel { func getTrending(_ index: Int) -> TrendingViewModel { return self.favorites[index] } + + func removeFavorite(for movieId: Int64) { + + } } diff --git a/MyMovies/View/Trending/TrendingCollectionViewCell.swift b/MyMovies/View/Trending/TrendingCollectionViewCell.swift index a1652a1c..9a521c6a 100644 --- a/MyMovies/View/Trending/TrendingCollectionViewCell.swift +++ b/MyMovies/View/Trending/TrendingCollectionViewCell.swift @@ -22,6 +22,15 @@ class TrendingCollectionViewCell: UICollectionViewCell { return imageView }() + private lazy var removeImageView: UIImageView = { + let image = UIImage(systemName: "trash.fill") + let imageView = UIImageView(image: image) + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.tintColor = .systemYellow + imageView.isHidden = true + return imageView + }() + private lazy var favoriteBackground: UIView = { let view = UIView() view.backgroundColor = .systemRed @@ -29,6 +38,13 @@ class TrendingCollectionViewCell: UICollectionViewCell { return view }() + private lazy var removeBackground: UIView = { + let view = UIView() + view.backgroundColor = .systemRed + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + private lazy var containerView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false @@ -117,6 +133,21 @@ class TrendingCollectionViewCell: UICollectionViewCell { favoriteImageView.centerXAnchor.constraint(equalTo: favoriteBackground.centerXAnchor), favoriteImageView.centerYAnchor.constraint(equalTo: favoriteBackground.centerYAnchor) ]) + + setupRemoveButton() + } + + private func setupRemoveButton() { + removeBackground.addSubview(removeImageView) + addSubview(removeBackground) + + NSLayoutConstraint.activate([ + removeBackground.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), + removeBackground.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor), + removeBackground.bottomAnchor.constraint(equalTo: posterImageView.bottomAnchor), + removeImageView.centerXAnchor.constraint(equalTo: removeBackground.centerXAnchor), + removeImageView.centerYAnchor.constraint(equalTo: removeBackground.centerYAnchor) + ]) } func setupData(model: TrendingViewModel) { @@ -131,28 +162,54 @@ class TrendingCollectionViewCell: UICollectionViewCell { return movieId ?? 0 } - func animateFavorite() { + func animateFavorite(for direction: UISwipeGestureRecognizer.Direction) { var frame = containerView.frame - frame.origin.y += 75.0 - self.favoriteImageView.isHidden = false + if direction == .down { - UIView.animate(withDuration: 0.30) { - self.containerView.frame = frame - self.favoriteBackground.frame.size.height = 75 - self.favoriteImageView.frame.origin.y = 50 - } + frame.origin.y += 75.0 + self.favoriteImageView.isHidden = false - frame.origin.y -= 75.0 + UIView.animate(withDuration: 0.30) { + self.containerView.frame = frame + self.favoriteBackground.frame.size.height = 75 + self.favoriteImageView.frame.origin.y = 40 + } - UIView.animate(withDuration: 0.30, delay: 0.30) { - self.containerView.frame = frame - self.favoriteBackground.frame.size.height = 0 - self.favoriteImageView.frame.origin.y = 0 - } + frame.origin.y -= 75.0 + + UIView.animate(withDuration: 0.30, delay: 0.30) { + self.containerView.frame = frame + self.favoriteBackground.frame.size.height = 0 + self.favoriteImageView.frame.origin.y = 0 + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.50) { + self.favoriteImageView.isHidden = true + } + + } else { + frame.origin.x += 75.0 + self.removeImageView.isHidden = false + + UIView.animate(withDuration: 0.30) { + self.containerView.frame = frame + self.removeBackground.frame.size.width = 75 + self.removeImageView.frame.origin.x = 50 + } + + + frame.origin.x -= 75.0 + + UIView.animate(withDuration: 0.30, delay: 0.30) { + self.containerView.frame = frame + self.removeBackground.frame.size.width = 0 + self.removeImageView.frame.origin.x = 0 + } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { - self.favoriteImageView.isHidden = true + DispatchQueue.main.asyncAfter(deadline: .now() + 0.50) { + self.removeImageView.isHidden = true + } } } diff --git a/MyMovies/View/Trending/TrendingViewController.swift b/MyMovies/View/Trending/TrendingViewController.swift index f95a558b..c70640a9 100644 --- a/MyMovies/View/Trending/TrendingViewController.swift +++ b/MyMovies/View/Trending/TrendingViewController.swift @@ -105,6 +105,10 @@ extension TrendingViewController: UICollectionViewDelegate, UICollectionViewData swipeUpsideDown.direction = .down cell?.addGestureRecognizer(swipeUpsideDown) + let swipeLeftToRight = UISwipeGestureRecognizer(target: self, action: #selector(favorite(_:))) + swipeLeftToRight.direction = .right + cell?.addGestureRecognizer(swipeLeftToRight) + return cell ?? UICollectionViewCell() } @@ -121,10 +125,10 @@ extension TrendingViewController: UICollectionViewDelegate, UICollectionViewData return } - cell.animateFavorite() + cell.animateFavorite(for: sender.direction) self.trendingListViewModel?.insertFavorite(for: cell.getMovieId()) self.topRatedListViewModel?.insertFavorite(for: cell.getMovieId()) -// self.favoriteViewModel?.removeFavorite(for: cell.getMovieId()) + self.favoriteViewModel?.removeFavorite(for: cell.getMovieId()) } } From dc32820e0706d1c258baaae7d83843d9526dc76e Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Mon, 16 Jan 2023 20:58:47 -0300 Subject: [PATCH 54/65] Fix function that deletes elements from repository and also fix animation --- MyMovies/Model/Favorite/FavoriteRepository.swift | 12 ++++++++---- MyMovies/Model/Favorite/FavoriteViewModel.swift | 3 ++- MyMovies/Model/TrendingListViewModel.swift | 1 - MyMovies/View/Trending/TrendingView.swift | 3 ++- MyMovies/View/Trending/TrendingViewController.swift | 8 +++++++- 5 files changed, 19 insertions(+), 8 deletions(-) diff --git a/MyMovies/Model/Favorite/FavoriteRepository.swift b/MyMovies/Model/Favorite/FavoriteRepository.swift index ffcf133a..15b83cb8 100644 --- a/MyMovies/Model/Favorite/FavoriteRepository.swift +++ b/MyMovies/Model/Favorite/FavoriteRepository.swift @@ -38,13 +38,17 @@ class FavoriteRepository { func deleteMovie(for movieId: Int64) { let movieIdString = String(movieId) - let keys: [String]? = readValue(for: Key.keys.rawValue) - if var keys = keys, - keys.contains(movieIdString) { + var keys: [String]? = readValue(for: Key.keys.rawValue) + if var keys { keys.removeAll(where: { $0 == movieIdString }) + if keys.count > 0 { + saveValue(for: Key.keys.rawValue, value: keys) + } else { + removeValue(for: Key.keys.rawValue) + } } - removeValue(for: String(movieId)) + removeValue(for: movieIdString) } func getAllFavorites() -> [TrendingViewModel] { diff --git a/MyMovies/Model/Favorite/FavoriteViewModel.swift b/MyMovies/Model/Favorite/FavoriteViewModel.swift index 7fcaaade..0a256868 100644 --- a/MyMovies/Model/Favorite/FavoriteViewModel.swift +++ b/MyMovies/Model/Favorite/FavoriteViewModel.swift @@ -44,6 +44,7 @@ class FavoriteViewModel { } func removeFavorite(for movieId: Int64) { - + self.favorites.removeAll(where: { $0.movieId == movieId }) + favoriteRepository.deleteMovie(for: movieId) } } diff --git a/MyMovies/Model/TrendingListViewModel.swift b/MyMovies/Model/TrendingListViewModel.swift index 5fd5ea47..10bd0123 100644 --- a/MyMovies/Model/TrendingListViewModel.swift +++ b/MyMovies/Model/TrendingListViewModel.swift @@ -68,6 +68,5 @@ class TrendingListViewModel { func insertFavorite(for movieId: Int64) { guard let trending = self.trendings.first(where: { $0.movieId == movieId }) else { return } favoriteRepository.insetOrUpdateMovie(for: movieId, trending) - print(favoriteRepository.getAllFavorites()) } } diff --git a/MyMovies/View/Trending/TrendingView.swift b/MyMovies/View/Trending/TrendingView.swift index 7c06b80d..9137bb05 100644 --- a/MyMovies/View/Trending/TrendingView.swift +++ b/MyMovies/View/Trending/TrendingView.swift @@ -163,6 +163,8 @@ class TrendingView: UIView { func renderFavoriteState(with viewModel: FavoriteViewModel) { self.activityIndicator.stopAnimating() + self.trendingCollectionView.reloadData() + if viewModel.numberOfRowsInSection == 0 { self.errorLabel.text = "You do not have any favorite, to select one just swipe down in a movie" @@ -174,7 +176,6 @@ class TrendingView: UIView { self.trendingLabel.isHidden = false self.trendingLabel.text = viewModel.titlePage self.trendingCollectionView.isHidden = false - self.trendingCollectionView.reloadData() collectionViewLeadingContraint?.constant = 10 collectionViewTrailingContraint?.constant = -10 diff --git a/MyMovies/View/Trending/TrendingViewController.swift b/MyMovies/View/Trending/TrendingViewController.swift index c70640a9..48929ba2 100644 --- a/MyMovies/View/Trending/TrendingViewController.swift +++ b/MyMovies/View/Trending/TrendingViewController.swift @@ -129,6 +129,12 @@ extension TrendingViewController: UICollectionViewDelegate, UICollectionViewData self.trendingListViewModel?.insertFavorite(for: cell.getMovieId()) self.topRatedListViewModel?.insertFavorite(for: cell.getMovieId()) - self.favoriteViewModel?.removeFavorite(for: cell.getMovieId()) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.50) { + self.favoriteViewModel?.removeFavorite(for: cell.getMovieId()) + if let favoriteViewModel = self.favoriteViewModel { + self.trendingView?.renderFavoriteState(with: favoriteViewModel) + } + } } } From 19b456a11c349f831f836de4683697535107b70b Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Mon, 16 Jan 2023 21:03:42 -0300 Subject: [PATCH 55/65] Save movieId in view model instead of view controller --- MyMovies/Coordinator/MainCoordinator.swift | 4 ++-- MyMovies/Model/MovieViewModel.swift | 5 +++-- MyMovies/View/Movie/MovieViewController.swift | 5 +---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/MyMovies/Coordinator/MainCoordinator.swift b/MyMovies/Coordinator/MainCoordinator.swift index ce0ed1f5..83701e87 100644 --- a/MyMovies/Coordinator/MainCoordinator.swift +++ b/MyMovies/Coordinator/MainCoordinator.swift @@ -22,8 +22,8 @@ class MainCoordinator: Coordinator { let api = MovieAPI() let apiLoader = APILoader(apiHandler: api) - let viewModel = MovieViewModel(apiLoader: apiLoader) - let viewController = MovieViewController(movieId: movieId, movieViewModel: viewModel) + let viewModel = MovieViewModel(apiLoader: apiLoader, movieId: movieId) + let viewController = MovieViewController(movieViewModel: viewModel) navigationController.pushViewController(viewController, animated: true) diff --git a/MyMovies/Model/MovieViewModel.swift b/MyMovies/Model/MovieViewModel.swift index 836194e7..4dd8e4ef 100644 --- a/MyMovies/Model/MovieViewModel.swift +++ b/MyMovies/Model/MovieViewModel.swift @@ -12,6 +12,7 @@ class MovieViewModel { // MARK: - Properties private var movie: Movie? = nil + private var movieId: Int64 private let apiLoader: APILoader @@ -51,14 +52,14 @@ class MovieViewModel { // MARK: - Initializer - init(apiLoader: APILoader) { + init(apiLoader: APILoader, movieId: Int64) { self.apiLoader = apiLoader + self.movieId = movieId } // MARK: - Functions func fetchMovie( - for movieId: Int64, completion: @escaping (MovieViewModel?, ServiceError?) -> () ) { apiLoader.loadAPIRequest(requestData: movieId) { [weak self] movieResponse, error in diff --git a/MyMovies/View/Movie/MovieViewController.swift b/MyMovies/View/Movie/MovieViewController.swift index dbd88707..83dfcab1 100644 --- a/MyMovies/View/Movie/MovieViewController.swift +++ b/MyMovies/View/Movie/MovieViewController.swift @@ -11,13 +11,10 @@ class MovieViewController: UIViewController { private var movieView: MovieView? = nil private var movieViewModel: MovieViewModel - private var movieId: Int64 init( - movieId: Int64, movieViewModel: MovieViewModel ) { - self.movieId = movieId self.movieViewModel = movieViewModel super.init(nibName: nil, bundle: nil) } @@ -36,7 +33,7 @@ class MovieViewController: UIViewController { movieView?.renderLoadingState() - movieViewModel.fetchMovie(for: self.movieId) { [weak self] movieViewModel, error in + movieViewModel.fetchMovie { [weak self] movieViewModel, error in guard let self = self else { return } From ea3ded3125e4eb78c64195d69d04dadb0ae9db90 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Mon, 16 Jan 2023 21:09:50 -0300 Subject: [PATCH 56/65] Refact tests to extract duplicated code --- MyMovies.xcodeproj/project.pbxproj | 12 +++++++ MyMoviesTests/Helper/JsonHelper.swift | 34 +++++++++++++++++++ MyMoviesTests/Model/MovieViewModelSpec.swift | 24 ++++--------- .../Model/TopRatedListViewModelSpec.swift | 16 ++------- .../Model/TrendingListViewModelSpec.swift | 16 ++------- .../Model/TrendingViewModelSpec.swift | 32 +++-------------- 6 files changed, 60 insertions(+), 74 deletions(-) create mode 100644 MyMoviesTests/Helper/JsonHelper.swift diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index 3f7ccb67..49120428 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -44,6 +44,7 @@ 7D35A48129720F0E0054954E /* MovieView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A48029720F0E0054954E /* MovieView.swift */; }; 7D35A4872972D6D40054954E /* MockURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A4862972D6D40054954E /* MockURLProtocol.swift */; }; 7D35A4892972D70E0054954E /* TrendingListViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A4882972D70E0054954E /* TrendingListViewModelSpec.swift */; }; + 7DBF51A429761D7D0084587B /* JsonHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DBF51A329761D7D0084587B /* JsonHelper.swift */; }; 7DF3DD092972DC32007BF9A3 /* trending-error.json in Resources */ = {isa = PBXBuildFile; fileRef = 7DF3DD082972DC32007BF9A3 /* trending-error.json */; }; 7DF3DD0B2972DC3B007BF9A3 /* trending-success.json in Resources */ = {isa = PBXBuildFile; fileRef = 7DF3DD0A2972DC3B007BF9A3 /* trending-success.json */; }; 7DF3DD0D2972DE25007BF9A3 /* TopRatedListViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD0C2972DE25007BF9A3 /* TopRatedListViewModelSpec.swift */; }; @@ -121,6 +122,7 @@ 7D35A48029720F0E0054954E /* MovieView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieView.swift; sourceTree = ""; }; 7D35A4862972D6D40054954E /* MockURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLProtocol.swift; sourceTree = ""; }; 7D35A4882972D70E0054954E /* TrendingListViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingListViewModelSpec.swift; sourceTree = ""; }; + 7DBF51A329761D7D0084587B /* JsonHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonHelper.swift; sourceTree = ""; }; 7DF3DD082972DC32007BF9A3 /* trending-error.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "trending-error.json"; sourceTree = ""; }; 7DF3DD0A2972DC3B007BF9A3 /* trending-success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "trending-success.json"; sourceTree = ""; }; 7DF3DD0C2972DE25007BF9A3 /* TopRatedListViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopRatedListViewModelSpec.swift; sourceTree = ""; }; @@ -204,6 +206,7 @@ 7D35A419296E22450054954E /* MyMoviesTests */ = { isa = PBXGroup; children = ( + 7DBF51A229761D710084587B /* Helper */, 7DF3DD212972E6EE007BF9A3 /* Coordinator */, 7DF3DD012972DB63007BF9A3 /* Model */, 7D35A4822972D5B00054954E /* Mocks */, @@ -348,6 +351,14 @@ path = Trending; sourceTree = ""; }; + 7DBF51A229761D710084587B /* Helper */ = { + isa = PBXGroup; + children = ( + 7DBF51A329761D7D0084587B /* JsonHelper.swift */, + ); + path = Helper; + sourceTree = ""; + }; 7DF3DD012972DB63007BF9A3 /* Model */ = { isa = PBXGroup; children = ( @@ -590,6 +601,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7DBF51A429761D7D0084587B /* JsonHelper.swift in Sources */, 7D35A4892972D70E0054954E /* TrendingListViewModelSpec.swift in Sources */, 7DF3DD0D2972DE25007BF9A3 /* TopRatedListViewModelSpec.swift in Sources */, 7DF3DD1B2972E23B007BF9A3 /* TrendingViewModelSpec.swift in Sources */, diff --git a/MyMoviesTests/Helper/JsonHelper.swift b/MyMoviesTests/Helper/JsonHelper.swift new file mode 100644 index 00000000..4ed8f986 --- /dev/null +++ b/MyMoviesTests/Helper/JsonHelper.swift @@ -0,0 +1,34 @@ +// +// JsonHelper.swift +// MyMoviesTests +// +// Created by Caio dos Santos Ambrosio on 1/16/23. +// + +import Foundation +@testable import MyMovies + +class JsonHelper { + func getMockData(for path: String) -> Data? { + guard let pathString = Bundle(for: type(of: self)).path(forResource: path, ofType: "json") else { + fatalError("Mock json not found") + } + + guard let jsonString = try? String(contentsOfFile: pathString, encoding: .utf8) else { + fatalError("Mock json can not be converted to String") + } + + return jsonString.data(using: .utf8) + } + + func getMockJson(for data: Data) -> TrendingResult { + let jsonDecoder = JSONDecoder() + jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase + do { + let body = try jsonDecoder.decode(TrendingResult.self, from: data) + return body + } catch { + fatalError("Error when decoding json") + } + } +} diff --git a/MyMoviesTests/Model/MovieViewModelSpec.swift b/MyMoviesTests/Model/MovieViewModelSpec.swift index d25da8a4..12b99916 100644 --- a/MyMoviesTests/Model/MovieViewModelSpec.swift +++ b/MyMoviesTests/Model/MovieViewModelSpec.swift @@ -23,20 +23,8 @@ final class MovieViewModelSpec: XCTestCase { expectation = expectation(description: "Expectation") } - private func getMockData(for path: String) -> Data? { - guard let pathString = Bundle(for: type(of: self)).path(forResource: path, ofType: "json") else { - fatalError("Mock json not found") - } - - guard let jsonString = try? String(contentsOfFile: pathString, encoding: .utf8) else { - fatalError("Mock json can not be converted to String") - } - - return jsonString.data(using: .utf8) - } - func testSuccessCall() { - let data = getMockData(for: "movie-success") + let data = JsonHelper().getMockData(for: "movie-success") MockURLProtocol.requestHandler = { request in guard let url = request.url, url == self.apiURL else { @@ -47,9 +35,9 @@ final class MovieViewModelSpec: XCTestCase { return (response, data) } - let viewModel = MovieViewModel(apiLoader: self.apiLoader) + let viewModel = MovieViewModel(apiLoader: self.apiLoader, movieId: movieId) - viewModel.fetchMovie(for: movieId) { viewModel, error in + viewModel.fetchMovie { viewModel, error in XCTAssertNotNil(viewModel) XCTAssertEqual(viewModel?.title, "The Godfather") XCTAssertEqual(viewModel?.imageUrl, URL(string: "https://image.tmdb.org/t/p/w185/3bhkrj58Vtu7enYsRolD1fZdja1.jpg?api_key=16094d8ca19f9c0407db3d0b5203bd21")) @@ -64,7 +52,7 @@ final class MovieViewModelSpec: XCTestCase { } func testErrorCall() { - let data = getMockData(for: "movie-error") + let data = JsonHelper().getMockData(for: "movie-error") MockURLProtocol.requestHandler = { request in guard let url = request.url, url == self.apiURL else { @@ -75,9 +63,9 @@ final class MovieViewModelSpec: XCTestCase { return (response, data) } - let viewModel = MovieViewModel(apiLoader: self.apiLoader) + let viewModel = MovieViewModel(apiLoader: self.apiLoader, movieId: movieId) - viewModel.fetchMovie(for: movieId) { viewModel, error in + viewModel.fetchMovie { viewModel, error in XCTAssertNotNil(error) XCTAssertEqual(error?.message, "Error when communicating with API") self.expectation.fulfill() diff --git a/MyMoviesTests/Model/TopRatedListViewModelSpec.swift b/MyMoviesTests/Model/TopRatedListViewModelSpec.swift index 33c671b5..0f5bce19 100644 --- a/MyMoviesTests/Model/TopRatedListViewModelSpec.swift +++ b/MyMoviesTests/Model/TopRatedListViewModelSpec.swift @@ -22,20 +22,8 @@ final class TopRatedListViewModelSpec: XCTestCase { expectation = expectation(description: "Expectation") } - private func getMockData(for path: String) -> Data? { - guard let pathString = Bundle(for: type(of: self)).path(forResource: path, ofType: "json") else { - fatalError("Mock json not found") - } - - guard let jsonString = try? String(contentsOfFile: pathString, encoding: .utf8) else { - fatalError("Mock json can not be converted to String") - } - - return jsonString.data(using: .utf8) - } - func testSuccessCall() { - let data = getMockData(for: "top-rated-success") + let data = JsonHelper().getMockData(for: "top-rated-success") MockURLProtocol.requestHandler = { request in guard let url = request.url, url == self.apiURL else { @@ -59,7 +47,7 @@ final class TopRatedListViewModelSpec: XCTestCase { } func testErrorCall() { - let data = getMockData(for: "top-rated-error") + let data = JsonHelper().getMockData(for: "top-rated-error") MockURLProtocol.requestHandler = { request in guard let url = request.url, url == self.apiURL else { diff --git a/MyMoviesTests/Model/TrendingListViewModelSpec.swift b/MyMoviesTests/Model/TrendingListViewModelSpec.swift index c40b88e3..a11e451a 100644 --- a/MyMoviesTests/Model/TrendingListViewModelSpec.swift +++ b/MyMoviesTests/Model/TrendingListViewModelSpec.swift @@ -22,20 +22,8 @@ final class TrendingListViewModelSpec: XCTestCase { expectation = expectation(description: "Expectation") } - private func getMockData(for path: String) -> Data? { - guard let pathString = Bundle(for: type(of: self)).path(forResource: path, ofType: "json") else { - fatalError("Mock json not found") - } - - guard let jsonString = try? String(contentsOfFile: pathString, encoding: .utf8) else { - fatalError("Mock json can not be converted to String") - } - - return jsonString.data(using: .utf8) - } - func testSuccessCall() { - let data = getMockData(for: "trending-success") + let data = JsonHelper().getMockData(for: "trending-success") MockURLProtocol.requestHandler = { request in guard let url = request.url, url == self.apiURL else { @@ -59,7 +47,7 @@ final class TrendingListViewModelSpec: XCTestCase { } func testErrorCall() { - let data = getMockData(for: "trending-error") + let data = JsonHelper().getMockData(for: "trending-error") MockURLProtocol.requestHandler = { request in guard let url = request.url, url == self.apiURL else { diff --git a/MyMoviesTests/Model/TrendingViewModelSpec.swift b/MyMoviesTests/Model/TrendingViewModelSpec.swift index a75cefab..2af45411 100644 --- a/MyMoviesTests/Model/TrendingViewModelSpec.swift +++ b/MyMoviesTests/Model/TrendingViewModelSpec.swift @@ -9,36 +9,12 @@ import XCTest @testable import MyMovies final class TrendingViewModelSpec: XCTestCase { - - private func getMockData(for path: String) -> Data? { - guard let pathString = Bundle(for: type(of: self)).path(forResource: path, ofType: "json") else { - fatalError("Mock json not found") - } - - guard let jsonString = try? String(contentsOfFile: pathString, encoding: .utf8) else { - fatalError("Mock json can not be converted to String") - } - - return jsonString.data(using: .utf8) - } - - private func getMockJson(for data: Data) -> TrendingResult { - let jsonDecoder = JSONDecoder() - jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase - do { - let body = try jsonDecoder.decode(TrendingResult.self, from: data) - return body - } catch { - fatalError("Error when decoding json") - } - } - func testSuccessJson() { - guard let data = getMockData(for: "trending-result") else { + guard let data = JsonHelper().getMockData(for: "trending-result") else { fatalError("Mock json can not be converted to Data") } - let trending = getMockJson(for: data) + let trending = JsonHelper().getMockJson(for: data) let viewModel = TrendingViewModel(trending: trending) XCTAssertEqual(viewModel.title, trending.originalTitle) @@ -47,11 +23,11 @@ final class TrendingViewModelSpec: XCTestCase { } func testNullableJson() { - guard let data = getMockData(for: "trending-result-nullable") else { + guard let data = JsonHelper().getMockData(for: "trending-result-nullable") else { fatalError("Mock json can not be converted to Data") } - let trending = getMockJson(for: data) + let trending = JsonHelper().getMockJson(for: data) let viewModel = TrendingViewModel(trending: trending) XCTAssertEqual(viewModel.title, "") From 9216529bc8d29b1ca92e7c4fc882055b96a06b85 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Mon, 16 Jan 2023 21:11:04 -0300 Subject: [PATCH 57/65] Fix warning --- MyMovies/Model/Favorite/FavoriteRepository.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MyMovies/Model/Favorite/FavoriteRepository.swift b/MyMovies/Model/Favorite/FavoriteRepository.swift index 15b83cb8..188a1b84 100644 --- a/MyMovies/Model/Favorite/FavoriteRepository.swift +++ b/MyMovies/Model/Favorite/FavoriteRepository.swift @@ -38,7 +38,7 @@ class FavoriteRepository { func deleteMovie(for movieId: Int64) { let movieIdString = String(movieId) - var keys: [String]? = readValue(for: Key.keys.rawValue) + let keys: [String]? = readValue(for: Key.keys.rawValue) if var keys { keys.removeAll(where: { $0 == movieIdString }) if keys.count > 0 { From 38dbf70db846aa09a778400fb9c18bd2dcd247e9 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Mon, 16 Jan 2023 21:15:43 -0300 Subject: [PATCH 58/65] Refact get all favorites to use compactMap --- .../Model/Favorite/FavoriteRepository.swift | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/MyMovies/Model/Favorite/FavoriteRepository.swift b/MyMovies/Model/Favorite/FavoriteRepository.swift index 188a1b84..079d5f97 100644 --- a/MyMovies/Model/Favorite/FavoriteRepository.swift +++ b/MyMovies/Model/Favorite/FavoriteRepository.swift @@ -54,24 +54,19 @@ class FavoriteRepository { func getAllFavorites() -> [TrendingViewModel] { let favorites: [String]? = readValue(for: Key.keys.rawValue) - guard let favorites = favorites else { + guard let favorites else { return [] } - var result: [TrendingViewModel] = [] - - - favorites.forEach { movieId in - guard let data: String = readValue(for: String(movieId)) else { return } + return favorites.compactMap { movieId in + guard let data: String = self.readValue(for: movieId) else { return nil } do { - let trendingViewModel = try decode(Data(data.utf8)) - result.append(trendingViewModel) + let trendingViewModel = try self.decode(Data(data.utf8)) + return trendingViewModel } catch { - return + return nil } } - - return result } private func encode(_ value: TrendingViewModel) -> String { From 23abebadf2ebc538ab142d9d627d4d62d4c546ab Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Mon, 16 Jan 2023 21:25:58 -0300 Subject: [PATCH 59/65] Fix MainCoordinatorSpec to actually test MainCoordinator logic --- MyMovies.xcodeproj/project.pbxproj | 8 +++--- .../Coordinator/MainCoordinatorSpec.swift | 25 ++++++++++++----- MyMoviesTests/Mocks/MainCoordinatorMock.swift | 28 ------------------- .../Mocks/MockUINavigationController.swift | 26 +++++++++++++++++ 4 files changed, 48 insertions(+), 39 deletions(-) delete mode 100644 MyMoviesTests/Mocks/MainCoordinatorMock.swift create mode 100644 MyMoviesTests/Mocks/MockUINavigationController.swift diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index 49120428..a830a3e9 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -57,7 +57,7 @@ 7DF3DD1E2972E3A7007BF9A3 /* trending-result.json in Resources */ = {isa = PBXBuildFile; fileRef = 7DF3DD1D2972E3A7007BF9A3 /* trending-result.json */; }; 7DF3DD202972E641007BF9A3 /* trending-result-nullable.json in Resources */ = {isa = PBXBuildFile; fileRef = 7DF3DD1F2972E641007BF9A3 /* trending-result-nullable.json */; }; 7DF3DD232972E725007BF9A3 /* MainCoordinatorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD222972E725007BF9A3 /* MainCoordinatorSpec.swift */; }; - 7DF3DD252972E7AC007BF9A3 /* MainCoordinatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD242972E7AC007BF9A3 /* MainCoordinatorMock.swift */; }; + 7DF3DD252972E7AC007BF9A3 /* MockUINavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD242972E7AC007BF9A3 /* MockUINavigationController.swift */; }; 7DF3DD282972F5A4007BF9A3 /* FavoriteRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD272972F5A4007BF9A3 /* FavoriteRepository.swift */; }; 7DF3DD2B29730F28007BF9A3 /* FavoriteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD2A29730F28007BF9A3 /* FavoriteViewModel.swift */; }; 7DF3DD2E29731AAD007BF9A3 /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DF3DD2D29731AAD007BF9A3 /* Button.swift */; }; @@ -135,7 +135,7 @@ 7DF3DD1D2972E3A7007BF9A3 /* trending-result.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "trending-result.json"; sourceTree = ""; }; 7DF3DD1F2972E641007BF9A3 /* trending-result-nullable.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "trending-result-nullable.json"; sourceTree = ""; }; 7DF3DD222972E725007BF9A3 /* MainCoordinatorSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCoordinatorSpec.swift; sourceTree = ""; }; - 7DF3DD242972E7AC007BF9A3 /* MainCoordinatorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCoordinatorMock.swift; sourceTree = ""; }; + 7DF3DD242972E7AC007BF9A3 /* MockUINavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUINavigationController.swift; sourceTree = ""; }; 7DF3DD272972F5A4007BF9A3 /* FavoriteRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteRepository.swift; sourceTree = ""; }; 7DF3DD2A29730F28007BF9A3 /* FavoriteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteViewModel.swift; sourceTree = ""; }; 7DF3DD2D29731AAD007BF9A3 /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = ""; }; @@ -337,7 +337,7 @@ 7DF3DD0E2972DEDE007BF9A3 /* TopRated */, 7D35A4832972D5BE0054954E /* Trending */, 7D35A4862972D6D40054954E /* MockURLProtocol.swift */, - 7DF3DD242972E7AC007BF9A3 /* MainCoordinatorMock.swift */, + 7DF3DD242972E7AC007BF9A3 /* MockUINavigationController.swift */, ); path = Mocks; sourceTree = ""; @@ -608,7 +608,7 @@ 7DF3DD142972DF75007BF9A3 /* MovieViewModelSpec.swift in Sources */, 7DF3DD232972E725007BF9A3 /* MainCoordinatorSpec.swift in Sources */, 7D35A4872972D6D40054954E /* MockURLProtocol.swift in Sources */, - 7DF3DD252972E7AC007BF9A3 /* MainCoordinatorMock.swift in Sources */, + 7DF3DD252972E7AC007BF9A3 /* MockUINavigationController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MyMoviesTests/Coordinator/MainCoordinatorSpec.swift b/MyMoviesTests/Coordinator/MainCoordinatorSpec.swift index 03af68a2..08a1f666 100644 --- a/MyMoviesTests/Coordinator/MainCoordinatorSpec.swift +++ b/MyMoviesTests/Coordinator/MainCoordinatorSpec.swift @@ -9,24 +9,35 @@ import XCTest @testable import MyMovies final class MainCoordinatorSpec: XCTestCase { - private var mainCoordinator: MainCoordinatorMock! + private var mainCoordinator: MainCoordinator! + private var navigationController: MockUINavigationController! override func setUp() { super.setUp() - let navigationController = UINavigationController(rootViewController: UIViewController()) - mainCoordinator = MainCoordinatorMock(navigationController: navigationController) + navigationController = MockUINavigationController() + mainCoordinator = MainCoordinator(navigationController: navigationController) } func testMainCoordinatorStart() { mainCoordinator.start() - XCTAssertTrue(mainCoordinator.startCalled) + XCTAssertTrue(navigationController.setViewControllersCalled) + XCTAssertTrue(navigationController.controllers[0] is HomeViewController) } - func testEventOccurred() { - mainCoordinator.eventOccurred(with: .movieClicked, parameters: nil) + func testEventMovieClicked() { + let movieId: Int64 = 238 + mainCoordinator.eventOccurred(with: .movieClicked, parameters: movieId) - XCTAssertTrue(mainCoordinator.eventOccurredCalled) + XCTAssertTrue(navigationController.pushViewControllerCalled) + XCTAssertTrue(navigationController.controllers[0] is MovieViewController) + } + + func testEventFavoritesClicked() { + mainCoordinator.eventOccurred(with: .favoriteClicked, parameters: nil) + + XCTAssertTrue(navigationController.pushViewControllerCalled) + XCTAssertTrue(navigationController.controllers[0] is TrendingViewController) } } diff --git a/MyMoviesTests/Mocks/MainCoordinatorMock.swift b/MyMoviesTests/Mocks/MainCoordinatorMock.swift deleted file mode 100644 index 75044c89..00000000 --- a/MyMoviesTests/Mocks/MainCoordinatorMock.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// UINavigationControllerMock.swift -// MyMoviesTests -// -// Created by Caio dos Santos Ambrosio on 1/14/23. -// - -import UIKit -@testable import MyMovies - -final class MainCoordinatorMock: Coordinator { - var startCalled = false - var eventOccurredCalled = false - - var navigationController: UINavigationController - - init(navigationController: UINavigationController) { - self.navigationController = navigationController - } - - func eventOccurred(with type: Event, parameters params: Any?) { - self.eventOccurredCalled = true - } - - func start() { - self.startCalled = true - } -} diff --git a/MyMoviesTests/Mocks/MockUINavigationController.swift b/MyMoviesTests/Mocks/MockUINavigationController.swift new file mode 100644 index 00000000..f0d9f53a --- /dev/null +++ b/MyMoviesTests/Mocks/MockUINavigationController.swift @@ -0,0 +1,26 @@ +// +// UINavigationControllerMock.swift +// MyMoviesTests +// +// Created by Caio dos Santos Ambrosio on 1/14/23. +// + +import UIKit +@testable import MyMovies + +final class MockUINavigationController: UINavigationController { + var controllers: [UIViewController] = [] + + var setViewControllersCalled = false + var pushViewControllerCalled = false + + override func setViewControllers(_ viewControllers: [UIViewController], animated: Bool) { + controllers = viewControllers + setViewControllersCalled = true + } + + override func pushViewController(_ viewController: UIViewController, animated: Bool) { + controllers.insert(viewController, at: 0) + pushViewControllerCalled = true + } +} From 5deb073b8cd2de62ec80901cdba0a3398fc9f39a Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Mon, 16 Jan 2023 21:29:45 -0300 Subject: [PATCH 60/65] Remove not used variables from TrendingReulst and Movie --- MyMovies/Model/DTO/Movie.swift | 18 ------------------ MyMovies/Model/DTO/TrendingResult.swift | 12 ------------ 2 files changed, 30 deletions(-) diff --git a/MyMovies/Model/DTO/Movie.swift b/MyMovies/Model/DTO/Movie.swift index 625e6255..8ac384c2 100644 --- a/MyMovies/Model/DTO/Movie.swift +++ b/MyMovies/Model/DTO/Movie.swift @@ -8,34 +8,16 @@ import Foundation struct Movie: Decodable { - let adult: Bool - let backdropPath: String? - let belongsToCollection: MovieCollection? - let budget: Int64 let genres: [Genre] - let homepage: String? - let id: Int64 let originalTitle: String? let overview: String? - let popularity: Double let posterPath: String? let releaseDate: String? - let revenue: Int64 - let runtime: Int? - let status: String let title: String? let voteAverage: Double let voteCount: Int64 } -struct MovieCollection: Decodable { - let id: Int64? - let name: String? - let posterPath: String? - let backdropPath: String? -} - struct Genre: Decodable { - let id: Int let name: String } diff --git a/MyMovies/Model/DTO/TrendingResult.swift b/MyMovies/Model/DTO/TrendingResult.swift index 091d2879..8b2471e7 100644 --- a/MyMovies/Model/DTO/TrendingResult.swift +++ b/MyMovies/Model/DTO/TrendingResult.swift @@ -9,19 +9,7 @@ import Foundation struct TrendingResult: Codable { let posterPath: String? - let adult: Bool? - let overview: String? - let releaseDate: String? - let genreId: [Int]? let id: Int64? let originalTitle: String? - let originalName: String? - let originalLanguage: String? - let title: String? let name: String? - let backdropPath: String? - let popularity: Double? - let voteCount: Int64? - let video: Bool? - let voteAverage: Double? } From 469d1194aaf68bfb118e62885fcdfebb3faa936f Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Mon, 16 Jan 2023 21:55:19 -0300 Subject: [PATCH 61/65] Write testes for FavoriteRepository and remove UI Example tests --- MyMovies.xcodeproj/project.pbxproj | 20 + MyMoviesTests/Helper/JsonHelper.swift | 4 +- MyMoviesTests/Mocks/Favorite/favorite.json | 418 ++++++++++++++++++ MyMoviesTests/Mocks/MockUserDefaults.swift | 26 ++ .../Model/FavoriteRepositorySpec.swift | 58 +++ .../Model/TrendingViewModelSpec.swift | 4 +- MyMoviesUITests/MyMoviesUITests.swift | 17 - .../MyMoviesUITestsLaunchTests.swift | 13 - 8 files changed, 526 insertions(+), 34 deletions(-) create mode 100644 MyMoviesTests/Mocks/Favorite/favorite.json create mode 100644 MyMoviesTests/Mocks/MockUserDefaults.swift create mode 100644 MyMoviesTests/Model/FavoriteRepositorySpec.swift diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index a830a3e9..35eb05b9 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 7D0D8686297623AE00BCD50D /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0D8685297623AE00BCD50D /* MockUserDefaults.swift */; }; + 7D0D8688297624D300BCD50D /* FavoriteRepositorySpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0D8687297624D300BCD50D /* FavoriteRepositorySpec.swift */; }; + 7D0D868B2976260200BCD50D /* favorite.json in Resources */ = {isa = PBXBuildFile; fileRef = 7D0D868A2976260200BCD50D /* favorite.json */; }; 7D35A404296E22430054954E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A403296E22430054954E /* AppDelegate.swift */; }; 7D35A406296E22430054954E /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A405296E22430054954E /* SceneDelegate.swift */; }; 7D35A40D296E22450054954E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7D35A40C296E22450054954E /* Assets.xcassets */; }; @@ -81,6 +84,9 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 7D0D8685297623AE00BCD50D /* MockUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserDefaults.swift; sourceTree = ""; }; + 7D0D8687297624D300BCD50D /* FavoriteRepositorySpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteRepositorySpec.swift; sourceTree = ""; }; + 7D0D868A2976260200BCD50D /* favorite.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = favorite.json; sourceTree = ""; }; 7D35A400296E22430054954E /* MyMovies.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MyMovies.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7D35A403296E22430054954E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7D35A405296E22430054954E /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -166,6 +172,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 7D0D8689297625C600BCD50D /* Favorite */ = { + isa = PBXGroup; + children = ( + 7D0D868A2976260200BCD50D /* favorite.json */, + ); + path = Favorite; + sourceTree = ""; + }; 7D35A3F7296E22430054954E = { isa = PBXGroup; children = ( @@ -332,12 +346,14 @@ 7D35A4822972D5B00054954E /* Mocks */ = { isa = PBXGroup; children = ( + 7D0D8689297625C600BCD50D /* Favorite */, 7DF3DD1C2972E36E007BF9A3 /* TrendingResult */, 7DF3DD152972E0F6007BF9A3 /* Movie */, 7DF3DD0E2972DEDE007BF9A3 /* TopRated */, 7D35A4832972D5BE0054954E /* Trending */, 7D35A4862972D6D40054954E /* MockURLProtocol.swift */, 7DF3DD242972E7AC007BF9A3 /* MockUINavigationController.swift */, + 7D0D8685297623AE00BCD50D /* MockUserDefaults.swift */, ); path = Mocks; sourceTree = ""; @@ -366,6 +382,7 @@ 7DF3DD0C2972DE25007BF9A3 /* TopRatedListViewModelSpec.swift */, 7DF3DD132972DF75007BF9A3 /* MovieViewModelSpec.swift */, 7DF3DD1A2972E23B007BF9A3 /* TrendingViewModelSpec.swift */, + 7D0D8687297624D300BCD50D /* FavoriteRepositorySpec.swift */, ); path = Model; sourceTree = ""; @@ -536,6 +553,7 @@ buildActionMask = 2147483647; files = ( 7DF3DD202972E641007BF9A3 /* trending-result-nullable.json in Resources */, + 7D0D868B2976260200BCD50D /* favorite.json in Resources */, 7DF3DD0B2972DC3B007BF9A3 /* trending-success.json in Resources */, 7DF3DD102972DEED007BF9A3 /* top-rated-success.json in Resources */, 7DF3DD1E2972E3A7007BF9A3 /* trending-result.json in Resources */, @@ -606,6 +624,8 @@ 7DF3DD0D2972DE25007BF9A3 /* TopRatedListViewModelSpec.swift in Sources */, 7DF3DD1B2972E23B007BF9A3 /* TrendingViewModelSpec.swift in Sources */, 7DF3DD142972DF75007BF9A3 /* MovieViewModelSpec.swift in Sources */, + 7D0D8686297623AE00BCD50D /* MockUserDefaults.swift in Sources */, + 7D0D8688297624D300BCD50D /* FavoriteRepositorySpec.swift in Sources */, 7DF3DD232972E725007BF9A3 /* MainCoordinatorSpec.swift in Sources */, 7D35A4872972D6D40054954E /* MockURLProtocol.swift in Sources */, 7DF3DD252972E7AC007BF9A3 /* MockUINavigationController.swift in Sources */, diff --git a/MyMoviesTests/Helper/JsonHelper.swift b/MyMoviesTests/Helper/JsonHelper.swift index 4ed8f986..ae489f84 100644 --- a/MyMoviesTests/Helper/JsonHelper.swift +++ b/MyMoviesTests/Helper/JsonHelper.swift @@ -21,11 +21,11 @@ class JsonHelper { return jsonString.data(using: .utf8) } - func getMockJson(for data: Data) -> TrendingResult { + func getMockJson(for data: Data) -> T { let jsonDecoder = JSONDecoder() jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase do { - let body = try jsonDecoder.decode(TrendingResult.self, from: data) + let body = try jsonDecoder.decode(T.self, from: data) return body } catch { fatalError("Error when decoding json") diff --git a/MyMoviesTests/Mocks/Favorite/favorite.json b/MyMoviesTests/Mocks/Favorite/favorite.json new file mode 100644 index 00000000..cc3f207d --- /dev/null +++ b/MyMoviesTests/Mocks/Favorite/favorite.json @@ -0,0 +1,418 @@ +[ + { + "adult": false, + "backdrop_path": "/r9PkFnRUIthgBp2JZZzD380MWZy.jpg", + "id": 315162, + "title": "Puss in Boots: The Last Wish", + "original_language": "en", + "original_title": "Puss in Boots: The Last Wish", + "overview": "Puss in Boots discovers that his passion for adventure has taken its toll: He has burned through eight of his nine lives, leaving him with only one life left. Puss sets out on an epic journey to find the mythical Last Wish and restore his nine lives.", + "poster_path": "/1NqwE6LP9IEdOZ57NCT51ftHtWT.jpg", + "media_type": "movie", + "genre_ids": [ + 16, + 28, + 12, + 35, + 10751, + 14 + ], + "popularity": 9724.995, + "release_date": "2022-12-21", + "video": false, + "vote_average": 8.6, + "vote_count": 1400 + }, + { + "adult": false, + "backdrop_path": "/ng9a2oVoZuohU0Mzls9GAVH0MCn.jpg", + "id": 858408, + "title": "Dog Gone", + "original_language": "en", + "original_title": "Dog Gone", + "overview": "When his beloved dog goes missing, a young man embarks on an incredible search with his parents to find him and give him life-saving medication.", + "poster_path": "/eYWdLZCS9W1Xli9bynzFSddLx02.jpg", + "media_type": "movie", + "genre_ids": [ + 10751, + 12, + 18 + ], + "popularity": 51.754, + "release_date": "2023-01-13", + "video": false, + "vote_average": 6.7, + "vote_count": 6 + }, + { + "adult": false, + "backdrop_path": "/s16H6tpK2utvwDtzZ8Qy4qm5Emw.jpg", + "id": 76600, + "title": "Avatar: The Way of Water", + "original_language": "en", + "original_title": "Avatar: The Way of Water", + "overview": "Set more than a decade after the events of the first film, learn the story of the Sully family (Jake, Neytiri, and their kids), the trouble that follows them, the lengths they go to keep each other safe, the battles they fight to stay alive, and the tragedies they endure.", + "poster_path": "/t6HIqrRAclMCA60NsSmeqe9RmNV.jpg", + "media_type": "movie", + "genre_ids": [ + 878, + 12, + 28 + ], + "popularity": 3003.545, + "release_date": "2022-12-16", + "video": false, + "vote_average": 7.7, + "vote_count": 4094 + }, + { + "adult": false, + "backdrop_path": "/lacnOoTHyS8g0eSu7cyQ8TgdGtv.jpg", + "id": 872954, + "title": "The Old Way", + "original_language": "en", + "original_title": "The Old Way", + "overview": "An old gunslinger and his daughter must face the consequences of his past, when the son of a man he murdered years ago arrives to take his revenge.", + "poster_path": "/8HCCYAIocXxMKn7J9yQfDX1vBM5.jpg", + "media_type": "movie", + "genre_ids": [ + 28, + 37 + ], + "popularity": 24.751, + "release_date": "2023-01-06", + "video": false, + "vote_average": 5.375, + "vote_count": 8 + }, + { + "adult": false, + "backdrop_path": "/5kAGbi9MFAobQTVfK4kWPnIfnP0.jpg", + "id": 536554, + "title": "M3GAN", + "original_language": "en", + "original_title": "M3GAN", + "overview": "A brilliant toy company roboticist uses artificial intelligence to develop M3GAN, a life-like doll programmed to emotionally bond with her newly orphaned niece. But when the doll's programming works too well, she becomes overprotective of her new friend with terrifying results.", + "poster_path": "/7CNCv9uhqdwK7Fv4bR4nmDysnd9.jpg", + "media_type": "movie", + "genre_ids": [ + 878, + 27, + 35 + ], + "popularity": 2067, + "release_date": "2023-01-06", + "video": false, + "vote_average": 7.208, + "vote_count": 209 + }, + { + "adult": false, + "backdrop_path": "/b08BDQPq42AoLMfhi7DtTOoYqVu.jpg", + "id": 800815, + "title": "The Pale Blue Eye", + "original_language": "en", + "original_title": "The Pale Blue Eye", + "overview": "West Point, New York, 1830. When a cadet at the burgeoning military academy is found hanged with his heart cut out, the top brass summons former New York City constable Augustus Landor to investigate. While attempting to solve this grisly mystery, the reluctant detective engages the help of one of the cadets: a strange but brilliant young fellow by the name of Edgar Allan Poe", + "poster_path": "/9xkGlFRqrN8btTLU0KQvOfn2PHr.jpg", + "media_type": "movie", + "genre_ids": [ + 9648, + 53, + 27, + 80 + ], + "popularity": 637.882, + "release_date": "2022-12-23", + "video": false, + "vote_average": 7.138, + "vote_count": 508 + }, + { + "adult": false, + "backdrop_path": "/mSyQoValhBsJdq3JNGXJww2Q5yL.jpg", + "id": 593643, + "title": "The Menu", + "original_language": "en", + "original_title": "The Menu", + "overview": "A young couple travels to a remote island to eat at an exclusive restaurant where the chef has prepared a lavish menu, with some shocking surprises.", + "poster_path": "/fPtUgMcLIboqlTlPrq0bQpKK8eq.jpg", + "media_type": "movie", + "genre_ids": [ + 35, + 27, + 53 + ], + "popularity": 500.984, + "release_date": "2022-11-18", + "video": false, + "vote_average": 7.306, + "vote_count": 1335 + }, + { + "adult": false, + "backdrop_path": "/e1cC9muSRtAHVtF5GJtKAfATYIT.jpg", + "id": 1063242, + "title": "Disconnect: The Wedding Planner", + "original_language": "fr", + "original_title": "Disconnect: The Wedding Planner", + "overview": "After falling victim to a scam, a desperate man races the clock as he attempts to plan a luxurious destination wedding for an important investor.", + "poster_path": "/tGmCxGkVMOqig2TrbXAsE9dOVvX.jpg", + "media_type": "movie", + "genre_ids": [ + 10749, + 35 + ], + "popularity": 40.16, + "release_date": "2023-01-13", + "video": false, + "vote_average": 2, + "vote_count": 1 + }, + { + "adult": false, + "backdrop_path": "/bTqmsKOr9cSdYptFhbFEOmP3qCm.jpg", + "id": 640146, + "title": "Ant-Man and the Wasp: Quantumania", + "original_language": "en", + "original_title": "Ant-Man and the Wasp: Quantumania", + "overview": "Super-Hero partners Scott Lang and Hope van Dyne, along with with Hope's parents Janet van Dyne and Hank Pym, and Scott's daughter Cassie Lang, find themselves exploring the Quantum Realm, interacting with strange new creatures and embarking on an adventure that will push them beyond the limits of what they thought possible.", + "poster_path": "/ynWwsafy7ib7hitxImQ8Lso3lvI.jpg", + "media_type": "movie", + "genre_ids": [ + 12, + 878, + 35 + ], + "popularity": 156.416, + "release_date": "2023-02-17", + "video": false, + "vote_average": 0, + "vote_count": 0 + }, + { + "adult": false, + "backdrop_path": "/5pMy5LF2JAleBNBtuzizfCMWM7k.jpg", + "id": 653851, + "title": "Devotion", + "original_language": "en", + "original_title": "Devotion", + "overview": "The harrowing true story of two elite US Navy fighter pilots during the Korean War. Their heroic sacrifices would ultimately make them the Navy's most celebrated wingmen.", + "poster_path": "/26yQPXymbWeCLKwcmyL8dRjAzth.jpg", + "media_type": "movie", + "genre_ids": [ + 10752, + 36, + 18 + ], + "popularity": 207.761, + "release_date": "2022-11-23", + "video": false, + "vote_average": 6.653, + "vote_count": 49 + }, + { + "adult": false, + "backdrop_path": "/aROq5hpE1WgDMjwIxxmk42kFFQZ.jpg", + "id": 829410, + "title": "Sick", + "original_language": "en", + "original_title": "Sick", + "overview": "In the midst of the pandemic, college student Parker and her best friend Miri decide to self-quarantine at her family's lake house, where they will be alone — or so they think...", + "poster_path": "/yisnA4KdZOfJeDve3DekgUpeCe9.jpg", + "media_type": "movie", + "genre_ids": [ + 27, + 53 + ], + "popularity": 15.085, + "release_date": "2023-01-13", + "video": false, + "vote_average": 6.4, + "vote_count": 9 + }, + { + "adult": false, + "backdrop_path": "/dKqa850uvbNSCaQCV4Im1XlzEtQ.jpg", + "id": 661374, + "title": "Glass Onion: A Knives Out Mystery", + "original_language": "en", + "original_title": "Glass Onion: A Knives Out Mystery", + "overview": "World-famous detective Benoit Blanc heads to Greece to peel back the layers of a mystery surrounding a tech billionaire and his eclectic crew of friends.", + "poster_path": "/vDGr1YdrlfbU9wxTOdpf3zChmv9.jpg", + "media_type": "movie", + "genre_ids": [ + 9648, + 53, + 35 + ], + "popularity": 1656.736, + "release_date": "2022-11-23", + "video": false, + "vote_average": 7.072, + "vote_count": 2576 + }, + { + "adult": false, + "backdrop_path": "/tt79dbOPd9Z9ykEOpvckttgYXwH.jpg", + "id": 545611, + "title": "Everything Everywhere All at Once", + "original_language": "en", + "original_title": "Everything Everywhere All at Once", + "overview": "An aging Chinese immigrant is swept up in an insane adventure, where she alone can save what's important to her by connecting with the lives she could have led in other universes.", + "poster_path": "/w3LxiVYdWWRvEVdn5RYq6jIqkb1.jpg", + "media_type": "movie", + "genre_ids": [ + 28, + 12, + 878 + ], + "popularity": 224.437, + "release_date": "2022-03-24", + "video": false, + "vote_average": 8.067, + "vote_count": 2648 + }, + { + "adult": false, + "backdrop_path": "/bQXAqRx2Fgc46uCVWgoPz5L5Dtr.jpg", + "id": 436270, + "title": "Black Adam", + "original_language": "en", + "original_title": "Black Adam", + "overview": "Nearly 5,000 years after he was bestowed with the almighty powers of the Egyptian gods—and imprisoned just as quickly—Black Adam is freed from his earthly tomb, ready to unleash his unique form of justice on the modern world.", + "poster_path": "/pFlaoHTZeyNkG83vxsAJiGzfSsa.jpg", + "media_type": "movie", + "genre_ids": [ + 14, + 28, + 878 + ], + "popularity": 1876.042, + "release_date": "2022-10-21", + "video": false, + "vote_average": 7.218, + "vote_count": 3656 + }, + { + "adult": false, + "backdrop_path": "/iBEs4PnQLkL4BbsAGvsuBgeTCM3.jpg", + "id": 960519, + "title": "Noise", + "original_language": "es", + "original_title": "Ruido", + "overview": "Julia is a mother, or rather, one of many mothers, sisters, daughters, colleagues, who have had their lives torn apart by the widespread violence in a country waging a war against its women. Julia is searching for Ger, her daughter. And in her search, she will weave through the stories and struggles of the different women she will meet.", + "poster_path": "/6GemI78Ge0ABs5qM6CnvCPduKEU.jpg", + "media_type": "movie", + "genre_ids": [ + 18 + ], + "popularity": 122.966, + "release_date": "2022-10-13", + "video": false, + "vote_average": 7.429, + "vote_count": 7 + }, + { + "adult": false, + "backdrop_path": "/8I37NtDffNV7AZlDa7uDvvqhovU.jpg", + "id": 19995, + "title": "Avatar", + "original_language": "en", + "original_title": "Avatar", + "overview": "In the 22nd century, a paraplegic Marine is dispatched to the moon Pandora on a unique mission, but becomes torn between following orders and protecting an alien civilization.", + "poster_path": "/jRXYjXNq0Cs2TcJjLkki24MLp7u.jpg", + "media_type": "movie", + "genre_ids": [ + 28, + 12, + 14, + 878 + ], + "popularity": 1552.689, + "release_date": "2009-12-15", + "video": false, + "vote_average": 7.561, + "vote_count": 27754 + }, + { + "adult": false, + "backdrop_path": "/AaV1YIdWKnjAIAOe8UUKBFm327v.jpg", + "id": 361743, + "title": "Top Gun: Maverick", + "original_language": "en", + "original_title": "Top Gun: Maverick", + "overview": "After more than thirty years of service as one of the Navy’s top aviators, and dodging the advancement in rank that would ground him, Pete “Maverick” Mitchell finds himself training a detachment of TOP GUN graduates for a specialized mission the likes of which no living pilot has ever seen.", + "poster_path": "/62HCnUTziyWcpDaBO2i1DX17ljH.jpg", + "media_type": "movie", + "genre_ids": [ + 28, + 18 + ], + "popularity": 519.672, + "release_date": "2022-05-27", + "video": false, + "vote_average": 8.3, + "vote_count": 5597 + }, + { + "adult": false, + "backdrop_path": "/yYrvN5WFeGYjJnRzhY0QXuo4Isw.jpg", + "id": 505642, + "title": "Black Panther: Wakanda Forever", + "original_language": "en", + "original_title": "Black Panther: Wakanda Forever", + "overview": "Queen Ramonda, Shuri, M’Baku, Okoye and the Dora Milaje fight to protect their nation from intervening world powers in the wake of King T’Challa’s death. As the Wakandans strive to embrace their next chapter, the heroes must band together with the help of War Dog Nakia and Everett Ross and forge a new path for the kingdom of Wakanda.", + "poster_path": "/sv1xJUazXeYqALzczSZ3O6nkH75.jpg", + "media_type": "movie", + "genre_ids": [ + 28, + 12, + 878 + ], + "popularity": 1180.588, + "release_date": "2022-11-11", + "video": false, + "vote_average": 7.47, + "vote_count": 1604 + }, + { + "adult": false, + "backdrop_path": "/9Md4CqzUGDtK5oEkRRvozLkGc9d.jpg", + "id": 674324, + "title": "The Banshees of Inisherin", + "original_language": "en", + "original_title": "The Banshees of Inisherin", + "overview": "Two lifelong friends find themselves at an impasse when one abruptly ends their relationship, with alarming consequences for both of them.", + "poster_path": "/dpeR3ue0Q93Rk8jhE2DzVPMRGIm.jpg", + "media_type": "movie", + "genre_ids": [ + 18, + 35 + ], + "popularity": 203.388, + "release_date": "2022-10-21", + "video": false, + "vote_average": 7.474, + "vote_count": 463 + }, + { + "adult": false, + "backdrop_path": "/6RCf9jzKxyjblYV4CseayK6bcJo.jpg", + "id": 804095, + "title": "The Fabelmans", + "original_language": "en", + "original_title": "The Fabelmans", + "overview": "Growing up in post-World War II era Arizona, young Sammy Fabelman aspires to become a filmmaker as he reaches adolescence, but soon discovers a shattering family secret and explores how the power of films can help him see the truth.", + "poster_path": "/xId9zoiv5WPQOuxqykUDrlpmrUz.jpg", + "media_type": "movie", + "genre_ids": [ + 18 + ], + "popularity": 161.117, + "release_date": "2022-11-11", + "video": false, + "vote_average": 8.018, + "vote_count": 420 + } +] \ No newline at end of file diff --git a/MyMoviesTests/Mocks/MockUserDefaults.swift b/MyMoviesTests/Mocks/MockUserDefaults.swift new file mode 100644 index 00000000..b5e9f29c --- /dev/null +++ b/MyMoviesTests/Mocks/MockUserDefaults.swift @@ -0,0 +1,26 @@ +// +// MockUserDefaults.swift +// MyMoviesTests +// +// Created by Caio dos Santos Ambrosio on 1/16/23. +// + +import Foundation +@testable import MyMovies + +class MockUserDefaults: UserDefaults { + + var persisted: [String : Any] = [:] + + override func set(_ value: Any?, forKey key: String) { + persisted[key] = value + } + + override func value(forKey key: String) -> Any? { + return persisted[key] as Any? + } + + override func removeObject(forKey defaultName: String) { + persisted[defaultName] = nil + } +} diff --git a/MyMoviesTests/Model/FavoriteRepositorySpec.swift b/MyMoviesTests/Model/FavoriteRepositorySpec.swift new file mode 100644 index 00000000..fbc1cfd9 --- /dev/null +++ b/MyMoviesTests/Model/FavoriteRepositorySpec.swift @@ -0,0 +1,58 @@ +// +// FavoriteRepositorySpec.swift +// MyMoviesTests +// +// Created by Caio dos Santos Ambrosio on 1/16/23. +// + +import XCTest +@testable import MyMovies + +final class FavoriteRepositorySpec: XCTestCase { + var favoritesRepository: FavoriteRepository! + var userDefaults: MockUserDefaults! + + override func setUp() { + userDefaults = MockUserDefaults() + favoritesRepository = FavoriteRepository(userDefaults: userDefaults) + } + + func testInsertOrUpdateMovie() { + guard let data = JsonHelper().getMockData(for: "favorite") else { + fatalError("Mock json can not be converted to Data") + } + + let trendings: [TrendingResult] = JsonHelper().getMockJson(for: data) + + trendings.forEach { + let viewModel = TrendingViewModel(trending: $0) + favoritesRepository.insetOrUpdateMovie(for: $0.id!, viewModel) + } + + XCTAssertEqual(userDefaults.persisted.count, 21) + XCTAssertEqual(favoritesRepository.getAllFavorites().count, 20) + + let movieId: Int64 = 1 + let trending = TrendingResult(posterPath: "", id: movieId, originalTitle: "", name: "") + let viewModel = TrendingViewModel(trending: trending) + + favoritesRepository.insetOrUpdateMovie(for: movieId, viewModel) + + XCTAssertEqual(userDefaults.persisted.count, 22) + XCTAssertEqual(favoritesRepository.getAllFavorites().count, 21) + } + + func testDeleteMovie() { + let movieId: Int64 = 1 + let trending = TrendingResult(posterPath: "", id: movieId, originalTitle: "", name: "") + let viewModel = TrendingViewModel(trending: trending) + + favoritesRepository.insetOrUpdateMovie(for: movieId, viewModel) + XCTAssertEqual(userDefaults.persisted.count, 2) + XCTAssertEqual(favoritesRepository.getAllFavorites().count, 1) + + favoritesRepository.deleteMovie(for: movieId) + XCTAssertEqual(userDefaults.persisted.count, 0) + XCTAssertEqual(favoritesRepository.getAllFavorites().count, 0) + } +} diff --git a/MyMoviesTests/Model/TrendingViewModelSpec.swift b/MyMoviesTests/Model/TrendingViewModelSpec.swift index 2af45411..a88dc4bf 100644 --- a/MyMoviesTests/Model/TrendingViewModelSpec.swift +++ b/MyMoviesTests/Model/TrendingViewModelSpec.swift @@ -14,7 +14,7 @@ final class TrendingViewModelSpec: XCTestCase { fatalError("Mock json can not be converted to Data") } - let trending = JsonHelper().getMockJson(for: data) + let trending: TrendingResult = JsonHelper().getMockJson(for: data) let viewModel = TrendingViewModel(trending: trending) XCTAssertEqual(viewModel.title, trending.originalTitle) @@ -27,7 +27,7 @@ final class TrendingViewModelSpec: XCTestCase { fatalError("Mock json can not be converted to Data") } - let trending = JsonHelper().getMockJson(for: data) + let trending: TrendingResult = JsonHelper().getMockJson(for: data) let viewModel = TrendingViewModel(trending: trending) XCTAssertEqual(viewModel.title, "") diff --git a/MyMoviesUITests/MyMoviesUITests.swift b/MyMoviesUITests/MyMoviesUITests.swift index d93ba831..0b9348f5 100644 --- a/MyMoviesUITests/MyMoviesUITests.swift +++ b/MyMoviesUITests/MyMoviesUITests.swift @@ -21,21 +21,4 @@ final class MyMoviesUITests: XCTestCase { 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 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/MyMoviesUITests/MyMoviesUITestsLaunchTests.swift b/MyMoviesUITests/MyMoviesUITestsLaunchTests.swift index 3f06c257..4754da7e 100644 --- a/MyMoviesUITests/MyMoviesUITestsLaunchTests.swift +++ b/MyMoviesUITests/MyMoviesUITestsLaunchTests.swift @@ -16,17 +16,4 @@ final class MyMoviesUITestsLaunchTests: XCTestCase { 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) - } } From e2c15f698579ff234bc01a27fb50bbdc34441e52 Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Mon, 16 Jan 2023 22:06:31 -0300 Subject: [PATCH 62/65] Add comments and minor fixes in the code --- MyMovies/Model/TopRatedListViewModel.swift | 1 - MyMovies/Model/TrendingListViewModel.swift | 1 - MyMovies/Model/TrendingViewModel.swift | 7 ++++++ MyMovies/View/Home/HomeView.swift | 10 ++++++++ MyMovies/View/Home/HomeViewController.swift | 8 +++++++ MyMovies/View/Movie/MovieView.swift | 8 +++++++ MyMovies/View/Movie/MovieViewController.swift | 6 +++++ .../Trending/TrendingCollectionViewCell.swift | 8 +++++++ MyMovies/View/Trending/TrendingView.swift | 23 +++++++++++-------- .../Trending/TrendingViewController.swift | 8 +++++++ 10 files changed, 69 insertions(+), 11 deletions(-) diff --git a/MyMovies/Model/TopRatedListViewModel.swift b/MyMovies/Model/TopRatedListViewModel.swift index 2a54c8c0..24abd49e 100644 --- a/MyMovies/Model/TopRatedListViewModel.swift +++ b/MyMovies/Model/TopRatedListViewModel.swift @@ -12,7 +12,6 @@ class TopRatedListViewModel { // MARK: - Properties private var topRated: [TrendingViewModel] = [] - private var selected = 0 private let favoriteRepository: FavoriteRepository private let apiLoader: APILoader diff --git a/MyMovies/Model/TrendingListViewModel.swift b/MyMovies/Model/TrendingListViewModel.swift index 10bd0123..bc83afe5 100644 --- a/MyMovies/Model/TrendingListViewModel.swift +++ b/MyMovies/Model/TrendingListViewModel.swift @@ -12,7 +12,6 @@ class TrendingListViewModel { // MARK: - Properties private var trendings: [TrendingViewModel] = [] - private var selected = 0 private let favoriteRepository: FavoriteRepository private let apiLoader: APILoader diff --git a/MyMovies/Model/TrendingViewModel.swift b/MyMovies/Model/TrendingViewModel.swift index d40f082c..33ed070e 100644 --- a/MyMovies/Model/TrendingViewModel.swift +++ b/MyMovies/Model/TrendingViewModel.swift @@ -8,6 +8,9 @@ import Foundation class TrendingViewModel { + + // MARK: - Properties + private var trending: TrendingResult var title: String { @@ -22,10 +25,14 @@ class TrendingViewModel { self.trending.id } + // MARK: - Initializer + init(trending: TrendingResult) { self.trending = trending } + // MARK: - Functions + func getTrendingResult() -> TrendingResult { return trending } diff --git a/MyMovies/View/Home/HomeView.swift b/MyMovies/View/Home/HomeView.swift index eb663970..573646ec 100644 --- a/MyMovies/View/Home/HomeView.swift +++ b/MyMovies/View/Home/HomeView.swift @@ -13,6 +13,8 @@ protocol FavoriteButtonDelegate: AnyObject { class HomeView: UIView { + // MARK: - SubViews + private var trendingView: UIView private var topRatedView: UIView @@ -24,8 +26,12 @@ class HomeView: UIView { return button }() + // MARK: - Properties + weak var delegate: FavoriteButtonDelegate? + // MARK: - Initializer + init( trendingViewController: TrendingViewController, topRatedViewController: TrendingViewController @@ -41,6 +47,8 @@ class HomeView: UIView { fatalError() } + // MARK: - View Setup + private func setup() { self.backgroundColor = .systemBackground @@ -87,6 +95,8 @@ class HomeView: UIView { ]) } + // MARK: - Selectors + @objc func favoriteMoviesButtonClicked() { delegate?.favoriteMoviesButtonClicked() } diff --git a/MyMovies/View/Home/HomeViewController.swift b/MyMovies/View/Home/HomeViewController.swift index 4c0f211e..304ee342 100644 --- a/MyMovies/View/Home/HomeViewController.swift +++ b/MyMovies/View/Home/HomeViewController.swift @@ -9,6 +9,8 @@ import UIKit class HomeViewController: UIViewController, Coordinating { + // MARK: - Properties + var coordinator: Coordinator? private var trendingViewController: TrendingViewController @@ -16,6 +18,8 @@ class HomeViewController: UIViewController, Coordinating { private var homeView: HomeView? = nil + // MARK: - Initializer + init( trendingViewController: TrendingViewController, topRatedViewController: TrendingViewController, @@ -31,6 +35,8 @@ class HomeViewController: UIViewController, Coordinating { fatalError() } + // MARK: - View Lifecycle + override func loadView() { self.homeView = HomeView( trendingViewController: trendingViewController, @@ -51,6 +57,8 @@ class HomeViewController: UIViewController, Coordinating { } } +// MARK: - FavoriteButtonDelegate implementation + extension HomeViewController: FavoriteButtonDelegate { func favoriteMoviesButtonClicked() { self.coordinator?.eventOccurred(with: .favoriteClicked, parameters: nil) diff --git a/MyMovies/View/Movie/MovieView.swift b/MyMovies/View/Movie/MovieView.swift index 3c3eb4f5..b996cd75 100644 --- a/MyMovies/View/Movie/MovieView.swift +++ b/MyMovies/View/Movie/MovieView.swift @@ -9,6 +9,8 @@ import UIKit class MovieView: UIView { + // MARK: - SubViews + private lazy var titleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false @@ -97,6 +99,8 @@ class MovieView: UIView { return label }() + // MARK: - Initializer + override init(frame: CGRect) { super.init(frame: CGRect()) @@ -107,6 +111,8 @@ class MovieView: UIView { fatalError() } + // MARK: - View Setup + private func setup() { self.backgroundColor = .systemBackground @@ -199,6 +205,8 @@ class MovieView: UIView { ]) } + // MARK: - Renders + func renderLoadingState() { self.titleLabel.isHidden = true self.posterImageView.isHidden = true diff --git a/MyMovies/View/Movie/MovieViewController.swift b/MyMovies/View/Movie/MovieViewController.swift index 83dfcab1..93c0ca03 100644 --- a/MyMovies/View/Movie/MovieViewController.swift +++ b/MyMovies/View/Movie/MovieViewController.swift @@ -9,9 +9,13 @@ import UIKit class MovieViewController: UIViewController { + // MARK: - Properties + private var movieView: MovieView? = nil private var movieViewModel: MovieViewModel + // MARK: - Initializer + init( movieViewModel: MovieViewModel ) { @@ -23,6 +27,8 @@ class MovieViewController: UIViewController { fatalError() } + // MARK: - View Lifecycle + override func loadView() { self.movieView = MovieView() view = movieView diff --git a/MyMovies/View/Trending/TrendingCollectionViewCell.swift b/MyMovies/View/Trending/TrendingCollectionViewCell.swift index 9a521c6a..99c4cbf6 100644 --- a/MyMovies/View/Trending/TrendingCollectionViewCell.swift +++ b/MyMovies/View/Trending/TrendingCollectionViewCell.swift @@ -9,6 +9,8 @@ import UIKit class TrendingCollectionViewCell: UICollectionViewCell { + // MARK: - SubViews + static let identifier = "TrendingCollectionViewCell" private var movieId: Int64? @@ -67,6 +69,8 @@ class TrendingCollectionViewCell: UICollectionViewCell { return label }() + // MARK: - Initializer + override init(frame: CGRect) { super.init(frame: CGRect()) @@ -77,6 +81,8 @@ class TrendingCollectionViewCell: UICollectionViewCell { fatalError() } + // MARK: - View Setup + private func setup() { self.backgroundColor = .systemBackground @@ -150,6 +156,8 @@ class TrendingCollectionViewCell: UICollectionViewCell { ]) } + // MARK: - Public Functions + func setupData(model: TrendingViewModel) { titleLabel.text = model.title movieId = model.movieId diff --git a/MyMovies/View/Trending/TrendingView.swift b/MyMovies/View/Trending/TrendingView.swift index 9137bb05..9669133b 100644 --- a/MyMovies/View/Trending/TrendingView.swift +++ b/MyMovies/View/Trending/TrendingView.swift @@ -13,6 +13,8 @@ protocol FavoriteDelegate: AnyObject { class TrendingView: UIView { + // MARK: - SubViews + private lazy var trendingLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false @@ -52,6 +54,8 @@ class TrendingView: UIView { return label }() + // MARK: - Properties + weak var collectionViewDelegate: UICollectionViewDelegate? { get { self.trendingCollectionView.delegate @@ -70,6 +74,12 @@ class TrendingView: UIView { } } + var collectionViewLeadingContraint: NSLayoutConstraint? = nil + var collectionViewTrailingContraint: NSLayoutConstraint? = nil + var collectionViewBottomContraint: NSLayoutConstraint? = nil + + // MARK: - Initializer + override init(frame: CGRect) { super.init(frame: frame) setup() @@ -79,6 +89,8 @@ class TrendingView: UIView { fatalError() } + // MARK: - View Setup + private func setup() { self.backgroundColor = .systemBackground @@ -96,10 +108,6 @@ class TrendingView: UIView { setupTrendingCollectionView() } - var collectionViewLeadingContraint: NSLayoutConstraint? = nil - var collectionViewTrailingContraint: NSLayoutConstraint? = nil - var collectionViewBottomContraint: NSLayoutConstraint? = nil - private func setupTrendingCollectionView() { addSubview(trendingCollectionView) @@ -139,6 +147,8 @@ class TrendingView: UIView { ]) } + // MARK: - Renders + func renderLoadingState() { self.trendingCollectionView.isHidden = true self.errorLabel.isHidden = true @@ -189,9 +199,4 @@ class TrendingView: UIView { self.layoutIfNeeded() } } - - func favoriteTrending(for row: Int) { - print("Yaaay!") -// self.delegate.favorite(row) - } } diff --git a/MyMovies/View/Trending/TrendingViewController.swift b/MyMovies/View/Trending/TrendingViewController.swift index 48929ba2..ef4012ac 100644 --- a/MyMovies/View/Trending/TrendingViewController.swift +++ b/MyMovies/View/Trending/TrendingViewController.swift @@ -9,6 +9,8 @@ import UIKit class TrendingViewController: UIViewController, Coordinating { + // MARK: - Properties + var coordinator: Coordinator? private var trendingView: TrendingView? = nil @@ -16,6 +18,8 @@ class TrendingViewController: UIViewController, Coordinating { private var topRatedListViewModel: TopRatedListViewModel? private var favoriteViewModel: FavoriteViewModel? + // MARK: - Initializer + init( trendingListViewModel: TrendingListViewModel? = nil, topRatedListViewModel: TopRatedListViewModel? = nil, @@ -33,6 +37,8 @@ class TrendingViewController: UIViewController, Coordinating { fatalError() } + // MARK: - View Lifecycle + override func loadView() { self.trendingView = TrendingView() self.trendingView?.collectionDataSource = self @@ -86,6 +92,8 @@ class TrendingViewController: UIViewController, Coordinating { } } +// MARK: - UICollectionViewDelegate and UICollectionViewDataSource implementation + extension TrendingViewController: UICollectionViewDelegate, UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return self.trendingListViewModel?.numberOfRowsInSection ?? From 17c12af4a2403ce09deb96d8bc9d3ba717f717ec Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Mon, 16 Jan 2023 22:06:55 -0300 Subject: [PATCH 63/65] Remove comment --- MyMovies/Service/ImageLoader/ImageLoader.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/MyMovies/Service/ImageLoader/ImageLoader.swift b/MyMovies/Service/ImageLoader/ImageLoader.swift index c61db14f..af0b50e2 100644 --- a/MyMovies/Service/ImageLoader/ImageLoader.swift +++ b/MyMovies/Service/ImageLoader/ImageLoader.swift @@ -39,7 +39,6 @@ class ImageLoader { } task.resume() - // 6 runningRequests[uuid] = task return uuid } From 0a15fd8004afbc277fb564adb1de4c1921a8892d Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Mon, 16 Jan 2023 22:10:35 -0300 Subject: [PATCH 64/65] Remame TrendingViewController and it sub files to MovieList --- MyMovies.xcodeproj/project.pbxproj | 24 +++++++++---------- MyMovies/Coordinator/MainCoordinator.swift | 6 ++--- MyMovies/View/Home/HomeView.swift | 4 ++-- MyMovies/View/Home/HomeViewController.swift | 8 +++---- ...wift => MovieListCollectionViewCell.swift} | 6 ++--- ...TrendingView.swift => MovieListView.swift} | 6 ++--- ...er.swift => MovieListViewController.swift} | 14 +++++------ .../Coordinator/MainCoordinatorSpec.swift | 2 +- 8 files changed, 35 insertions(+), 35 deletions(-) rename MyMovies/View/Trending/{TrendingCollectionViewCell.swift => MovieListCollectionViewCell.swift} (97%) rename MyMovies/View/Trending/{TrendingView.swift => MovieListView.swift} (97%) rename MyMovies/View/Trending/{TrendingViewController.swift => MovieListViewController.swift} (91%) diff --git a/MyMovies.xcodeproj/project.pbxproj b/MyMovies.xcodeproj/project.pbxproj index 35eb05b9..61e72cf2 100644 --- a/MyMovies.xcodeproj/project.pbxproj +++ b/MyMovies.xcodeproj/project.pbxproj @@ -28,10 +28,10 @@ 7D35A44B296E2C920054954E /* TrendingDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A44A296E2C920054954E /* TrendingDTO.swift */; }; 7D35A44D296E2CA80054954E /* TrendingResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A44C296E2CA80054954E /* TrendingResult.swift */; }; 7D35A450296E31C30054954E /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A44F296E31C30054954E /* HomeViewController.swift */; }; - 7D35A453296E32EE0054954E /* TrendingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A452296E32EE0054954E /* TrendingViewController.swift */; }; + 7D35A453296E32EE0054954E /* MovieListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A452296E32EE0054954E /* MovieListViewController.swift */; }; 7D35A45B296E36DD0054954E /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A45A296E36DD0054954E /* HomeView.swift */; }; - 7D35A45F296E3D360054954E /* TrendingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A45E296E3D360054954E /* TrendingView.swift */; }; - 7D35A461296E3D5F0054954E /* TrendingCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A460296E3D5F0054954E /* TrendingCollectionViewCell.swift */; }; + 7D35A45F296E3D360054954E /* MovieListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A45E296E3D360054954E /* MovieListView.swift */; }; + 7D35A461296E3D5F0054954E /* MovieListCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A460296E3D5F0054954E /* MovieListCollectionViewCell.swift */; }; 7D35A463296E3E410054954E /* TrendingListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A462296E3E410054954E /* TrendingListViewModel.swift */; }; 7D35A465296E45170054954E /* TrendingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A464296E45170054954E /* TrendingViewModel.swift */; }; 7D35A467296E4C170054954E /* UIImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D35A466296E4C170054954E /* UIImageLoader.swift */; }; @@ -109,10 +109,10 @@ 7D35A44A296E2C920054954E /* TrendingDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingDTO.swift; sourceTree = ""; }; 7D35A44C296E2CA80054954E /* TrendingResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingResult.swift; sourceTree = ""; }; 7D35A44F296E31C30054954E /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; - 7D35A452296E32EE0054954E /* TrendingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingViewController.swift; sourceTree = ""; }; + 7D35A452296E32EE0054954E /* MovieListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieListViewController.swift; sourceTree = ""; }; 7D35A45A296E36DD0054954E /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; - 7D35A45E296E3D360054954E /* TrendingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingView.swift; sourceTree = ""; }; - 7D35A460296E3D5F0054954E /* TrendingCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingCollectionViewCell.swift; sourceTree = ""; }; + 7D35A45E296E3D360054954E /* MovieListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieListView.swift; sourceTree = ""; }; + 7D35A460296E3D5F0054954E /* MovieListCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieListCollectionViewCell.swift; sourceTree = ""; }; 7D35A462296E3E410054954E /* TrendingListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingListViewModel.swift; sourceTree = ""; }; 7D35A464296E45170054954E /* TrendingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingViewModel.swift; sourceTree = ""; }; 7D35A466296E4C170054954E /* UIImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageLoader.swift; sourceTree = ""; }; @@ -308,9 +308,9 @@ 7D35A451296E32DC0054954E /* Trending */ = { isa = PBXGroup; children = ( - 7D35A452296E32EE0054954E /* TrendingViewController.swift */, - 7D35A45E296E3D360054954E /* TrendingView.swift */, - 7D35A460296E3D5F0054954E /* TrendingCollectionViewCell.swift */, + 7D35A452296E32EE0054954E /* MovieListViewController.swift */, + 7D35A45E296E3D360054954E /* MovieListView.swift */, + 7D35A460296E3D5F0054954E /* MovieListCollectionViewCell.swift */, ); path = Trending; sourceTree = ""; @@ -579,15 +579,15 @@ buildActionMask = 2147483647; files = ( 7D35A442296E293D0054954E /* ServiceError.swift in Sources */, - 7D35A453296E32EE0054954E /* TrendingViewController.swift in Sources */, + 7D35A453296E32EE0054954E /* MovieListViewController.swift in Sources */, 7D35A447296E2A390054954E /* TrendingAPI.swift in Sources */, 7D35A435296E233F0054954E /* MainCoordinator.swift in Sources */, 7D35A44B296E2C920054954E /* TrendingDTO.swift in Sources */, 7D35A450296E31C30054954E /* HomeViewController.swift in Sources */, 7D35A43A296E27AE0054954E /* APIEnvironment.swift in Sources */, - 7D35A461296E3D5F0054954E /* TrendingCollectionViewCell.swift in Sources */, + 7D35A461296E3D5F0054954E /* MovieListCollectionViewCell.swift in Sources */, 7D35A476297207C20054954E /* Movie.swift in Sources */, - 7D35A45F296E3D360054954E /* TrendingView.swift in Sources */, + 7D35A45F296E3D360054954E /* MovieListView.swift in Sources */, 7D35A48129720F0E0054954E /* MovieView.swift in Sources */, 7D35A43E296E29000054954E /* APIHandler.swift in Sources */, 7D35A47F29720DB50054954E /* MovieViewController.swift in Sources */, diff --git a/MyMovies/Coordinator/MainCoordinator.swift b/MyMovies/Coordinator/MainCoordinator.swift index 83701e87..7176206b 100644 --- a/MyMovies/Coordinator/MainCoordinator.swift +++ b/MyMovies/Coordinator/MainCoordinator.swift @@ -30,7 +30,7 @@ class MainCoordinator: Coordinator { case .favoriteClicked: let repository = FavoriteRepository() let viewModel = FavoriteViewModel(favoriteRepository: repository) - let viewController = TrendingViewController(favoriteViewModel: viewModel, coordinator: self) + let viewController = MovieListViewController(favoriteViewModel: viewModel, coordinator: self) navigationController.pushViewController(viewController, animated: true) } @@ -40,7 +40,7 @@ class MainCoordinator: Coordinator { let apiTrending = TrendingAPI() let apiLoaderTrending = APILoader(apiHandler: apiTrending) let trendingViewModel = TrendingListViewModel(apiLoader: apiLoaderTrending) - let trendingViewController = TrendingViewController( + let trendingViewController = MovieListViewController( trendingListViewModel: trendingViewModel, coordinator: self ) @@ -48,7 +48,7 @@ class MainCoordinator: Coordinator { let apiTopRated = TopRatedAPI() let apiLoaderTopRated = APILoader(apiHandler: apiTopRated) let topRatedListViewModel = TopRatedListViewModel(apiLoader: apiLoaderTopRated) - let topRatedViewController = TrendingViewController( + let topRatedViewController = MovieListViewController( topRatedListViewModel: topRatedListViewModel, coordinator: self ) diff --git a/MyMovies/View/Home/HomeView.swift b/MyMovies/View/Home/HomeView.swift index 573646ec..e1f7f2d2 100644 --- a/MyMovies/View/Home/HomeView.swift +++ b/MyMovies/View/Home/HomeView.swift @@ -33,8 +33,8 @@ class HomeView: UIView { // MARK: - Initializer init( - trendingViewController: TrendingViewController, - topRatedViewController: TrendingViewController + trendingViewController: MovieListViewController, + topRatedViewController: MovieListViewController ) { self.trendingView = trendingViewController.view self.topRatedView = topRatedViewController.view diff --git a/MyMovies/View/Home/HomeViewController.swift b/MyMovies/View/Home/HomeViewController.swift index 304ee342..dbfdb63b 100644 --- a/MyMovies/View/Home/HomeViewController.swift +++ b/MyMovies/View/Home/HomeViewController.swift @@ -13,16 +13,16 @@ class HomeViewController: UIViewController, Coordinating { var coordinator: Coordinator? - private var trendingViewController: TrendingViewController - private var topRatedViewController: TrendingViewController + private var trendingViewController: MovieListViewController + private var topRatedViewController: MovieListViewController private var homeView: HomeView? = nil // MARK: - Initializer init( - trendingViewController: TrendingViewController, - topRatedViewController: TrendingViewController, + trendingViewController: MovieListViewController, + topRatedViewController: MovieListViewController, coordinator: Coordinator ) { self.trendingViewController = trendingViewController diff --git a/MyMovies/View/Trending/TrendingCollectionViewCell.swift b/MyMovies/View/Trending/MovieListCollectionViewCell.swift similarity index 97% rename from MyMovies/View/Trending/TrendingCollectionViewCell.swift rename to MyMovies/View/Trending/MovieListCollectionViewCell.swift index 99c4cbf6..66dda82f 100644 --- a/MyMovies/View/Trending/TrendingCollectionViewCell.swift +++ b/MyMovies/View/Trending/MovieListCollectionViewCell.swift @@ -1,5 +1,5 @@ // -// TrendingCollectionViewCell.swift +// MovieListCollectionViewCell.swift // MyMovies // // Created by Caio dos Santos Ambrosio on 1/10/23. @@ -7,11 +7,11 @@ import UIKit -class TrendingCollectionViewCell: UICollectionViewCell { +class MovieListCollectionViewCell: UICollectionViewCell { // MARK: - SubViews - static let identifier = "TrendingCollectionViewCell" + static let identifier = "MovieListCollectionViewCell" private var movieId: Int64? diff --git a/MyMovies/View/Trending/TrendingView.swift b/MyMovies/View/Trending/MovieListView.swift similarity index 97% rename from MyMovies/View/Trending/TrendingView.swift rename to MyMovies/View/Trending/MovieListView.swift index 9669133b..ce0246d4 100644 --- a/MyMovies/View/Trending/TrendingView.swift +++ b/MyMovies/View/Trending/MovieListView.swift @@ -1,5 +1,5 @@ // -// TrendingView.swift +// MovieListView.swift // MyMovies // // Created by Caio dos Santos Ambrosio on 1/10/23. @@ -11,7 +11,7 @@ protocol FavoriteDelegate: AnyObject { func favorite(_ row: Int) } -class TrendingView: UIView { +class MovieListView: UIView { // MARK: - SubViews @@ -32,7 +32,7 @@ class TrendingView: UIView { let collectionView = UICollectionView(frame: CGRect(), collectionViewLayout: layout) collectionView.showsHorizontalScrollIndicator = false collectionView.translatesAutoresizingMaskIntoConstraints = false - collectionView.register(TrendingCollectionViewCell.self, forCellWithReuseIdentifier: TrendingCollectionViewCell.identifier) + collectionView.register(MovieListCollectionViewCell.self, forCellWithReuseIdentifier: MovieListCollectionViewCell.identifier) return collectionView }() diff --git a/MyMovies/View/Trending/TrendingViewController.swift b/MyMovies/View/Trending/MovieListViewController.swift similarity index 91% rename from MyMovies/View/Trending/TrendingViewController.swift rename to MyMovies/View/Trending/MovieListViewController.swift index ef4012ac..5b0e0cf4 100644 --- a/MyMovies/View/Trending/TrendingViewController.swift +++ b/MyMovies/View/Trending/MovieListViewController.swift @@ -1,5 +1,5 @@ // -// TrendingViewController.swift +// MovieListViewController.swift // MyMovies // // Created by Caio dos Santos Ambrosio on 1/10/23. @@ -7,13 +7,13 @@ import UIKit -class TrendingViewController: UIViewController, Coordinating { +class MovieListViewController: UIViewController, Coordinating { // MARK: - Properties var coordinator: Coordinator? - private var trendingView: TrendingView? = nil + private var trendingView: MovieListView? = nil private var trendingListViewModel: TrendingListViewModel? private var topRatedListViewModel: TopRatedListViewModel? private var favoriteViewModel: FavoriteViewModel? @@ -40,7 +40,7 @@ class TrendingViewController: UIViewController, Coordinating { // MARK: - View Lifecycle override func loadView() { - self.trendingView = TrendingView() + self.trendingView = MovieListView() self.trendingView?.collectionDataSource = self self.trendingView?.collectionViewDelegate = self view = trendingView @@ -94,7 +94,7 @@ class TrendingViewController: UIViewController, Coordinating { // MARK: - UICollectionViewDelegate and UICollectionViewDataSource implementation -extension TrendingViewController: UICollectionViewDelegate, UICollectionViewDataSource { +extension MovieListViewController: UICollectionViewDelegate, UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return self.trendingListViewModel?.numberOfRowsInSection ?? self.topRatedListViewModel?.numberOfRowsInSection ?? @@ -106,7 +106,7 @@ extension TrendingViewController: UICollectionViewDelegate, UICollectionViewData topRatedListViewModel?.getTrending(indexPath.row) ?? favoriteViewModel?.getTrending(indexPath.row) else { return UICollectionViewCell() } - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TrendingCollectionViewCell.identifier, for: indexPath) as? TrendingCollectionViewCell + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MovieListCollectionViewCell.identifier, for: indexPath) as? MovieListCollectionViewCell cell?.setupData(model: trending) let swipeUpsideDown = UISwipeGestureRecognizer(target: self, action: #selector(favorite(_:))) @@ -129,7 +129,7 @@ extension TrendingViewController: UICollectionViewDelegate, UICollectionViewData } @objc func favorite(_ sender: UISwipeGestureRecognizer) { - guard let cell = sender.view as? TrendingCollectionViewCell else { + guard let cell = sender.view as? MovieListCollectionViewCell else { return } diff --git a/MyMoviesTests/Coordinator/MainCoordinatorSpec.swift b/MyMoviesTests/Coordinator/MainCoordinatorSpec.swift index 08a1f666..2f8dfebc 100644 --- a/MyMoviesTests/Coordinator/MainCoordinatorSpec.swift +++ b/MyMoviesTests/Coordinator/MainCoordinatorSpec.swift @@ -38,6 +38,6 @@ final class MainCoordinatorSpec: XCTestCase { mainCoordinator.eventOccurred(with: .favoriteClicked, parameters: nil) XCTAssertTrue(navigationController.pushViewControllerCalled) - XCTAssertTrue(navigationController.controllers[0] is TrendingViewController) + XCTAssertTrue(navigationController.controllers[0] is MovieListViewController) } } From 0d4f42aacff3aa4b472b5f6b09a084621f702e3c Mon Sep 17 00:00:00 2001 From: Caio Ambrosio Date: Mon, 16 Jan 2023 22:16:04 -0300 Subject: [PATCH 65/65] Fix click in a movie when in favorite screen --- MyMovies/View/Trending/MovieListViewController.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MyMovies/View/Trending/MovieListViewController.swift b/MyMovies/View/Trending/MovieListViewController.swift index 5b0e0cf4..1ccf3e20 100644 --- a/MyMovies/View/Trending/MovieListViewController.swift +++ b/MyMovies/View/Trending/MovieListViewController.swift @@ -122,7 +122,8 @@ extension MovieListViewController: UICollectionViewDelegate, UICollectionViewDat func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { guard let trending = trendingListViewModel?.getTrending(indexPath.row) ?? - topRatedListViewModel?.getTrending(indexPath.row), + topRatedListViewModel?.getTrending(indexPath.row) ?? + favoriteViewModel?.getTrending(indexPath.row), let movieId = trending.movieId else { return } coordinator?.eventOccurred(with: Event.movieClicked, parameters: movieId)