From 2e10a747c72c4fb906d7ae1cba7e2899b87c9202 Mon Sep 17 00:00:00 2001 From: Guilherme Antunes Date: Wed, 28 Aug 2019 14:36:40 -0300 Subject: [PATCH 01/14] feat(networking): creating network layer --- .DS_Store | Bin 6148 -> 0 bytes .gitignore | 6 +- iOSBooks/.DS_Store | Bin 6148 -> 0 bytes iOSBooks/Podfile | 19 ++ iOSBooks/iOSBooks.xcodeproj/project.pbxproj | 205 +++++++++++++++++- .../iOSBooks/Data/Clients/BooksClient.swift | 23 ++ .../iOSBooks/Data/Endpoints/BooksAPI.swift | 67 ++++++ iOSBooks/iOSBooks/Data/Models/Book.swift | 24 ++ iOSBooks/iOSBooks/Data/Models/BooksList.swift | 18 ++ .../iOSBooks/Data/Models/ImageLinks.swift | 19 ++ iOSBooks/iOSBooks/Data/Models/Item.swift | 21 ++ iOSBooks/iOSBooks/Data/Models/SalesInfo.swift | 14 ++ .../iOSBooks/Data/Network/APIClient.swift | 61 ++++++ iOSBooks/iOSBooks/Data/Network/Endpoint.swift | 18 ++ .../iOSBooks/Data/Network/Reachability.swift | 36 +++ iOSBooks/iOSBooks/ViewController.swift | 7 +- 16 files changed, 530 insertions(+), 8 deletions(-) delete mode 100644 .DS_Store delete mode 100644 iOSBooks/.DS_Store create mode 100644 iOSBooks/Podfile create mode 100644 iOSBooks/iOSBooks/Data/Clients/BooksClient.swift create mode 100644 iOSBooks/iOSBooks/Data/Endpoints/BooksAPI.swift create mode 100644 iOSBooks/iOSBooks/Data/Models/Book.swift create mode 100644 iOSBooks/iOSBooks/Data/Models/BooksList.swift create mode 100644 iOSBooks/iOSBooks/Data/Models/ImageLinks.swift create mode 100644 iOSBooks/iOSBooks/Data/Models/Item.swift create mode 100644 iOSBooks/iOSBooks/Data/Models/SalesInfo.swift create mode 100644 iOSBooks/iOSBooks/Data/Network/APIClient.swift create mode 100644 iOSBooks/iOSBooks/Data/Network/Endpoint.swift create mode 100644 iOSBooks/iOSBooks/Data/Network/Reachability.swift diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index fa2896dcff2798c279311fd1d1d903f1bbbf0549..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK&2G~`5S~o~wFMG6AkpJqkT^srQHXj#NUli_91sXsZ~#Xa?9fhe21|{%%@~lm8SXtALoVif_7<#CiE0| zpAISKrGJLHABZ|j9jYS2fG{8oTs{MS>kDpN{;$X%2m`{v6=Q(+2MuM6Jhl$))`7x4 z0f2msv;v#2jGUuAMjl&-@IaJH1-ew_pBT!eqh0&B$Ybl!rIYfH59NDS{)M7!@A$s9 z;iMvmQVIjYzYN0+t3Tgn@s`z#ldRYzhDX diff --git a/.gitignore b/.gitignore index 312d1f6..030d1ea 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ DerivedData/ *.perspectivev3 !default.perspectivev3 xcuserdata/ +*.DS_Store +##xcshareddata ## Other *.moved-aside @@ -46,7 +48,9 @@ playground.xcworkspace # you should judge for yourself, the pros and cons are mentioned at: # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control # -# Pods/ +iOSBooks/Pods/ +iOSBooks/iOSBooks.xcworkspace +iOSBooks/Podfile.lock # Carthage # diff --git a/iOSBooks/.DS_Store b/iOSBooks/.DS_Store deleted file mode 100644 index 7cb1ea8a5d5013006b8ca411622f3eea14a4ffb9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKJ5Izf5PgO%BGIgC40(=(glpX55cd-k{lzZX z`#vslf(8|L|GEDa{SIg7aYOHFeZFW`Y18XREMK?fZBkvg>(ny7IDJVE?c?*&_T28r z-2US0exJCcn=9Z7xB{+#E07i7oh?@EDSGV+xB{-ghXQgwL>9r)a8S%g2dkU{5ak)2 zjlSd(!ik2Z;h@M7T2Lxcslif=pmg>}iz^KWMWrKH@)2z1_v9ty)j5C6;RvOo*RFso zFjin!n{&DUkNL|?Ci!EEw_E{N;GZcFW_43n{3yFyzkQzEwUK4XA|`QzI5hgBM*tgg gjvVAc9gpHOt~4AJWftkrbfP~5vLIf$0>7ZZ8)R)ic>n+a diff --git a/iOSBooks/Podfile b/iOSBooks/Podfile new file mode 100644 index 0000000..21ec943 --- /dev/null +++ b/iOSBooks/Podfile @@ -0,0 +1,19 @@ +# Uncomment the next line to define a global platform for your project +platform :ios, '12.0' + +target 'iOSBooks' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + # Pods for iOSBooks + pod 'PromiseKit' + pod 'IQKeyboardManagerSwift' + + target 'iOSBooksTests' do + inherit! :search_paths + # Pods for testing + pod 'Quick', '~> 2.1.0' + pod 'Nimble', '~> 8.0.1' + end + +end diff --git a/iOSBooks/iOSBooks.xcodeproj/project.pbxproj b/iOSBooks/iOSBooks.xcodeproj/project.pbxproj index b0482bb..4ee20cf 100644 --- a/iOSBooks/iOSBooks.xcodeproj/project.pbxproj +++ b/iOSBooks/iOSBooks.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 51; objects = { /* Begin PBXBuildFile section */ @@ -13,6 +13,18 @@ 0E3964DF2314BB4A0093738B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0E3964DE2314BB4A0093738B /* Assets.xcassets */; }; 0E3964E22314BB4A0093738B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0E3964E02314BB4A0093738B /* LaunchScreen.storyboard */; }; 0E3964ED2314BB4A0093738B /* iOSBooksTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3964EC2314BB4A0093738B /* iOSBooksTests.swift */; }; + 477DA993F6CBE89C179034EE /* Pods_iOSBooksTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 28F20F388C60D311258C5680 /* Pods_iOSBooksTests.framework */; }; + 7447D35E2316C91700E01BD3 /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7447D35D2316C91700E01BD3 /* APIClient.swift */; }; + 7447D3602316CB1200E01BD3 /* Reachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7447D35F2316CB1200E01BD3 /* Reachability.swift */; }; + 7447D3622316DECA00E01BD3 /* BooksAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7447D3612316DECA00E01BD3 /* BooksAPI.swift */; }; + 7447D3642316E02500E01BD3 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7447D3632316E02500E01BD3 /* Endpoint.swift */; }; + 7447D3662316E3C100E01BD3 /* BooksClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7447D3652316E3C100E01BD3 /* BooksClient.swift */; }; + 7447D3682316E57500E01BD3 /* Book.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7447D3672316E57500E01BD3 /* Book.swift */; }; + 7447D36A2316F11800E01BD3 /* BooksList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7447D3692316F11800E01BD3 /* BooksList.swift */; }; + 7447D36C2316F12800E01BD3 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7447D36B2316F12800E01BD3 /* Item.swift */; }; + 7447D36E2316F13A00E01BD3 /* ImageLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7447D36D2316F13A00E01BD3 /* ImageLinks.swift */; }; + 7447D3702316F14D00E01BD3 /* SalesInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7447D36F2316F14D00E01BD3 /* SalesInfo.swift */; }; + 768F74E39672B1F50C632913 /* Pods_iOSBooks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2D2FDB2EC5907498F6A3C173 /* Pods_iOSBooks.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -36,6 +48,22 @@ 0E3964E82314BB4A0093738B /* iOSBooksTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = iOSBooksTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 0E3964EC2314BB4A0093738B /* iOSBooksTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSBooksTests.swift; sourceTree = ""; }; 0E3964EE2314BB4A0093738B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 18F9E4DBC4A549B0B6E5DFAB /* Pods-iOSBooksTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOSBooksTests.release.xcconfig"; path = "Target Support Files/Pods-iOSBooksTests/Pods-iOSBooksTests.release.xcconfig"; sourceTree = ""; }; + 275296C36B2B5A1EBD78A5F1 /* Pods-iOSBooks.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOSBooks.release.xcconfig"; path = "Target Support Files/Pods-iOSBooks/Pods-iOSBooks.release.xcconfig"; sourceTree = ""; }; + 28F20F388C60D311258C5680 /* Pods_iOSBooksTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iOSBooksTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 2D2FDB2EC5907498F6A3C173 /* Pods_iOSBooks.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iOSBooks.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 48CC3F84588BF21024860E64 /* Pods-iOSBooksTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOSBooksTests.debug.xcconfig"; path = "Target Support Files/Pods-iOSBooksTests/Pods-iOSBooksTests.debug.xcconfig"; sourceTree = ""; }; + 7447D35D2316C91700E01BD3 /* APIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClient.swift; sourceTree = ""; }; + 7447D35F2316CB1200E01BD3 /* Reachability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reachability.swift; sourceTree = ""; }; + 7447D3612316DECA00E01BD3 /* BooksAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BooksAPI.swift; sourceTree = ""; }; + 7447D3632316E02500E01BD3 /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = ""; }; + 7447D3652316E3C100E01BD3 /* BooksClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BooksClient.swift; sourceTree = ""; }; + 7447D3672316E57500E01BD3 /* Book.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Book.swift; sourceTree = ""; }; + 7447D3692316F11800E01BD3 /* BooksList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BooksList.swift; sourceTree = ""; }; + 7447D36B2316F12800E01BD3 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = ""; }; + 7447D36D2316F13A00E01BD3 /* ImageLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageLinks.swift; sourceTree = ""; }; + 7447D36F2316F14D00E01BD3 /* SalesInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SalesInfo.swift; sourceTree = ""; }; + E6741F045EBC12BB315EE1CB /* Pods-iOSBooks.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOSBooks.debug.xcconfig"; path = "Target Support Files/Pods-iOSBooks/Pods-iOSBooks.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -43,6 +71,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 768F74E39672B1F50C632913 /* Pods_iOSBooks.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -50,6 +79,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 477DA993F6CBE89C179034EE /* Pods_iOSBooksTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -62,6 +92,8 @@ 0E3964D62314BB470093738B /* iOSBooks */, 0E3964EB2314BB4A0093738B /* iOSBooksTests */, 0E3964D52314BB470093738B /* Products */, + E4186846F20EA500EF61D605 /* Pods */, + 46A8A270FB95E2805738E1EE /* Frameworks */, ); sourceTree = ""; }; @@ -77,6 +109,7 @@ 0E3964D62314BB470093738B /* iOSBooks */ = { isa = PBXGroup; children = ( + 7447D3712316F16E00E01BD3 /* Data */, 0E3964D72314BB470093738B /* AppDelegate.swift */, 0E3964D92314BB470093738B /* ViewController.swift */, 0E3964DB2314BB470093738B /* Main.storyboard */, @@ -96,6 +129,75 @@ path = iOSBooksTests; sourceTree = ""; }; + 46A8A270FB95E2805738E1EE /* Frameworks */ = { + isa = PBXGroup; + children = ( + 2D2FDB2EC5907498F6A3C173 /* Pods_iOSBooks.framework */, + 28F20F388C60D311258C5680 /* Pods_iOSBooksTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 7447D3712316F16E00E01BD3 /* Data */ = { + isa = PBXGroup; + children = ( + 7447D3752316F1A200E01BD3 /* Clients */, + 7447D3742316F19000E01BD3 /* Network */, + 7447D3732316F18400E01BD3 /* Endpoints */, + 7447D3722316F17900E01BD3 /* Models */, + ); + path = Data; + sourceTree = ""; + }; + 7447D3722316F17900E01BD3 /* Models */ = { + isa = PBXGroup; + children = ( + 7447D3672316E57500E01BD3 /* Book.swift */, + 7447D3692316F11800E01BD3 /* BooksList.swift */, + 7447D36B2316F12800E01BD3 /* Item.swift */, + 7447D36D2316F13A00E01BD3 /* ImageLinks.swift */, + 7447D36F2316F14D00E01BD3 /* SalesInfo.swift */, + ); + path = Models; + sourceTree = ""; + }; + 7447D3732316F18400E01BD3 /* Endpoints */ = { + isa = PBXGroup; + children = ( + 7447D3612316DECA00E01BD3 /* BooksAPI.swift */, + ); + path = Endpoints; + sourceTree = ""; + }; + 7447D3742316F19000E01BD3 /* Network */ = { + isa = PBXGroup; + children = ( + 7447D35D2316C91700E01BD3 /* APIClient.swift */, + 7447D35F2316CB1200E01BD3 /* Reachability.swift */, + 7447D3632316E02500E01BD3 /* Endpoint.swift */, + ); + path = Network; + sourceTree = ""; + }; + 7447D3752316F1A200E01BD3 /* Clients */ = { + isa = PBXGroup; + children = ( + 7447D3652316E3C100E01BD3 /* BooksClient.swift */, + ); + path = Clients; + sourceTree = ""; + }; + E4186846F20EA500EF61D605 /* Pods */ = { + isa = PBXGroup; + children = ( + E6741F045EBC12BB315EE1CB /* Pods-iOSBooks.debug.xcconfig */, + 275296C36B2B5A1EBD78A5F1 /* Pods-iOSBooks.release.xcconfig */, + 48CC3F84588BF21024860E64 /* Pods-iOSBooksTests.debug.xcconfig */, + 18F9E4DBC4A549B0B6E5DFAB /* Pods-iOSBooksTests.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -103,9 +205,11 @@ isa = PBXNativeTarget; buildConfigurationList = 0E3964F12314BB4A0093738B /* Build configuration list for PBXNativeTarget "iOSBooks" */; buildPhases = ( + 15C7FF8C9D9E1A03AF7AE485 /* [CP] Check Pods Manifest.lock */, 0E3964D02314BB470093738B /* Sources */, 0E3964D12314BB470093738B /* Frameworks */, 0E3964D22314BB470093738B /* Resources */, + 29B8A3AC18D11FEF43C48244 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -120,9 +224,11 @@ isa = PBXNativeTarget; buildConfigurationList = 0E3964F42314BB4A0093738B /* Build configuration list for PBXNativeTarget "iOSBooksTests" */; buildPhases = ( + B1160ABF1429D7BD27416220 /* [CP] Check Pods Manifest.lock */, 0E3964E42314BB4A0093738B /* Sources */, 0E3964E52314BB4A0093738B /* Frameworks */, 0E3964E62314BB4A0093738B /* Resources */, + 6A60DF844467B985FBAD6155 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -192,13 +298,104 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 15C7FF8C9D9E1A03AF7AE485 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-iOSBooks-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 29B8A3AC18D11FEF43C48244 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-iOSBooks/Pods-iOSBooks-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-iOSBooks/Pods-iOSBooks-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iOSBooks/Pods-iOSBooks-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 6A60DF844467B985FBAD6155 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-iOSBooksTests/Pods-iOSBooksTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-iOSBooksTests/Pods-iOSBooksTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iOSBooksTests/Pods-iOSBooksTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + B1160ABF1429D7BD27416220 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-iOSBooksTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 0E3964D02314BB470093738B /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7447D36C2316F12800E01BD3 /* Item.swift in Sources */, + 7447D3682316E57500E01BD3 /* Book.swift in Sources */, + 7447D3702316F14D00E01BD3 /* SalesInfo.swift in Sources */, + 7447D36E2316F13A00E01BD3 /* ImageLinks.swift in Sources */, 0E3964DA2314BB470093738B /* ViewController.swift in Sources */, 0E3964D82314BB470093738B /* AppDelegate.swift in Sources */, + 7447D3602316CB1200E01BD3 /* Reachability.swift in Sources */, + 7447D35E2316C91700E01BD3 /* APIClient.swift in Sources */, + 7447D3642316E02500E01BD3 /* Endpoint.swift in Sources */, + 7447D36A2316F11800E01BD3 /* BooksList.swift in Sources */, + 7447D3662316E3C100E01BD3 /* BooksClient.swift in Sources */, + 7447D3622316DECA00E01BD3 /* BooksAPI.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -358,11 +555,13 @@ }; 0E3964F22314BB4A0093738B /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = E6741F045EBC12BB315EE1CB /* Pods-iOSBooks.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 7AH86L34EQ; INFOPLIST_FILE = iOSBooks/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -376,11 +575,13 @@ }; 0E3964F32314BB4A0093738B /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 275296C36B2B5A1EBD78A5F1 /* Pods-iOSBooks.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 7AH86L34EQ; INFOPLIST_FILE = iOSBooks/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -394,6 +595,7 @@ }; 0E3964F52314BB4A0093738B /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 48CC3F84588BF21024860E64 /* Pods-iOSBooksTests.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -415,6 +617,7 @@ }; 0E3964F62314BB4A0093738B /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 18F9E4DBC4A549B0B6E5DFAB /* Pods-iOSBooksTests.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; diff --git a/iOSBooks/iOSBooks/Data/Clients/BooksClient.swift b/iOSBooks/iOSBooks/Data/Clients/BooksClient.swift new file mode 100644 index 0000000..3e291a3 --- /dev/null +++ b/iOSBooks/iOSBooks/Data/Clients/BooksClient.swift @@ -0,0 +1,23 @@ +// +// BooksClient.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 28/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import Foundation +import PromiseKit + +class BooksClient { + + let apiClient: APIClient + + init(apiClient: APIClient = APIClient()) { + self.apiClient = apiClient + } + + func fetchBooksList() -> Promise { + return apiClient.request(model: BooksList.self, BooksAPI.list.request) + } +} diff --git a/iOSBooks/iOSBooks/Data/Endpoints/BooksAPI.swift b/iOSBooks/iOSBooks/Data/Endpoints/BooksAPI.swift new file mode 100644 index 0000000..e598eb0 --- /dev/null +++ b/iOSBooks/iOSBooks/Data/Endpoints/BooksAPI.swift @@ -0,0 +1,67 @@ +// +// BooksAPI.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 28/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import Foundation + +enum BooksAPI: Endpoint { + + case list + + var path: String { + switch self { + case .list: + return "https://www.googleapis.com/books/v1/volumes" + } + } + + var headers: [String : String]? { + switch self { + case .list: + return nil + } + } + + var body: Data? { + switch self { + case .list: + return nil + } + } + + var httpMethod: String { + switch self { + case .list: + return "GET" + } + } + + var queryItems: [URLQueryItem]? { + switch self { + case .list: + let subjectItem = URLQueryItem(name: "q", value: "ios") + let maxResultsItem = URLQueryItem(name: "maxResults", value: "20") + let startIndexItem = URLQueryItem(name: "startIndex", value: "0") + return [subjectItem, startIndexItem, maxResultsItem] + } + } + + var request: URLRequest { + switch self { + case .list: + guard var components = URLComponents(string: path) else { fatalError() } + components.queryItems = queryItems + guard let url = components.url else { fatalError() } + var request = URLRequest(url: url) + request.allHTTPHeaderFields = headers + request.httpMethod = httpMethod + request.httpBody = body + return request + } + } +} + diff --git a/iOSBooks/iOSBooks/Data/Models/Book.swift b/iOSBooks/iOSBooks/Data/Models/Book.swift new file mode 100644 index 0000000..50078a8 --- /dev/null +++ b/iOSBooks/iOSBooks/Data/Models/Book.swift @@ -0,0 +1,24 @@ +// +// Book.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 28/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import Foundation + +class Book: Codable { + let authors: [String]? + let title: String? + let subtitle: String? + let description: String? + let imageLinks: ImageLinks? + + enum CodingKeys: String, CodingKey { + case authors, title, subtitle, description, imageLinks + } +} + + + diff --git a/iOSBooks/iOSBooks/Data/Models/BooksList.swift b/iOSBooks/iOSBooks/Data/Models/BooksList.swift new file mode 100644 index 0000000..2a2de78 --- /dev/null +++ b/iOSBooks/iOSBooks/Data/Models/BooksList.swift @@ -0,0 +1,18 @@ +// +// BooksList.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 28/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import Foundation + +class BooksList: Codable { + let items: [Item]? + let totalItems : Int? + + enum CodingKeys: String, CodingKey { + case items, totalItems + } +} diff --git a/iOSBooks/iOSBooks/Data/Models/ImageLinks.swift b/iOSBooks/iOSBooks/Data/Models/ImageLinks.swift new file mode 100644 index 0000000..3f19a98 --- /dev/null +++ b/iOSBooks/iOSBooks/Data/Models/ImageLinks.swift @@ -0,0 +1,19 @@ +// +// ImageLinks.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 28/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import Foundation + +class ImageLinks: Codable { + let smallThumbnail: String? + let thumbnail: String? + + enum CodingKeys: String, CodingKey { + case smallThumbnail, thumbnail + } + +} diff --git a/iOSBooks/iOSBooks/Data/Models/Item.swift b/iOSBooks/iOSBooks/Data/Models/Item.swift new file mode 100644 index 0000000..b06c4ed --- /dev/null +++ b/iOSBooks/iOSBooks/Data/Models/Item.swift @@ -0,0 +1,21 @@ +// +// Item.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 28/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import Foundation + +class Item: Codable { + let id: String? + let book: Book? + let salesInfo : SalesInfo? + + enum CodingKeys: String, CodingKey { + case book = "volumeInfo" + case id + case salesInfo = "saleInfo" + } +} diff --git a/iOSBooks/iOSBooks/Data/Models/SalesInfo.swift b/iOSBooks/iOSBooks/Data/Models/SalesInfo.swift new file mode 100644 index 0000000..1a395db --- /dev/null +++ b/iOSBooks/iOSBooks/Data/Models/SalesInfo.swift @@ -0,0 +1,14 @@ +// +// SalesInfo.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 28/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import Foundation + +class SalesInfo: Codable { + let country: String? + let buyLink: String? +} diff --git a/iOSBooks/iOSBooks/Data/Network/APIClient.swift b/iOSBooks/iOSBooks/Data/Network/APIClient.swift new file mode 100644 index 0000000..e5d2682 --- /dev/null +++ b/iOSBooks/iOSBooks/Data/Network/APIClient.swift @@ -0,0 +1,61 @@ +// +// APIClient.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 28/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import Foundation +import PromiseKit + +enum CustomError: Error { + case connectionError(String) + case mappingError(String) + case APIError(String) + case generalError(String) + case deallocatedClass(String) +} + +class APIClient { + + let connectionChecker: Reachability + let decoder: JSONDecoder + let session: URLSession + + init(connectionChecker: Reachability = Reachability(), decoder: JSONDecoder = JSONDecoder(), session: URLSession = URLSession.shared) { + self.connectionChecker = connectionChecker + self.decoder = decoder + self.session = session + } + + func request(model: T.Type, _ request: URLRequest) -> Promise { + return Promise { [weak self] seal in + guard let _self = self else { + print("Class deinitialized!") + return + } + + guard connectionChecker.isConnected else { + seal.reject(CustomError.connectionError("Por favor, verifique sua conexão com a internet!")) + return + } + + session.dataTask(with: request, completionHandler: { (data, response, error) in + guard let data = data else { + seal.reject(CustomError.APIError(error?.localizedDescription ?? "Ocorreu um erro inesperado, por favor, tente novamente!")) + return + } + + guard let model = try? _self.decoder.decode(T.self, from: data) else { + seal.reject(CustomError.mappingError("Houve um erro ao fazer o parse do modelo \(T.self)!")) + return + } + + seal.fulfill(model) + }).resume() + } + } + +} + diff --git a/iOSBooks/iOSBooks/Data/Network/Endpoint.swift b/iOSBooks/iOSBooks/Data/Network/Endpoint.swift new file mode 100644 index 0000000..61de764 --- /dev/null +++ b/iOSBooks/iOSBooks/Data/Network/Endpoint.swift @@ -0,0 +1,18 @@ +// +// Endpoint.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 28/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import Foundation + +protocol Endpoint { + var path: String { get } + var headers: [String: String]? { get } + var body: Data? { get } + var httpMethod: String { get } + var request: URLRequest { get } + var queryItems: [URLQueryItem]? { get } +} diff --git a/iOSBooks/iOSBooks/Data/Network/Reachability.swift b/iOSBooks/iOSBooks/Data/Network/Reachability.swift new file mode 100644 index 0000000..7657b05 --- /dev/null +++ b/iOSBooks/iOSBooks/Data/Network/Reachability.swift @@ -0,0 +1,36 @@ +// +// Reachability.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 28/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import Foundation +import SystemConfiguration + +class Reachability { + var isConnected: Bool { + var zeroAddress = sockaddr_in() + zeroAddress.sin_len = UInt8(MemoryLayout.size) + zeroAddress.sin_family = sa_family_t(AF_INET) + + guard let defaultRouteReachability = withUnsafePointer(to: &zeroAddress, { + $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { + SCNetworkReachabilityCreateWithAddress(nil, $0) + } + }) else { + return false + } + + var flags: SCNetworkReachabilityFlags = [] + if !SCNetworkReachabilityGetFlags(defaultRouteReachability, &flags) { + return false + } + + let isReachable = flags.contains(.reachable) + let needsConnection = flags.contains(.connectionRequired) + + return (isReachable && !needsConnection) + } +} diff --git a/iOSBooks/iOSBooks/ViewController.swift b/iOSBooks/iOSBooks/ViewController.swift index da791b1..9d3dfa9 100644 --- a/iOSBooks/iOSBooks/ViewController.swift +++ b/iOSBooks/iOSBooks/ViewController.swift @@ -7,14 +7,9 @@ // import UIKit +import PromiseKit class ViewController: UIViewController { - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view. - } - - } From 98154e47cb854b798d58987cd07fc58bb981ccdd Mon Sep 17 00:00:00 2001 From: Guilherme Antunes Date: Wed, 28 Aug 2019 14:50:21 -0300 Subject: [PATCH 02/14] refactor(endpoint): including starting index on endpoint build --- iOSBooks/iOSBooks.xcodeproj/project.pbxproj | 4 +++- iOSBooks/iOSBooks/Data/Clients/BooksClient.swift | 2 +- iOSBooks/iOSBooks/Data/Endpoints/BooksAPI.swift | 11 +++++++++-- .../Data/{Network => Endpoints}/Endpoint.swift | 0 4 files changed, 13 insertions(+), 4 deletions(-) rename iOSBooks/iOSBooks/Data/{Network => Endpoints}/Endpoint.swift (100%) diff --git a/iOSBooks/iOSBooks.xcodeproj/project.pbxproj b/iOSBooks/iOSBooks.xcodeproj/project.pbxproj index 4ee20cf..2d57161 100644 --- a/iOSBooks/iOSBooks.xcodeproj/project.pbxproj +++ b/iOSBooks/iOSBooks.xcodeproj/project.pbxproj @@ -164,6 +164,7 @@ 7447D3732316F18400E01BD3 /* Endpoints */ = { isa = PBXGroup; children = ( + 7447D3632316E02500E01BD3 /* Endpoint.swift */, 7447D3612316DECA00E01BD3 /* BooksAPI.swift */, ); path = Endpoints; @@ -174,7 +175,6 @@ children = ( 7447D35D2316C91700E01BD3 /* APIClient.swift */, 7447D35F2316CB1200E01BD3 /* Reachability.swift */, - 7447D3632316E02500E01BD3 /* Endpoint.swift */, ); path = Network; sourceTree = ""; @@ -602,6 +602,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 7AH86L34EQ; INFOPLIST_FILE = iOSBooksTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -624,6 +625,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 7AH86L34EQ; INFOPLIST_FILE = iOSBooksTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/iOSBooks/iOSBooks/Data/Clients/BooksClient.swift b/iOSBooks/iOSBooks/Data/Clients/BooksClient.swift index 3e291a3..c541b85 100644 --- a/iOSBooks/iOSBooks/Data/Clients/BooksClient.swift +++ b/iOSBooks/iOSBooks/Data/Clients/BooksClient.swift @@ -18,6 +18,6 @@ class BooksClient { } func fetchBooksList() -> Promise { - return apiClient.request(model: BooksList.self, BooksAPI.list.request) + return apiClient.request(model: BooksList.self, BooksAPI.list(startingIndex: 0).request) } } diff --git a/iOSBooks/iOSBooks/Data/Endpoints/BooksAPI.swift b/iOSBooks/iOSBooks/Data/Endpoints/BooksAPI.swift index e598eb0..70390e6 100644 --- a/iOSBooks/iOSBooks/Data/Endpoints/BooksAPI.swift +++ b/iOSBooks/iOSBooks/Data/Endpoints/BooksAPI.swift @@ -10,7 +10,7 @@ import Foundation enum BooksAPI: Endpoint { - case list + case list(startingIndex: Int) var path: String { switch self { @@ -19,6 +19,13 @@ enum BooksAPI: Endpoint { } } + var startingIndex: Int { + switch self { + case .list(let index): + return index + } + } + var headers: [String : String]? { switch self { case .list: @@ -45,7 +52,7 @@ enum BooksAPI: Endpoint { case .list: let subjectItem = URLQueryItem(name: "q", value: "ios") let maxResultsItem = URLQueryItem(name: "maxResults", value: "20") - let startIndexItem = URLQueryItem(name: "startIndex", value: "0") + let startIndexItem = URLQueryItem(name: "startIndex", value: String(startingIndex)) return [subjectItem, startIndexItem, maxResultsItem] } } diff --git a/iOSBooks/iOSBooks/Data/Network/Endpoint.swift b/iOSBooks/iOSBooks/Data/Endpoints/Endpoint.swift similarity index 100% rename from iOSBooks/iOSBooks/Data/Network/Endpoint.swift rename to iOSBooks/iOSBooks/Data/Endpoints/Endpoint.swift From 6b7d67d2e00e07ab252341115b3cf878c3122fd5 Mon Sep 17 00:00:00 2001 From: Guilherme Antunes Date: Fri, 30 Aug 2019 14:53:27 -0300 Subject: [PATCH 03/14] feat(bookslist): adding books list screen feature --- iOSBooks/Podfile | 1 + iOSBooks/iOSBooks.xcodeproj/project.pbxproj | 104 +++++++++++++++--- iOSBooks/iOSBooks/AppDelegate.swift | 19 +++- iOSBooks/iOSBooks/Base.lproj/Main.storyboard | 24 ---- .../BooksList/View/BooksList.storyboard | 73 ++++++++++++ ...oksListViewController+CollectionView.swift | 23 ++++ .../View/BooksListViewController+View.swift | 30 +++++ .../View/BooksListViewController.swift | 22 ++++ .../BooksList/View/Subviews/BooksCell.swift | 30 +++++ .../ViewModel/BooksListViewModel.swift | 57 ++++++++++ .../AppCoordinator/AppCoordinator.swift | 29 +++++ .../AppCoordinatorDependencyInjector.swift | 29 +++++ .../iOSBooks/Data/Clients/BooksClient.swift | 4 +- iOSBooks/iOSBooks/Identifiable.swift | 71 ++++++++++++ iOSBooks/iOSBooks/Info.plist | 9 +- .../UIViewController+QuickInstance.swift | 47 ++++++++ iOSBooks/iOSBooks/ViewController.swift | 15 --- 17 files changed, 523 insertions(+), 64 deletions(-) delete mode 100644 iOSBooks/iOSBooks/Base.lproj/Main.storyboard create mode 100644 iOSBooks/iOSBooks/BooksList/View/BooksList.storyboard create mode 100644 iOSBooks/iOSBooks/BooksList/View/BooksListViewController+CollectionView.swift create mode 100644 iOSBooks/iOSBooks/BooksList/View/BooksListViewController+View.swift create mode 100644 iOSBooks/iOSBooks/BooksList/View/BooksListViewController.swift create mode 100644 iOSBooks/iOSBooks/BooksList/View/Subviews/BooksCell.swift create mode 100644 iOSBooks/iOSBooks/BooksList/ViewModel/BooksListViewModel.swift create mode 100644 iOSBooks/iOSBooks/Coordinators/AppCoordinator/AppCoordinator.swift create mode 100644 iOSBooks/iOSBooks/Coordinators/AppCoordinator/AppCoordinatorDependencyInjector.swift create mode 100644 iOSBooks/iOSBooks/Identifiable.swift create mode 100644 iOSBooks/iOSBooks/UIViewController+QuickInstance.swift delete mode 100644 iOSBooks/iOSBooks/ViewController.swift diff --git a/iOSBooks/Podfile b/iOSBooks/Podfile index 21ec943..a563546 100644 --- a/iOSBooks/Podfile +++ b/iOSBooks/Podfile @@ -8,6 +8,7 @@ target 'iOSBooks' do # Pods for iOSBooks pod 'PromiseKit' pod 'IQKeyboardManagerSwift' + pod 'Kingfisher', "~>5.1" target 'iOSBooksTests' do inherit! :search_paths diff --git a/iOSBooks/iOSBooks.xcodeproj/project.pbxproj b/iOSBooks/iOSBooks.xcodeproj/project.pbxproj index 2d57161..ed6db10 100644 --- a/iOSBooks/iOSBooks.xcodeproj/project.pbxproj +++ b/iOSBooks/iOSBooks.xcodeproj/project.pbxproj @@ -8,8 +8,6 @@ /* Begin PBXBuildFile section */ 0E3964D82314BB470093738B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3964D72314BB470093738B /* AppDelegate.swift */; }; - 0E3964DA2314BB470093738B /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3964D92314BB470093738B /* ViewController.swift */; }; - 0E3964DD2314BB470093738B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0E3964DB2314BB470093738B /* Main.storyboard */; }; 0E3964DF2314BB4A0093738B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0E3964DE2314BB4A0093738B /* Assets.xcassets */; }; 0E3964E22314BB4A0093738B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0E3964E02314BB4A0093738B /* LaunchScreen.storyboard */; }; 0E3964ED2314BB4A0093738B /* iOSBooksTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3964EC2314BB4A0093738B /* iOSBooksTests.swift */; }; @@ -24,6 +22,16 @@ 7447D36C2316F12800E01BD3 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7447D36B2316F12800E01BD3 /* Item.swift */; }; 7447D36E2316F13A00E01BD3 /* ImageLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7447D36D2316F13A00E01BD3 /* ImageLinks.swift */; }; 7447D3702316F14D00E01BD3 /* SalesInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7447D36F2316F14D00E01BD3 /* SalesInfo.swift */; }; + 7447D37A2316F71200E01BD3 /* BooksListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7447D3792316F71200E01BD3 /* BooksListViewModel.swift */; }; + 7447D37C2316F71D00E01BD3 /* BooksListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7447D37B2316F71D00E01BD3 /* BooksListViewController.swift */; }; + 7447D37E2316F72A00E01BD3 /* BooksList.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7447D37D2316F72A00E01BD3 /* BooksList.storyboard */; }; + 74ADE9B823196A7A006D2644 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ADE9B723196A7A006D2644 /* AppCoordinator.swift */; }; + 74ADE9BE23196CE3006D2644 /* AppCoordinatorDependencyInjector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ADE9BD23196CE3006D2644 /* AppCoordinatorDependencyInjector.swift */; }; + 74ADE9C023196E44006D2644 /* UIViewController+QuickInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ADE9BF23196E44006D2644 /* UIViewController+QuickInstance.swift */; }; + 74ADE9C223196E99006D2644 /* Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ADE9C123196E99006D2644 /* Identifiable.swift */; }; + 74ADE9C52319730F006D2644 /* BooksCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ADE9C42319730F006D2644 /* BooksCell.swift */; }; + 74ADE9C7231973B5006D2644 /* BooksListViewController+CollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ADE9C6231973B5006D2644 /* BooksListViewController+CollectionView.swift */; }; + 74ADE9C9231981F5006D2644 /* BooksListViewController+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ADE9C8231981F5006D2644 /* BooksListViewController+View.swift */; }; 768F74E39672B1F50C632913 /* Pods_iOSBooks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2D2FDB2EC5907498F6A3C173 /* Pods_iOSBooks.framework */; }; /* End PBXBuildFile section */ @@ -40,8 +48,6 @@ /* Begin PBXFileReference section */ 0E3964D42314BB470093738B /* iOSBooks.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iOSBooks.app; sourceTree = BUILT_PRODUCTS_DIR; }; 0E3964D72314BB470093738B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 0E3964D92314BB470093738B /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - 0E3964DC2314BB470093738B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 0E3964DE2314BB4A0093738B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 0E3964E12314BB4A0093738B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 0E3964E32314BB4A0093738B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -63,6 +69,16 @@ 7447D36B2316F12800E01BD3 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = ""; }; 7447D36D2316F13A00E01BD3 /* ImageLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageLinks.swift; sourceTree = ""; }; 7447D36F2316F14D00E01BD3 /* SalesInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SalesInfo.swift; sourceTree = ""; }; + 7447D3792316F71200E01BD3 /* BooksListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BooksListViewModel.swift; sourceTree = ""; }; + 7447D37B2316F71D00E01BD3 /* BooksListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BooksListViewController.swift; sourceTree = ""; }; + 7447D37D2316F72A00E01BD3 /* BooksList.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = BooksList.storyboard; sourceTree = ""; }; + 74ADE9B723196A7A006D2644 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; + 74ADE9BD23196CE3006D2644 /* AppCoordinatorDependencyInjector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorDependencyInjector.swift; sourceTree = ""; }; + 74ADE9BF23196E44006D2644 /* UIViewController+QuickInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+QuickInstance.swift"; sourceTree = ""; }; + 74ADE9C123196E99006D2644 /* Identifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Identifiable.swift; sourceTree = ""; }; + 74ADE9C42319730F006D2644 /* BooksCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BooksCell.swift; sourceTree = ""; }; + 74ADE9C6231973B5006D2644 /* BooksListViewController+CollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BooksListViewController+CollectionView.swift"; sourceTree = ""; }; + 74ADE9C8231981F5006D2644 /* BooksListViewController+View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BooksListViewController+View.swift"; sourceTree = ""; }; E6741F045EBC12BB315EE1CB /* Pods-iOSBooks.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOSBooks.debug.xcconfig"; path = "Target Support Files/Pods-iOSBooks/Pods-iOSBooks.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -109,13 +125,15 @@ 0E3964D62314BB470093738B /* iOSBooks */ = { isa = PBXGroup; children = ( + 74ADE9B5231969F0006D2644 /* Coordinators */, + 7447D3762316F6C700E01BD3 /* BooksList */, 7447D3712316F16E00E01BD3 /* Data */, 0E3964D72314BB470093738B /* AppDelegate.swift */, - 0E3964D92314BB470093738B /* ViewController.swift */, - 0E3964DB2314BB470093738B /* Main.storyboard */, 0E3964DE2314BB4A0093738B /* Assets.xcassets */, 0E3964E02314BB4A0093738B /* LaunchScreen.storyboard */, 0E3964E32314BB4A0093738B /* Info.plist */, + 74ADE9BF23196E44006D2644 /* UIViewController+QuickInstance.swift */, + 74ADE9C123196E99006D2644 /* Identifiable.swift */, ); path = iOSBooks; sourceTree = ""; @@ -187,6 +205,60 @@ path = Clients; sourceTree = ""; }; + 7447D3762316F6C700E01BD3 /* BooksList */ = { + isa = PBXGroup; + children = ( + 7447D3782316F6D700E01BD3 /* View */, + 7447D3772316F6CF00E01BD3 /* ViewModel */, + ); + path = BooksList; + sourceTree = ""; + }; + 7447D3772316F6CF00E01BD3 /* ViewModel */ = { + isa = PBXGroup; + children = ( + 7447D3792316F71200E01BD3 /* BooksListViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 7447D3782316F6D700E01BD3 /* View */ = { + isa = PBXGroup; + children = ( + 7447D37D2316F72A00E01BD3 /* BooksList.storyboard */, + 7447D37B2316F71D00E01BD3 /* BooksListViewController.swift */, + 74ADE9C6231973B5006D2644 /* BooksListViewController+CollectionView.swift */, + 74ADE9C8231981F5006D2644 /* BooksListViewController+View.swift */, + 74ADE9C3231972ED006D2644 /* Subviews */, + ); + path = View; + sourceTree = ""; + }; + 74ADE9B5231969F0006D2644 /* Coordinators */ = { + isa = PBXGroup; + children = ( + 74ADE9B6231969F7006D2644 /* AppCoordinator */, + ); + path = Coordinators; + sourceTree = ""; + }; + 74ADE9B6231969F7006D2644 /* AppCoordinator */ = { + isa = PBXGroup; + children = ( + 74ADE9B723196A7A006D2644 /* AppCoordinator.swift */, + 74ADE9BD23196CE3006D2644 /* AppCoordinatorDependencyInjector.swift */, + ); + path = AppCoordinator; + sourceTree = ""; + }; + 74ADE9C3231972ED006D2644 /* Subviews */ = { + isa = PBXGroup; + children = ( + 74ADE9C42319730F006D2644 /* BooksCell.swift */, + ); + path = Subviews; + sourceTree = ""; + }; E4186846F20EA500EF61D605 /* Pods */ = { isa = PBXGroup; children = ( @@ -284,8 +356,8 @@ buildActionMask = 2147483647; files = ( 0E3964E22314BB4A0093738B /* LaunchScreen.storyboard in Resources */, + 7447D37E2316F72A00E01BD3 /* BooksList.storyboard in Resources */, 0E3964DF2314BB4A0093738B /* Assets.xcassets in Resources */, - 0E3964DD2314BB470093738B /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -384,12 +456,20 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 74ADE9C9231981F5006D2644 /* BooksListViewController+View.swift in Sources */, + 74ADE9C223196E99006D2644 /* Identifiable.swift in Sources */, 7447D36C2316F12800E01BD3 /* Item.swift in Sources */, 7447D3682316E57500E01BD3 /* Book.swift in Sources */, + 74ADE9BE23196CE3006D2644 /* AppCoordinatorDependencyInjector.swift in Sources */, 7447D3702316F14D00E01BD3 /* SalesInfo.swift in Sources */, + 7447D37A2316F71200E01BD3 /* BooksListViewModel.swift in Sources */, 7447D36E2316F13A00E01BD3 /* ImageLinks.swift in Sources */, - 0E3964DA2314BB470093738B /* ViewController.swift in Sources */, + 7447D37C2316F71D00E01BD3 /* BooksListViewController.swift in Sources */, + 74ADE9C7231973B5006D2644 /* BooksListViewController+CollectionView.swift in Sources */, + 74ADE9C52319730F006D2644 /* BooksCell.swift in Sources */, + 74ADE9B823196A7A006D2644 /* AppCoordinator.swift in Sources */, 0E3964D82314BB470093738B /* AppDelegate.swift in Sources */, + 74ADE9C023196E44006D2644 /* UIViewController+QuickInstance.swift in Sources */, 7447D3602316CB1200E01BD3 /* Reachability.swift in Sources */, 7447D35E2316C91700E01BD3 /* APIClient.swift in Sources */, 7447D3642316E02500E01BD3 /* Endpoint.swift in Sources */, @@ -418,14 +498,6 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ - 0E3964DB2314BB470093738B /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 0E3964DC2314BB470093738B /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; 0E3964E02314BB4A0093738B /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( diff --git a/iOSBooks/iOSBooks/AppDelegate.swift b/iOSBooks/iOSBooks/AppDelegate.swift index cc5fb9d..4e75ef8 100644 --- a/iOSBooks/iOSBooks/AppDelegate.swift +++ b/iOSBooks/iOSBooks/AppDelegate.swift @@ -6,16 +6,27 @@ // Copyright © 2019 Guilherme Antunes. All rights reserved. // +import IQKeyboardManagerSwift import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - - + var coordinator: AppCoordinator? + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. + + window = UIWindow(frame: UIScreen.main.bounds) + guard let window = window else { + print("window is unexpectedly nil") + return false + } + IQKeyboardManager.shared.enable = true + IQKeyboardManager.shared.shouldShowToolbarPlaceholder = false + coordinator = AppCoordinator(window: window) + coordinator?.start() + return true } @@ -38,7 +49,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } func applicationWillTerminate(_ application: UIApplication) { - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + window = nil } diff --git a/iOSBooks/iOSBooks/Base.lproj/Main.storyboard b/iOSBooks/iOSBooks/Base.lproj/Main.storyboard deleted file mode 100644 index f1bcf38..0000000 --- a/iOSBooks/iOSBooks/Base.lproj/Main.storyboard +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/iOSBooks/iOSBooks/BooksList/View/BooksList.storyboard b/iOSBooks/iOSBooks/BooksList/View/BooksList.storyboard new file mode 100644 index 0000000..511c79e --- /dev/null +++ b/iOSBooks/iOSBooks/BooksList/View/BooksList.storyboard @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOSBooks/iOSBooks/BooksList/View/BooksListViewController+CollectionView.swift b/iOSBooks/iOSBooks/BooksList/View/BooksListViewController+CollectionView.swift new file mode 100644 index 0000000..7ac7c72 --- /dev/null +++ b/iOSBooks/iOSBooks/BooksList/View/BooksListViewController+CollectionView.swift @@ -0,0 +1,23 @@ +// +// BooksListViewController+CollectionView.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 30/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import UIKit + +extension BooksListViewController: UICollectionViewDataSource, UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return viewModel?.numberOfItemsInSection() ?? 0 + } + + func numberOfSections(in collectionView: UICollectionView) -> Int { + return viewModel?.numberOfSections() ?? 0 + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + return viewModel?.cellForItem(inCollectionView: collectionView, atIndexPath: indexPath) ?? UICollectionViewCell() + } +} diff --git a/iOSBooks/iOSBooks/BooksList/View/BooksListViewController+View.swift b/iOSBooks/iOSBooks/BooksList/View/BooksListViewController+View.swift new file mode 100644 index 0000000..e2ccd98 --- /dev/null +++ b/iOSBooks/iOSBooks/BooksList/View/BooksListViewController+View.swift @@ -0,0 +1,30 @@ +// +// BooksListViewController+View.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 30/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import UIKit + +protocol BooksListViewControllerPresentable { + func reloadView() + func presentError(message: String) +} + +extension BooksListViewController: BooksListViewControllerPresentable { + func reloadView() { + booksCollectionView?.reloadData() + } + + func presentError(message: String) { + alert(message: message) + } + + func setupCollectionView() { + title = "iOS Books" + booksCollectionView?.dataSource = self + booksCollectionView?.delegate = self + } +} diff --git a/iOSBooks/iOSBooks/BooksList/View/BooksListViewController.swift b/iOSBooks/iOSBooks/BooksList/View/BooksListViewController.swift new file mode 100644 index 0000000..7661e23 --- /dev/null +++ b/iOSBooks/iOSBooks/BooksList/View/BooksListViewController.swift @@ -0,0 +1,22 @@ +// +// BooksListViewController.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 28/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import UIKit + +class BooksListViewController: UIViewController { + + // MARK: - Properties + var viewModel: BooksListViewModelProtocol? + @IBOutlet weak var booksCollectionView: UICollectionView? + + // MARK: - View Life Cycle + override func viewDidLoad() { + setupCollectionView() + viewModel?.loadBooks() + } +} diff --git a/iOSBooks/iOSBooks/BooksList/View/Subviews/BooksCell.swift b/iOSBooks/iOSBooks/BooksList/View/Subviews/BooksCell.swift new file mode 100644 index 0000000..da5e82c --- /dev/null +++ b/iOSBooks/iOSBooks/BooksList/View/Subviews/BooksCell.swift @@ -0,0 +1,30 @@ +// +// BooksCell.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 30/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import Kingfisher +import UIKit + +class BooksCell: UICollectionViewCell { + @IBOutlet weak var bookImageView: UIImageView? + + func setup(withBook book: Book?) { + if let book = book, let bookURLString = book.imageLinks?.thumbnail, let url = URL(string: bookURLString) { + bookImageView?.kf.setImage(with: url) + } + } + + override func prepareForReuse() { + super.prepareForReuse() + bookImageView?.kf.cancelDownloadTask() + } + + deinit { + bookImageView?.kf.cancelDownloadTask() + } + +} diff --git a/iOSBooks/iOSBooks/BooksList/ViewModel/BooksListViewModel.swift b/iOSBooks/iOSBooks/BooksList/ViewModel/BooksListViewModel.swift new file mode 100644 index 0000000..d913009 --- /dev/null +++ b/iOSBooks/iOSBooks/BooksList/ViewModel/BooksListViewModel.swift @@ -0,0 +1,57 @@ +// +// BooksListViewModel.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 28/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import UIKit + +protocol BooksListViewModelProtocol { + func loadBooks() + func numberOfSections() -> Int + func numberOfItemsInSection() -> Int + func cellForItem(inCollectionView collectionView: UICollectionView, atIndexPath indexPath: IndexPath) -> UICollectionViewCell +} + +class BooksListViewModel: BooksListViewModelProtocol { + + var items: [Item] = [] + var service = BooksClient() + var startingIndex = 0 + var view: BooksListViewControllerPresentable? + + func numberOfSections() -> Int { + return 1 + } + + func numberOfItemsInSection() -> Int { + return items.count + } + + func cellForItem(inCollectionView collectionView: UICollectionView, atIndexPath indexPath: IndexPath) -> UICollectionViewCell { + let cell: BooksCell = collectionView.dequeueReusableCell(for: indexPath) + cell.setup(withBook: items[indexPath.item].book) + if indexPath.item == (items.count - 1) { + loadBooks() + } + return cell + } + + func loadBooks() { + service.fetchBooksList(startingIndex: startingIndex).done { [weak self] (list) in + guard let self = self, let items = list.items else { return } + self.items.append(contentsOf: items) + self.startingIndex = items.count + DispatchQueue.main.async { + self.view?.reloadView() + } + }.catch { (error) in + DispatchQueue.main.async { + self.view?.presentError(message: error.localizedDescription) + } + } + + } +} diff --git a/iOSBooks/iOSBooks/Coordinators/AppCoordinator/AppCoordinator.swift b/iOSBooks/iOSBooks/Coordinators/AppCoordinator/AppCoordinator.swift new file mode 100644 index 0000000..f0129ef --- /dev/null +++ b/iOSBooks/iOSBooks/Coordinators/AppCoordinator/AppCoordinator.swift @@ -0,0 +1,29 @@ +// +// AppCoordinator.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 30/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import UIKit + +class AppCoordinator { + + lazy var injector = AppCoordinatorDependencyInjector() + var window: UIWindow + + init(window: UIWindow) { + self.window = window + } + + func start() { + setupNavigationController() + window.rootViewController = injector.navigationController + window.makeKeyAndVisible() + } + + func setupNavigationController() { + injector.navigationController.viewControllers.append(injector.booksListViewController) + } +} diff --git a/iOSBooks/iOSBooks/Coordinators/AppCoordinator/AppCoordinatorDependencyInjector.swift b/iOSBooks/iOSBooks/Coordinators/AppCoordinator/AppCoordinatorDependencyInjector.swift new file mode 100644 index 0000000..4ed7ff9 --- /dev/null +++ b/iOSBooks/iOSBooks/Coordinators/AppCoordinator/AppCoordinatorDependencyInjector.swift @@ -0,0 +1,29 @@ +// +// AppCoordinatorDependencyInjector.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 30/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import UIKit + +class AppCoordinatorDependencyInjector { + lazy var navigationController: UINavigationController = { + let navigation = UINavigationController() + navigation.navigationBar.prefersLargeTitles = true + navigation.navigationBar.barStyle = .black + return navigation + }() + + lazy var booksListViewController: BooksListViewController = { + let controller: BooksListViewController = BooksListViewController.instantiate() + booksListViewModel.view = controller + controller.viewModel = booksListViewModel + return controller + }() + + lazy var booksListViewModel: BooksListViewModel = { + return BooksListViewModel() + }() +} diff --git a/iOSBooks/iOSBooks/Data/Clients/BooksClient.swift b/iOSBooks/iOSBooks/Data/Clients/BooksClient.swift index c541b85..224d3e3 100644 --- a/iOSBooks/iOSBooks/Data/Clients/BooksClient.swift +++ b/iOSBooks/iOSBooks/Data/Clients/BooksClient.swift @@ -17,7 +17,7 @@ class BooksClient { self.apiClient = apiClient } - func fetchBooksList() -> Promise { - return apiClient.request(model: BooksList.self, BooksAPI.list(startingIndex: 0).request) + func fetchBooksList(startingIndex index: Int) -> Promise { + return apiClient.request(model: BooksList.self, BooksAPI.list(startingIndex: index).request) } } diff --git a/iOSBooks/iOSBooks/Identifiable.swift b/iOSBooks/iOSBooks/Identifiable.swift new file mode 100644 index 0000000..a3ff390 --- /dev/null +++ b/iOSBooks/iOSBooks/Identifiable.swift @@ -0,0 +1,71 @@ +// +// Identifiable.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 30/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import UIKit + +protocol Identifiable: class {} + +extension Identifiable where Self: UIViewController { + static var storyboardIdentifier: String { + return String(describing: self) + } + static var xibIdentifier: String { + return String(describing: self) + } +} + +extension Identifiable where Self: UITableViewCell { + static var reuseIdentifier: String { + return String(describing: self) + } +} + +extension Identifiable where Self: UICollectionViewCell { + static var reuseIdentifier: String { + return String(describing: self) + } +} + +extension Identifiable where Self: UITableViewHeaderFooterView { + static var reuseIdentifier: String { + return String(describing: self) + } +} + + +extension UICollectionView { + func dequeueReusableCell(for indexPath: IndexPath) -> T { + guard let cell = dequeueReusableCell(withReuseIdentifier: T.reuseIdentifier, for: indexPath) as? T else { + // TODO: set crashlytics to warn here + fatalError("Could not dequeue cell with identifier: \(T.reuseIdentifier)") + } + return cell + } +} + +extension UITableView { + func dequeueReusableCell(for indexPath: IndexPath) -> T { + guard let cell = dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as? T else { + fatalError("Could not dequeue cell with identifier: \(T.reuseIdentifier)") + } + return cell + } + + func dequeueReusableHeaderFooterView() -> T { + guard let headerFooterView = dequeueReusableHeaderFooterView(withIdentifier: T.reuseIdentifier) as? T else { + fatalError("Could not dequeue cell with identifier: \(T.reuseIdentifier)") + } + return headerFooterView + } +} + +extension UICollectionViewCell: Identifiable {} +extension UITableViewCell: Identifiable {} +extension UIViewController: Identifiable {} +extension UITableViewHeaderFooterView: Identifiable {} + diff --git a/iOSBooks/iOSBooks/Info.plist b/iOSBooks/iOSBooks/Info.plist index 16be3b6..1c45a97 100644 --- a/iOSBooks/iOSBooks/Info.plist +++ b/iOSBooks/iOSBooks/Info.plist @@ -2,6 +2,11 @@ + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable @@ -23,7 +28,7 @@ UILaunchStoryboardName LaunchScreen UIMainStoryboardFile - Main + BooksList UIRequiredDeviceCapabilities armv7 @@ -31,8 +36,6 @@ UISupportedInterfaceOrientations UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad diff --git a/iOSBooks/iOSBooks/UIViewController+QuickInstance.swift b/iOSBooks/iOSBooks/UIViewController+QuickInstance.swift new file mode 100644 index 0000000..3ccb0ed --- /dev/null +++ b/iOSBooks/iOSBooks/UIViewController+QuickInstance.swift @@ -0,0 +1,47 @@ +// +// UIViewController+QuickInstance.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 30/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// +import UIKit + +extension UIViewController { + + static func instantiate() -> T { + guard let controller = UIStoryboard(name: T.storyboardIdentifier.replacingOccurrences(of: "Controller", with: "").replacingOccurrences(of: "View", with: ""), bundle: T.bundle).instantiateViewController(withIdentifier: T.storyboardIdentifier) as? T else { + fatalError("failed to create storyboard")} + return controller + } + + static func instantiateFromXIB() -> T { + return T(nibName: T.xibIdentifier.replacingOccurrences(of: "Controller", with: "").replacingOccurrences(of: "View", with: ""), bundle: .main) + } + + static var bundle: Bundle { + return Bundle(for: self) + } +} + +extension UIViewController { + + func setBackButton(_ backFunction: Selector) { +// navigationItem.hidesBackButton = true +// let newBackButton = UIBarButtonItem(image: UIImage(named: "back_arrow"), style: .plain, target: self, action: backFunction) +// newBackButton.title = "Voltar" +// navigationItem.leftBarButtonItem = newBackButton + } + +} + +extension UIViewController { + + func alert(title: String = "", message: String, completion: (() -> Void)? = nil, okActionHandler: ((UIAlertAction) -> Void)? = nil) { + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + let OKAction = UIAlertAction(title: "OK", style: .default, handler: okActionHandler) + alertController.addAction(OKAction) + present(alertController, animated: true, completion: completion) + } + +} diff --git a/iOSBooks/iOSBooks/ViewController.swift b/iOSBooks/iOSBooks/ViewController.swift deleted file mode 100644 index 9d3dfa9..0000000 --- a/iOSBooks/iOSBooks/ViewController.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// ViewController.swift -// iOSBooks -// -// Created by Guilherme Antunes on 26/08/19. -// Copyright © 2019 Guilherme Antunes. All rights reserved. -// - -import UIKit -import PromiseKit - -class ViewController: UIViewController { - -} - From 15124dfbb911269974672fc649f515417383d004 Mon Sep 17 00:00:00 2001 From: Guilherme Antunes Date: Fri, 30 Aug 2019 16:17:27 -0300 Subject: [PATCH 04/14] feat(book detail): adding book detail screen --- iOSBooks/iOSBooks.xcodeproj/project.pbxproj | 40 +++++++++ .../back_arrow.imageset/Contents.json | 23 +++++ .../back_arrow.imageset/back_arrow.png | Bin 0 -> 254 bytes .../back_arrow.imageset/back_arrow@2x.png | Bin 0 -> 315 bytes .../back_arrow.imageset/back_arrow@3x.png | Bin 0 -> 446 bytes .../BookDetail/View/BookDetail.storyboard | 83 ++++++++++++++++++ .../View/BookDetailViewController+View.swift | 32 +++++++ .../View/BookDetailViewController.swift | 38 ++++++++ .../ViewModel/BookDetailViewModel.swift | 66 ++++++++++++++ .../BooksList/View/BooksList.storyboard | 1 + ...oksListViewController+CollectionView.swift | 4 + .../View/BooksListViewController+View.swift | 6 +- .../View/BooksListViewController.swift | 5 ++ .../BooksList/View/Subviews/BooksCell.swift | 2 +- .../ViewModel/BooksListViewModel.swift | 16 +++- .../AppCoordinator/AppCoordinator.swift | 36 +++++++- .../AppCoordinatorDependencyInjector.swift | 18 ++++ .../UIViewController+QuickInstance.swift | 8 +- 18 files changed, 370 insertions(+), 8 deletions(-) create mode 100644 iOSBooks/iOSBooks/Assets.xcassets/back_arrow.imageset/Contents.json create mode 100644 iOSBooks/iOSBooks/Assets.xcassets/back_arrow.imageset/back_arrow.png create mode 100644 iOSBooks/iOSBooks/Assets.xcassets/back_arrow.imageset/back_arrow@2x.png create mode 100644 iOSBooks/iOSBooks/Assets.xcassets/back_arrow.imageset/back_arrow@3x.png create mode 100644 iOSBooks/iOSBooks/BookDetail/View/BookDetail.storyboard create mode 100644 iOSBooks/iOSBooks/BookDetail/View/BookDetailViewController+View.swift create mode 100644 iOSBooks/iOSBooks/BookDetail/View/BookDetailViewController.swift create mode 100644 iOSBooks/iOSBooks/BookDetail/ViewModel/BookDetailViewModel.swift diff --git a/iOSBooks/iOSBooks.xcodeproj/project.pbxproj b/iOSBooks/iOSBooks.xcodeproj/project.pbxproj index ed6db10..7c3f9ec 100644 --- a/iOSBooks/iOSBooks.xcodeproj/project.pbxproj +++ b/iOSBooks/iOSBooks.xcodeproj/project.pbxproj @@ -32,6 +32,10 @@ 74ADE9C52319730F006D2644 /* BooksCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ADE9C42319730F006D2644 /* BooksCell.swift */; }; 74ADE9C7231973B5006D2644 /* BooksListViewController+CollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ADE9C6231973B5006D2644 /* BooksListViewController+CollectionView.swift */; }; 74ADE9C9231981F5006D2644 /* BooksListViewController+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ADE9C8231981F5006D2644 /* BooksListViewController+View.swift */; }; + 74ADE9D023199C1F006D2644 /* BookDetail.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 74ADE9CF23199C1F006D2644 /* BookDetail.storyboard */; }; + 74ADE9D223199D97006D2644 /* BookDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ADE9D123199D97006D2644 /* BookDetailViewController.swift */; }; + 74B9185223199E8F00ADDEA4 /* BookDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74B9185123199E8F00ADDEA4 /* BookDetailViewModel.swift */; }; + 74B9185423199F4500ADDEA4 /* BookDetailViewController+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74B9185323199F4500ADDEA4 /* BookDetailViewController+View.swift */; }; 768F74E39672B1F50C632913 /* Pods_iOSBooks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2D2FDB2EC5907498F6A3C173 /* Pods_iOSBooks.framework */; }; /* End PBXBuildFile section */ @@ -79,6 +83,10 @@ 74ADE9C42319730F006D2644 /* BooksCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BooksCell.swift; sourceTree = ""; }; 74ADE9C6231973B5006D2644 /* BooksListViewController+CollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BooksListViewController+CollectionView.swift"; sourceTree = ""; }; 74ADE9C8231981F5006D2644 /* BooksListViewController+View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BooksListViewController+View.swift"; sourceTree = ""; }; + 74ADE9CF23199C1F006D2644 /* BookDetail.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = BookDetail.storyboard; sourceTree = ""; }; + 74ADE9D123199D97006D2644 /* BookDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookDetailViewController.swift; sourceTree = ""; }; + 74B9185123199E8F00ADDEA4 /* BookDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookDetailViewModel.swift; sourceTree = ""; }; + 74B9185323199F4500ADDEA4 /* BookDetailViewController+View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BookDetailViewController+View.swift"; sourceTree = ""; }; E6741F045EBC12BB315EE1CB /* Pods-iOSBooks.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOSBooks.debug.xcconfig"; path = "Target Support Files/Pods-iOSBooks/Pods-iOSBooks.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -127,6 +135,7 @@ children = ( 74ADE9B5231969F0006D2644 /* Coordinators */, 7447D3762316F6C700E01BD3 /* BooksList */, + 74ADE9D323199E0F006D2644 /* BookDetail */, 7447D3712316F16E00E01BD3 /* Data */, 0E3964D72314BB470093738B /* AppDelegate.swift */, 0E3964DE2314BB4A0093738B /* Assets.xcassets */, @@ -259,6 +268,33 @@ path = Subviews; sourceTree = ""; }; + 74ADE9D323199E0F006D2644 /* BookDetail */ = { + isa = PBXGroup; + children = ( + 74B9184F23199E6D00ADDEA4 /* View */, + 74B9185023199E7700ADDEA4 /* ViewModel */, + ); + path = BookDetail; + sourceTree = ""; + }; + 74B9184F23199E6D00ADDEA4 /* View */ = { + isa = PBXGroup; + children = ( + 74ADE9CF23199C1F006D2644 /* BookDetail.storyboard */, + 74ADE9D123199D97006D2644 /* BookDetailViewController.swift */, + 74B9185323199F4500ADDEA4 /* BookDetailViewController+View.swift */, + ); + path = View; + sourceTree = ""; + }; + 74B9185023199E7700ADDEA4 /* ViewModel */ = { + isa = PBXGroup; + children = ( + 74B9185123199E8F00ADDEA4 /* BookDetailViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; E4186846F20EA500EF61D605 /* Pods */ = { isa = PBXGroup; children = ( @@ -357,6 +393,7 @@ files = ( 0E3964E22314BB4A0093738B /* LaunchScreen.storyboard in Resources */, 7447D37E2316F72A00E01BD3 /* BooksList.storyboard in Resources */, + 74ADE9D023199C1F006D2644 /* BookDetail.storyboard in Resources */, 0E3964DF2314BB4A0093738B /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -460,6 +497,7 @@ 74ADE9C223196E99006D2644 /* Identifiable.swift in Sources */, 7447D36C2316F12800E01BD3 /* Item.swift in Sources */, 7447D3682316E57500E01BD3 /* Book.swift in Sources */, + 74B9185423199F4500ADDEA4 /* BookDetailViewController+View.swift in Sources */, 74ADE9BE23196CE3006D2644 /* AppCoordinatorDependencyInjector.swift in Sources */, 7447D3702316F14D00E01BD3 /* SalesInfo.swift in Sources */, 7447D37A2316F71200E01BD3 /* BooksListViewModel.swift in Sources */, @@ -467,12 +505,14 @@ 7447D37C2316F71D00E01BD3 /* BooksListViewController.swift in Sources */, 74ADE9C7231973B5006D2644 /* BooksListViewController+CollectionView.swift in Sources */, 74ADE9C52319730F006D2644 /* BooksCell.swift in Sources */, + 74ADE9D223199D97006D2644 /* BookDetailViewController.swift in Sources */, 74ADE9B823196A7A006D2644 /* AppCoordinator.swift in Sources */, 0E3964D82314BB470093738B /* AppDelegate.swift in Sources */, 74ADE9C023196E44006D2644 /* UIViewController+QuickInstance.swift in Sources */, 7447D3602316CB1200E01BD3 /* Reachability.swift in Sources */, 7447D35E2316C91700E01BD3 /* APIClient.swift in Sources */, 7447D3642316E02500E01BD3 /* Endpoint.swift in Sources */, + 74B9185223199E8F00ADDEA4 /* BookDetailViewModel.swift in Sources */, 7447D36A2316F11800E01BD3 /* BooksList.swift in Sources */, 7447D3662316E3C100E01BD3 /* BooksClient.swift in Sources */, 7447D3622316DECA00E01BD3 /* BooksAPI.swift in Sources */, diff --git a/iOSBooks/iOSBooks/Assets.xcassets/back_arrow.imageset/Contents.json b/iOSBooks/iOSBooks/Assets.xcassets/back_arrow.imageset/Contents.json new file mode 100644 index 0000000..b7b948c --- /dev/null +++ b/iOSBooks/iOSBooks/Assets.xcassets/back_arrow.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "back_arrow.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "back_arrow@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "back_arrow@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/iOSBooks/iOSBooks/Assets.xcassets/back_arrow.imageset/back_arrow.png b/iOSBooks/iOSBooks/Assets.xcassets/back_arrow.imageset/back_arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..6575439e6b760480456067b332cca7581a454214 GIT binary patch literal 254 zcmeAS@N?(olHy`uVBq!ia0vp@KrF_=1|+3_iKzf7&H|6fVg?3oVGw3ym^DWND9BhG zw$eVM3 z&22*$Z%+b?&jGfag1F|Ee;2ZYKTewNJ*!4Ti`Cd=iJ^$qCdOTtCfX%7@GN!@KFI0d zdH&3bF0QQj3lRw%j4zijC~Z8%G`HYL&7HGRm)A@0zq|HR)ltI*H}6X^ebq16=U6Ow wwoyy0WPXh1)24}wPwzD0%saEc!uA$lds)NX$sq}+fX-v^boFyt=akR{0Q^{4IRF3v literal 0 HcmV?d00001 diff --git a/iOSBooks/iOSBooks/Assets.xcassets/back_arrow.imageset/back_arrow@2x.png b/iOSBooks/iOSBooks/Assets.xcassets/back_arrow.imageset/back_arrow@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..11c6c4c90696ccaf70dd9b85a1a26fab449acff5 GIT binary patch literal 315 zcmeAS@N?(olHy`uVBq!ia0vp^l0dA*!3HGV*gqryDb50q$YKTtF;x&|^bAt@02E{_ z4sv&5Sa(k5C6L3C?&#~tz_78O`%fY(kbl9`#WAFU@$J<_-a`fgZ4dP|X0&+CU@kCH zd5}=d#5bX##cM^&qX!mF&Q6C;UEE#T_{DeMoAcM6$Fu)Qa=G%+E$%^Vg>}R+_FW}E zyf3|EZcdNuR7+&6{;$v+@?z6W1H87)$o; zXZLkF5$(AA=)x9H1@>GfjRQ({8K*QiXt5vMWXW-DZ{s0e%S}@*>R!4v0q8ddPgg&e IbxsLQ05;!r#{d8T literal 0 HcmV?d00001 diff --git a/iOSBooks/iOSBooks/Assets.xcassets/back_arrow.imageset/back_arrow@3x.png b/iOSBooks/iOSBooks/Assets.xcassets/back_arrow.imageset/back_arrow@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..1ce9897c80682995e0e2c5c49b8be60fe8f49ce4 GIT binary patch literal 446 zcmV;v0YUzWP)&=J_6-GC9A4S)&S4HyMBC>xXw9%mFJKtjzqyo(k2FGZ31#XmJt zbg_X->#%Snd%GpO^8K~zx_2y{q3-Rv@Mi}sSqAX$9 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOSBooks/iOSBooks/BookDetail/View/BookDetailViewController+View.swift b/iOSBooks/iOSBooks/BookDetail/View/BookDetailViewController+View.swift new file mode 100644 index 0000000..8f76f56 --- /dev/null +++ b/iOSBooks/iOSBooks/BookDetail/View/BookDetailViewController+View.swift @@ -0,0 +1,32 @@ +// +// BookDetailViewController+View.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 30/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import Kingfisher +import UIKit + +protocol BookDetailViewControllerPresentable: class { + +} + +extension BookDetailViewController: BookDetailViewControllerPresentable { + + func setupView() { + title = viewModel?.getScreenTitle() + bookImageView?.kf.setImage(with: viewModel?.getBookImageLink()) + bookTitleLabel?.text = viewModel?.getBookTitle() + bookAuthorsLabel?.text = viewModel?.getBookAuthors() + buyLinkLabel?.text = viewModel?.getBookBuyLink() + descriptionTextView?.text = viewModel?.getBookDescription() + } + + func setupNavigation() { + navigationController?.navigationBar.prefersLargeTitles = false + setBackButton(#selector(dismissScreen)) + } + +} diff --git a/iOSBooks/iOSBooks/BookDetail/View/BookDetailViewController.swift b/iOSBooks/iOSBooks/BookDetail/View/BookDetailViewController.swift new file mode 100644 index 0000000..ac2e68a --- /dev/null +++ b/iOSBooks/iOSBooks/BookDetail/View/BookDetailViewController.swift @@ -0,0 +1,38 @@ +// +// BookDetailViewController.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 30/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import UIKit + +class BookDetailViewController: UIViewController { + + // MARK: - Outlets + @IBOutlet weak var bookImageView: UIImageView? + @IBOutlet weak var bookTitleLabel: UILabel? + @IBOutlet weak var bookAuthorsLabel: UILabel? + @IBOutlet weak var buyLinkLabel: UILabel? + @IBOutlet weak var descriptionTextView: UITextView? + + // MARK: - Properties + var viewModel: BookDetailViewModelProtocol? + + // MARK: - View Life Cycle + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + setupView() + setupNavigation() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + descriptionTextView?.scrollRangeToVisible(NSRange(location:0, length:0)) + } + + @objc func dismissScreen() { + viewModel?.presentPreviousStep() + } +} diff --git a/iOSBooks/iOSBooks/BookDetail/ViewModel/BookDetailViewModel.swift b/iOSBooks/iOSBooks/BookDetail/ViewModel/BookDetailViewModel.swift new file mode 100644 index 0000000..d7260bb --- /dev/null +++ b/iOSBooks/iOSBooks/BookDetail/ViewModel/BookDetailViewModel.swift @@ -0,0 +1,66 @@ +// +// BookDetailViewModel.swift +// iOSBooks +// +// Created by Guilherme Antunes Ferreira on 30/08/19. +// Copyright © 2019 Guilherme Antunes. All rights reserved. +// + +import Foundation + +protocol BookDetailViewModelProtocol { + func getBookTitle() -> String + func getBookAuthors() -> String + func getBookBuyLink() -> String + func getBookDescription() -> String + func getBookImageLink() -> URL? + func presentPreviousStep() + func getScreenTitle() -> String? +} + +class BookDetailViewModel: BookDetailViewModelProtocol { + + // MARK: - Properties + var selectedBook: Item? + weak var view: BookDetailViewControllerPresentable? + var coordinator: AppCoordinatorProtocol? + + // MARK: - ViewModel Protocol Methods + func getBookTitle() -> String { + guard let bookTitle = selectedBook?.book?.title else { return "Título indisponível" } + var title = "Título: " + title.append(bookTitle) + return title + } + + func getBookAuthors() -> String { + guard let bookAuthor = selectedBook?.book?.authors?.first else { return "Autores não disponíveis" } + var author = "Autor: " + author.append(bookAuthor) + return author + } + + func getBookBuyLink() -> String { + return selectedBook?.salesInfo?.buyLink ?? "Link de compra não disponível" + } + + func getBookDescription() -> String { + return selectedBook?.book?.description ?? "Sem descrição disponível" + } + + func getBookImageLink() -> URL? { + if let bookURLString = selectedBook?.book?.imageLinks?.thumbnail { + return URL(string: bookURLString) + } + + return nil + } + + func getScreenTitle() -> String? { + return selectedBook?.book?.title + } + + func presentPreviousStep() { + coordinator?.presentPreviousStep() + } +} diff --git a/iOSBooks/iOSBooks/BooksList/View/BooksList.storyboard b/iOSBooks/iOSBooks/BooksList/View/BooksList.storyboard index 511c79e..14bcfd6 100644 --- a/iOSBooks/iOSBooks/BooksList/View/BooksList.storyboard +++ b/iOSBooks/iOSBooks/BooksList/View/BooksList.storyboard @@ -70,4 +70,5 @@ + diff --git a/iOSBooks/iOSBooks/BooksList/View/BooksListViewController+CollectionView.swift b/iOSBooks/iOSBooks/BooksList/View/BooksListViewController+CollectionView.swift index 7ac7c72..e5fd44c 100644 --- a/iOSBooks/iOSBooks/BooksList/View/BooksListViewController+CollectionView.swift +++ b/iOSBooks/iOSBooks/BooksList/View/BooksListViewController+CollectionView.swift @@ -20,4 +20,8 @@ extension BooksListViewController: UICollectionViewDataSource, UICollectionViewD func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { return viewModel?.cellForItem(inCollectionView: collectionView, atIndexPath: indexPath) ?? UICollectionViewCell() } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + viewModel?.selectBook(atIndexPath: indexPath) + } } diff --git a/iOSBooks/iOSBooks/BooksList/View/BooksListViewController+View.swift b/iOSBooks/iOSBooks/BooksList/View/BooksListViewController+View.swift index e2ccd98..75e57a0 100644 --- a/iOSBooks/iOSBooks/BooksList/View/BooksListViewController+View.swift +++ b/iOSBooks/iOSBooks/BooksList/View/BooksListViewController+View.swift @@ -8,7 +8,7 @@ import UIKit -protocol BooksListViewControllerPresentable { +protocol BooksListViewControllerPresentable: class { func reloadView() func presentError(message: String) } @@ -27,4 +27,8 @@ extension BooksListViewController: BooksListViewControllerPresentable { booksCollectionView?.dataSource = self booksCollectionView?.delegate = self } + + func setupNavigationBar() { + navigationController?.navigationBar.prefersLargeTitles = true + } } diff --git a/iOSBooks/iOSBooks/BooksList/View/BooksListViewController.swift b/iOSBooks/iOSBooks/BooksList/View/BooksListViewController.swift index 7661e23..e3a99da 100644 --- a/iOSBooks/iOSBooks/BooksList/View/BooksListViewController.swift +++ b/iOSBooks/iOSBooks/BooksList/View/BooksListViewController.swift @@ -19,4 +19,9 @@ class BooksListViewController: UIViewController { setupCollectionView() viewModel?.loadBooks() } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + setupNavigationBar() + } } diff --git a/iOSBooks/iOSBooks/BooksList/View/Subviews/BooksCell.swift b/iOSBooks/iOSBooks/BooksList/View/Subviews/BooksCell.swift index da5e82c..515a938 100644 --- a/iOSBooks/iOSBooks/BooksList/View/Subviews/BooksCell.swift +++ b/iOSBooks/iOSBooks/BooksList/View/Subviews/BooksCell.swift @@ -13,7 +13,7 @@ class BooksCell: UICollectionViewCell { @IBOutlet weak var bookImageView: UIImageView? func setup(withBook book: Book?) { - if let book = book, let bookURLString = book.imageLinks?.thumbnail, let url = URL(string: bookURLString) { + if let book = book, let bookURLString = book.imageLinks?.smallThumbnail, let url = URL(string: bookURLString) { bookImageView?.kf.setImage(with: url) } } diff --git a/iOSBooks/iOSBooks/BooksList/ViewModel/BooksListViewModel.swift b/iOSBooks/iOSBooks/BooksList/ViewModel/BooksListViewModel.swift index d913009..81dfa05 100644 --- a/iOSBooks/iOSBooks/BooksList/ViewModel/BooksListViewModel.swift +++ b/iOSBooks/iOSBooks/BooksList/ViewModel/BooksListViewModel.swift @@ -10,6 +10,8 @@ import UIKit protocol BooksListViewModelProtocol { func loadBooks() + func selectBook(atIndexPath indexPath: IndexPath) + func presentPreviousStep() func numberOfSections() -> Int func numberOfItemsInSection() -> Int func cellForItem(inCollectionView collectionView: UICollectionView, atIndexPath indexPath: IndexPath) -> UICollectionViewCell @@ -20,7 +22,9 @@ class BooksListViewModel: BooksListViewModelProtocol { var items: [Item] = [] var service = BooksClient() var startingIndex = 0 - var view: BooksListViewControllerPresentable? + weak var view: BooksListViewControllerPresentable? + var selectedBook: Item? + var coordinator: AppCoordinatorProtocol? func numberOfSections() -> Int { return 1 @@ -54,4 +58,14 @@ class BooksListViewModel: BooksListViewModelProtocol { } } + + func selectBook(atIndexPath indexPath: IndexPath) { + selectedBook = nil + selectedBook = items[indexPath.item] + coordinator?.presentNextStep() + } + + func presentPreviousStep() { + coordinator?.presentPreviousStep() + } } diff --git a/iOSBooks/iOSBooks/Coordinators/AppCoordinator/AppCoordinator.swift b/iOSBooks/iOSBooks/Coordinators/AppCoordinator/AppCoordinator.swift index f0129ef..eb21a1e 100644 --- a/iOSBooks/iOSBooks/Coordinators/AppCoordinator/AppCoordinator.swift +++ b/iOSBooks/iOSBooks/Coordinators/AppCoordinator/AppCoordinator.swift @@ -8,10 +8,21 @@ import UIKit -class AppCoordinator { +protocol AppCoordinatorProtocol: class { + func presentNextStep() + func presentPreviousStep() +} + +enum RoutingState { + case list + case detail +} + +class AppCoordinator: AppCoordinatorProtocol { lazy var injector = AppCoordinatorDependencyInjector() var window: UIWindow + var state: RoutingState = .list init(window: UIWindow) { self.window = window @@ -25,5 +36,28 @@ class AppCoordinator { func setupNavigationController() { injector.navigationController.viewControllers.append(injector.booksListViewController) + injector.booksListViewModel.coordinator = self + } + + func presentNextStep() { + switch state { + case .list: + injector.injectNewBook() + injector.bookDetailViewModel.coordinator = self + state = .detail + injector.navigationController.pushViewController(injector.bookDetailViewController, animated: true) + case.detail: + print("Nenhuma tela para frente neste fluxo") + } + } + + func presentPreviousStep() { + switch state { + case .list: + print("Nenhuma tela para trás neste fluxo") + case.detail: + state = .list + injector.navigationController.popViewController(animated: true) + } } } diff --git a/iOSBooks/iOSBooks/Coordinators/AppCoordinator/AppCoordinatorDependencyInjector.swift b/iOSBooks/iOSBooks/Coordinators/AppCoordinator/AppCoordinatorDependencyInjector.swift index 4ed7ff9..cd5e575 100644 --- a/iOSBooks/iOSBooks/Coordinators/AppCoordinator/AppCoordinatorDependencyInjector.swift +++ b/iOSBooks/iOSBooks/Coordinators/AppCoordinator/AppCoordinatorDependencyInjector.swift @@ -13,6 +13,7 @@ class AppCoordinatorDependencyInjector { let navigation = UINavigationController() navigation.navigationBar.prefersLargeTitles = true navigation.navigationBar.barStyle = .black + navigation.navigationBar.tintColor = .white return navigation }() @@ -26,4 +27,21 @@ class AppCoordinatorDependencyInjector { lazy var booksListViewModel: BooksListViewModel = { return BooksListViewModel() }() + + lazy var bookDetailViewController: BookDetailViewController = { + let controller: BookDetailViewController = BookDetailViewController.instantiate() + bookDetailViewModel.view = controller + controller.viewModel = bookDetailViewModel + return controller + }() + + lazy var bookDetailViewModel: BookDetailViewModel = { + let viewModel = BookDetailViewModel() + viewModel.selectedBook = booksListViewModel.selectedBook + return viewModel + }() + + func injectNewBook() { + bookDetailViewModel.selectedBook = booksListViewModel.selectedBook + } } diff --git a/iOSBooks/iOSBooks/UIViewController+QuickInstance.swift b/iOSBooks/iOSBooks/UIViewController+QuickInstance.swift index 3ccb0ed..dd6e636 100644 --- a/iOSBooks/iOSBooks/UIViewController+QuickInstance.swift +++ b/iOSBooks/iOSBooks/UIViewController+QuickInstance.swift @@ -27,10 +27,10 @@ extension UIViewController { extension UIViewController { func setBackButton(_ backFunction: Selector) { -// navigationItem.hidesBackButton = true -// let newBackButton = UIBarButtonItem(image: UIImage(named: "back_arrow"), style: .plain, target: self, action: backFunction) -// newBackButton.title = "Voltar" -// navigationItem.leftBarButtonItem = newBackButton + navigationItem.hidesBackButton = true + let newBackButton = UIBarButtonItem(image: UIImage(named: "back_arrow"), style: .plain, target: self, action: backFunction) + newBackButton.title = "Voltar" + navigationItem.leftBarButtonItem = newBackButton } } From a52f8dc66d916cd46ab904716e463284a93dba60 Mon Sep 17 00:00:00 2001 From: Guilherme Antunes Date: Fri, 30 Aug 2019 16:33:54 -0300 Subject: [PATCH 05/14] feat(open link): adding open buy link feature --- iOSBooks/iOSBooks.xcodeproj/project.pbxproj | 4 ++++ .../BookDetail/View/BookDetail.storyboard | 4 ++-- .../View/BookDetailViewController+Safari.swift | 16 ++++++++++++++++ .../View/BookDetailViewController+View.swift | 5 +++++ .../View/BookDetailViewController.swift | 1 + .../ViewModel/BookDetailViewModel.swift | 9 +++++++++ 6 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 iOSBooks/iOSBooks/BookDetail/View/BookDetailViewController+Safari.swift diff --git a/iOSBooks/iOSBooks.xcodeproj/project.pbxproj b/iOSBooks/iOSBooks.xcodeproj/project.pbxproj index 7c3f9ec..7161d36 100644 --- a/iOSBooks/iOSBooks.xcodeproj/project.pbxproj +++ b/iOSBooks/iOSBooks.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ 74ADE9D223199D97006D2644 /* BookDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74ADE9D123199D97006D2644 /* BookDetailViewController.swift */; }; 74B9185223199E8F00ADDEA4 /* BookDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74B9185123199E8F00ADDEA4 /* BookDetailViewModel.swift */; }; 74B9185423199F4500ADDEA4 /* BookDetailViewController+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74B9185323199F4500ADDEA4 /* BookDetailViewController+View.swift */; }; + 74B918562319AE0300ADDEA4 /* BookDetailViewController+Safari.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74B918552319AE0300ADDEA4 /* BookDetailViewController+Safari.swift */; }; 768F74E39672B1F50C632913 /* Pods_iOSBooks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2D2FDB2EC5907498F6A3C173 /* Pods_iOSBooks.framework */; }; /* End PBXBuildFile section */ @@ -87,6 +88,7 @@ 74ADE9D123199D97006D2644 /* BookDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookDetailViewController.swift; sourceTree = ""; }; 74B9185123199E8F00ADDEA4 /* BookDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookDetailViewModel.swift; sourceTree = ""; }; 74B9185323199F4500ADDEA4 /* BookDetailViewController+View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BookDetailViewController+View.swift"; sourceTree = ""; }; + 74B918552319AE0300ADDEA4 /* BookDetailViewController+Safari.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BookDetailViewController+Safari.swift"; sourceTree = ""; }; E6741F045EBC12BB315EE1CB /* Pods-iOSBooks.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOSBooks.debug.xcconfig"; path = "Target Support Files/Pods-iOSBooks/Pods-iOSBooks.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -283,6 +285,7 @@ 74ADE9CF23199C1F006D2644 /* BookDetail.storyboard */, 74ADE9D123199D97006D2644 /* BookDetailViewController.swift */, 74B9185323199F4500ADDEA4 /* BookDetailViewController+View.swift */, + 74B918552319AE0300ADDEA4 /* BookDetailViewController+Safari.swift */, ); path = View; sourceTree = ""; @@ -507,6 +510,7 @@ 74ADE9C52319730F006D2644 /* BooksCell.swift in Sources */, 74ADE9D223199D97006D2644 /* BookDetailViewController.swift in Sources */, 74ADE9B823196A7A006D2644 /* AppCoordinator.swift in Sources */, + 74B918562319AE0300ADDEA4 /* BookDetailViewController+Safari.swift in Sources */, 0E3964D82314BB470093738B /* AppDelegate.swift in Sources */, 74ADE9C023196E44006D2644 /* UIViewController+QuickInstance.swift in Sources */, 7447D3602316CB1200E01BD3 /* Reachability.swift in Sources */, diff --git a/iOSBooks/iOSBooks/BookDetail/View/BookDetail.storyboard b/iOSBooks/iOSBooks/BookDetail/View/BookDetail.storyboard index fadaade..c032e64 100644 --- a/iOSBooks/iOSBooks/BookDetail/View/BookDetail.storyboard +++ b/iOSBooks/iOSBooks/BookDetail/View/BookDetail.storyboard @@ -39,7 +39,7 @@ -