From 17d276435fc376c4d0ba3b7c286d2a03810b158b Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Sat, 10 Oct 2020 09:57:16 -0300 Subject: [PATCH 01/18] Setup build CLI and CI (#3) * Setup fastlane * Configure github actions --- .github/workflows/tests.yml | 21 ++ .gitignore | 38 +++ Chuck Norris Facts.xcodeproj/project.pbxproj | 119 +++++++++ .../contents.xcworkspacedata | 10 + Gemfile | 4 + Gemfile.lock | 243 ++++++++++++++++++ Podfile | 13 + Podfile.lock | 3 + fastlane/Appfile | 2 + fastlane/Fastfile | 20 ++ fastlane/README.md | 34 +++ 11 files changed, 507 insertions(+) create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 Chuck Norris Facts.xcworkspace/contents.xcworkspacedata create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 Podfile create mode 100644 Podfile.lock create mode 100644 fastlane/Appfile create mode 100644 fastlane/Fastfile create mode 100644 fastlane/README.md diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..1b8c0aa --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,21 @@ +name: Run tests + +on: pull_request + +jobs: + tests: + name: Tests + runs-on: macOS-latest + + env: + DEVELOPER_DIR: /Applications/Xcode_11.7.app + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install bundle + run: bundle install + + - name: Build and Test + run: bundle exec fastlane ios tests \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e4c2a18 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace +Pods/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md + +**/fastlane/report.xml +**/fastlane/Preview.html +**/fastlane/screenshots +**/fastlane/test_output + +coverage diff --git a/Chuck Norris Facts.xcodeproj/project.pbxproj b/Chuck Norris Facts.xcodeproj/project.pbxproj index ee03216..7ce9ef0 100644 --- a/Chuck Norris Facts.xcodeproj/project.pbxproj +++ b/Chuck Norris Facts.xcodeproj/project.pbxproj @@ -15,6 +15,9 @@ 1EE0714925314AF600F6BF6D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1EE0714725314AF600F6BF6D /* LaunchScreen.storyboard */; }; 1EE0715425314AF600F6BF6D /* Chuck_Norris_FactsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE0715325314AF600F6BF6D /* Chuck_Norris_FactsTests.swift */; }; 1EE0715F25314AF600F6BF6D /* Chuck_Norris_FactsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE0715E25314AF600F6BF6D /* Chuck_Norris_FactsUITests.swift */; }; + 54AACBFAFA5E5798DDE49218 /* Pods_Chuck_Norris_Facts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02186229E02F0A408D2EE0C2 /* Pods_Chuck_Norris_Facts.framework */; }; + D5197BBE6E28E81FC84E5C48 /* Pods_Chuck_Norris_FactsTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EF9A8C577644798596588861 /* Pods_Chuck_Norris_FactsTests.framework */; }; + E08677EA93CEAFF961473756 /* Pods_Chuck_Norris_Facts_Chuck_Norris_FactsUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7C1F7C697FAAC85D6A1F91F4 /* Pods_Chuck_Norris_Facts_Chuck_Norris_FactsUITests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -35,6 +38,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 02186229E02F0A408D2EE0C2 /* Pods_Chuck_Norris_Facts.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Chuck_Norris_Facts.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1EE0713925314AF500F6BF6D /* Chuck Norris Facts.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Chuck Norris Facts.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 1EE0713C25314AF500F6BF6D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 1EE0713E25314AF500F6BF6D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -49,6 +53,14 @@ 1EE0715A25314AF600F6BF6D /* Chuck Norris FactsUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Chuck Norris FactsUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 1EE0715E25314AF600F6BF6D /* Chuck_Norris_FactsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Chuck_Norris_FactsUITests.swift; sourceTree = ""; }; 1EE0716025314AF600F6BF6D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 21EE31E3903E76B09B8FBA96 /* Pods-Chuck Norris Facts-Chuck Norris FactsUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Chuck Norris Facts-Chuck Norris FactsUITests.debug.xcconfig"; path = "Target Support Files/Pods-Chuck Norris Facts-Chuck Norris FactsUITests/Pods-Chuck Norris Facts-Chuck Norris FactsUITests.debug.xcconfig"; sourceTree = ""; }; + 5BFEC696AC24CC9BF87C5195 /* Pods-Chuck Norris FactsTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Chuck Norris FactsTests.release.xcconfig"; path = "Target Support Files/Pods-Chuck Norris FactsTests/Pods-Chuck Norris FactsTests.release.xcconfig"; sourceTree = ""; }; + 74304E6C0D317767335DC2AB /* Pods-Chuck Norris Facts.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Chuck Norris Facts.release.xcconfig"; path = "Target Support Files/Pods-Chuck Norris Facts/Pods-Chuck Norris Facts.release.xcconfig"; sourceTree = ""; }; + 7C1F7C697FAAC85D6A1F91F4 /* Pods_Chuck_Norris_Facts_Chuck_Norris_FactsUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Chuck_Norris_Facts_Chuck_Norris_FactsUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BA72CEAC53E67FEFA608F6B0 /* Pods-Chuck Norris Facts-Chuck Norris FactsUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Chuck Norris Facts-Chuck Norris FactsUITests.release.xcconfig"; path = "Target Support Files/Pods-Chuck Norris Facts-Chuck Norris FactsUITests/Pods-Chuck Norris Facts-Chuck Norris FactsUITests.release.xcconfig"; sourceTree = ""; }; + BBB43EE571174B99828001E3 /* Pods-Chuck Norris Facts.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Chuck Norris Facts.debug.xcconfig"; path = "Target Support Files/Pods-Chuck Norris Facts/Pods-Chuck Norris Facts.debug.xcconfig"; sourceTree = ""; }; + EA8CB0ABD6CE41AE1F636AB6 /* Pods-Chuck Norris FactsTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Chuck Norris FactsTests.debug.xcconfig"; path = "Target Support Files/Pods-Chuck Norris FactsTests/Pods-Chuck Norris FactsTests.debug.xcconfig"; sourceTree = ""; }; + EF9A8C577644798596588861 /* Pods_Chuck_Norris_FactsTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Chuck_Norris_FactsTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -56,6 +68,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 54AACBFAFA5E5798DDE49218 /* Pods_Chuck_Norris_Facts.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -63,6 +76,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D5197BBE6E28E81FC84E5C48 /* Pods_Chuck_Norris_FactsTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -70,6 +84,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + E08677EA93CEAFF961473756 /* Pods_Chuck_Norris_Facts_Chuck_Norris_FactsUITests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -83,6 +98,8 @@ 1EE0715225314AF600F6BF6D /* Chuck Norris FactsTests */, 1EE0715D25314AF600F6BF6D /* Chuck Norris FactsUITests */, 1EE0713A25314AF500F6BF6D /* Products */, + 4AEC7A1E4DDCD345F1E9B6DA /* Pods */, + 9E3EC709E58402C598992352 /* Frameworks */, ); sourceTree = ""; }; @@ -128,6 +145,30 @@ path = "Chuck Norris FactsUITests"; sourceTree = ""; }; + 4AEC7A1E4DDCD345F1E9B6DA /* Pods */ = { + isa = PBXGroup; + children = ( + BBB43EE571174B99828001E3 /* Pods-Chuck Norris Facts.debug.xcconfig */, + 74304E6C0D317767335DC2AB /* Pods-Chuck Norris Facts.release.xcconfig */, + 21EE31E3903E76B09B8FBA96 /* Pods-Chuck Norris Facts-Chuck Norris FactsUITests.debug.xcconfig */, + BA72CEAC53E67FEFA608F6B0 /* Pods-Chuck Norris Facts-Chuck Norris FactsUITests.release.xcconfig */, + EA8CB0ABD6CE41AE1F636AB6 /* Pods-Chuck Norris FactsTests.debug.xcconfig */, + 5BFEC696AC24CC9BF87C5195 /* Pods-Chuck Norris FactsTests.release.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 9E3EC709E58402C598992352 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 02186229E02F0A408D2EE0C2 /* Pods_Chuck_Norris_Facts.framework */, + 7C1F7C697FAAC85D6A1F91F4 /* Pods_Chuck_Norris_Facts_Chuck_Norris_FactsUITests.framework */, + EF9A8C577644798596588861 /* Pods_Chuck_Norris_FactsTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -135,6 +176,7 @@ isa = PBXNativeTarget; buildConfigurationList = 1EE0716325314AF600F6BF6D /* Build configuration list for PBXNativeTarget "Chuck Norris Facts" */; buildPhases = ( + B4BFEDAE06C957BE8D534340 /* [CP] Check Pods Manifest.lock */, 1EE0713525314AF500F6BF6D /* Sources */, 1EE0713625314AF500F6BF6D /* Frameworks */, 1EE0713725314AF500F6BF6D /* Resources */, @@ -152,6 +194,7 @@ isa = PBXNativeTarget; buildConfigurationList = 1EE0716625314AF600F6BF6D /* Build configuration list for PBXNativeTarget "Chuck Norris FactsTests" */; buildPhases = ( + 1D4F1BF917788782D822C23A /* [CP] Check Pods Manifest.lock */, 1EE0714B25314AF600F6BF6D /* Sources */, 1EE0714C25314AF600F6BF6D /* Frameworks */, 1EE0714D25314AF600F6BF6D /* Resources */, @@ -170,6 +213,7 @@ isa = PBXNativeTarget; buildConfigurationList = 1EE0716925314AF600F6BF6D /* Build configuration list for PBXNativeTarget "Chuck Norris FactsUITests" */; buildPhases = ( + 47A6A25B92C403E329DCC297 /* [CP] Check Pods Manifest.lock */, 1EE0715625314AF600F6BF6D /* Sources */, 1EE0715725314AF600F6BF6D /* Frameworks */, 1EE0715825314AF600F6BF6D /* Resources */, @@ -254,6 +298,75 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 1D4F1BF917788782D822C23A /* [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-Chuck Norris FactsTests-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; + }; + 47A6A25B92C403E329DCC297 /* [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-Chuck Norris Facts-Chuck Norris FactsUITests-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; + }; + B4BFEDAE06C957BE8D534340 /* [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-Chuck Norris Facts-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 */ 1EE0713525314AF500F6BF6D /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -432,6 +545,7 @@ }; 1EE0716425314AF600F6BF6D /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = BBB43EE571174B99828001E3 /* Pods-Chuck Norris Facts.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; @@ -450,6 +564,7 @@ }; 1EE0716525314AF600F6BF6D /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 74304E6C0D317767335DC2AB /* Pods-Chuck Norris Facts.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; @@ -468,6 +583,7 @@ }; 1EE0716725314AF600F6BF6D /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = EA8CB0ABD6CE41AE1F636AB6 /* Pods-Chuck Norris FactsTests.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -490,6 +606,7 @@ }; 1EE0716825314AF600F6BF6D /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 5BFEC696AC24CC9BF87C5195 /* Pods-Chuck Norris FactsTests.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -512,6 +629,7 @@ }; 1EE0716A25314AF600F6BF6D /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 21EE31E3903E76B09B8FBA96 /* Pods-Chuck Norris Facts-Chuck Norris FactsUITests.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; @@ -532,6 +650,7 @@ }; 1EE0716B25314AF600F6BF6D /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = BA72CEAC53E67FEFA608F6B0 /* Pods-Chuck Norris Facts-Chuck Norris FactsUITests.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; diff --git a/Chuck Norris Facts.xcworkspace/contents.xcworkspacedata b/Chuck Norris Facts.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..79c6c09 --- /dev/null +++ b/Chuck Norris Facts.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..82d1e30 --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +gem "fastlane" +gem "cocoapods" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..94cfa47 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,243 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.2) + activesupport (4.2.11.3) + i18n (~> 0.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + addressable (2.7.0) + public_suffix (>= 2.0.2, < 5.0) + algoliasearch (1.27.4) + httpclient (~> 2.8, >= 2.8.3) + json (>= 1.5.1) + atomos (0.1.3) + aws-eventstream (1.1.0) + aws-partitions (1.381.0) + aws-sdk-core (3.109.1) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.239.0) + aws-sigv4 (~> 1.1) + jmespath (~> 1.0) + aws-sdk-kms (1.39.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.83.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.1) + aws-sigv4 (1.2.2) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + claide (1.0.3) + cocoapods (1.9.3) + activesupport (>= 4.0.2, < 5) + claide (>= 1.0.2, < 2.0) + cocoapods-core (= 1.9.3) + cocoapods-deintegrate (>= 1.0.3, < 2.0) + cocoapods-downloader (>= 1.2.2, < 2.0) + cocoapods-plugins (>= 1.0.0, < 2.0) + cocoapods-search (>= 1.0.0, < 2.0) + cocoapods-stats (>= 1.0.0, < 2.0) + cocoapods-trunk (>= 1.4.0, < 2.0) + cocoapods-try (>= 1.1.0, < 2.0) + colored2 (~> 3.1) + escape (~> 0.0.4) + fourflusher (>= 2.3.0, < 3.0) + gh_inspector (~> 1.0) + molinillo (~> 0.6.6) + nap (~> 1.0) + ruby-macho (~> 1.4) + xcodeproj (>= 1.14.0, < 2.0) + cocoapods-core (1.9.3) + activesupport (>= 4.0.2, < 6) + algoliasearch (~> 1.0) + concurrent-ruby (~> 1.1) + fuzzy_match (~> 2.0.4) + nap (~> 1.0) + netrc (~> 0.11) + typhoeus (~> 1.0) + cocoapods-deintegrate (1.0.4) + cocoapods-downloader (1.4.0) + cocoapods-plugins (1.0.0) + nap + cocoapods-search (1.0.0) + cocoapods-stats (1.1.0) + cocoapods-trunk (1.5.0) + nap (>= 0.8, < 2.0) + netrc (~> 0.11) + cocoapods-try (1.2.0) + colored (1.2) + colored2 (3.1.2) + commander-fastlane (4.4.6) + highline (~> 1.7.2) + concurrent-ruby (1.1.7) + declarative (0.0.20) + declarative-option (0.1.0) + digest-crc (0.6.1) + rake (~> 13.0) + domain_name (0.5.20190701) + unf (>= 0.0.5, < 1.0.0) + dotenv (2.7.6) + emoji_regex (3.0.0) + escape (0.0.4) + ethon (0.12.0) + ffi (>= 1.3.0) + excon (0.76.0) + faraday (1.0.1) + multipart-post (>= 1.2, < 3) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday_middleware (1.0.0) + faraday (~> 1.0) + fastimage (2.2.0) + fastlane (2.162.0) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.3, < 3.0.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored + commander-fastlane (>= 4.4.6, < 5.0.0) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-api-client (>= 0.37.0, < 0.39.0) + google-cloud-storage (>= 1.15.0, < 2.0.0) + highline (>= 1.7.2, < 2.0.0) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (~> 2.0.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.3) + simctl (~> 1.6.3) + slack-notifier (>= 2.0.0, < 3.0.0) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (>= 1.4.5, < 2.0.0) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3) + ffi (1.13.1) + fourflusher (2.3.1) + fuzzy_match (2.0.4) + gh_inspector (1.1.3) + google-api-client (0.38.0) + addressable (~> 2.5, >= 2.5.1) + googleauth (~> 0.9) + httpclient (>= 2.8.1, < 3.0) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.0) + signet (~> 0.12) + google-cloud-core (1.5.0) + google-cloud-env (~> 1.0) + google-cloud-errors (~> 1.0) + google-cloud-env (1.3.3) + faraday (>= 0.17.3, < 2.0) + google-cloud-errors (1.0.1) + google-cloud-storage (1.29.1) + addressable (~> 2.5) + digest-crc (~> 0.4) + google-api-client (~> 0.33) + google-cloud-core (~> 1.2) + googleauth (~> 0.9) + mini_mime (~> 1.0) + googleauth (0.14.0) + faraday (>= 0.17.3, < 2.0) + jwt (>= 1.4, < 3.0) + memoist (~> 0.16) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (~> 0.14) + highline (1.7.10) + http-cookie (1.0.3) + domain_name (~> 0.5) + httpclient (2.8.3) + i18n (0.9.5) + concurrent-ruby (~> 1.0) + jmespath (1.4.0) + json (2.3.1) + jwt (2.2.2) + memoist (0.16.2) + mini_magick (4.10.1) + mini_mime (1.0.2) + minitest (5.14.2) + molinillo (0.6.6) + multi_json (1.15.0) + multipart-post (2.0.0) + nanaimo (0.3.0) + nap (1.1.0) + naturally (2.2.0) + netrc (0.11.0) + os (1.1.1) + plist (3.5.0) + public_suffix (4.0.6) + rake (13.0.1) + representable (3.0.4) + declarative (< 0.1.0) + declarative-option (< 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rouge (2.0.7) + ruby-macho (1.4.0) + rubyzip (2.3.0) + security (0.1.3) + signet (0.14.0) + addressable (~> 2.3) + faraday (>= 0.17.3, < 2.0) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.8) + CFPropertyList + naturally + slack-notifier (2.3.2) + terminal-notifier (2.0.0) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) + thread_safe (0.3.6) + tty-cursor (0.7.1) + tty-screen (0.8.1) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + typhoeus (1.4.0) + ethon (>= 0.9.0) + tzinfo (1.2.7) + thread_safe (~> 0.1) + uber (0.1.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.7) + unicode-display_width (1.7.0) + word_wrap (1.0.0) + xcodeproj (1.19.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.3.0) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.0) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + ruby + +DEPENDENCIES + cocoapods + fastlane + +BUNDLED WITH + 2.1.2 diff --git a/Podfile b/Podfile new file mode 100644 index 0000000..6beaf8d --- /dev/null +++ b/Podfile @@ -0,0 +1,13 @@ +platform :ios, '11.0' + +target 'Chuck Norris Facts' do + use_frameworks! + + target 'Chuck Norris FactsTests' do + inherit! :search_paths + end + + target 'Chuck Norris FactsUITests' do + end + +end diff --git a/Podfile.lock b/Podfile.lock new file mode 100644 index 0000000..783a459 --- /dev/null +++ b/Podfile.lock @@ -0,0 +1,3 @@ +PODFILE CHECKSUM: 31117450d5e0e25b4bf1f4cec34837ab5d14976e + +COCOAPODS: 1.9.3 diff --git a/fastlane/Appfile b/fastlane/Appfile new file mode 100644 index 0000000..a937f61 --- /dev/null +++ b/fastlane/Appfile @@ -0,0 +1,2 @@ +# app_identifier("net.djorkaeff.Chuck-Norris-Facts") # The bundle identifier of your app +# apple_id("[[APPLE_ID]]") # Your Apple email address diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 0000000..0c629b7 --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,20 @@ +default_platform(:ios) + +platform :ios do + desc "Run Tests" + lane :tests do + + cocoapods + scan(workspace: "Chuck Norris Facts.xcworkspace", code_coverage: true) + + end + + desc "Build and Deploy Beta" + lane :beta do + + cocoapods + build_app(workspace: "Chuck Norris Facts.xcworkspace") + upload_to_testflight(skip_waiting_for_build_processing: true) + + end +end diff --git a/fastlane/README.md b/fastlane/README.md new file mode 100644 index 0000000..80b6772 --- /dev/null +++ b/fastlane/README.md @@ -0,0 +1,34 @@ +fastlane documentation +================ +# Installation + +Make sure you have the latest version of the Xcode command line tools installed: + +``` +xcode-select --install +``` + +Install _fastlane_ using +``` +[sudo] gem install fastlane -NV +``` +or alternatively using `brew install fastlane` + +# Available Actions +## iOS +### ios tests +``` +fastlane ios tests +``` +Run Tests +### ios beta +``` +fastlane ios beta +``` +Build and Deploy Beta + +---- + +This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. +More information about fastlane can be found on [fastlane.tools](https://fastlane.tools). +The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). From 79ec6415e64e642e32ea9abdd020a9fa4af1a6cd Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Sat, 10 Oct 2020 11:30:43 -0300 Subject: [PATCH 02/18] Setup SwiftLint (#6) --- .github/workflows/tests.yml | 2 +- .swiftlint.yml | 5 +++++ Chuck Norris Facts.xcodeproj/project.pbxproj | 20 ++++++++++++++++++- .../xcschemes/xcschememanagement.plist | 2 +- .../xcshareddata/IDEWorkspaceChecks.plist | 8 ++++++++ Chuck Norris Facts/AppDelegate.swift | 4 ---- Chuck Norris Facts/SceneDelegate.swift | 3 --- Chuck Norris Facts/ViewController.swift | 2 -- Podfile | 2 ++ Podfile.lock | 15 +++++++++++++- 10 files changed, 50 insertions(+), 13 deletions(-) create mode 100644 .swiftlint.yml create mode 100644 Chuck Norris Facts.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1b8c0aa..1a2c6b5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,4 +18,4 @@ jobs: run: bundle install - name: Build and Test - run: bundle exec fastlane ios tests \ No newline at end of file + run: bundle exec fastlane ios tests diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..d0fea50 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,5 @@ +disabled_rules: + - type_name + +excluded: + - Pods diff --git a/Chuck Norris Facts.xcodeproj/project.pbxproj b/Chuck Norris Facts.xcodeproj/project.pbxproj index 7ce9ef0..665af9b 100644 --- a/Chuck Norris Facts.xcodeproj/project.pbxproj +++ b/Chuck Norris Facts.xcodeproj/project.pbxproj @@ -155,7 +155,6 @@ EA8CB0ABD6CE41AE1F636AB6 /* Pods-Chuck Norris FactsTests.debug.xcconfig */, 5BFEC696AC24CC9BF87C5195 /* Pods-Chuck Norris FactsTests.release.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -180,6 +179,7 @@ 1EE0713525314AF500F6BF6D /* Sources */, 1EE0713625314AF500F6BF6D /* Frameworks */, 1EE0713725314AF500F6BF6D /* Resources */, + 1EFBC28C2531F8FB00594676 /* SwiftLint */, ); buildRules = ( ); @@ -321,6 +321,24 @@ 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; }; + 1EFBC28C2531F8FB00594676 /* SwiftLint */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = SwiftLint; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/SwiftLint/swiftlint\"\n"; + }; 47A6A25B92C403E329DCC297 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/Chuck Norris Facts.xcodeproj/xcuserdata/djorkaeff.xcuserdatad/xcschemes/xcschememanagement.plist b/Chuck Norris Facts.xcodeproj/xcuserdata/djorkaeff.xcuserdatad/xcschemes/xcschememanagement.plist index 6508c79..ef069bd 100644 --- a/Chuck Norris Facts.xcodeproj/xcuserdata/djorkaeff.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Chuck Norris Facts.xcodeproj/xcuserdata/djorkaeff.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ Chuck Norris Facts.xcscheme_^#shared#^_ orderHint - 0 + 4 diff --git a/Chuck Norris Facts.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Chuck Norris Facts.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Chuck Norris Facts.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Chuck Norris Facts/AppDelegate.swift b/Chuck Norris Facts/AppDelegate.swift index babef19..3b4c2af 100644 --- a/Chuck Norris Facts/AppDelegate.swift +++ b/Chuck Norris Facts/AppDelegate.swift @@ -11,8 +11,6 @@ import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. return true @@ -32,6 +30,4 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } - } - diff --git a/Chuck Norris Facts/SceneDelegate.swift b/Chuck Norris Facts/SceneDelegate.swift index 74a0e4e..eb2857a 100644 --- a/Chuck Norris Facts/SceneDelegate.swift +++ b/Chuck Norris Facts/SceneDelegate.swift @@ -12,7 +12,6 @@ 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. @@ -48,6 +47,4 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // to restore the scene back to its current state. } - } - diff --git a/Chuck Norris Facts/ViewController.swift b/Chuck Norris Facts/ViewController.swift index 2a216ef..4943cb8 100644 --- a/Chuck Norris Facts/ViewController.swift +++ b/Chuck Norris Facts/ViewController.swift @@ -15,6 +15,4 @@ class ViewController: UIViewController { // Do any additional setup after loading the view. } - } - diff --git a/Podfile b/Podfile index 6beaf8d..50debeb 100644 --- a/Podfile +++ b/Podfile @@ -3,6 +3,8 @@ platform :ios, '11.0' target 'Chuck Norris Facts' do use_frameworks! + pod 'SwiftLint' + target 'Chuck Norris FactsTests' do inherit! :search_paths end diff --git a/Podfile.lock b/Podfile.lock index 783a459..477c345 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,3 +1,16 @@ -PODFILE CHECKSUM: 31117450d5e0e25b4bf1f4cec34837ab5d14976e +PODS: + - SwiftLint (0.40.3) + +DEPENDENCIES: + - SwiftLint + +SPEC REPOS: + trunk: + - SwiftLint + +SPEC CHECKSUMS: + SwiftLint: dfd554ff0dff17288ee574814ccdd5cea85d76f7 + +PODFILE CHECKSUM: 1014476eb228f779613820833300a2cf4d17398c COCOAPODS: 1.9.3 From 74b5ad0c54eeef502d4fd94102618c23b7bb079e Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Sat, 10 Oct 2020 12:33:57 -0300 Subject: [PATCH 03/18] Setup initial architecture (#7) * chore: Remove xcuserdata folder * chore: Remove Main.storyboard references * chore: Setup a folder structure * Setup Rx Coordinators --- Chuck Norris Facts.xcodeproj/project.pbxproj | 134 ++++++++++++++---- .../xcschemes/xcschememanagement.plist | 14 -- Chuck Norris Facts/App/AppCoordinator.swift | 24 ++++ .../{ => App}/AppDelegate.swift | 0 Chuck Norris Facts/App/SceneDelegate.swift | 30 ++++ Chuck Norris Facts/Base.lproj/Main.storyboard | 24 ---- .../Library/BaseCoordinator.swift | 40 ++++++ .../AppIcon.appiconset/Contents.json | 0 .../Assets.xcassets/Contents.json | 0 .../Base.lproj/LaunchScreen.storyboard | 0 Chuck Norris Facts/{ => Resources}/Info.plist | 4 - Chuck Norris Facts/SceneDelegate.swift | 50 ------- .../{ => Scenes}/ViewController.swift | 7 +- .../Scenes/ViewControllerCoordinator.swift | 28 ++++ Podfile | 4 + Podfile.lock | 6 +- 16 files changed, 246 insertions(+), 119 deletions(-) delete mode 100644 Chuck Norris Facts.xcodeproj/xcuserdata/djorkaeff.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 Chuck Norris Facts/App/AppCoordinator.swift rename Chuck Norris Facts/{ => App}/AppDelegate.swift (100%) create mode 100644 Chuck Norris Facts/App/SceneDelegate.swift delete mode 100644 Chuck Norris Facts/Base.lproj/Main.storyboard create mode 100644 Chuck Norris Facts/Library/BaseCoordinator.swift rename Chuck Norris Facts/{ => Resources}/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename Chuck Norris Facts/{ => Resources}/Assets.xcassets/Contents.json (100%) rename Chuck Norris Facts/{ => Resources}/Base.lproj/LaunchScreen.storyboard (100%) rename Chuck Norris Facts/{ => Resources}/Info.plist (93%) delete mode 100644 Chuck Norris Facts/SceneDelegate.swift rename Chuck Norris Facts/{ => Scenes}/ViewController.swift (75%) create mode 100644 Chuck Norris Facts/Scenes/ViewControllerCoordinator.swift diff --git a/Chuck Norris Facts.xcodeproj/project.pbxproj b/Chuck Norris Facts.xcodeproj/project.pbxproj index 665af9b..3387c2f 100644 --- a/Chuck Norris Facts.xcodeproj/project.pbxproj +++ b/Chuck Norris Facts.xcodeproj/project.pbxproj @@ -3,18 +3,20 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 51; objects = { /* Begin PBXBuildFile section */ 1EE0713D25314AF500F6BF6D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE0713C25314AF500F6BF6D /* AppDelegate.swift */; }; 1EE0713F25314AF500F6BF6D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE0713E25314AF500F6BF6D /* SceneDelegate.swift */; }; 1EE0714125314AF500F6BF6D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE0714025314AF500F6BF6D /* ViewController.swift */; }; - 1EE0714425314AF500F6BF6D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1EE0714225314AF500F6BF6D /* Main.storyboard */; }; 1EE0714625314AF600F6BF6D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1EE0714525314AF600F6BF6D /* Assets.xcassets */; }; 1EE0714925314AF600F6BF6D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1EE0714725314AF600F6BF6D /* LaunchScreen.storyboard */; }; 1EE0715425314AF600F6BF6D /* Chuck_Norris_FactsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE0715325314AF600F6BF6D /* Chuck_Norris_FactsTests.swift */; }; 1EE0715F25314AF600F6BF6D /* Chuck_Norris_FactsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE0715E25314AF600F6BF6D /* Chuck_Norris_FactsUITests.swift */; }; + 1EFE2867253206D3008806B9 /* BaseCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE2866253206D3008806B9 /* BaseCoordinator.swift */; }; + 1EFE2869253207B2008806B9 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE2868253207B2008806B9 /* AppCoordinator.swift */; }; + 1EFE286B253207DF008806B9 /* ViewControllerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE286A253207DF008806B9 /* ViewControllerCoordinator.swift */; }; 54AACBFAFA5E5798DDE49218 /* Pods_Chuck_Norris_Facts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02186229E02F0A408D2EE0C2 /* Pods_Chuck_Norris_Facts.framework */; }; D5197BBE6E28E81FC84E5C48 /* Pods_Chuck_Norris_FactsTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EF9A8C577644798596588861 /* Pods_Chuck_Norris_FactsTests.framework */; }; E08677EA93CEAFF961473756 /* Pods_Chuck_Norris_Facts_Chuck_Norris_FactsUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7C1F7C697FAAC85D6A1F91F4 /* Pods_Chuck_Norris_Facts_Chuck_Norris_FactsUITests.framework */; }; @@ -43,7 +45,6 @@ 1EE0713C25314AF500F6BF6D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 1EE0713E25314AF500F6BF6D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 1EE0714025314AF500F6BF6D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - 1EE0714325314AF500F6BF6D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 1EE0714525314AF600F6BF6D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 1EE0714825314AF600F6BF6D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 1EE0714A25314AF600F6BF6D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -53,6 +54,9 @@ 1EE0715A25314AF600F6BF6D /* Chuck Norris FactsUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Chuck Norris FactsUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 1EE0715E25314AF600F6BF6D /* Chuck_Norris_FactsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Chuck_Norris_FactsUITests.swift; sourceTree = ""; }; 1EE0716025314AF600F6BF6D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 1EFE2866253206D3008806B9 /* BaseCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseCoordinator.swift; sourceTree = ""; }; + 1EFE2868253207B2008806B9 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; + 1EFE286A253207DF008806B9 /* ViewControllerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerCoordinator.swift; sourceTree = ""; }; 21EE31E3903E76B09B8FBA96 /* Pods-Chuck Norris Facts-Chuck Norris FactsUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Chuck Norris Facts-Chuck Norris FactsUITests.debug.xcconfig"; path = "Target Support Files/Pods-Chuck Norris Facts-Chuck Norris FactsUITests/Pods-Chuck Norris Facts-Chuck Norris FactsUITests.debug.xcconfig"; sourceTree = ""; }; 5BFEC696AC24CC9BF87C5195 /* Pods-Chuck Norris FactsTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Chuck Norris FactsTests.release.xcconfig"; path = "Target Support Files/Pods-Chuck Norris FactsTests/Pods-Chuck Norris FactsTests.release.xcconfig"; sourceTree = ""; }; 74304E6C0D317767335DC2AB /* Pods-Chuck Norris Facts.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Chuck Norris Facts.release.xcconfig"; path = "Target Support Files/Pods-Chuck Norris Facts/Pods-Chuck Norris Facts.release.xcconfig"; sourceTree = ""; }; @@ -116,13 +120,11 @@ 1EE0713B25314AF500F6BF6D /* Chuck Norris Facts */ = { isa = PBXGroup; children = ( - 1EE0713C25314AF500F6BF6D /* AppDelegate.swift */, - 1EE0713E25314AF500F6BF6D /* SceneDelegate.swift */, - 1EE0714025314AF500F6BF6D /* ViewController.swift */, - 1EE0714225314AF500F6BF6D /* Main.storyboard */, - 1EE0714525314AF600F6BF6D /* Assets.xcassets */, - 1EE0714725314AF600F6BF6D /* LaunchScreen.storyboard */, - 1EE0714A25314AF600F6BF6D /* Info.plist */, + 1EFE2865253206C8008806B9 /* Library */, + 1EFE28632532062A008806B9 /* Scenes */, + 1EFE286225320625008806B9 /* Data */, + 1EFE286125320620008806B9 /* App */, + 1EFE286025320614008806B9 /* Resources */, ); path = "Chuck Norris Facts"; sourceTree = ""; @@ -145,6 +147,58 @@ path = "Chuck Norris FactsUITests"; sourceTree = ""; }; + 1EFE286025320614008806B9 /* Resources */ = { + isa = PBXGroup; + children = ( + 1EE0714525314AF600F6BF6D /* Assets.xcassets */, + 1EE0714725314AF600F6BF6D /* LaunchScreen.storyboard */, + 1EE0714A25314AF600F6BF6D /* Info.plist */, + ); + path = Resources; + sourceTree = ""; + }; + 1EFE286125320620008806B9 /* App */ = { + isa = PBXGroup; + children = ( + 1EE0713C25314AF500F6BF6D /* AppDelegate.swift */, + 1EE0713E25314AF500F6BF6D /* SceneDelegate.swift */, + 1EFE2868253207B2008806B9 /* AppCoordinator.swift */, + ); + path = App; + sourceTree = ""; + }; + 1EFE286225320625008806B9 /* Data */ = { + isa = PBXGroup; + children = ( + 1EFE28642532063C008806B9 /* Networking */, + ); + path = Data; + sourceTree = ""; + }; + 1EFE28632532062A008806B9 /* Scenes */ = { + isa = PBXGroup; + children = ( + 1EE0714025314AF500F6BF6D /* ViewController.swift */, + 1EFE286A253207DF008806B9 /* ViewControllerCoordinator.swift */, + ); + path = Scenes; + sourceTree = ""; + }; + 1EFE28642532063C008806B9 /* Networking */ = { + isa = PBXGroup; + children = ( + ); + path = Networking; + sourceTree = ""; + }; + 1EFE2865253206C8008806B9 /* Library */ = { + isa = PBXGroup; + children = ( + 1EFE2866253206D3008806B9 /* BaseCoordinator.swift */, + ); + path = Library; + sourceTree = ""; + }; 4AEC7A1E4DDCD345F1E9B6DA /* Pods */ = { isa = PBXGroup; children = ( @@ -180,6 +234,7 @@ 1EE0713625314AF500F6BF6D /* Frameworks */, 1EE0713725314AF500F6BF6D /* Resources */, 1EFBC28C2531F8FB00594676 /* SwiftLint */, + 735DB507838707E07E18E902 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -217,6 +272,7 @@ 1EE0715625314AF600F6BF6D /* Sources */, 1EE0715725314AF600F6BF6D /* Frameworks */, 1EE0715825314AF600F6BF6D /* Resources */, + 431049B51AA3B09194F28889 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -278,7 +334,6 @@ files = ( 1EE0714925314AF600F6BF6D /* LaunchScreen.storyboard in Resources */, 1EE0714625314AF600F6BF6D /* Assets.xcassets in Resources */, - 1EE0714425314AF500F6BF6D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -339,6 +394,23 @@ shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/SwiftLint/swiftlint\"\n"; }; + 431049B51AA3B09194F28889 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Chuck Norris Facts-Chuck Norris FactsUITests/Pods-Chuck Norris Facts-Chuck Norris FactsUITests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Chuck Norris Facts-Chuck Norris FactsUITests/Pods-Chuck Norris Facts-Chuck Norris FactsUITests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Chuck Norris Facts-Chuck Norris FactsUITests/Pods-Chuck Norris Facts-Chuck Norris FactsUITests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 47A6A25B92C403E329DCC297 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -361,6 +433,23 @@ 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; }; + 735DB507838707E07E18E902 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Chuck Norris Facts/Pods-Chuck Norris Facts-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Chuck Norris Facts/Pods-Chuck Norris Facts-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Chuck Norris Facts/Pods-Chuck Norris Facts-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; B4BFEDAE06C957BE8D534340 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -391,8 +480,11 @@ buildActionMask = 2147483647; files = ( 1EE0714125314AF500F6BF6D /* ViewController.swift in Sources */, + 1EFE286B253207DF008806B9 /* ViewControllerCoordinator.swift in Sources */, 1EE0713D25314AF500F6BF6D /* AppDelegate.swift in Sources */, + 1EFE2869253207B2008806B9 /* AppCoordinator.swift in Sources */, 1EE0713F25314AF500F6BF6D /* SceneDelegate.swift in Sources */, + 1EFE2867253206D3008806B9 /* BaseCoordinator.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -428,14 +520,6 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ - 1EE0714225314AF500F6BF6D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 1EE0714325314AF500F6BF6D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; 1EE0714725314AF600F6BF6D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -568,7 +652,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = PHM6B5Y356; - INFOPLIST_FILE = "Chuck Norris Facts/Info.plist"; + INFOPLIST_FILE = "Chuck Norris Facts/Resources/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -587,7 +671,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = PHM6B5Y356; - INFOPLIST_FILE = "Chuck Norris Facts/Info.plist"; + INFOPLIST_FILE = "Chuck Norris Facts/Resources/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -603,7 +687,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = EA8CB0ABD6CE41AE1F636AB6 /* Pods-Chuck Norris FactsTests.debug.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = PHM6B5Y356; @@ -626,7 +710,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 5BFEC696AC24CC9BF87C5195 /* Pods-Chuck Norris FactsTests.release.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = PHM6B5Y356; @@ -649,7 +733,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 21EE31E3903E76B09B8FBA96 /* Pods-Chuck Norris Facts-Chuck Norris FactsUITests.debug.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = PHM6B5Y356; INFOPLIST_FILE = "Chuck Norris FactsUITests/Info.plist"; @@ -670,7 +754,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = BA72CEAC53E67FEFA608F6B0 /* Pods-Chuck Norris Facts-Chuck Norris FactsUITests.release.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = PHM6B5Y356; INFOPLIST_FILE = "Chuck Norris FactsUITests/Info.plist"; diff --git a/Chuck Norris Facts.xcodeproj/xcuserdata/djorkaeff.xcuserdatad/xcschemes/xcschememanagement.plist b/Chuck Norris Facts.xcodeproj/xcuserdata/djorkaeff.xcuserdatad/xcschemes/xcschememanagement.plist deleted file mode 100644 index ef069bd..0000000 --- a/Chuck Norris Facts.xcodeproj/xcuserdata/djorkaeff.xcuserdatad/xcschemes/xcschememanagement.plist +++ /dev/null @@ -1,14 +0,0 @@ - - - - - SchemeUserState - - Chuck Norris Facts.xcscheme_^#shared#^_ - - orderHint - 4 - - - - diff --git a/Chuck Norris Facts/App/AppCoordinator.swift b/Chuck Norris Facts/App/AppCoordinator.swift new file mode 100644 index 0000000..936877b --- /dev/null +++ b/Chuck Norris Facts/App/AppCoordinator.swift @@ -0,0 +1,24 @@ +// +// AppCoordinator.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/10/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import UIKit +import RxSwift + +class AppCoordinator: BaseCoordinator { + + private let window: UIWindow + + init(window: UIWindow) { + self.window = window + } + + override func start() -> Observable { + let viewControllerCoordinator = ViewControllerCoordinator(window: window) + return coordinate(to: viewControllerCoordinator) + } +} diff --git a/Chuck Norris Facts/AppDelegate.swift b/Chuck Norris Facts/App/AppDelegate.swift similarity index 100% rename from Chuck Norris Facts/AppDelegate.swift rename to Chuck Norris Facts/App/AppDelegate.swift diff --git a/Chuck Norris Facts/App/SceneDelegate.swift b/Chuck Norris Facts/App/SceneDelegate.swift new file mode 100644 index 0000000..92c30b0 --- /dev/null +++ b/Chuck Norris Facts/App/SceneDelegate.swift @@ -0,0 +1,30 @@ +// +// SceneDelegate.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/9/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import UIKit +import RxSwift + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + private var appCoordinator: AppCoordinator? + private let disposeBag = DisposeBag() + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let scene = (scene as? UIWindowScene) else { return } + + let window = UIWindow(windowScene: scene) + self.window = window + + appCoordinator = AppCoordinator(window: window) + appCoordinator?.start() + .subscribe() + .disposed(by: disposeBag) + } + +} diff --git a/Chuck Norris Facts/Base.lproj/Main.storyboard b/Chuck Norris Facts/Base.lproj/Main.storyboard deleted file mode 100644 index 25a7638..0000000 --- a/Chuck Norris Facts/Base.lproj/Main.storyboard +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Chuck Norris Facts/Library/BaseCoordinator.swift b/Chuck Norris Facts/Library/BaseCoordinator.swift new file mode 100644 index 0000000..4384042 --- /dev/null +++ b/Chuck Norris Facts/Library/BaseCoordinator.swift @@ -0,0 +1,40 @@ +// +// BaseCoordinator.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/10/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import RxSwift +import Foundation + +class BaseCoordinator { + + typealias CoordinationResult = ResultType + + let disposeBag = DisposeBag() + + private let identifier = UUID() + + private var childCoordinators = [UUID: Any]() + + private func store(coordinator: BaseCoordinator) { + childCoordinators[coordinator.identifier] = coordinator + } + + private func free(coordinator: BaseCoordinator) { + childCoordinators[coordinator.identifier] = nil + } + + func coordinate(to coordinator: BaseCoordinator) -> Observable { + store(coordinator: coordinator) + return coordinator.start() + .do(onNext: { [weak self] _ in self?.free(coordinator: coordinator) }) + } + + func start() -> Observable { + fatalError("Start method should be implemented.") + } + +} diff --git a/Chuck Norris Facts/Assets.xcassets/AppIcon.appiconset/Contents.json b/Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Chuck Norris Facts/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Chuck Norris Facts/Assets.xcassets/Contents.json b/Chuck Norris Facts/Resources/Assets.xcassets/Contents.json similarity index 100% rename from Chuck Norris Facts/Assets.xcassets/Contents.json rename to Chuck Norris Facts/Resources/Assets.xcassets/Contents.json diff --git a/Chuck Norris Facts/Base.lproj/LaunchScreen.storyboard b/Chuck Norris Facts/Resources/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from Chuck Norris Facts/Base.lproj/LaunchScreen.storyboard rename to Chuck Norris Facts/Resources/Base.lproj/LaunchScreen.storyboard diff --git a/Chuck Norris Facts/Info.plist b/Chuck Norris Facts/Resources/Info.plist similarity index 93% rename from Chuck Norris Facts/Info.plist rename to Chuck Norris Facts/Resources/Info.plist index 2a3483c..9742bf0 100644 --- a/Chuck Norris Facts/Info.plist +++ b/Chuck Norris Facts/Resources/Info.plist @@ -33,16 +33,12 @@ Default Configuration UISceneDelegateClassName $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main UILaunchStoryboardName LaunchScreen - UIMainStoryboardFile - Main UIRequiredDeviceCapabilities armv7 diff --git a/Chuck Norris Facts/SceneDelegate.swift b/Chuck Norris Facts/SceneDelegate.swift deleted file mode 100644 index eb2857a..0000000 --- a/Chuck Norris Facts/SceneDelegate.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// SceneDelegate.swift -// Chuck Norris Facts -// -// Created by Djorkaeff Alexandre Vilela Pereira on 10/9/20. -// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. -// - -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 neccessarily 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/Chuck Norris Facts/ViewController.swift b/Chuck Norris Facts/Scenes/ViewController.swift similarity index 75% rename from Chuck Norris Facts/ViewController.swift rename to Chuck Norris Facts/Scenes/ViewController.swift index 4943cb8..e2acbca 100644 --- a/Chuck Norris Facts/ViewController.swift +++ b/Chuck Norris Facts/Scenes/ViewController.swift @@ -12,7 +12,12 @@ class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - // Do any additional setup after loading the view. + + setupView() + } + + private func setupView() { + view.backgroundColor = .systemBackground } } diff --git a/Chuck Norris Facts/Scenes/ViewControllerCoordinator.swift b/Chuck Norris Facts/Scenes/ViewControllerCoordinator.swift new file mode 100644 index 0000000..d70cd41 --- /dev/null +++ b/Chuck Norris Facts/Scenes/ViewControllerCoordinator.swift @@ -0,0 +1,28 @@ +// +// ViewControllerCoordinator.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/10/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import UIKit +import RxSwift + +final class ViewControllerCoordinator: BaseCoordinator { + + private let window: UIWindow + + init(window: UIWindow) { + self.window = window + } + + override func start() -> Observable { + let viewController = ViewController() + + window.rootViewController = viewController + window.makeKeyAndVisible() + + return Observable.never() + } +} diff --git a/Podfile b/Podfile index 50debeb..9e92881 100644 --- a/Podfile +++ b/Podfile @@ -3,6 +3,10 @@ platform :ios, '11.0' target 'Chuck Norris Facts' do use_frameworks! + # Rx + pod 'RxSwift' + + # Tools pod 'SwiftLint' target 'Chuck Norris FactsTests' do diff --git a/Podfile.lock b/Podfile.lock index 477c345..7e3c763 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,16 +1,20 @@ PODS: + - RxSwift (5.1.1) - SwiftLint (0.40.3) DEPENDENCIES: + - RxSwift - SwiftLint SPEC REPOS: trunk: + - RxSwift - SwiftLint SPEC CHECKSUMS: + RxSwift: 81470a2074fa8780320ea5fe4102807cb7118178 SwiftLint: dfd554ff0dff17288ee574814ccdd5cea85d76f7 -PODFILE CHECKSUM: 1014476eb228f779613820833300a2cf4d17398c +PODFILE CHECKSUM: 81e3cadf48d11d7a2c25a92e9453ceb6b62e7c1d COCOAPODS: 1.9.3 From 32a9e4d37bf007f88f767bc507d53ba416bef41d Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Mon, 12 Oct 2020 13:54:55 -0300 Subject: [PATCH 04/18] List facts (#10) * Create FactsService with Stub data * Create Facts List Scene * Show a stub of Facts * Create FactTableViewCell * Setup Share Fact * Setup Facts List EmptyView * Use RxDataSources * chore: SwiftLint warnings * Setup tests * Add a label to EmptyList View * Remove UI Logic on FactViewModel * Add a new Fact Stub * Move transformation resposability to ViewModel * Fix Podfile * Load 10 random Facts * Testing FactsList Scene * Unit tests of FactsList using stubs * FactsList UI Tests --- .swiftlint.yml | 1 + Chuck Norris Facts.xcodeproj/project.pbxproj | 278 ++++++++++++++++-- Chuck Norris Facts/App/AppCoordinator.swift | 4 +- Chuck Norris Facts/App/AppDelegate.swift | 19 +- Chuck Norris Facts/App/SceneDelegate.swift | 6 +- Chuck Norris Facts/Data/Models/Fact.swift | 16 + .../Data/Networking/FactsService.swift | 45 +++ .../Extensions/UIViewController+Rx.swift | 20 ++ Chuck Norris Facts/Library/JSON.swift | 21 ++ .../Resources/Lottie/empty-box.json | 1 + .../Resources/Stubs/search-facts.json | 153 ++++++++++ .../FactsList/Cells/FactTableViewCell.swift | 89 ++++++ .../Facts/FactsList/Cells/FactViewModel.swift | 33 +++ .../FactsList/FactsListCoordinator.swift | 49 +++ .../FactsList/FactsListViewController.swift | 116 ++++++++ .../Facts/FactsList/FactsListViewModel.swift | 43 +++ .../Facts/FactsList/Views/EmptyListView.swift | 67 +++++ .../Scenes/ViewController.swift | 23 -- .../Scenes/ViewControllerCoordinator.swift | 28 -- .../Chuck_Norris_FactsTests.swift | 34 --- .../Library/XCTestCase+Stub.swift | 27 ++ .../Mocks/FactsServiceMock.swift | 20 ++ .../FactsList/Cells/FactViewModelTests.swift | 34 +++ .../FactsListViewControllerTests.swift | 107 +++++++ .../FactsList/FactsListViewModelTests.swift | 74 +++++ .../Facts/FactsList/Stubs/facts-list.json | 150 ++++++++++ .../Facts/FactsList/Stubs/long-fact.json | 6 + .../Facts/FactsList/Stubs/short-fact.json | 7 + .../Chuck_Norris_FactsUITests.swift | 43 --- .../Scenes/FactsListScene.swift | 26 ++ .../Tests/FactsListUITests.swift | 57 ++++ Podfile | 15 + Podfile.lock | 36 ++- 33 files changed, 1475 insertions(+), 173 deletions(-) create mode 100644 Chuck Norris Facts/Data/Models/Fact.swift create mode 100644 Chuck Norris Facts/Data/Networking/FactsService.swift create mode 100644 Chuck Norris Facts/Extensions/UIViewController+Rx.swift create mode 100644 Chuck Norris Facts/Library/JSON.swift create mode 100644 Chuck Norris Facts/Resources/Lottie/empty-box.json create mode 100644 Chuck Norris Facts/Resources/Stubs/search-facts.json create mode 100644 Chuck Norris Facts/Scenes/Facts/FactsList/Cells/FactTableViewCell.swift create mode 100644 Chuck Norris Facts/Scenes/Facts/FactsList/Cells/FactViewModel.swift create mode 100644 Chuck Norris Facts/Scenes/Facts/FactsList/FactsListCoordinator.swift create mode 100644 Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift create mode 100644 Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift create mode 100644 Chuck Norris Facts/Scenes/Facts/FactsList/Views/EmptyListView.swift delete mode 100644 Chuck Norris Facts/Scenes/ViewController.swift delete mode 100644 Chuck Norris Facts/Scenes/ViewControllerCoordinator.swift delete mode 100644 Chuck Norris FactsTests/Chuck_Norris_FactsTests.swift create mode 100644 Chuck Norris FactsTests/Library/XCTestCase+Stub.swift create mode 100644 Chuck Norris FactsTests/Mocks/FactsServiceMock.swift create mode 100644 Chuck Norris FactsTests/Scenes/Facts/FactsList/Cells/FactViewModelTests.swift create mode 100644 Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift create mode 100644 Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift create mode 100644 Chuck Norris FactsTests/Scenes/Facts/FactsList/Stubs/facts-list.json create mode 100644 Chuck Norris FactsTests/Scenes/Facts/FactsList/Stubs/long-fact.json create mode 100644 Chuck Norris FactsTests/Scenes/Facts/FactsList/Stubs/short-fact.json delete mode 100644 Chuck Norris FactsUITests/Chuck_Norris_FactsUITests.swift create mode 100644 Chuck Norris FactsUITests/Scenes/FactsListScene.swift create mode 100644 Chuck Norris FactsUITests/Tests/FactsListUITests.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index d0fea50..7d45740 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,5 +1,6 @@ disabled_rules: - type_name + - identifier_name excluded: - Pods diff --git a/Chuck Norris Facts.xcodeproj/project.pbxproj b/Chuck Norris Facts.xcodeproj/project.pbxproj index 3387c2f..b78f160 100644 --- a/Chuck Norris Facts.xcodeproj/project.pbxproj +++ b/Chuck Norris Facts.xcodeproj/project.pbxproj @@ -7,16 +7,34 @@ objects = { /* Begin PBXBuildFile section */ + 1E32758F2532A2C0007E838A /* EmptyListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E32758E2532A2C0007E838A /* EmptyListView.swift */; }; + 1E3275922532A2CD007E838A /* empty-box.json in Resources */ = {isa = PBXBuildFile; fileRef = 1E3275912532A2CD007E838A /* empty-box.json */; }; + 1E7F15BE253324CF0006887B /* FactsListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7F15BD253324CF0006887B /* FactsListViewModelTests.swift */; }; + 1E7F15C0253324FD0006887B /* FactsListViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7F15BF253324FD0006887B /* FactsListViewControllerTests.swift */; }; + 1E7F15C6253329780006887B /* FactViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7F15C5253329780006887B /* FactViewModelTests.swift */; }; + 1ED5D18D25348FD50035046C /* XCTestCase+Stub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED5D18C25348FD50035046C /* XCTestCase+Stub.swift */; }; + 1ED5D19025349E9D0035046C /* UIViewController+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED5D18F25349E9D0035046C /* UIViewController+Rx.swift */; }; + 1ED5D1932534A56F0035046C /* FactsServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED5D1922534A56F0035046C /* FactsServiceMock.swift */; }; + 1ED5D1982534AA700035046C /* long-fact.json in Resources */ = {isa = PBXBuildFile; fileRef = 1ED5D1972534AA700035046C /* long-fact.json */; }; + 1ED5D19A2534AA7A0035046C /* facts-list.json in Resources */ = {isa = PBXBuildFile; fileRef = 1ED5D1992534AA7A0035046C /* facts-list.json */; }; + 1ED5D19C2534AAE40035046C /* short-fact.json in Resources */ = {isa = PBXBuildFile; fileRef = 1ED5D19B2534AAE40035046C /* short-fact.json */; }; + 1ED5D19F2534B0E30035046C /* FactsListScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED5D19E2534B0E30035046C /* FactsListScene.swift */; }; + 1ED5D1A22534B0F40035046C /* FactsListUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED5D1A12534B0F40035046C /* FactsListUITests.swift */; }; 1EE0713D25314AF500F6BF6D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE0713C25314AF500F6BF6D /* AppDelegate.swift */; }; 1EE0713F25314AF500F6BF6D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE0713E25314AF500F6BF6D /* SceneDelegate.swift */; }; - 1EE0714125314AF500F6BF6D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE0714025314AF500F6BF6D /* ViewController.swift */; }; 1EE0714625314AF600F6BF6D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1EE0714525314AF600F6BF6D /* Assets.xcassets */; }; 1EE0714925314AF600F6BF6D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1EE0714725314AF600F6BF6D /* LaunchScreen.storyboard */; }; - 1EE0715425314AF600F6BF6D /* Chuck_Norris_FactsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE0715325314AF600F6BF6D /* Chuck_Norris_FactsTests.swift */; }; - 1EE0715F25314AF600F6BF6D /* Chuck_Norris_FactsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE0715E25314AF600F6BF6D /* Chuck_Norris_FactsUITests.swift */; }; 1EFE2867253206D3008806B9 /* BaseCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE2866253206D3008806B9 /* BaseCoordinator.swift */; }; 1EFE2869253207B2008806B9 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE2868253207B2008806B9 /* AppCoordinator.swift */; }; - 1EFE286B253207DF008806B9 /* ViewControllerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE286A253207DF008806B9 /* ViewControllerCoordinator.swift */; }; + 1EFE287E25321071008806B9 /* search-facts.json in Resources */ = {isa = PBXBuildFile; fileRef = 1EFE287D25321071008806B9 /* search-facts.json */; }; + 1EFE2884253210B2008806B9 /* FactsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE2883253210B2008806B9 /* FactsService.swift */; }; + 1EFE288725321119008806B9 /* Fact.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE288625321119008806B9 /* Fact.swift */; }; + 1EFE288925321123008806B9 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE288825321123008806B9 /* JSON.swift */; }; + 1EFE288E2532135B008806B9 /* FactsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE288D2532135B008806B9 /* FactsListViewController.swift */; }; + 1EFE28902532137C008806B9 /* FactsListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE288F2532137C008806B9 /* FactsListCoordinator.swift */; }; + 1EFE2892253214DB008806B9 /* FactsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE2891253214DB008806B9 /* FactsListViewModel.swift */; }; + 1EFE289525321CD2008806B9 /* FactViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE289425321CD2008806B9 /* FactViewModel.swift */; }; + 1EFE289725321CE2008806B9 /* FactTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE289625321CE2008806B9 /* FactTableViewCell.swift */; }; 54AACBFAFA5E5798DDE49218 /* Pods_Chuck_Norris_Facts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02186229E02F0A408D2EE0C2 /* Pods_Chuck_Norris_Facts.framework */; }; D5197BBE6E28E81FC84E5C48 /* Pods_Chuck_Norris_FactsTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EF9A8C577644798596588861 /* Pods_Chuck_Norris_FactsTests.framework */; }; E08677EA93CEAFF961473756 /* Pods_Chuck_Norris_Facts_Chuck_Norris_FactsUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7C1F7C697FAAC85D6A1F91F4 /* Pods_Chuck_Norris_Facts_Chuck_Norris_FactsUITests.framework */; }; @@ -41,22 +59,40 @@ /* Begin PBXFileReference section */ 02186229E02F0A408D2EE0C2 /* Pods_Chuck_Norris_Facts.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Chuck_Norris_Facts.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1E32758E2532A2C0007E838A /* EmptyListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyListView.swift; sourceTree = ""; }; + 1E3275912532A2CD007E838A /* empty-box.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "empty-box.json"; sourceTree = ""; }; + 1E7F15BD253324CF0006887B /* FactsListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListViewModelTests.swift; sourceTree = ""; }; + 1E7F15BF253324FD0006887B /* FactsListViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListViewControllerTests.swift; sourceTree = ""; }; + 1E7F15C5253329780006887B /* FactViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactViewModelTests.swift; sourceTree = ""; }; + 1ED5D18C25348FD50035046C /* XCTestCase+Stub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+Stub.swift"; sourceTree = ""; }; + 1ED5D18F25349E9D0035046C /* UIViewController+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Rx.swift"; sourceTree = ""; }; + 1ED5D1922534A56F0035046C /* FactsServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsServiceMock.swift; sourceTree = ""; }; + 1ED5D1972534AA700035046C /* long-fact.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "long-fact.json"; sourceTree = ""; }; + 1ED5D1992534AA7A0035046C /* facts-list.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "facts-list.json"; sourceTree = ""; }; + 1ED5D19B2534AAE40035046C /* short-fact.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "short-fact.json"; sourceTree = ""; }; + 1ED5D19E2534B0E30035046C /* FactsListScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListScene.swift; sourceTree = ""; }; + 1ED5D1A12534B0F40035046C /* FactsListUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListUITests.swift; sourceTree = ""; }; 1EE0713925314AF500F6BF6D /* Chuck Norris Facts.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Chuck Norris Facts.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 1EE0713C25314AF500F6BF6D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 1EE0713E25314AF500F6BF6D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - 1EE0714025314AF500F6BF6D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 1EE0714525314AF600F6BF6D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 1EE0714825314AF600F6BF6D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 1EE0714A25314AF600F6BF6D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 1EE0714F25314AF600F6BF6D /* Chuck Norris FactsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Chuck Norris FactsTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; - 1EE0715325314AF600F6BF6D /* Chuck_Norris_FactsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Chuck_Norris_FactsTests.swift; sourceTree = ""; }; 1EE0715525314AF600F6BF6D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 1EE0715A25314AF600F6BF6D /* Chuck Norris FactsUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Chuck Norris FactsUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; - 1EE0715E25314AF600F6BF6D /* Chuck_Norris_FactsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Chuck_Norris_FactsUITests.swift; sourceTree = ""; }; 1EE0716025314AF600F6BF6D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 1EFE2866253206D3008806B9 /* BaseCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseCoordinator.swift; sourceTree = ""; }; 1EFE2868253207B2008806B9 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; - 1EFE286A253207DF008806B9 /* ViewControllerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerCoordinator.swift; sourceTree = ""; }; + 1EFE287D25321071008806B9 /* search-facts.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "search-facts.json"; sourceTree = ""; }; + 1EFE2883253210B2008806B9 /* FactsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsService.swift; sourceTree = ""; }; + 1EFE288625321119008806B9 /* Fact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fact.swift; sourceTree = ""; }; + 1EFE288825321123008806B9 /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = ""; }; + 1EFE288D2532135B008806B9 /* FactsListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListViewController.swift; sourceTree = ""; }; + 1EFE288F2532137C008806B9 /* FactsListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListCoordinator.swift; sourceTree = ""; }; + 1EFE2891253214DB008806B9 /* FactsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListViewModel.swift; sourceTree = ""; }; + 1EFE289425321CD2008806B9 /* FactViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactViewModel.swift; sourceTree = ""; }; + 1EFE289625321CE2008806B9 /* FactTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactTableViewCell.swift; sourceTree = ""; }; 21EE31E3903E76B09B8FBA96 /* Pods-Chuck Norris Facts-Chuck Norris FactsUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Chuck Norris Facts-Chuck Norris FactsUITests.debug.xcconfig"; path = "Target Support Files/Pods-Chuck Norris Facts-Chuck Norris FactsUITests/Pods-Chuck Norris Facts-Chuck Norris FactsUITests.debug.xcconfig"; sourceTree = ""; }; 5BFEC696AC24CC9BF87C5195 /* Pods-Chuck Norris FactsTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Chuck Norris FactsTests.release.xcconfig"; path = "Target Support Files/Pods-Chuck Norris FactsTests/Pods-Chuck Norris FactsTests.release.xcconfig"; sourceTree = ""; }; 74304E6C0D317767335DC2AB /* Pods-Chuck Norris Facts.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Chuck Norris Facts.release.xcconfig"; path = "Target Support Files/Pods-Chuck Norris Facts/Pods-Chuck Norris Facts.release.xcconfig"; sourceTree = ""; }; @@ -95,6 +131,107 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 1E32758D2532A2A3007E838A /* Views */ = { + isa = PBXGroup; + children = ( + 1E32758E2532A2C0007E838A /* EmptyListView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 1E3275902532A2C4007E838A /* Lottie */ = { + isa = PBXGroup; + children = ( + 1E3275912532A2CD007E838A /* empty-box.json */, + ); + path = Lottie; + sourceTree = ""; + }; + 1E7F15BA253324760006887B /* Scenes */ = { + isa = PBXGroup; + children = ( + 1E7F15BB253324B10006887B /* Facts */, + ); + path = Scenes; + sourceTree = ""; + }; + 1E7F15BB253324B10006887B /* Facts */ = { + isa = PBXGroup; + children = ( + 1E7F15BC253324BD0006887B /* FactsList */, + ); + path = Facts; + sourceTree = ""; + }; + 1E7F15BC253324BD0006887B /* FactsList */ = { + isa = PBXGroup; + children = ( + 1ED5D1942534AA460035046C /* Stubs */, + 1E7F15C4253329600006887B /* Cells */, + 1E7F15BD253324CF0006887B /* FactsListViewModelTests.swift */, + 1E7F15BF253324FD0006887B /* FactsListViewControllerTests.swift */, + ); + path = FactsList; + sourceTree = ""; + }; + 1E7F15C4253329600006887B /* Cells */ = { + isa = PBXGroup; + children = ( + 1E7F15C5253329780006887B /* FactViewModelTests.swift */, + ); + path = Cells; + sourceTree = ""; + }; + 1ED5D18B25348FC40035046C /* Library */ = { + isa = PBXGroup; + children = ( + 1ED5D18C25348FD50035046C /* XCTestCase+Stub.swift */, + ); + path = Library; + sourceTree = ""; + }; + 1ED5D18E25349E8D0035046C /* Extensions */ = { + isa = PBXGroup; + children = ( + 1ED5D18F25349E9D0035046C /* UIViewController+Rx.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 1ED5D1912534A55D0035046C /* Mocks */ = { + isa = PBXGroup; + children = ( + 1ED5D1922534A56F0035046C /* FactsServiceMock.swift */, + ); + path = Mocks; + sourceTree = ""; + }; + 1ED5D1942534AA460035046C /* Stubs */ = { + isa = PBXGroup; + children = ( + 1ED5D19B2534AAE40035046C /* short-fact.json */, + 1ED5D1972534AA700035046C /* long-fact.json */, + 1ED5D1992534AA7A0035046C /* facts-list.json */, + ); + path = Stubs; + sourceTree = ""; + }; + 1ED5D19D2534B0D60035046C /* Scenes */ = { + isa = PBXGroup; + children = ( + 1ED5D19E2534B0E30035046C /* FactsListScene.swift */, + ); + path = Scenes; + sourceTree = ""; + }; + 1ED5D1A02534B0E80035046C /* Tests */ = { + isa = PBXGroup; + children = ( + 1ED5D1A12534B0F40035046C /* FactsListUITests.swift */, + ); + path = Tests; + sourceTree = ""; + }; 1EE0713025314AF500F6BF6D = { isa = PBXGroup; children = ( @@ -120,9 +257,10 @@ 1EE0713B25314AF500F6BF6D /* Chuck Norris Facts */ = { isa = PBXGroup; children = ( + 1ED5D18E25349E8D0035046C /* Extensions */, + 1EFE2881253210A4008806B9 /* Data */, 1EFE2865253206C8008806B9 /* Library */, 1EFE28632532062A008806B9 /* Scenes */, - 1EFE286225320625008806B9 /* Data */, 1EFE286125320620008806B9 /* App */, 1EFE286025320614008806B9 /* Resources */, ); @@ -132,7 +270,9 @@ 1EE0715225314AF600F6BF6D /* Chuck Norris FactsTests */ = { isa = PBXGroup; children = ( - 1EE0715325314AF600F6BF6D /* Chuck_Norris_FactsTests.swift */, + 1ED5D1912534A55D0035046C /* Mocks */, + 1ED5D18B25348FC40035046C /* Library */, + 1E7F15BA253324760006887B /* Scenes */, 1EE0715525314AF600F6BF6D /* Info.plist */, ); path = "Chuck Norris FactsTests"; @@ -141,7 +281,8 @@ 1EE0715D25314AF600F6BF6D /* Chuck Norris FactsUITests */ = { isa = PBXGroup; children = ( - 1EE0715E25314AF600F6BF6D /* Chuck_Norris_FactsUITests.swift */, + 1ED5D1A02534B0E80035046C /* Tests */, + 1ED5D19D2534B0D60035046C /* Scenes */, 1EE0716025314AF600F6BF6D /* Info.plist */, ); path = "Chuck Norris FactsUITests"; @@ -150,6 +291,8 @@ 1EFE286025320614008806B9 /* Resources */ = { isa = PBXGroup; children = ( + 1E3275902532A2C4007E838A /* Lottie */, + 1EFE287C2532105D008806B9 /* Stubs */, 1EE0714525314AF600F6BF6D /* Assets.xcassets */, 1EE0714725314AF600F6BF6D /* LaunchScreen.storyboard */, 1EE0714A25314AF600F6BF6D /* Info.plist */, @@ -167,36 +310,83 @@ path = App; sourceTree = ""; }; - 1EFE286225320625008806B9 /* Data */ = { + 1EFE28632532062A008806B9 /* Scenes */ = { isa = PBXGroup; children = ( - 1EFE28642532063C008806B9 /* Networking */, + 1EFE288A25321327008806B9 /* Facts */, ); - path = Data; + path = Scenes; sourceTree = ""; }; - 1EFE28632532062A008806B9 /* Scenes */ = { + 1EFE2865253206C8008806B9 /* Library */ = { isa = PBXGroup; children = ( - 1EE0714025314AF500F6BF6D /* ViewController.swift */, - 1EFE286A253207DF008806B9 /* ViewControllerCoordinator.swift */, + 1EFE2866253206D3008806B9 /* BaseCoordinator.swift */, + 1EFE288825321123008806B9 /* JSON.swift */, ); - path = Scenes; + path = Library; sourceTree = ""; }; - 1EFE28642532063C008806B9 /* Networking */ = { + 1EFE287C2532105D008806B9 /* Stubs */ = { isa = PBXGroup; children = ( + 1EFE287D25321071008806B9 /* search-facts.json */, + ); + path = Stubs; + sourceTree = ""; + }; + 1EFE2881253210A4008806B9 /* Data */ = { + isa = PBXGroup; + children = ( + 1EFE288525321111008806B9 /* Models */, + 1EFE2882253210A8008806B9 /* Networking */, + ); + path = Data; + sourceTree = ""; + }; + 1EFE2882253210A8008806B9 /* Networking */ = { + isa = PBXGroup; + children = ( + 1EFE2883253210B2008806B9 /* FactsService.swift */, ); path = Networking; sourceTree = ""; }; - 1EFE2865253206C8008806B9 /* Library */ = { + 1EFE288525321111008806B9 /* Models */ = { isa = PBXGroup; children = ( - 1EFE2866253206D3008806B9 /* BaseCoordinator.swift */, + 1EFE288625321119008806B9 /* Fact.swift */, ); - path = Library; + path = Models; + sourceTree = ""; + }; + 1EFE288A25321327008806B9 /* Facts */ = { + isa = PBXGroup; + children = ( + 1EFE288C25321337008806B9 /* FactsList */, + ); + path = Facts; + sourceTree = ""; + }; + 1EFE288C25321337008806B9 /* FactsList */ = { + isa = PBXGroup; + children = ( + 1E32758D2532A2A3007E838A /* Views */, + 1EFE289325321CB4008806B9 /* Cells */, + 1EFE288D2532135B008806B9 /* FactsListViewController.swift */, + 1EFE288F2532137C008806B9 /* FactsListCoordinator.swift */, + 1EFE2891253214DB008806B9 /* FactsListViewModel.swift */, + ); + path = FactsList; + sourceTree = ""; + }; + 1EFE289325321CB4008806B9 /* Cells */ = { + isa = PBXGroup; + children = ( + 1EFE289425321CD2008806B9 /* FactViewModel.swift */, + 1EFE289625321CE2008806B9 /* FactTableViewCell.swift */, + ); + path = Cells; sourceTree = ""; }; 4AEC7A1E4DDCD345F1E9B6DA /* Pods */ = { @@ -253,6 +443,7 @@ 1EE0714B25314AF600F6BF6D /* Sources */, 1EE0714C25314AF600F6BF6D /* Frameworks */, 1EE0714D25314AF600F6BF6D /* Resources */, + BAF061AC1596B6EFF9116436 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -332,8 +523,10 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1EFE287E25321071008806B9 /* search-facts.json in Resources */, 1EE0714925314AF600F6BF6D /* LaunchScreen.storyboard in Resources */, 1EE0714625314AF600F6BF6D /* Assets.xcassets in Resources */, + 1E3275922532A2CD007E838A /* empty-box.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -341,6 +534,9 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1ED5D19C2534AAE40035046C /* short-fact.json in Resources */, + 1ED5D19A2534AA7A0035046C /* facts-list.json in Resources */, + 1ED5D1982534AA700035046C /* long-fact.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -472,6 +668,23 @@ 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; }; + BAF061AC1596B6EFF9116436 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Chuck Norris FactsTests/Pods-Chuck Norris FactsTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Chuck Norris FactsTests/Pods-Chuck Norris FactsTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Chuck Norris FactsTests/Pods-Chuck Norris FactsTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -479,11 +692,19 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 1EE0714125314AF500F6BF6D /* ViewController.swift in Sources */, - 1EFE286B253207DF008806B9 /* ViewControllerCoordinator.swift in Sources */, + 1EFE2884253210B2008806B9 /* FactsService.swift in Sources */, + 1E32758F2532A2C0007E838A /* EmptyListView.swift in Sources */, + 1EFE288925321123008806B9 /* JSON.swift in Sources */, + 1EFE288725321119008806B9 /* Fact.swift in Sources */, + 1EFE289725321CE2008806B9 /* FactTableViewCell.swift in Sources */, 1EE0713D25314AF500F6BF6D /* AppDelegate.swift in Sources */, + 1ED5D19025349E9D0035046C /* UIViewController+Rx.swift in Sources */, 1EFE2869253207B2008806B9 /* AppCoordinator.swift in Sources */, 1EE0713F25314AF500F6BF6D /* SceneDelegate.swift in Sources */, + 1EFE288E2532135B008806B9 /* FactsListViewController.swift in Sources */, + 1EFE28902532137C008806B9 /* FactsListCoordinator.swift in Sources */, + 1EFE289525321CD2008806B9 /* FactViewModel.swift in Sources */, + 1EFE2892253214DB008806B9 /* FactsListViewModel.swift in Sources */, 1EFE2867253206D3008806B9 /* BaseCoordinator.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -492,7 +713,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 1EE0715425314AF600F6BF6D /* Chuck_Norris_FactsTests.swift in Sources */, + 1E7F15C6253329780006887B /* FactViewModelTests.swift in Sources */, + 1ED5D18D25348FD50035046C /* XCTestCase+Stub.swift in Sources */, + 1ED5D1932534A56F0035046C /* FactsServiceMock.swift in Sources */, + 1E7F15BE253324CF0006887B /* FactsListViewModelTests.swift in Sources */, + 1E7F15C0253324FD0006887B /* FactsListViewControllerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -500,7 +725,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 1EE0715F25314AF600F6BF6D /* Chuck_Norris_FactsUITests.swift in Sources */, + 1ED5D1A22534B0F40035046C /* FactsListUITests.swift in Sources */, + 1ED5D19F2534B0E30035046C /* FactsListScene.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Chuck Norris Facts/App/AppCoordinator.swift b/Chuck Norris Facts/App/AppCoordinator.swift index 936877b..00b8889 100644 --- a/Chuck Norris Facts/App/AppCoordinator.swift +++ b/Chuck Norris Facts/App/AppCoordinator.swift @@ -18,7 +18,7 @@ class AppCoordinator: BaseCoordinator { } override func start() -> Observable { - let viewControllerCoordinator = ViewControllerCoordinator(window: window) - return coordinate(to: viewControllerCoordinator) + let factsListCoordinator = FactsListCoordinator(window: window) + return coordinate(to: factsListCoordinator) } } diff --git a/Chuck Norris Facts/App/AppDelegate.swift b/Chuck Norris Facts/App/AppDelegate.swift index 3b4c2af..5cbdb2e 100644 --- a/Chuck Norris Facts/App/AppDelegate.swift +++ b/Chuck Norris Facts/App/AppDelegate.swift @@ -11,23 +11,12 @@ import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { // Override point for customization after application launch. return true } - // MARK: UISceneSession Lifecycle - - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { - // Called when a new scene session is being created. - // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) - } - - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { - // Called when the user discards a scene session. - // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. - // Use this method to release any resources that were specific to the discarded scenes, as they will not return. - } - } diff --git a/Chuck Norris Facts/App/SceneDelegate.swift b/Chuck Norris Facts/App/SceneDelegate.swift index 92c30b0..986e5cc 100644 --- a/Chuck Norris Facts/App/SceneDelegate.swift +++ b/Chuck Norris Facts/App/SceneDelegate.swift @@ -15,7 +15,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { private var appCoordinator: AppCoordinator? private let disposeBag = DisposeBag() - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { guard let scene = (scene as? UIWindowScene) else { return } let window = UIWindow(windowScene: scene) diff --git a/Chuck Norris Facts/Data/Models/Fact.swift b/Chuck Norris Facts/Data/Models/Fact.swift new file mode 100644 index 0000000..4618f76 --- /dev/null +++ b/Chuck Norris Facts/Data/Models/Fact.swift @@ -0,0 +1,16 @@ +// +// Fact.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/10/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation + +struct Fact: Decodable { + let id: String + let value: String + let url: String? + let iconUrl: String +} diff --git a/Chuck Norris Facts/Data/Networking/FactsService.swift b/Chuck Norris Facts/Data/Networking/FactsService.swift new file mode 100644 index 0000000..ad644d9 --- /dev/null +++ b/Chuck Norris Facts/Data/Networking/FactsService.swift @@ -0,0 +1,45 @@ +// +// FactsService.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/10/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation +import RxSwift + +struct SearchFactsResponse: Decodable { + let total: Int + let result: [Fact] +} + +protocol FactsServiceType { + func searchFacts(query: String) -> Observable<[Fact]> +} + +final class FactsService: FactsServiceType { + + func searchFacts(query: String) -> Observable<[Fact]> { + if CommandLine.arguments.contains("--empty-facts") { + return .just([]) + } + + return Observable<[Fact]>.create { observer in + do { + guard let file = Bundle.main.url(forResource: "search-facts", withExtension: ".json") else { + return Disposables.create {} + } + + let data = try Data(contentsOf: file) + let searchFactsResponse = try JSON.decoder.decode(SearchFactsResponse.self, from: data) + observer.onNext(searchFactsResponse.result) + } catch { + observer.onError(error) + } + observer.onCompleted() + + return Disposables.create {} + } + } +} diff --git a/Chuck Norris Facts/Extensions/UIViewController+Rx.swift b/Chuck Norris Facts/Extensions/UIViewController+Rx.swift new file mode 100644 index 0000000..44b2758 --- /dev/null +++ b/Chuck Norris Facts/Extensions/UIViewController+Rx.swift @@ -0,0 +1,20 @@ +// +// UIViewController+Rx.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/12/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import UIKit +import RxSwift + +extension Reactive where Base: UIViewController { + var viewDidAppear: Observable { + sentMessage(#selector(Base.viewDidAppear(_:))).map { _ in () } + } + + var viewWillAppear: Observable { + sentMessage(#selector(Base.viewWillAppear(_:))).map { _ in () } + } +} diff --git a/Chuck Norris Facts/Library/JSON.swift b/Chuck Norris Facts/Library/JSON.swift new file mode 100644 index 0000000..f2e390a --- /dev/null +++ b/Chuck Norris Facts/Library/JSON.swift @@ -0,0 +1,21 @@ +// +// JSON.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/10/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation + +public struct JSON { + public static var decoder: JSONDecoder { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + return decoder + } + + public static var encoder: JSONEncoder { + JSONEncoder() + } +} diff --git a/Chuck Norris Facts/Resources/Lottie/empty-box.json b/Chuck Norris Facts/Resources/Lottie/empty-box.json new file mode 100644 index 0000000..16e6952 --- /dev/null +++ b/Chuck Norris Facts/Resources/Lottie/empty-box.json @@ -0,0 +1 @@ +{"v":"4.7.0","fr":25,"ip":0,"op":50,"w":120,"h":120,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"ruoi","ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.967]},"o":{"x":[0.167],"y":[0.033]},"n":["0p833_0p967_0p167_0p033"],"t":35,"s":[100],"e":[0]},{"t":49}]},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0,"y":0},"n":"0p833_0p833_0_0","t":0,"s":[57.361,61.016,0],"e":[57.699,41.796,0],"to":[-4.67500305175781,-4.12800598144531,0],"ti":[-13.9099960327148,5.27300262451172,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10.219,"s":[57.699,41.796,0],"e":[79.084,33.982,0],"to":[12.8159942626953,-4.85800170898438,0],"ti":[-4.54498291015625,3.73400115966797,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.445,"s":[79.084,33.982,0],"e":[59.691,9.121,0],"to":[6.61601257324219,-5.43799591064453,0],"ti":[20.0290069580078,1.20700073242188,0]},{"t":35}]},"a":{"a":0,"k":[60.531,10.945,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.994,0],[0,-0.994],[0.995,0],[0,0.994]],"o":[[0.995,0],[0,0.994],[-0.994,0],[0,-0.994]],"v":[[-0.001,-1.801],[1.801,-0.001],[-0.001,1.801],[-1.801,-0.001]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.529,0.529,0.529,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[62.4,13.144],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.422,0],[0,-1.422],[1.421,0],[0,1.422]],"o":[[1.421,0],[0,1.422],[-1.422,0],[0,-1.422]],"v":[[0.001,-2.574],[2.574,0],[0.001,2.574],[-2.574,0]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[0.529,0.529,0.529,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":0.7},"lc":1,"lj":1,"ml":10,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[64.145,9.606],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"ix":2,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.996,0],[0,-1.996],[1.996,0],[0,1.996]],"o":[[1.996,0],[0,1.996],[-1.996,0],[0,-1.996]],"v":[[0,-3.614],[3.614,0],[0,3.614],[-3.614,0]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[0.529,0.529,0.529,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":0.7},"lc":1,"lj":1,"ml":10,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[57.957,10.552],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":3,"cix":2,"ix":3,"mn":"ADBE Vector Group"},{"ty":"tr","p":{"a":0,"k":[60.531,10.941],"ix":2},"a":{"a":0,"k":[60.531,10.941],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"ruoi","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":50,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 2","ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.967]},"o":{"x":[0.167],"y":[0.033]},"n":["0p833_0p967_0p167_0p033"],"t":35,"s":[100],"e":[0]},{"t":49}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[-0.75,-0.75,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-13.91,5.273],[-4.545,3.734],[20.029,1.207]],"o":[[-4.675,-4.128],[12.816,-4.858],[6.616,-5.438],[0,0]],"v":[[-7.383,24.76],[-7.046,5.54],[14.34,-2.273],[-3.178,-24.76]],"c":false}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[0.627,0.627,0.627,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1},"lc":2,"lj":2,"d":[{"n":"d","nm":"dash","v":{"a":0,"k":2.028}},{"n":"g","nm":"gap","v":{"a":0,"k":2.028}},{"n":"o","nm":"offset","v":{"a":0,"k":0}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"tr","p":{"a":0,"k":[67.87,37.631],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.953]},"o":{"x":[0.167],"y":[0.033]},"n":["0p833_0p953_0p167_0p033"],"t":0,"s":[0],"e":[100]},{"t":35}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim"}],"ip":0,"op":50,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":3,"ty":4,"nm":"im_emptyBox Outlines","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[60,60,0]},"a":{"a":0,"k":[60,60,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-0.001,-16.607],[-32.143,-0.002],[-0.001,16.607],[32.144,-0.002]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.8,0.82,0.851,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[60,55.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 7","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[12.856,-23.249],[0,-16.605],[-12.857,-23.249],[-45,-6.641],[-32.144,0.001],[-45,6.645],[-12.857,23.249],[0,16.609],[12.856,23.249],[45,6.645],[32.143,0.001],[45,-6.641]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.957,0.957,0.957,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[60,55.748],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 8","np":2,"cix":2,"ix":2,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-16.072,24.171],[16.072,11.312],[16.072,-24.171],[-16.072,-24.171]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.902,0.914,0.929,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[76.072,83.33],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 9","np":2,"cix":2,"ix":3,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-32.143,-24.171],[-32.143,11.311],[-0.001,24.171],[32.144,11.311],[32.144,-24.171]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.8,0.82,0.851,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[60,83.33],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 10","np":2,"cix":2,"ix":4,"mn":"ADBE Vector Group"},{"ty":"tr","p":{"a":0,"k":[60,60.186],"ix":2},"a":{"a":0,"k":[60,60.186],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"box","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":50,"st":0,"bm":0,"sr":1}]} diff --git a/Chuck Norris Facts/Resources/Stubs/search-facts.json b/Chuck Norris Facts/Resources/Stubs/search-facts.json new file mode 100644 index 0000000..3cb93ab --- /dev/null +++ b/Chuck Norris Facts/Resources/Stubs/search-facts.json @@ -0,0 +1,153 @@ +{ + "total": 16, + "result": [ + { + "categories": [ + "movie" + ], + "created_at": "2020-01-05 13:42:19.576875", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "sudkgw_tr_ejehjag7cqwq", + "updated_at": "2020-01-05 13:42:19.576875", + "url": "https:\/\/api.chucknorris.io\/jokes\/sudkgw_tr_ejehjag7cqwq", + "value": "The opening scene of the movie \"Saving Private Ryan\" is loosely based on games of dodgeball Chuck Norris played in second grade." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:19.576875", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "H7lHICEVSsW25ffciJEjxw", + "updated_at": "2020-01-05 13:42:19.576875", + "url": "https:\/\/api.chucknorris.io\/jokes\/H7lHICEVSsW25ffciJEjxw", + "value": "Chuck Norris can play Xbox Kinect games on his PlayStation4 and PlayStation Move games on his Xbox 720." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:20.568859", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "0fvCgPtrRqe3BzC8jxEkUA", + "updated_at": "2020-01-05 13:42:20.568859", + "url": "https:\/\/api.chucknorris.io\/jokes\/0fvCgPtrRqe3BzC8jxEkUA", + "value": "Chuck Norris doesn't need to play games against people to beat their high scores. He just plays with himself and beats every highscore on every game on every console in the whole entire universe." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:21.795084", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "2COz4ZY4SJaM7WKJUmSZ3Q", + "updated_at": "2020-01-05 13:42:21.795084", + "url": "https:\/\/api.chucknorris.io\/jokes\/2COz4ZY4SJaM7WKJUmSZ3Q", + "value": "Michael Phelps currently holds the record for most Olympic gold medals in a single Games with 8. That record will be broken in 2012, when Chuck Norris wins 22." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:23.484083", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "eOcHK252SCmv6T5MsJiexA", + "updated_at": "2020-01-05 13:42:23.484083", + "url": "https:\/\/api.chucknorris.io\/jokes\/eOcHK252SCmv6T5MsJiexA", + "value": "Why did Chuck Norris hasn't appeared on any mortal kombat games. Simple, the name says it all. \"mortal\". Also there won't be any fatality tha will work on him, he will just roundhouse kick anyone either he wins or loose." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:23.484083", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "BUBK6qDSRqWevu0YGEEZvw", + "updated_at": "2020-01-05 13:42:23.484083", + "url": "https:\/\/api.chucknorris.io\/jokes\/BUBK6qDSRqWevu0YGEEZvw", + "value": "Chuck Norris can fight better than all fighting video games. How? He instantly wins." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:25.099703", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "pYt9of-uQPqyPhk85Z-zUA", + "updated_at": "2020-01-05 13:42:25.099703", + "url": "https:\/\/api.chucknorris.io\/jokes\/pYt9of-uQPqyPhk85Z-zUA", + "value": "If Chuck Norris were a PC or Mac he'd be a Mac because you can't play games with Chuck Norris" + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:25.628594", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "x6hL23bhTEK03DUlagsIUQ", + "updated_at": "2020-01-05 13:42:25.628594", + "url": "https:\/\/api.chucknorris.io\/jokes\/x6hL23bhTEK03DUlagsIUQ", + "value": "Chuck Norris enjoys playing backyard games with his grandchildren. They often play badminton. But instead of using little sissy racquets & a plastic birdie, they use boat oars & dead chickens." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:25.905626", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "-j_jS99eTIi7hrDpRQ9qLw", + "updated_at": "2020-01-05 13:42:25.905626", + "url": "https:\/\/api.chucknorris.io\/jokes\/-j_jS99eTIi7hrDpRQ9qLw", + "value": "Chuck Norris is forbidden from competing in paintball games... for very fucking obvious reasons." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:26.194739", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "20QgKwidT1-ySGoHJCpwSw", + "updated_at": "2020-01-05 13:42:26.194739", + "url": "https:\/\/api.chucknorris.io\/jokes\/20QgKwidT1-ySGoHJCpwSw", + "value": "It's all fun and games until Chuck Norris pulls your eyes out with a socket wrench." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:26.766831", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "RB2hbqTzTd2ORXy53ITqqQ", + "updated_at": "2020-01-05 13:42:26.766831", + "url": "https:\/\/api.chucknorris.io\/jokes\/RB2hbqTzTd2ORXy53ITqqQ", + "value": "Chuck Norris finished every Call of Duty games in less than 15 minutes..........without shooting a single bullet." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:26.766831", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "1Iy7_hYKT5GgOfxkYuTK3A", + "updated_at": "2020-01-05 13:42:26.766831", + "url": "https:\/\/api.chucknorris.io\/jokes\/1Iy7_hYKT5GgOfxkYuTK3A", + "value": "Chuck Norris is unstoppable in all games of Call of Duty" + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:27.496799", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "4QsnKWP-QFar62XWvYTTsw", + "updated_at": "2020-01-05 13:42:27.496799", + "url": "https:\/\/api.chucknorris.io\/jokes\/4QsnKWP-QFar62XWvYTTsw", + "value": "Chuck Norris invented the olympic games. with his left pinky." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:28.664997", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "R3xVlG1FR7qySlLqsK5Yjw", + "updated_at": "2020-01-05 13:42:28.664997", + "url": "https:\/\/api.chucknorris.io\/jokes\/R3xVlG1FR7qySlLqsK5Yjw", + "value": "Chuck Norris' last birthday party was held at the La Brea Tar Pits where he enjoyed all of the party games and easily won the 'dunking for dinosaurs' event." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:29.296379", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "baXxcGBqQG6an7udXMTQWA", + "updated_at": "2020-01-05 13:42:29.296379", + "url": "https:\/\/api.chucknorris.io\/jokes\/baXxcGBqQG6an7udXMTQWA", + "value": "When Chuck Norris plays a game, every minute is potentially \"Sudden Death\" for his opponents...including cards and board games." + }, + { + "categories": [ + "celebrity" + ], + "created_at": "2020-01-05 13:42:29.855523", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "l7QlUREJQzOIJVB88DY9jg", + "updated_at": "2020-01-05 13:42:29.855523", + "url": "https:\/\/api.chucknorris.io\/jokes\/l7QlUREJQzOIJVB88DY9jg", + "value": "Chuck Norris was at the X-games getting ready for competition when he got a message from Paris Hilton saying that she had sent him a friend request on MySpace. An infuriated Chuck Norris logged on to MySpace using his skateboard and rejected the request immediately." + } + ] +} diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/Cells/FactTableViewCell.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/Cells/FactTableViewCell.swift new file mode 100644 index 0000000..41142af --- /dev/null +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/Cells/FactTableViewCell.swift @@ -0,0 +1,89 @@ +// +// FactTableViewCell.swift +// Chuck +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/10/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import UIKit + +class FactTableViewCell: UITableViewCell { + + static let cellIdentifier = "FactTableViewCell" + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private lazy var shadowView: UIView = { + let view = UIView() + + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .systemBackground + + view.layer.shadowColor = UIColor.systemGray.cgColor + view.layer.shadowOpacity = 0.5 + view.layer.shadowOffset = .zero + view.layer.cornerRadius = 16 + + return view + }() + + lazy var bodyLabel: UILabel = { + let label = UILabel() + + label.translatesAutoresizingMaskIntoConstraints = false + label.lineBreakMode = .byWordWrapping + label.numberOfLines = 0 + + return label + }() + + lazy var shareButton: UIButton = { + let button = UIButton(type: .system) + + button.translatesAutoresizingMaskIntoConstraints = false + button.accessibilityLabel = "Share" + button.setImage(UIImage(systemName: "square.and.arrow.up"), for: .normal) + button.accessibilityIdentifier = "shareFactButton" + + return button + }() + + private func setupView() { + clipsToBounds = false + selectionStyle = .none + + contentView.clipsToBounds = false + contentView.addSubview(shadowView) + + shadowView.addSubview(bodyLabel) + shadowView.addSubview(shareButton) + + shadowView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16).isActive = true + shadowView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16).isActive = true + shadowView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16 / 2).isActive = true + shadowView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16 / 2).isActive = true + + bodyLabel.topAnchor.constraint(equalTo: shadowView.topAnchor, constant: 16).isActive = true + bodyLabel.leadingAnchor.constraint(equalTo: shadowView.leadingAnchor, constant: 16).isActive = true + bodyLabel.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor, constant: -16).isActive = true + + shareButton.topAnchor.constraint(equalTo: bodyLabel.bottomAnchor).isActive = true + shareButton.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor, constant: -16).isActive = true + shareButton.bottomAnchor.constraint(equalTo: shadowView.bottomAnchor, constant: -16).isActive = true + } + + func setup(_ fact: FactViewModel) { + bodyLabel.text = fact.text + + let fontSize = fact.text.count > 80 ? 16 : 24 + bodyLabel.font = .systemFont(ofSize: CGFloat(fontSize), weight: .bold) + } +} diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/Cells/FactViewModel.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/Cells/FactViewModel.swift new file mode 100644 index 0000000..b00ebd7 --- /dev/null +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/Cells/FactViewModel.swift @@ -0,0 +1,33 @@ +// +// FactViewModel.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/10/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation +import RxDataSources + +final class FactViewModel { + let text: String + var url: URL? + + init(fact: Fact) { + self.text = fact.value + if let factUrl = fact.url { + self.url = URL(string: factUrl) + } + } +} + +extension FactViewModel: IdentifiableType { + var identity: String { + text + } +} + +extension FactViewModel: Equatable { } +func == (lhs: FactViewModel, rhs: FactViewModel) -> Bool { + return lhs.text == rhs.text +} diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListCoordinator.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListCoordinator.swift new file mode 100644 index 0000000..c86f2fb --- /dev/null +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListCoordinator.swift @@ -0,0 +1,49 @@ +// +// FactsListCoordinator.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/10/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import UIKit +import RxSwift + +final class FactsListCoordinator: BaseCoordinator { + + private let window: UIWindow + + init(window: UIWindow) { + self.window = window + } + + override func start() -> Observable { + let factsListViewModel = FactsListViewModel() + let factsListViewController = FactsListViewController() + factsListViewController.viewModel = factsListViewModel + + let navigationController = UINavigationController(rootViewController: factsListViewController) + + factsListViewModel.showShareFact + .bind(onNext: { [weak self] in + self?.showShareFact(fact: $0, in: navigationController) + }) + .disposed(by: disposeBag) + + window.rootViewController = navigationController + window.makeKeyAndVisible() + + return Observable.never() + } + + private func showShareFact(fact: FactViewModel, in navigationController: UINavigationController) { + var activityItems: [Any] = [fact.text] + + if let factUrl = fact.url { + activityItems.append(factUrl) + } + + let shareActivity = UIActivityViewController(activityItems: activityItems, applicationActivities: nil) + navigationController.present(shareActivity, animated: true, completion: nil) + } +} diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift new file mode 100644 index 0000000..12e9ad7 --- /dev/null +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift @@ -0,0 +1,116 @@ +// +// FactsListViewController.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/10/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import UIKit +import RxSwift +import RxCocoa +import RxDataSources + +class FactsListViewController: UIViewController { + + var viewModel: FactsListViewModel! + + private let disposeBag = DisposeBag() + + let tableView = UITableView() + let emptyListView = EmptyListView() + + private lazy var factsDataSource = RxTableViewSectionedAnimatedDataSource( + configureCell: { [weak self] _, tableView, indexPath, fact -> UITableViewCell in + + guard let viewModel = self?.viewModel, let disposeBag = self?.disposeBag else { return UITableViewCell() } + let cell = tableView.dequeueReusableCell(withIdentifier: FactTableViewCell.cellIdentifier, for: indexPath) + + if let cell = cell as? FactTableViewCell { + cell.setup(fact) + cell.shareButton.rx.tap + .map { fact } + .bind(to: viewModel.startShareFact) + .disposed(by: disposeBag) + } + + return cell + } + ) + + override func viewDidLoad() { + super.viewDidLoad() + + setupView() + setupBindings() + setupTableView() + setupEmptyListView() + setupNavigationBar() + } + + private func setupView() { + view.backgroundColor = .systemBackground + } + + private func setupTableView() { + view.addSubview(tableView) + + tableView.separatorStyle = .none + + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + + tableView.register(FactTableViewCell.self, forCellReuseIdentifier: FactTableViewCell.cellIdentifier) + + tableView.accessibilityIdentifier = "factsTableView" + } + + private func setupEmptyListView() { + view.addSubview(emptyListView) + + emptyListView.translatesAutoresizingMaskIntoConstraints = false + emptyListView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true + emptyListView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true + emptyListView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + emptyListView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + } + + private func setupNavigationBar() { + navigationItem.title = "Chuck Norris Facts" + navigationController?.navigationBar.prefersLargeTitles = true + } + + private func setupBindings() { + rx.viewDidAppear + .bind(to: viewModel.viewDidAppear) + .disposed(by: disposeBag) + + viewModel.facts + .map { $0.flatMap { $0.items } } + .map { $0.isEmpty } + .asDriver(onErrorJustReturn: true) + .drive(onNext: { [weak self] isEmpty in + self?.showEmptyView(isEmpty) + }) + .disposed(by: disposeBag) + + viewModel.facts + .observeOn(MainScheduler.instance) + .bind(to: tableView.rx.items(dataSource: factsDataSource)) + .disposed(by: disposeBag) + } + + private func showEmptyView(_ isEmpty: Bool) { + tableView.isHidden = isEmpty + emptyListView.isHidden = !isEmpty + + if isEmpty { + emptyListView.play() + } else { + emptyListView.stop() + } + } +} diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift new file mode 100644 index 0000000..97d0455 --- /dev/null +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift @@ -0,0 +1,43 @@ +// +// FactsListViewModel.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/10/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation +import RxSwift +import RxDataSources + +typealias FactsSectionModel = AnimatableSectionModel + +final class FactsListViewModel { + + // MARK: - Inputs + + let viewDidAppear: AnyObserver + + let startShareFact: AnyObserver + + // MARK: - Outputs + + let facts: Observable<[FactsSectionModel]> + + let showShareFact: Observable + + init(factsService: FactsServiceType = FactsService()) { + + let viewDidAppearSubject = PublishSubject() + self.viewDidAppear = viewDidAppearSubject.asObserver() + + self.facts = viewDidAppearSubject.flatMapLatest { _ in factsService.searchFacts(query: "") } + .map { Array($0.shuffled().prefix(10)) } + .map { $0.map { FactViewModel(fact: $0) } } + .map { [FactsSectionModel(model: "", items: $0)] } + + let startShareFactSubject = PublishSubject() + self.startShareFact = startShareFactSubject.asObserver() + self.showShareFact = startShareFactSubject.asObservable() + } +} diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/Views/EmptyListView.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/Views/EmptyListView.swift new file mode 100644 index 0000000..dcf17c1 --- /dev/null +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/Views/EmptyListView.swift @@ -0,0 +1,67 @@ +// +// EmptyView.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/10/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import UIKit +import Lottie + +final class EmptyListView: UIView { + + private lazy var animation: AnimationView = { + let animation = AnimationView() + + animation.animation = Animation.named("empty-box") + animation.loopMode = .loop + + return animation + }() + + private lazy var label: UILabel = { + let label = UILabel() + + label.translatesAutoresizingMaskIntoConstraints = false + label.lineBreakMode = .byWordWrapping + label.numberOfLines = 0 + + label.text = "Looks like there are no Facts" + label.font = UIFont.systemFont(ofSize: 18, weight: .semibold) + + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + addSubview(animation) + addSubview(label) + + animation.translatesAutoresizingMaskIntoConstraints = false + animation.widthAnchor.constraint(equalToConstant: 200).isActive = true + animation.heightAnchor.constraint(equalToConstant: 200).isActive = true + animation.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true + animation.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true + + label.translatesAutoresizingMaskIntoConstraints = false + label.topAnchor.constraint(equalTo: animation.bottomAnchor).isActive = true + label.centerXAnchor.constraint(equalTo: animation.centerXAnchor).isActive = true + + accessibilityIdentifier = "emptyListView" + label.accessibilityIdentifier = "emptyListLabelView" + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func play() { + animation.play() + } + + func stop() { + animation.stop() + } +} diff --git a/Chuck Norris Facts/Scenes/ViewController.swift b/Chuck Norris Facts/Scenes/ViewController.swift deleted file mode 100644 index e2acbca..0000000 --- a/Chuck Norris Facts/Scenes/ViewController.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// ViewController.swift -// Chuck Norris Facts -// -// Created by Djorkaeff Alexandre Vilela Pereira on 10/9/20. -// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. -// - -import UIKit - -class ViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - - setupView() - } - - private func setupView() { - view.backgroundColor = .systemBackground - } - -} diff --git a/Chuck Norris Facts/Scenes/ViewControllerCoordinator.swift b/Chuck Norris Facts/Scenes/ViewControllerCoordinator.swift deleted file mode 100644 index d70cd41..0000000 --- a/Chuck Norris Facts/Scenes/ViewControllerCoordinator.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// ViewControllerCoordinator.swift -// Chuck Norris Facts -// -// Created by Djorkaeff Alexandre Vilela Pereira on 10/10/20. -// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. -// - -import UIKit -import RxSwift - -final class ViewControllerCoordinator: BaseCoordinator { - - private let window: UIWindow - - init(window: UIWindow) { - self.window = window - } - - override func start() -> Observable { - let viewController = ViewController() - - window.rootViewController = viewController - window.makeKeyAndVisible() - - return Observable.never() - } -} diff --git a/Chuck Norris FactsTests/Chuck_Norris_FactsTests.swift b/Chuck Norris FactsTests/Chuck_Norris_FactsTests.swift deleted file mode 100644 index 025a84c..0000000 --- a/Chuck Norris FactsTests/Chuck_Norris_FactsTests.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// Chuck_Norris_FactsTests.swift -// Chuck Norris FactsTests -// -// Created by Djorkaeff Alexandre Vilela Pereira on 10/9/20. -// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. -// - -import XCTest -@testable import Chuck_Norris_Facts - -class Chuck_Norris_FactsTests: 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. - } - - 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/Chuck Norris FactsTests/Library/XCTestCase+Stub.swift b/Chuck Norris FactsTests/Library/XCTestCase+Stub.swift new file mode 100644 index 0000000..50435dd --- /dev/null +++ b/Chuck Norris FactsTests/Library/XCTestCase+Stub.swift @@ -0,0 +1,27 @@ +// +// Fact+Extensions.swift +// Chuck Norris FactsTests +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/12/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation +import XCTest + +@testable import Chuck_Norris_Facts + +extension XCTestCase { + + func stub(_ resource: String, type: T.Type, decoder: JSONDecoder = JSON.decoder) throws -> T? { + let bundle = Bundle(for: Self.self) + + guard let url = bundle.url(forResource: resource, withExtension: ".json") else { + return nil + } + + let data = try Data(contentsOf: url) + let stub = try decoder.decode(type, from: data) + return stub + } +} diff --git a/Chuck Norris FactsTests/Mocks/FactsServiceMock.swift b/Chuck Norris FactsTests/Mocks/FactsServiceMock.swift new file mode 100644 index 0000000..eabc819 --- /dev/null +++ b/Chuck Norris FactsTests/Mocks/FactsServiceMock.swift @@ -0,0 +1,20 @@ +// +// FactsServiceMock.swift +// Chuck Norris FactsTests +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/12/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation +import RxSwift + +@testable import Chuck_Norris_Facts + +final class FactsServiceMock: FactsServiceType { + + var searchFactsReturnValue: Observable<[Fact]> = .just([]) + func searchFacts(query: String) -> Observable<[Fact]> { + return searchFactsReturnValue + } +} diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/Cells/FactViewModelTests.swift b/Chuck Norris FactsTests/Scenes/Facts/FactsList/Cells/FactViewModelTests.swift new file mode 100644 index 0000000..eb4af10 --- /dev/null +++ b/Chuck Norris FactsTests/Scenes/Facts/FactsList/Cells/FactViewModelTests.swift @@ -0,0 +1,34 @@ +// +// FactViewModel.swift +// Chuck Norris FactsTests +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/11/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import XCTest +import RxSwift +import RxBlocking +import RxTest + +@testable import Chuck_Norris_Facts + +class FactViewModelTests: XCTestCase { + + func test_factViewModelIsEquatable() throws { + let factStub = try stub("short-fact", type: Fact.self) + let fact = try XCTUnwrap(factStub, "looks like short-fact.json doesn't exists") + + let factViewModelTest = FactViewModel(fact: fact) + let factViewModel = FactViewModel(fact: fact) + XCTAssertEqual(factViewModelTest, factViewModel) + } + + func test_factViewModelIsIdentifiable() throws { + let factStub = try stub("short-fact", type: Fact.self) + let fact = try XCTUnwrap(factStub, "looks like short-fact.json doesn't exists") + + let factViewModelTest = FactViewModel(fact: fact) + XCTAssertEqual(factViewModelTest.identity, fact.value) + } +} diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift new file mode 100644 index 0000000..1f0a2d3 --- /dev/null +++ b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift @@ -0,0 +1,107 @@ +// +// FactsListViewControllerTests.swift +// Chuck Norris FactsTests +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/11/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import XCTest +import RxSwift +import RxBlocking +import RxTest + +@testable import Chuck_Norris_Facts + +class FactsListViewControllerTests: XCTestCase { + + var factsListViewController: FactsListViewController! + var factsListViewModel: FactsListViewModel! + var factsServiceMock: FactsServiceMock! + var disposeBag: DisposeBag! + + override func setUp() { + disposeBag = DisposeBag() + factsServiceMock = FactsServiceMock() + factsListViewModel = FactsListViewModel(factsService: factsServiceMock) + factsListViewController = FactsListViewController() + factsListViewController.viewModel = factsListViewModel + + factsListViewController.loadView() + factsListViewController.viewDidLoad() + } + + override func tearDown() { + disposeBag = nil + factsListViewModel = nil + factsListViewController = nil + } + + func test_factsListEmptyShouldShowEmptyList() { + factsServiceMock.searchFactsReturnValue = .just([]) + + factsListViewModel.viewDidAppear.onNext(()) + + XCTAssertFalse(factsListViewController.emptyListView.isHidden) + XCTAssertTrue(factsListViewController.tableView.isHidden) + } + + func test_factCellFontSizeShouldBe24ForShortContent() throws { + let factStub = try stub("short-fact", type: Fact.self) + let fact = try XCTUnwrap(factStub, "looks like short-fact.json doesn't exists") + + factsServiceMock.searchFactsReturnValue = .just([fact]) + + factsListViewModel.viewDidAppear.onNext(()) + + let factCell = factsListFirstCell() + + XCTAssertEqual(factCell?.bodyLabel.font.pointSize, 24) + } + + func test_factCellFontSizeShouldBe16ForLongContent() throws { + let factStub = try stub("long-fact", type: Fact.self) + let fact = try XCTUnwrap(factStub, "looks like long-fact.json doesn't exists") + + factsServiceMock.searchFactsReturnValue = .just([fact]) + + factsListViewModel.viewDidAppear.onNext(()) + + let factCell = factsListFirstCell() + + XCTAssertEqual(factCell?.bodyLabel.font.pointSize, 16) + } + + func test_shareFactButtonTapShouldShowShareFact() throws { + let factStub = try stub("short-fact", type: Fact.self) + let fact = try XCTUnwrap(factStub, "looks like short-fact.json doesn't exists") + + factsServiceMock.searchFactsReturnValue = .just([fact]) + + let testScheduler = TestScheduler(initialClock: 0) + let shareFactObserver = testScheduler.createObserver(FactViewModel.self) + + factsListViewModel.viewDidAppear + .onNext(()) + + factsListViewModel.showShareFact + .subscribe(shareFactObserver) + .disposed(by: disposeBag) + + testScheduler.start() + + let factCell = factsListFirstCell() + factCell?.shareButton.sendActions(for: .touchUpInside) + + let events = shareFactObserver.events.compactMap { $0.value.element } + XCTAssertEqual(events.count, 1) + } + +} + +extension FactsListViewControllerTests { + func factsListFirstCell() -> FactTableViewCell? { + let indexPath = IndexPath(row: 0, section: 0) + return factsListViewController.tableView.cellForRow(at: indexPath) as? FactTableViewCell + } +} diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift new file mode 100644 index 0000000..ded4a47 --- /dev/null +++ b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift @@ -0,0 +1,74 @@ +// +// FactsListViewModelTests.swift +// Chuck Norris FactsTests +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/11/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import XCTest +import RxSwift +import RxBlocking +import RxTest + +@testable import Chuck_Norris_Facts + +class FactsListViewModelTests: XCTestCase { + var factsListViewModel: FactsListViewModel! + var factsServiceMock: FactsServiceMock! + var testScheduler: TestScheduler! + + var disposeBag: DisposeBag! + + override func setUp() { + disposeBag = DisposeBag() + testScheduler = TestScheduler(initialClock: 0) + factsServiceMock = FactsServiceMock() + factsListViewModel = FactsListViewModel(factsService: factsServiceMock) + } + + override func tearDown() { + disposeBag = nil + testScheduler = nil + factsServiceMock = nil + factsListViewModel = nil + } + + func test_load10RandomFacts() throws { + let factsListStub = try stub("facts-list", type: [Fact].self) + let factsList = try XCTUnwrap(factsListStub, "looks like facts-list.json doesn't exists") + factsServiceMock.searchFactsReturnValue = .just(factsList) + + let factsObserver = testScheduler.createObserver([FactsSectionModel].self) + + factsListViewModel.facts + .subscribe(factsObserver) + .disposed(by: disposeBag) + + factsListViewModel.viewDidAppear.onNext(()) + + testScheduler.start() + + let sectionModels = factsObserver.events.compactMap { $0.value.element }.first + XCTAssertEqual(sectionModels?.first?.items.count, 10) + } + + func test_showShareFact() throws { + let factStub = try stub("short-fact", type: Fact.self) + let fact = try XCTUnwrap(factStub, "looks like short-fact.json doesn't exists") + + factsListViewModel.viewDidAppear.onNext(()) + + let factObserver = testScheduler.createObserver(FactViewModel.self) + + factsListViewModel.showShareFact + .subscribe(factObserver) + .disposed(by: disposeBag) + + let factViewModel = FactViewModel(fact: fact) + factsListViewModel.startShareFact.onNext(factViewModel) + + let shareFact = factObserver.events.compactMap { $0.value.element }.first + XCTAssertEqual(fact.value, shareFact?.text) + } +} diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/Stubs/facts-list.json b/Chuck Norris FactsTests/Scenes/Facts/FactsList/Stubs/facts-list.json new file mode 100644 index 0000000..1b7c6d5 --- /dev/null +++ b/Chuck Norris FactsTests/Scenes/Facts/FactsList/Stubs/facts-list.json @@ -0,0 +1,150 @@ +[ + { + "categories": [ + "movie" + ], + "created_at": "2020-01-05 13:42:19.576875", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "sudkgw_tr_ejehjag7cqwq", + "updated_at": "2020-01-05 13:42:19.576875", + "url": "https:\/\/api.chucknorris.io\/jokes\/sudkgw_tr_ejehjag7cqwq", + "value": "The opening scene of the movie \"Saving Private Ryan\" is loosely based on games of dodgeball Chuck Norris played in second grade." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:19.576875", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "H7lHICEVSsW25ffciJEjxw", + "updated_at": "2020-01-05 13:42:19.576875", + "url": "https:\/\/api.chucknorris.io\/jokes\/H7lHICEVSsW25ffciJEjxw", + "value": "Chuck Norris can play Xbox Kinect games on his PlayStation4 and PlayStation Move games on his Xbox 720." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:20.568859", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "0fvCgPtrRqe3BzC8jxEkUA", + "updated_at": "2020-01-05 13:42:20.568859", + "url": "https:\/\/api.chucknorris.io\/jokes\/0fvCgPtrRqe3BzC8jxEkUA", + "value": "Chuck Norris doesn't need to play games against people to beat their high scores. He just plays with himself and beats every highscore on every game on every console in the whole entire universe." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:21.795084", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "2COz4ZY4SJaM7WKJUmSZ3Q", + "updated_at": "2020-01-05 13:42:21.795084", + "url": "https:\/\/api.chucknorris.io\/jokes\/2COz4ZY4SJaM7WKJUmSZ3Q", + "value": "Michael Phelps currently holds the record for most Olympic gold medals in a single Games with 8. That record will be broken in 2012, when Chuck Norris wins 22." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:23.484083", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "eOcHK252SCmv6T5MsJiexA", + "updated_at": "2020-01-05 13:42:23.484083", + "url": "https:\/\/api.chucknorris.io\/jokes\/eOcHK252SCmv6T5MsJiexA", + "value": "Why did Chuck Norris hasn't appeared on any mortal kombat games. Simple, the name says it all. \"mortal\". Also there won't be any fatality tha will work on him, he will just roundhouse kick anyone either he wins or loose." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:23.484083", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "BUBK6qDSRqWevu0YGEEZvw", + "updated_at": "2020-01-05 13:42:23.484083", + "url": "https:\/\/api.chucknorris.io\/jokes\/BUBK6qDSRqWevu0YGEEZvw", + "value": "Chuck Norris can fight better than all fighting video games. How? He instantly wins." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:25.099703", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "pYt9of-uQPqyPhk85Z-zUA", + "updated_at": "2020-01-05 13:42:25.099703", + "url": "https:\/\/api.chucknorris.io\/jokes\/pYt9of-uQPqyPhk85Z-zUA", + "value": "If Chuck Norris were a PC or Mac he'd be a Mac because you can't play games with Chuck Norris" + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:25.628594", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "x6hL23bhTEK03DUlagsIUQ", + "updated_at": "2020-01-05 13:42:25.628594", + "url": "https:\/\/api.chucknorris.io\/jokes\/x6hL23bhTEK03DUlagsIUQ", + "value": "Chuck Norris enjoys playing backyard games with his grandchildren. They often play badminton. But instead of using little sissy racquets & a plastic birdie, they use boat oars & dead chickens." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:25.905626", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "-j_jS99eTIi7hrDpRQ9qLw", + "updated_at": "2020-01-05 13:42:25.905626", + "url": "https:\/\/api.chucknorris.io\/jokes\/-j_jS99eTIi7hrDpRQ9qLw", + "value": "Chuck Norris is forbidden from competing in paintball games... for very fucking obvious reasons." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:26.194739", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "20QgKwidT1-ySGoHJCpwSw", + "updated_at": "2020-01-05 13:42:26.194739", + "url": "https:\/\/api.chucknorris.io\/jokes\/20QgKwidT1-ySGoHJCpwSw", + "value": "It's all fun and games until Chuck Norris pulls your eyes out with a socket wrench." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:26.766831", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "RB2hbqTzTd2ORXy53ITqqQ", + "updated_at": "2020-01-05 13:42:26.766831", + "url": "https:\/\/api.chucknorris.io\/jokes\/RB2hbqTzTd2ORXy53ITqqQ", + "value": "Chuck Norris finished every Call of Duty games in less than 15 minutes..........without shooting a single bullet." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:26.766831", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "1Iy7_hYKT5GgOfxkYuTK3A", + "updated_at": "2020-01-05 13:42:26.766831", + "url": "https:\/\/api.chucknorris.io\/jokes\/1Iy7_hYKT5GgOfxkYuTK3A", + "value": "Chuck Norris is unstoppable in all games of Call of Duty" + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:27.496799", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "4QsnKWP-QFar62XWvYTTsw", + "updated_at": "2020-01-05 13:42:27.496799", + "url": "https:\/\/api.chucknorris.io\/jokes\/4QsnKWP-QFar62XWvYTTsw", + "value": "Chuck Norris invented the olympic games. with his left pinky." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:28.664997", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "R3xVlG1FR7qySlLqsK5Yjw", + "updated_at": "2020-01-05 13:42:28.664997", + "url": "https:\/\/api.chucknorris.io\/jokes\/R3xVlG1FR7qySlLqsK5Yjw", + "value": "Chuck Norris' last birthday party was held at the La Brea Tar Pits where he enjoyed all of the party games and easily won the 'dunking for dinosaurs' event." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:29.296379", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "baXxcGBqQG6an7udXMTQWA", + "updated_at": "2020-01-05 13:42:29.296379", + "url": "https:\/\/api.chucknorris.io\/jokes\/baXxcGBqQG6an7udXMTQWA", + "value": "When Chuck Norris plays a game, every minute is potentially \"Sudden Death\" for his opponents...including cards and board games." + }, + { + "categories": [ + "celebrity" + ], + "created_at": "2020-01-05 13:42:29.855523", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "l7QlUREJQzOIJVB88DY9jg", + "updated_at": "2020-01-05 13:42:29.855523", + "url": "https:\/\/api.chucknorris.io\/jokes\/l7QlUREJQzOIJVB88DY9jg", + "value": "Chuck Norris was at the X-games getting ready for competition when he got a message from Paris Hilton saying that she had sent him a friend request on MySpace. An infuriated Chuck Norris logged on to MySpace using his skateboard and rejected the request immediately." + } +] diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/Stubs/long-fact.json b/Chuck Norris FactsTests/Scenes/Facts/FactsList/Stubs/long-fact.json new file mode 100644 index 0000000..9c93886 --- /dev/null +++ b/Chuck Norris FactsTests/Scenes/Facts/FactsList/Stubs/long-fact.json @@ -0,0 +1,6 @@ +{ + "icon_url" : "https://assets.chucknorris.host/img/avatar/chuck-norris.png", + "id" : "irY3YudqS1qXxhfWxw12NQ", + "url" : "", + "value" : "Chuck Norris has a chainsaw bayonet attached to the end of his gatling gun. That's how he likes it." +} diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/Stubs/short-fact.json b/Chuck Norris FactsTests/Scenes/Facts/FactsList/Stubs/short-fact.json new file mode 100644 index 0000000..70c6d93 --- /dev/null +++ b/Chuck Norris FactsTests/Scenes/Facts/FactsList/Stubs/short-fact.json @@ -0,0 +1,7 @@ +{ + "icon_url" : "https://assets.chucknorris.host/img/avatar/chuck-norris.png", + "id" : "gusqVaYoSMKnJ3KKzca3GQ", + "url" : "", + "value" : "Chuck Norris doesn't pay attention, attention pays Chuck Norris." +} + diff --git a/Chuck Norris FactsUITests/Chuck_Norris_FactsUITests.swift b/Chuck Norris FactsUITests/Chuck_Norris_FactsUITests.swift deleted file mode 100644 index cafaed4..0000000 --- a/Chuck Norris FactsUITests/Chuck_Norris_FactsUITests.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// Chuck_Norris_FactsUITests.swift -// Chuck Norris FactsUITests -// -// Created by Djorkaeff Alexandre Vilela Pereira on 10/9/20. -// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. -// - -import XCTest - -class Chuck_Norris_FactsUITests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false - - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() - - // Use recording to get started writing UI tests. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testLaunchPerformance() throws { - if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { - // This measures how long it takes to launch your application. - measure(metrics: [XCTOSSignpostMetric.applicationLaunch]) { - XCUIApplication().launch() - } - } - } -} diff --git a/Chuck Norris FactsUITests/Scenes/FactsListScene.swift b/Chuck Norris FactsUITests/Scenes/FactsListScene.swift new file mode 100644 index 0000000..5ab83a4 --- /dev/null +++ b/Chuck Norris FactsUITests/Scenes/FactsListScene.swift @@ -0,0 +1,26 @@ +// +// FactsListScene.swift +// Chuck Norris FactsUITests +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/12/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation +import XCTest + +struct FactsListScene { + + let factsTableView: XCUIElement + let emptyListView: XCUIElement + let emptyListLabelView: XCUIElement + + init() { + let app = XCUIApplication() + + factsTableView = app.tables["factsTableView"] + emptyListView = app.otherElements["emptyListView"] + emptyListLabelView = app.staticTexts["emptyListLabelView"] + } + +} diff --git a/Chuck Norris FactsUITests/Tests/FactsListUITests.swift b/Chuck Norris FactsUITests/Tests/FactsListUITests.swift new file mode 100644 index 0000000..739b5d6 --- /dev/null +++ b/Chuck Norris FactsUITests/Tests/FactsListUITests.swift @@ -0,0 +1,57 @@ +// +// FactsListTests.swift +// Chuck Norris FactsUITests +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/12/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation +import XCTest + +final class FactsListUITests: XCTestCase { + + var app: XCUIApplication! + + override func setUp() { + app = XCUIApplication() + continueAfterFailure = false + } + + func test_showEmptyView() throws { + app.launchArguments = ["--empty-facts"] + app.launch() + + let factsListScene = FactsListScene() + + XCTAssertTrue(factsListScene.emptyListView.exists) + XCTAssertTrue(factsListScene.emptyListLabelView.exists) + } + + func test_show10RandomFacts() { + app.launch() + + let factsListScene = FactsListScene() + + XCTAssertEqual(factsListScene.factsTableView.cells.count, 10) + } + + func test_shareFact() { + app.launch() + + let factsListScene = FactsListScene() + let firstFactCell = factsListScene.factsTableView.firstMatch + let shareFactButton = firstFactCell.buttons["shareFactButton"] + XCTAssertTrue(shareFactButton.exists) + + shareFactButton.firstMatch.tap() + + let shareActivity = app.otherElements["ActivityListView"] + XCTAssertTrue(shareActivity.waitForExistence(timeout: 1)) + + let shareActivityClose = shareActivity.buttons["Close"] + shareActivityClose.tap() + + XCTAssertFalse(shareActivity.waitForExistence(timeout: 1)) + } +} diff --git a/Podfile b/Podfile index 9e92881..ccd5ccf 100644 --- a/Podfile +++ b/Podfile @@ -1,19 +1,34 @@ platform :ios, '11.0' +def test_pods + + # Rx + pod 'RxTest' + pod 'RxBlocking' + +end + target 'Chuck Norris Facts' do use_frameworks! # Rx pod 'RxSwift' + pod 'RxCocoa' + pod 'RxDataSources' # Tools pod 'SwiftLint' + # UI + pod 'lottie-ios' + target 'Chuck Norris FactsTests' do inherit! :search_paths + test_pods end target 'Chuck Norris FactsUITests' do + test_pods end end diff --git a/Podfile.lock b/Podfile.lock index 7e3c763..6135ede 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,20 +1,54 @@ PODS: + - Differentiator (4.0.1) + - lottie-ios (3.1.8) + - RxBlocking (5.1.1): + - RxSwift (~> 5) + - RxCocoa (5.1.1): + - RxRelay (~> 5) + - RxSwift (~> 5) + - RxDataSources (4.0.1): + - Differentiator (~> 4.0) + - RxCocoa (~> 5.0) + - RxSwift (~> 5.0) + - RxRelay (5.1.1): + - RxSwift (~> 5) - RxSwift (5.1.1) + - RxTest (5.1.1): + - RxSwift (~> 5) - SwiftLint (0.40.3) DEPENDENCIES: + - lottie-ios + - RxBlocking + - RxCocoa + - RxDataSources - RxSwift + - RxTest - SwiftLint SPEC REPOS: trunk: + - Differentiator + - lottie-ios + - RxBlocking + - RxCocoa + - RxDataSources + - RxRelay - RxSwift + - RxTest - SwiftLint SPEC CHECKSUMS: + Differentiator: 886080237d9f87f322641dedbc5be257061b0602 + lottie-ios: 48fac6be217c76937e36e340e2d09cf7b10b7f5f + RxBlocking: 5f700a78cad61ce253ebd37c9a39b5ccc76477b4 + RxCocoa: 32065309a38d29b5b0db858819b5bf9ef038b601 + RxDataSources: efee07fa4de48477eca0a4611e6d11e2da9c1114 + RxRelay: d77f7d771495f43c556cbc43eebd1bb54d01e8e9 RxSwift: 81470a2074fa8780320ea5fe4102807cb7118178 + RxTest: 711632d5644dffbeb62c936a521b5b008a1e1faa SwiftLint: dfd554ff0dff17288ee574814ccdd5cea85d76f7 -PODFILE CHECKSUM: 81e3cadf48d11d7a2c25a92e9453ceb6b62e7c1d +PODFILE CHECKSUM: 72e38736d56fcdc5c7652b03439f869ce6a2e7c7 COCOAPODS: 1.9.3 From 8e044817999a6b9af2fddb2616dd0ca5ecaaf976 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Tue, 20 Oct 2020 18:36:37 -0300 Subject: [PATCH 05/18] Search facts (#12) * Add Search Button to FactsList * Coordinate to SearchFacts Scene * SearchFacts scene Cancel button * List facts by a Search * Setup Search Facts Loading * Improve transition between fully list and empty * Improve networking layer * Fix FactsList Unit Tests * UI Tests of SearchFacts * Unit Tests of SearchFacts * Remove unnecessary code --- Chuck Norris Facts.xcodeproj/project.pbxproj | 90 +++++++++++++++-- .../xcschemes/Chuck Norris Facts.xcscheme | 98 +++++++++++++++++++ .../Data/Networking/FactsAPI.swift | 50 ++++++++++ .../Data/Networking/FactsService.swift | 45 --------- .../Responses/SearchFactsResponse.swift | 19 ++++ .../Data/Services/FactsService.swift | 28 ++++++ .../Library/ActivityIndicator.swift | 80 +++++++++++++++ .../{Lottie => Animations}/empty-box.json | 0 .../Resources/Animations/loading.json | 1 + .../FactsList/FactsListCoordinator.swift | 19 ++++ .../FactsList/FactsListViewController.swift | 47 +++++++++ .../Facts/FactsList/FactsListViewModel.swift | 49 +++++++++- .../Facts/FactsList/Views/EmptyListView.swift | 17 +++- .../SearchFacts/SearchFactsCoordinator.swift | 41 ++++++++ .../SearchFactsViewController.swift | 67 +++++++++++++ .../SearchFacts/SearchFactsViewModel.swift | 42 ++++++++ .../Mocks/FactsServiceMock.swift | 2 +- .../FactsListViewControllerTests.swift | 6 +- .../FactsList/FactsListViewModelTests.swift | 1 + .../SearchFactsViewModelTests.swift | 64 ++++++++++++ .../Scenes/FactsListScene.swift | 2 + .../Scenes/SearchFactsScene.swift | 26 +++++ .../Tests/FactsListUITests.swift | 18 ++++ .../Tests/SearchFactsUITests.swift | 49 ++++++++++ Podfile | 3 + Podfile.lock | 13 ++- 26 files changed, 810 insertions(+), 67 deletions(-) create mode 100644 Chuck Norris Facts.xcodeproj/xcshareddata/xcschemes/Chuck Norris Facts.xcscheme create mode 100644 Chuck Norris Facts/Data/Networking/FactsAPI.swift delete mode 100644 Chuck Norris Facts/Data/Networking/FactsService.swift create mode 100644 Chuck Norris Facts/Data/Networking/Responses/SearchFactsResponse.swift create mode 100644 Chuck Norris Facts/Data/Services/FactsService.swift create mode 100644 Chuck Norris Facts/Library/ActivityIndicator.swift rename Chuck Norris Facts/Resources/{Lottie => Animations}/empty-box.json (100%) create mode 100644 Chuck Norris Facts/Resources/Animations/loading.json create mode 100644 Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsCoordinator.swift create mode 100644 Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewController.swift create mode 100644 Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewModel.swift create mode 100644 Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewModelTests.swift create mode 100644 Chuck Norris FactsUITests/Scenes/SearchFactsScene.swift create mode 100644 Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift diff --git a/Chuck Norris Facts.xcodeproj/project.pbxproj b/Chuck Norris Facts.xcodeproj/project.pbxproj index b78f160..088fd53 100644 --- a/Chuck Norris Facts.xcodeproj/project.pbxproj +++ b/Chuck Norris Facts.xcodeproj/project.pbxproj @@ -9,9 +9,19 @@ /* Begin PBXBuildFile section */ 1E32758F2532A2C0007E838A /* EmptyListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E32758E2532A2C0007E838A /* EmptyListView.swift */; }; 1E3275922532A2CD007E838A /* empty-box.json in Resources */ = {isa = PBXBuildFile; fileRef = 1E3275912532A2CD007E838A /* empty-box.json */; }; + 1E463803253636160079D8E9 /* SearchFactsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E463802253636160079D8E9 /* SearchFactsViewController.swift */; }; + 1E463805253636D80079D8E9 /* SearchFactsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E463804253636D80079D8E9 /* SearchFactsCoordinator.swift */; }; + 1E46380B25363FF40079D8E9 /* SearchFactsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E46380A25363FF40079D8E9 /* SearchFactsViewModel.swift */; }; 1E7F15BE253324CF0006887B /* FactsListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7F15BD253324CF0006887B /* FactsListViewModelTests.swift */; }; 1E7F15C0253324FD0006887B /* FactsListViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7F15BF253324FD0006887B /* FactsListViewControllerTests.swift */; }; 1E7F15C6253329780006887B /* FactViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7F15C5253329780006887B /* FactViewModelTests.swift */; }; + 1E92112A253F6BB700DB340B /* SearchFactsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E921129253F6BB700DB340B /* SearchFactsResponse.swift */; }; + 1E92112D253F6D0000DB340B /* FactsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E92112C253F6D0000DB340B /* FactsService.swift */; }; + 1E92112F253F7A0B00DB340B /* SearchFactsScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E92112E253F7A0B00DB340B /* SearchFactsScene.swift */; }; + 1E921131253F7AAA00DB340B /* SearchFactsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E921130253F7AAA00DB340B /* SearchFactsUITests.swift */; }; + 1E921134253F84F100DB340B /* SearchFactsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E921133253F84F100DB340B /* SearchFactsViewModelTests.swift */; }; + 1EACEC99253649BD0006B36D /* loading.json in Resources */ = {isa = PBXBuildFile; fileRef = 1EACEC98253649BD0006B36D /* loading.json */; }; + 1EACEC9E25364B6C0006B36D /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EACEC9D25364B6C0006B36D /* ActivityIndicator.swift */; }; 1ED5D18D25348FD50035046C /* XCTestCase+Stub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED5D18C25348FD50035046C /* XCTestCase+Stub.swift */; }; 1ED5D19025349E9D0035046C /* UIViewController+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED5D18F25349E9D0035046C /* UIViewController+Rx.swift */; }; 1ED5D1932534A56F0035046C /* FactsServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED5D1922534A56F0035046C /* FactsServiceMock.swift */; }; @@ -27,7 +37,7 @@ 1EFE2867253206D3008806B9 /* BaseCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE2866253206D3008806B9 /* BaseCoordinator.swift */; }; 1EFE2869253207B2008806B9 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE2868253207B2008806B9 /* AppCoordinator.swift */; }; 1EFE287E25321071008806B9 /* search-facts.json in Resources */ = {isa = PBXBuildFile; fileRef = 1EFE287D25321071008806B9 /* search-facts.json */; }; - 1EFE2884253210B2008806B9 /* FactsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE2883253210B2008806B9 /* FactsService.swift */; }; + 1EFE2884253210B2008806B9 /* FactsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE2883253210B2008806B9 /* FactsAPI.swift */; }; 1EFE288725321119008806B9 /* Fact.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE288625321119008806B9 /* Fact.swift */; }; 1EFE288925321123008806B9 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE288825321123008806B9 /* JSON.swift */; }; 1EFE288E2532135B008806B9 /* FactsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE288D2532135B008806B9 /* FactsListViewController.swift */; }; @@ -61,9 +71,19 @@ 02186229E02F0A408D2EE0C2 /* Pods_Chuck_Norris_Facts.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Chuck_Norris_Facts.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1E32758E2532A2C0007E838A /* EmptyListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyListView.swift; sourceTree = ""; }; 1E3275912532A2CD007E838A /* empty-box.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "empty-box.json"; sourceTree = ""; }; + 1E463802253636160079D8E9 /* SearchFactsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFactsViewController.swift; sourceTree = ""; }; + 1E463804253636D80079D8E9 /* SearchFactsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFactsCoordinator.swift; sourceTree = ""; }; + 1E46380A25363FF40079D8E9 /* SearchFactsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFactsViewModel.swift; sourceTree = ""; }; 1E7F15BD253324CF0006887B /* FactsListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListViewModelTests.swift; sourceTree = ""; }; 1E7F15BF253324FD0006887B /* FactsListViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListViewControllerTests.swift; sourceTree = ""; }; 1E7F15C5253329780006887B /* FactViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactViewModelTests.swift; sourceTree = ""; }; + 1E921129253F6BB700DB340B /* SearchFactsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFactsResponse.swift; sourceTree = ""; }; + 1E92112C253F6D0000DB340B /* FactsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsService.swift; sourceTree = ""; }; + 1E92112E253F7A0B00DB340B /* SearchFactsScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFactsScene.swift; sourceTree = ""; }; + 1E921130253F7AAA00DB340B /* SearchFactsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFactsUITests.swift; sourceTree = ""; }; + 1E921133253F84F100DB340B /* SearchFactsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFactsViewModelTests.swift; sourceTree = ""; }; + 1EACEC98253649BD0006B36D /* loading.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = loading.json; sourceTree = ""; }; + 1EACEC9D25364B6C0006B36D /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = ""; }; 1ED5D18C25348FD50035046C /* XCTestCase+Stub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+Stub.swift"; sourceTree = ""; }; 1ED5D18F25349E9D0035046C /* UIViewController+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Rx.swift"; sourceTree = ""; }; 1ED5D1922534A56F0035046C /* FactsServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsServiceMock.swift; sourceTree = ""; }; @@ -85,7 +105,7 @@ 1EFE2866253206D3008806B9 /* BaseCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseCoordinator.swift; sourceTree = ""; }; 1EFE2868253207B2008806B9 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; 1EFE287D25321071008806B9 /* search-facts.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "search-facts.json"; sourceTree = ""; }; - 1EFE2883253210B2008806B9 /* FactsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsService.swift; sourceTree = ""; }; + 1EFE2883253210B2008806B9 /* FactsAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsAPI.swift; sourceTree = ""; }; 1EFE288625321119008806B9 /* Fact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fact.swift; sourceTree = ""; }; 1EFE288825321123008806B9 /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = ""; }; 1EFE288D2532135B008806B9 /* FactsListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListViewController.swift; sourceTree = ""; }; @@ -139,12 +159,23 @@ path = Views; sourceTree = ""; }; - 1E3275902532A2C4007E838A /* Lottie */ = { + 1E3275902532A2C4007E838A /* Animations */ = { isa = PBXGroup; children = ( + 1EACEC98253649BD0006B36D /* loading.json */, 1E3275912532A2CD007E838A /* empty-box.json */, ); - path = Lottie; + path = Animations; + sourceTree = ""; + }; + 1E463801253636050079D8E9 /* SearchFacts */ = { + isa = PBXGroup; + children = ( + 1E463802253636160079D8E9 /* SearchFactsViewController.swift */, + 1E463804253636D80079D8E9 /* SearchFactsCoordinator.swift */, + 1E46380A25363FF40079D8E9 /* SearchFactsViewModel.swift */, + ); + path = SearchFacts; sourceTree = ""; }; 1E7F15BA253324760006887B /* Scenes */ = { @@ -158,6 +189,7 @@ 1E7F15BB253324B10006887B /* Facts */ = { isa = PBXGroup; children = ( + 1E921132253F84DE00DB340B /* SearchFacts */, 1E7F15BC253324BD0006887B /* FactsList */, ); path = Facts; @@ -182,6 +214,30 @@ path = Cells; sourceTree = ""; }; + 1E921128253F6BA500DB340B /* Responses */ = { + isa = PBXGroup; + children = ( + 1E921129253F6BB700DB340B /* SearchFactsResponse.swift */, + ); + path = Responses; + sourceTree = ""; + }; + 1E92112B253F6CF500DB340B /* Services */ = { + isa = PBXGroup; + children = ( + 1E92112C253F6D0000DB340B /* FactsService.swift */, + ); + path = Services; + sourceTree = ""; + }; + 1E921132253F84DE00DB340B /* SearchFacts */ = { + isa = PBXGroup; + children = ( + 1E921133253F84F100DB340B /* SearchFactsViewModelTests.swift */, + ); + path = SearchFacts; + sourceTree = ""; + }; 1ED5D18B25348FC40035046C /* Library */ = { isa = PBXGroup; children = ( @@ -220,6 +276,7 @@ isa = PBXGroup; children = ( 1ED5D19E2534B0E30035046C /* FactsListScene.swift */, + 1E92112E253F7A0B00DB340B /* SearchFactsScene.swift */, ); path = Scenes; sourceTree = ""; @@ -228,6 +285,7 @@ isa = PBXGroup; children = ( 1ED5D1A12534B0F40035046C /* FactsListUITests.swift */, + 1E921130253F7AAA00DB340B /* SearchFactsUITests.swift */, ); path = Tests; sourceTree = ""; @@ -291,7 +349,7 @@ 1EFE286025320614008806B9 /* Resources */ = { isa = PBXGroup; children = ( - 1E3275902532A2C4007E838A /* Lottie */, + 1E3275902532A2C4007E838A /* Animations */, 1EFE287C2532105D008806B9 /* Stubs */, 1EE0714525314AF600F6BF6D /* Assets.xcassets */, 1EE0714725314AF600F6BF6D /* LaunchScreen.storyboard */, @@ -323,6 +381,7 @@ children = ( 1EFE2866253206D3008806B9 /* BaseCoordinator.swift */, 1EFE288825321123008806B9 /* JSON.swift */, + 1EACEC9D25364B6C0006B36D /* ActivityIndicator.swift */, ); path = Library; sourceTree = ""; @@ -338,6 +397,7 @@ 1EFE2881253210A4008806B9 /* Data */ = { isa = PBXGroup; children = ( + 1E92112B253F6CF500DB340B /* Services */, 1EFE288525321111008806B9 /* Models */, 1EFE2882253210A8008806B9 /* Networking */, ); @@ -347,7 +407,8 @@ 1EFE2882253210A8008806B9 /* Networking */ = { isa = PBXGroup; children = ( - 1EFE2883253210B2008806B9 /* FactsService.swift */, + 1E921128253F6BA500DB340B /* Responses */, + 1EFE2883253210B2008806B9 /* FactsAPI.swift */, ); path = Networking; sourceTree = ""; @@ -363,6 +424,7 @@ 1EFE288A25321327008806B9 /* Facts */ = { isa = PBXGroup; children = ( + 1E463801253636050079D8E9 /* SearchFacts */, 1EFE288C25321337008806B9 /* FactsList */, ); path = Facts; @@ -482,7 +544,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1170; - LastUpgradeCheck = 1170; + LastUpgradeCheck = 1220; ORGANIZATIONNAME = "Djorkaeff Alexandre Vilela Pereira"; TargetAttributes = { 1EE0713825314AF500F6BF6D = { @@ -527,6 +589,7 @@ 1EE0714925314AF600F6BF6D /* LaunchScreen.storyboard in Resources */, 1EE0714625314AF600F6BF6D /* Assets.xcassets in Resources */, 1E3275922532A2CD007E838A /* empty-box.json in Resources */, + 1EACEC99253649BD0006B36D /* loading.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -692,19 +755,25 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 1EFE2884253210B2008806B9 /* FactsService.swift in Sources */, + 1EFE2884253210B2008806B9 /* FactsAPI.swift in Sources */, 1E32758F2532A2C0007E838A /* EmptyListView.swift in Sources */, + 1E463805253636D80079D8E9 /* SearchFactsCoordinator.swift in Sources */, 1EFE288925321123008806B9 /* JSON.swift in Sources */, 1EFE288725321119008806B9 /* Fact.swift in Sources */, 1EFE289725321CE2008806B9 /* FactTableViewCell.swift in Sources */, 1EE0713D25314AF500F6BF6D /* AppDelegate.swift in Sources */, 1ED5D19025349E9D0035046C /* UIViewController+Rx.swift in Sources */, 1EFE2869253207B2008806B9 /* AppCoordinator.swift in Sources */, + 1E92112A253F6BB700DB340B /* SearchFactsResponse.swift in Sources */, 1EE0713F25314AF500F6BF6D /* SceneDelegate.swift in Sources */, 1EFE288E2532135B008806B9 /* FactsListViewController.swift in Sources */, 1EFE28902532137C008806B9 /* FactsListCoordinator.swift in Sources */, + 1E463803253636160079D8E9 /* SearchFactsViewController.swift in Sources */, 1EFE289525321CD2008806B9 /* FactViewModel.swift in Sources */, + 1E46380B25363FF40079D8E9 /* SearchFactsViewModel.swift in Sources */, + 1E92112D253F6D0000DB340B /* FactsService.swift in Sources */, 1EFE2892253214DB008806B9 /* FactsListViewModel.swift in Sources */, + 1EACEC9E25364B6C0006B36D /* ActivityIndicator.swift in Sources */, 1EFE2867253206D3008806B9 /* BaseCoordinator.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -713,6 +782,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1E921134253F84F100DB340B /* SearchFactsViewModelTests.swift in Sources */, 1E7F15C6253329780006887B /* FactViewModelTests.swift in Sources */, 1ED5D18D25348FD50035046C /* XCTestCase+Stub.swift in Sources */, 1ED5D1932534A56F0035046C /* FactsServiceMock.swift in Sources */, @@ -727,6 +797,8 @@ files = ( 1ED5D1A22534B0F40035046C /* FactsListUITests.swift in Sources */, 1ED5D19F2534B0E30035046C /* FactsListScene.swift in Sources */, + 1E92112F253F7A0B00DB340B /* SearchFactsScene.swift in Sources */, + 1E921131253F7AAA00DB340B /* SearchFactsUITests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -783,6 +855,7 @@ 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; @@ -843,6 +916,7 @@ 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; diff --git a/Chuck Norris Facts.xcodeproj/xcshareddata/xcschemes/Chuck Norris Facts.xcscheme b/Chuck Norris Facts.xcodeproj/xcshareddata/xcschemes/Chuck Norris Facts.xcscheme new file mode 100644 index 0000000..235e7c1 --- /dev/null +++ b/Chuck Norris Facts.xcodeproj/xcshareddata/xcschemes/Chuck Norris Facts.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Chuck Norris Facts/Data/Networking/FactsAPI.swift b/Chuck Norris Facts/Data/Networking/FactsAPI.swift new file mode 100644 index 0000000..35cf1b6 --- /dev/null +++ b/Chuck Norris Facts/Data/Networking/FactsAPI.swift @@ -0,0 +1,50 @@ +// +// FactsService.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/10/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Moya + +enum FactsAPI { + case searchFacts(searchTerm: String) +} + +extension FactsAPI: TargetType { + + var baseURL: URL { + return URL(string: "https://api.chucknorris.io/jokes")! + } + + var path: String { + switch self { + case .searchFacts: + return "/search" + } + } + + var method: Method { + switch self { + case .searchFacts: + return .get + } + } + + var task: Task { + switch self { + case .searchFacts(let searchTerm): + return .requestParameters(parameters: ["query": searchTerm], encoding: URLEncoding.queryString) + } + } + + var headers: [String: String]? { + return ["Content-type": "application/json"] + } + + var sampleData: Data { + return Data() + } + +} diff --git a/Chuck Norris Facts/Data/Networking/FactsService.swift b/Chuck Norris Facts/Data/Networking/FactsService.swift deleted file mode 100644 index ad644d9..0000000 --- a/Chuck Norris Facts/Data/Networking/FactsService.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// FactsService.swift -// Chuck Norris Facts -// -// Created by Djorkaeff Alexandre Vilela Pereira on 10/10/20. -// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. -// - -import Foundation -import RxSwift - -struct SearchFactsResponse: Decodable { - let total: Int - let result: [Fact] -} - -protocol FactsServiceType { - func searchFacts(query: String) -> Observable<[Fact]> -} - -final class FactsService: FactsServiceType { - - func searchFacts(query: String) -> Observable<[Fact]> { - if CommandLine.arguments.contains("--empty-facts") { - return .just([]) - } - - return Observable<[Fact]>.create { observer in - do { - guard let file = Bundle.main.url(forResource: "search-facts", withExtension: ".json") else { - return Disposables.create {} - } - - let data = try Data(contentsOf: file) - let searchFactsResponse = try JSON.decoder.decode(SearchFactsResponse.self, from: data) - observer.onNext(searchFactsResponse.result) - } catch { - observer.onError(error) - } - observer.onCompleted() - - return Disposables.create {} - } - } -} diff --git a/Chuck Norris Facts/Data/Networking/Responses/SearchFactsResponse.swift b/Chuck Norris Facts/Data/Networking/Responses/SearchFactsResponse.swift new file mode 100644 index 0000000..0a1fc48 --- /dev/null +++ b/Chuck Norris Facts/Data/Networking/Responses/SearchFactsResponse.swift @@ -0,0 +1,19 @@ +// +// SearchFactsResponse.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/20/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation + +struct SearchFactsResponse: Decodable { + let total: Int + let facts: [Fact] + + enum CodingKeys: String, CodingKey { + case total + case facts = "result" + } +} diff --git a/Chuck Norris Facts/Data/Services/FactsService.swift b/Chuck Norris Facts/Data/Services/FactsService.swift new file mode 100644 index 0000000..8639d98 --- /dev/null +++ b/Chuck Norris Facts/Data/Services/FactsService.swift @@ -0,0 +1,28 @@ +// +// FactsService.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/20/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import RxSwift +import Moya + +protocol FactsServiceType { + func searchFacts(searchTerm: String) -> Observable<[Fact]> +} + +struct FactsService: FactsServiceType { + + private let provider = MoyaProvider() + + func searchFacts(searchTerm: String) -> Observable<[Fact]> { + provider.rx + .request(.searchFacts(searchTerm: searchTerm)) + .asObservable() + .map(SearchFactsResponse.self, using: JSON.decoder) + .map { $0.facts } + } + +} diff --git a/Chuck Norris Facts/Library/ActivityIndicator.swift b/Chuck Norris Facts/Library/ActivityIndicator.swift new file mode 100644 index 0000000..9cc47dc --- /dev/null +++ b/Chuck Norris Facts/Library/ActivityIndicator.swift @@ -0,0 +1,80 @@ +// +// ActivityIndicator.swift +// RxExample +// +// Created by Krunoslav Zaher on 10/18/15. +// Copyright © 2015 Krunoslav Zaher. All rights reserved. +// +import RxSwift +import RxCocoa + +private struct ActivityToken: ObservableConvertibleType, Disposable { + private let _source: Observable + private let _dispose: Cancelable + + init(source: Observable, disposeAction: @escaping () -> Void) { + _source = source + _dispose = Disposables.create(with: disposeAction) + } + + func dispose() { + _dispose.dispose() + } + + func asObservable() -> Observable { + _source + } +} + +/** +Enables monitoring of sequence computation. +If there is at least one sequence computation in progress, `true` will be sent. +When all activities complete `false` will be sent. +*/ +public class ActivityIndicator: SharedSequenceConvertibleType { + public typealias Element = Bool + public typealias SharingStrategy = DriverSharingStrategy + + private let _lock = NSRecursiveLock() + private let _relay = BehaviorRelay(value: 0) + private let _loading: SharedSequence + + public init() { + _loading = _relay.asDriver() + .map { $0 > 0 } + .distinctUntilChanged() + } + + fileprivate func trackActivityOfObservable( + _ source: Source + ) -> Observable { + return Observable.using({ () -> ActivityToken in + self.increment() + return ActivityToken(source: source.asObservable(), disposeAction: self.decrement) + }, observableFactory: { t in + return t.asObservable() + }) + } + + private func increment() { + _lock.lock() + _relay.accept(_relay.value + 1) + _lock.unlock() + } + + private func decrement() { + _lock.lock() + _relay.accept(_relay.value - 1) + _lock.unlock() + } + + public func asSharedSequence() -> SharedSequence { + _loading + } +} + +extension ObservableConvertibleType { + public func trackActivity(_ activityIndicator: ActivityIndicator) -> Observable { + activityIndicator.trackActivityOfObservable(self) + } +} diff --git a/Chuck Norris Facts/Resources/Lottie/empty-box.json b/Chuck Norris Facts/Resources/Animations/empty-box.json similarity index 100% rename from Chuck Norris Facts/Resources/Lottie/empty-box.json rename to Chuck Norris Facts/Resources/Animations/empty-box.json diff --git a/Chuck Norris Facts/Resources/Animations/loading.json b/Chuck Norris Facts/Resources/Animations/loading.json new file mode 100644 index 0000000..aa67787 --- /dev/null +++ b/Chuck Norris Facts/Resources/Animations/loading.json @@ -0,0 +1 @@ +{"v":"5.5.2","fr":30,"ip":0,"op":30,"w":500,"h":500,"nm":"合成 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"形状图层 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[407,215,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":0,"s":[48,48]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":20,"s":[48,48]},{"t":30,"s":[72,72]}],"ix":2,"x":"var $bm_rt;\nvar amp, freq, decay, n, n, t, t, v;\namp = 0.1;\nfreq = 2;\ndecay = 2;\n$bm_rt = n = 0;\nif (numKeys > 0) {\n $bm_rt = n = nearestKey(time).index;\n if (key(n).time > time) {\n n--;\n }\n}\nif (n == 0) {\n $bm_rt = t = 0;\n} else {\n $bm_rt = t = $bm_sub(time, key(n).time);\n}\nif (n > 0) {\n v = velocityAtTime($bm_sub(key(n).time, $bm_div(thisComp.frameDuration, 10)));\n $bm_rt = $bm_sum(value, $bm_div($bm_mul($bm_mul(v, amp), Math.sin($bm_mul($bm_mul($bm_mul(freq, t), 2), Math.PI))), Math.exp($bm_mul(decay, t))));\n} else {\n $bm_rt = value;\n}"},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0.768627464771,0.784313738346,0.800000011921,1]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[0.768627464771,0.784313738346,0.800000011921,1]},{"t":30,"s":[0.388235300779,0.403921574354,0.419607847929,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-28,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"形状图层 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[279,215,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":0,"s":[48,48]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":10,"s":[48,48]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":20,"s":[72,72]},{"t":30,"s":[48,48]}],"ix":2,"x":"var $bm_rt;\nvar amp, freq, decay, n, n, t, t, v;\namp = 0.1;\nfreq = 2;\ndecay = 2;\n$bm_rt = n = 0;\nif (numKeys > 0) {\n $bm_rt = n = nearestKey(time).index;\n if (key(n).time > time) {\n n--;\n }\n}\nif (n == 0) {\n $bm_rt = t = 0;\n} else {\n $bm_rt = t = $bm_sub(time, key(n).time);\n}\nif (n > 0) {\n v = velocityAtTime($bm_sub(key(n).time, $bm_div(thisComp.frameDuration, 10)));\n $bm_rt = $bm_sum(value, $bm_div($bm_mul($bm_mul(v, amp), Math.sin($bm_mul($bm_mul($bm_mul(freq, t), 2), Math.PI))), Math.exp($bm_mul(decay, t))));\n} else {\n $bm_rt = value;\n}"},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0.768627464771,0.784313738346,0.800000011921,1]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[0.388235300779,0.403921574354,0.419607847929,1]},{"t":30,"s":[0.768627464771,0.784313738346,0.800000011921,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-28,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"形状图层 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[151,215,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":0,"s":[48,48]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":10,"s":[72,72]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":20,"s":[48,48]},{"t":30,"s":[48,48]}],"ix":2,"x":"var $bm_rt;\nvar amp, freq, decay, n, n, t, t, v;\namp = 0.1;\nfreq = 2;\ndecay = 2;\n$bm_rt = n = 0;\nif (numKeys > 0) {\n $bm_rt = n = nearestKey(time).index;\n if (key(n).time > time) {\n n--;\n }\n}\nif (n == 0) {\n $bm_rt = t = 0;\n} else {\n $bm_rt = t = $bm_sub(time, key(n).time);\n}\nif (n > 0) {\n v = velocityAtTime($bm_sub(key(n).time, $bm_div(thisComp.frameDuration, 10)));\n $bm_rt = $bm_sum(value, $bm_div($bm_mul($bm_mul(v, amp), Math.sin($bm_mul($bm_mul($bm_mul(freq, t), 2), Math.PI))), Math.exp($bm_mul(decay, t))));\n} else {\n $bm_rt = value;\n}"},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0.768627464771,0.784313738346,0.800000011921,1]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[0.388235300779,0.403921574354,0.419607847929,1]},{"t":20,"s":[0.768627464771,0.784313738346,0.800000011921,1]}],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-28,36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListCoordinator.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListCoordinator.swift index c86f2fb..2604ad1 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListCoordinator.swift +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListCoordinator.swift @@ -30,6 +30,14 @@ final class FactsListCoordinator: BaseCoordinator { }) .disposed(by: disposeBag) + factsListViewModel.showSearchFacts + .flatMap { [weak self] _ -> Observable in + self?.showSearchFacts(on: factsListViewController) ?? .empty() + } + .compactMap { $0 } + .bind(to: factsListViewModel.setSearchTerm) + .disposed(by: disposeBag) + window.rootViewController = navigationController window.makeKeyAndVisible() @@ -46,4 +54,15 @@ final class FactsListCoordinator: BaseCoordinator { let shareActivity = UIActivityViewController(activityItems: activityItems, applicationActivities: nil) navigationController.present(shareActivity, animated: true, completion: nil) } + + private func showSearchFacts(on rootViewController: UIViewController) -> Observable { + let searchFactsCoordinator = SearchFactsCoordinator(rootViewController: rootViewController) + return coordinate(to: searchFactsCoordinator) + .map { result in + switch result { + case .cancel: return nil + case .search(let searchTerm): return searchTerm + } + } + } } diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift index 12e9ad7..88479a5 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift @@ -10,6 +10,7 @@ import UIKit import RxSwift import RxCocoa import RxDataSources +import Lottie class FactsListViewController: UIViewController { @@ -19,6 +20,7 @@ class FactsListViewController: UIViewController { let tableView = UITableView() let emptyListView = EmptyListView() + let searchButton = UIBarButtonItem(barButtonSystemItem: .search, target: nil, action: nil) private lazy var factsDataSource = RxTableViewSectionedAnimatedDataSource( configureCell: { [weak self] _, tableView, indexPath, fact -> UITableViewCell in @@ -38,12 +40,22 @@ class FactsListViewController: UIViewController { } ) + private lazy var loadingView: AnimationView = { + let loading = AnimationView() + + loading.animation = Animation.named("loading") + loading.loopMode = .loop + + return loading + }() + override func viewDidLoad() { super.viewDidLoad() setupView() setupBindings() setupTableView() + setupLoadingView() setupEmptyListView() setupNavigationBar() } @@ -68,6 +80,16 @@ class FactsListViewController: UIViewController { tableView.accessibilityIdentifier = "factsTableView" } + private func setupLoadingView() { + view.addSubview(loadingView) + + loadingView.translatesAutoresizingMaskIntoConstraints = false + loadingView.widthAnchor.constraint(equalToConstant: 50).isActive = true + loadingView.heightAnchor.constraint(equalToConstant: 50).isActive = true + loadingView.centerXAnchor.constraint(equalTo: tableView.centerXAnchor).isActive = true + loadingView.centerYAnchor.constraint(equalTo: tableView.centerYAnchor).isActive = true + } + private func setupEmptyListView() { view.addSubview(emptyListView) @@ -80,7 +102,10 @@ class FactsListViewController: UIViewController { private func setupNavigationBar() { navigationItem.title = "Chuck Norris Facts" + navigationItem.rightBarButtonItem = searchButton navigationController?.navigationBar.prefersLargeTitles = true + + searchButton.accessibilityIdentifier = "searchButton" } private func setupBindings() { @@ -88,6 +113,12 @@ class FactsListViewController: UIViewController { .bind(to: viewModel.viewDidAppear) .disposed(by: disposeBag) + viewModel.isLoading + .drive(onNext: { [weak self] isLoading in + self?.showLoadingView(isLoading) + }) + .disposed(by: disposeBag) + viewModel.facts .map { $0.flatMap { $0.items } } .map { $0.isEmpty } @@ -101,6 +132,10 @@ class FactsListViewController: UIViewController { .observeOn(MainScheduler.instance) .bind(to: tableView.rx.items(dataSource: factsDataSource)) .disposed(by: disposeBag) + + searchButton.rx.tap + .bind(to: viewModel.startSearchFacts) + .disposed(by: disposeBag) } private func showEmptyView(_ isEmpty: Bool) { @@ -113,4 +148,16 @@ class FactsListViewController: UIViewController { emptyListView.stop() } } + + private func showLoadingView(_ isLoading: Bool) { + tableView.isHidden = isLoading + loadingView.isHidden = !isLoading + + if isLoading { + emptyListView.isHidden = isLoading + loadingView.play() + } else { + loadingView.stop() + } + } } diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift index 97d0455..457b68c 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift @@ -20,24 +20,63 @@ final class FactsListViewModel { let startShareFact: AnyObserver + let startSearchFacts: AnyObserver + + let setSearchTerm: AnyObserver + // MARK: - Outputs let facts: Observable<[FactsSectionModel]> let showShareFact: Observable + let showSearchFacts: Observable + + let searchTerm: Observable + + let isLoading: ActivityIndicator + init(factsService: FactsServiceType = FactsService()) { + let loadingIndicator = ActivityIndicator() + self.isLoading = loadingIndicator + let viewDidAppearSubject = PublishSubject() self.viewDidAppear = viewDidAppearSubject.asObserver() - self.facts = viewDidAppearSubject.flatMapLatest { _ in factsService.searchFacts(query: "") } - .map { Array($0.shuffled().prefix(10)) } - .map { $0.map { FactViewModel(fact: $0) } } - .map { [FactsSectionModel(model: "", items: $0)] } - let startShareFactSubject = PublishSubject() self.startShareFact = startShareFactSubject.asObserver() self.showShareFact = startShareFactSubject.asObservable() + + let startSearchFactsSubject = PublishSubject() + self.startSearchFacts = startSearchFactsSubject.asObserver() + self.showSearchFacts = startSearchFactsSubject.asObservable() + + let searchTermSubject = BehaviorSubject(value: "") + self.setSearchTerm = searchTermSubject.asObserver() + self.searchTerm = searchTermSubject.asObservable() + + self.facts = Observable.combineLatest(viewDidAppearSubject, searchTermSubject) + .flatMapLatest { _, searchTerm -> Observable<[Fact]> in + if CommandLine.arguments.contains("--search-facts") { + let bundle = Bundle(for: Self.self) + + guard let url = bundle.url(forResource: "search-facts", withExtension: ".json") else { + return .just([]) + } + + let data = try Data(contentsOf: url) + let stub = try JSON.decoder.decode(SearchFactsResponse.self, from: data) + return .just(stub.facts) + } + if !searchTerm.isEmpty { + return factsService.searchFacts(searchTerm: searchTerm) + .trackActivity(loadingIndicator) + } + return .just([]) + } + .map { Array($0.shuffled().prefix(10)) } + .map { $0.map { FactViewModel(fact: $0) } } + .map { [FactsSectionModel(model: "", items: $0)] } } } diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/Views/EmptyListView.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/Views/EmptyListView.swift index dcf17c1..1b44f36 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/Views/EmptyListView.swift +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/Views/EmptyListView.swift @@ -36,8 +36,17 @@ final class EmptyListView: UIView { override init(frame: CGRect) { super.init(frame: frame) + setupView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView() { + backgroundColor = .systemBackground + addSubview(animation) - addSubview(label) animation.translatesAutoresizingMaskIntoConstraints = false animation.widthAnchor.constraint(equalToConstant: 200).isActive = true @@ -45,6 +54,8 @@ final class EmptyListView: UIView { animation.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true animation.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true + addSubview(label) + label.translatesAutoresizingMaskIntoConstraints = false label.topAnchor.constraint(equalTo: animation.bottomAnchor).isActive = true label.centerXAnchor.constraint(equalTo: animation.centerXAnchor).isActive = true @@ -53,10 +64,6 @@ final class EmptyListView: UIView { label.accessibilityIdentifier = "emptyListLabelView" } - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - func play() { animation.play() } diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsCoordinator.swift b/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsCoordinator.swift new file mode 100644 index 0000000..d977898 --- /dev/null +++ b/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsCoordinator.swift @@ -0,0 +1,41 @@ +// +// SearchFactsCoordinator.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/13/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import UIKit +import RxSwift + +enum SearchFactsCoordinationResult { + case cancel + case search(String) +} + +class SearchFactsCoordinator: BaseCoordinator { + + private let rootViewController: UIViewController + + init(rootViewController: UIViewController) { + self.rootViewController = rootViewController + } + + override func start() -> Observable { + let searchFactsViewController = SearchFactsViewController() + let navigationController = UINavigationController(rootViewController: searchFactsViewController) + + let searchFactsViewModel = SearchFactsViewModel() + searchFactsViewController.viewModel = searchFactsViewModel + + let cancel = searchFactsViewModel.didCancel.map { _ in CoordinationResult.cancel } + let search = searchFactsViewModel.didSearchFacts.map { CoordinationResult.search($0) } + + rootViewController.present(navigationController, animated: true) + + return Observable.merge(cancel, search) + .take(1) + .do(onNext: { [weak self] _ in self?.rootViewController.dismiss(animated: true) }) + } +} diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewController.swift b/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewController.swift new file mode 100644 index 0000000..92e5e4d --- /dev/null +++ b/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewController.swift @@ -0,0 +1,67 @@ +// +// SearchFactsViewController.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/13/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import UIKit +import RxSwift + +final class SearchFactsViewController: UIViewController { + + var viewModel: SearchFactsViewModel! + + let disposeBag = DisposeBag() + + lazy var cancelButton: UIBarButtonItem = { + let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: nil, action: nil) + cancelButton.accessibilityIdentifier = "cancelButton" + return cancelButton + }() + + lazy var searchController: UISearchController = { + let searchController = UISearchController(searchResultsController: nil) + + searchController.obscuresBackgroundDuringPresentation = false + searchController.searchBar.enablesReturnKeyAutomatically = true + searchController.searchBar.returnKeyType = .search + + return searchController + }() + + override func viewDidLoad() { + super.viewDidLoad() + + setupView() + setupNavigationBar() + setupBindings() + } + + private func setupView() { + view.backgroundColor = .systemBackground + view.accessibilityIdentifier = "searchFactsView" + } + + private func setupNavigationBar() { + navigationItem.searchController = searchController + navigationItem.leftBarButtonItem = cancelButton + navigationItem.title = "Search" + } + + private func setupBindings() { + cancelButton.rx.tap + .bind(to: viewModel.cancel) + .disposed(by: disposeBag) + + searchController.searchBar.rx.text + .compactMap { $0 } + .bind(to: viewModel.searchTerm) + .disposed(by: disposeBag) + + searchController.searchBar.rx.textDidEndEditing + .bind(to: viewModel.searchAction) + .disposed(by: disposeBag) + } +} diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewModel.swift b/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewModel.swift new file mode 100644 index 0000000..b5d3be6 --- /dev/null +++ b/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewModel.swift @@ -0,0 +1,42 @@ +// +// SearchFactsViewModel.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/13/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import RxSwift + +class SearchFactsViewModel { + + // MARK: - Inputs + + let cancel: AnyObserver + + let searchTerm: AnyObserver + + let searchAction: AnyObserver + + // MARK: - Outputs + + let didCancel: Observable + + let didSearchFacts: Observable + + init() { + let cancelSubject = PublishSubject() + self.cancel = cancelSubject.asObserver() + self.didCancel = cancelSubject.asObservable() + + let searchTermSubject = BehaviorSubject(value: "") + self.searchTerm = searchTermSubject.asObserver() + + let searchActionSubject = PublishSubject() + self.searchAction = searchActionSubject.asObserver() + + self.didSearchFacts = searchActionSubject + .withLatestFrom(searchTermSubject) + .filter { !$0.isEmpty } + } +} diff --git a/Chuck Norris FactsTests/Mocks/FactsServiceMock.swift b/Chuck Norris FactsTests/Mocks/FactsServiceMock.swift index eabc819..c4bc8a7 100644 --- a/Chuck Norris FactsTests/Mocks/FactsServiceMock.swift +++ b/Chuck Norris FactsTests/Mocks/FactsServiceMock.swift @@ -14,7 +14,7 @@ import RxSwift final class FactsServiceMock: FactsServiceType { var searchFactsReturnValue: Observable<[Fact]> = .just([]) - func searchFacts(query: String) -> Observable<[Fact]> { + func searchFacts(searchTerm: String) -> Observable<[Fact]> { return searchFactsReturnValue } } diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift index 1f0a2d3..e7156a5 100644 --- a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift +++ b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift @@ -52,6 +52,7 @@ class FactsListViewControllerTests: XCTestCase { factsServiceMock.searchFactsReturnValue = .just([fact]) + factsListViewModel.setSearchTerm.onNext("games") factsListViewModel.viewDidAppear.onNext(()) let factCell = factsListFirstCell() @@ -65,6 +66,7 @@ class FactsListViewControllerTests: XCTestCase { factsServiceMock.searchFactsReturnValue = .just([fact]) + factsListViewModel.setSearchTerm.onNext("games") factsListViewModel.viewDidAppear.onNext(()) let factCell = factsListFirstCell() @@ -81,8 +83,8 @@ class FactsListViewControllerTests: XCTestCase { let testScheduler = TestScheduler(initialClock: 0) let shareFactObserver = testScheduler.createObserver(FactViewModel.self) - factsListViewModel.viewDidAppear - .onNext(()) + factsListViewModel.setSearchTerm.onNext("games") + factsListViewModel.viewDidAppear.onNext(()) factsListViewModel.showShareFact .subscribe(shareFactObserver) diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift index ded4a47..d2e8b88 100644 --- a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift +++ b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift @@ -45,6 +45,7 @@ class FactsListViewModelTests: XCTestCase { .subscribe(factsObserver) .disposed(by: disposeBag) + factsListViewModel.setSearchTerm.onNext("games") factsListViewModel.viewDidAppear.onNext(()) testScheduler.start() diff --git a/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewModelTests.swift b/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewModelTests.swift new file mode 100644 index 0000000..18021c7 --- /dev/null +++ b/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewModelTests.swift @@ -0,0 +1,64 @@ +// +// SearchFactsViewModelTests.swift +// Chuck Norris FactsTests +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/20/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import XCTest +import RxSwift +import RxBlocking +import RxTest + +@testable import Chuck_Norris_Facts + +class SearchFactsViewModelTests: XCTestCase { + + var searchFactsViewModel: SearchFactsViewModel! + var testScheduler: TestScheduler! + var disposeBag: DisposeBag! + + override func setUp() { + disposeBag = DisposeBag() + testScheduler = TestScheduler(initialClock: 0) + searchFactsViewModel = SearchFactsViewModel() + } + + override func tearDown() { + disposeBag = nil + testScheduler = nil + searchFactsViewModel = nil + } + + func test_searchFactsWhenSearchShouldSearchFacts() { + let searchFactsObserver = testScheduler.createObserver(String.self) + + searchFactsViewModel.didSearchFacts + .subscribe(searchFactsObserver) + .disposed(by: disposeBag) + + searchFactsViewModel.searchTerm.onNext("games") + searchFactsViewModel.searchAction.onNext(()) + + testScheduler.start() + + let searchFactsTerm = searchFactsObserver.events.compactMap { $0.value.element }.first + XCTAssertEqual(searchFactsTerm, "games") + } + + func test_cancelSearchShouldCallCancelSearch() { + let cancelObserver = testScheduler.createObserver(Void.self) + + searchFactsViewModel.didCancel + .subscribe(cancelObserver) + .disposed(by: disposeBag) + + searchFactsViewModel.cancel.onNext(()) + + testScheduler.start() + + let cancelCount = cancelObserver.events.compactMap { $0.value.element }.count + XCTAssertEqual(cancelCount, 1) + } +} diff --git a/Chuck Norris FactsUITests/Scenes/FactsListScene.swift b/Chuck Norris FactsUITests/Scenes/FactsListScene.swift index 5ab83a4..808972f 100644 --- a/Chuck Norris FactsUITests/Scenes/FactsListScene.swift +++ b/Chuck Norris FactsUITests/Scenes/FactsListScene.swift @@ -14,6 +14,7 @@ struct FactsListScene { let factsTableView: XCUIElement let emptyListView: XCUIElement let emptyListLabelView: XCUIElement + let searchButton: XCUIElement init() { let app = XCUIApplication() @@ -21,6 +22,7 @@ struct FactsListScene { factsTableView = app.tables["factsTableView"] emptyListView = app.otherElements["emptyListView"] emptyListLabelView = app.staticTexts["emptyListLabelView"] + searchButton = app.navigationBars.buttons["searchButton"] } } diff --git a/Chuck Norris FactsUITests/Scenes/SearchFactsScene.swift b/Chuck Norris FactsUITests/Scenes/SearchFactsScene.swift new file mode 100644 index 0000000..206aeb4 --- /dev/null +++ b/Chuck Norris FactsUITests/Scenes/SearchFactsScene.swift @@ -0,0 +1,26 @@ +// +// SearchFactsScene.swift +// Chuck Norris FactsUITests +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/20/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation +import XCTest + +struct SearchFactsScene { + + let searchFactsView: XCUIElement + let searchBarField: XCUIElement + let cancelButton: XCUIElement + + init() { + let app = XCUIApplication() + + searchFactsView = app.otherElements["searchFactsView"] + searchBarField = app.searchFields["Search"] + cancelButton = app.navigationBars.buttons["cancelButton"] + } + +} diff --git a/Chuck Norris FactsUITests/Tests/FactsListUITests.swift b/Chuck Norris FactsUITests/Tests/FactsListUITests.swift index 739b5d6..b93f04b 100644 --- a/Chuck Norris FactsUITests/Tests/FactsListUITests.swift +++ b/Chuck Norris FactsUITests/Tests/FactsListUITests.swift @@ -29,6 +29,7 @@ final class FactsListUITests: XCTestCase { } func test_show10RandomFacts() { + app.launchArguments = ["--search-facts"] app.launch() let factsListScene = FactsListScene() @@ -37,6 +38,7 @@ final class FactsListUITests: XCTestCase { } func test_shareFact() { + app.launchArguments = ["--search-facts"] app.launch() let factsListScene = FactsListScene() @@ -54,4 +56,20 @@ final class FactsListUITests: XCTestCase { XCTAssertFalse(shareActivity.waitForExistence(timeout: 1)) } + + func test_searchFacts() { + app.launch() + + let factsListScene = FactsListScene() + let searchFactsButton = factsListScene.searchButton + + let searchFactsScene = SearchFactsScene() + let searchFactsView = searchFactsScene.searchFactsView + + XCTAssertTrue(searchFactsButton.exists) + + searchFactsButton.firstMatch.tap() + + XCTAssertTrue(searchFactsView.exists) + } } diff --git a/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift b/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift new file mode 100644 index 0000000..727f67c --- /dev/null +++ b/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift @@ -0,0 +1,49 @@ +// +// SearchFactsUITests.swift +// Chuck Norris FactsUITests +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/20/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation +import XCTest + +final class SearchFactsUITests: XCTestCase { + + var app: XCUIApplication! + + override func setUp() { + app = XCUIApplication() + continueAfterFailure = false + } + + func test_searchFactsUsingSearchBar() throws { + app.launch() + + let factsListScene = FactsListScene() + factsListScene.searchButton.tap() + + let searchFactsScene = SearchFactsScene() + searchFactsScene.searchBarField.tap() + searchFactsScene.searchBarField.typeText("games") + + app.keyboards.buttons["Search"].tap() + + sleep(5) + + XCTAssertEqual(factsListScene.factsTableView.cells.count, 10) + } + + func test_cancelSearchFacts() throws { + app.launch() + + let factsListScene = FactsListScene() + factsListScene.searchButton.tap() + + let searchFactsScene = SearchFactsScene() + searchFactsScene.cancelButton.tap() + + XCTAssertFalse(searchFactsScene.searchFactsView.exists) + } +} diff --git a/Podfile b/Podfile index ccd5ccf..92e640d 100644 --- a/Podfile +++ b/Podfile @@ -22,6 +22,9 @@ target 'Chuck Norris Facts' do # UI pod 'lottie-ios' + # Networking + pod 'Moya/RxSwift' + target 'Chuck Norris FactsTests' do inherit! :search_paths test_pods diff --git a/Podfile.lock b/Podfile.lock index 6135ede..f91ebe1 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,6 +1,12 @@ PODS: + - Alamofire (5.2.2) - Differentiator (4.0.1) - lottie-ios (3.1.8) + - Moya/Core (14.0.0): + - Alamofire (~> 5.0) + - Moya/RxSwift (14.0.0): + - Moya/Core + - RxSwift (~> 5.0) - RxBlocking (5.1.1): - RxSwift (~> 5) - RxCocoa (5.1.1): @@ -19,6 +25,7 @@ PODS: DEPENDENCIES: - lottie-ios + - Moya/RxSwift - RxBlocking - RxCocoa - RxDataSources @@ -28,8 +35,10 @@ DEPENDENCIES: SPEC REPOS: trunk: + - Alamofire - Differentiator - lottie-ios + - Moya - RxBlocking - RxCocoa - RxDataSources @@ -39,8 +48,10 @@ SPEC REPOS: - SwiftLint SPEC CHECKSUMS: + Alamofire: 814429acc853c6c54ff123fc3d2ef66803823ce0 Differentiator: 886080237d9f87f322641dedbc5be257061b0602 lottie-ios: 48fac6be217c76937e36e340e2d09cf7b10b7f5f + Moya: 5b45dacb75adb009f97fde91c204c1e565d31916 RxBlocking: 5f700a78cad61ce253ebd37c9a39b5ccc76477b4 RxCocoa: 32065309a38d29b5b0db858819b5bf9ef038b601 RxDataSources: efee07fa4de48477eca0a4611e6d11e2da9c1114 @@ -49,6 +60,6 @@ SPEC CHECKSUMS: RxTest: 711632d5644dffbeb62c936a521b5b008a1e1faa SwiftLint: dfd554ff0dff17288ee574814ccdd5cea85d76f7 -PODFILE CHECKSUM: 72e38736d56fcdc5c7652b03439f869ce6a2e7c7 +PODFILE CHECKSUM: 3f9595b8539a5aeb69b06a4e9b182bffa2122f96 COCOAPODS: 1.9.3 From b6f5bfa2d1f5088625768d05b7feaa11d2dd8234 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Thu, 22 Oct 2020 11:20:42 -0300 Subject: [PATCH 06/18] List Facts Categories (#14) * Facts Storage using Realm * Get categories service * List stored categories on SearchFacts * SearchFacts categories as a CollectionView * Improve Sync Categories service * Improve FactCategory collectionView * Use 8 for edge insets * Sync categories when viewDidAppear * syncCategories as a ViewModel output * UITests of SearchFacts Categories * FactsService Unit Tests * Unit Tests of SearchFacts Scene * Unit Test to SyncCategories * Use xcode-select * Use mock factsService on SearchFacts Unit Tests * Fix assert of result when search by category * Get Categories stub as part of Test Bundle --- .github/workflows/tests.yml | 6 +- Chuck Norris Facts.xcodeproj/project.pbxproj | 86 ++++++++++++++++++- .../Data/Models/FactCategory.swift | 21 +++++ .../Data/Networking/FactsAPI.swift | 16 +++- .../Data/Services/FactsService.swift | 23 ++++- .../Storage/Entities/FactCategoryEntity.swift | 26 ++++++ .../Data/Storage/FactsStorage.swift | 38 ++++++++ Chuck Norris Facts/Extensions/Data+Stub.swift | 21 +++++ .../Resources/Stubs/get-categories.json | 1 + .../FactsList/FactsListViewController.swift | 5 ++ .../Facts/FactsList/FactsListViewModel.swift | 7 ++ .../SearchFacts/Cells/FactCategoryCell.swift | 52 +++++++++++ .../Cells/FactCategoryViewFlowLayout.swift | 32 +++++++ .../Cells/FactCategoryViewModel.swift | 30 +++++++ .../SearchFactsViewController.swift | 64 ++++++++++++++ .../SearchFacts/SearchFactsViewModel.swift | 19 +++- .../Data/Services/FactsServiceTests.swift | 72 ++++++++++++++++ .../Mocks/FactsServiceMock.swift | 10 +++ .../FactsList/FactsListViewModelTests.swift | 18 ++++ .../SearchFactsViewControllerTests.swift | 51 +++++++++++ .../SearchFactsViewModelTests.swift | 23 ++++- .../FactsList => }/Stubs/facts-list.json | 0 .../Stubs/get-categories.json | 1 + .../Facts/FactsList => }/Stubs/long-fact.json | 0 .../FactsList => }/Stubs/short-fact.json | 0 .../Scenes/SearchFactsScene.swift | 2 + .../Tests/SearchFactsUITests.swift | 32 +++++++ Podfile | 4 + Podfile.lock | 18 +++- 29 files changed, 669 insertions(+), 9 deletions(-) create mode 100644 Chuck Norris Facts/Data/Models/FactCategory.swift create mode 100644 Chuck Norris Facts/Data/Storage/Entities/FactCategoryEntity.swift create mode 100644 Chuck Norris Facts/Data/Storage/FactsStorage.swift create mode 100644 Chuck Norris Facts/Extensions/Data+Stub.swift create mode 100644 Chuck Norris Facts/Resources/Stubs/get-categories.json create mode 100644 Chuck Norris Facts/Scenes/Facts/SearchFacts/Cells/FactCategoryCell.swift create mode 100644 Chuck Norris Facts/Scenes/Facts/SearchFacts/Cells/FactCategoryViewFlowLayout.swift create mode 100644 Chuck Norris Facts/Scenes/Facts/SearchFacts/Cells/FactCategoryViewModel.swift create mode 100644 Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift create mode 100644 Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewControllerTests.swift rename Chuck Norris FactsTests/{Scenes/Facts/FactsList => }/Stubs/facts-list.json (100%) create mode 100644 Chuck Norris FactsTests/Stubs/get-categories.json rename Chuck Norris FactsTests/{Scenes/Facts/FactsList => }/Stubs/long-fact.json (100%) rename Chuck Norris FactsTests/{Scenes/Facts/FactsList => }/Stubs/short-fact.json (100%) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1a2c6b5..57bc3a0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,13 +7,13 @@ jobs: name: Tests runs-on: macOS-latest - env: - DEVELOPER_DIR: /Applications/Xcode_11.7.app - steps: - name: Checkout uses: actions/checkout@v2 + - name: Select Xcode 11 + run: sudo xcode-select -switch /Applications/Xcode_11.7.app + - name: Install bundle run: bundle install diff --git a/Chuck Norris Facts.xcodeproj/project.pbxproj b/Chuck Norris Facts.xcodeproj/project.pbxproj index 088fd53..0732ba9 100644 --- a/Chuck Norris Facts.xcodeproj/project.pbxproj +++ b/Chuck Norris Facts.xcodeproj/project.pbxproj @@ -7,11 +7,17 @@ objects = { /* Begin PBXBuildFile section */ + 1E23683E253FB07200BE17F3 /* FactCategoryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E23683D253FB07200BE17F3 /* FactCategoryCell.swift */; }; + 1E236840253FB13A00BE17F3 /* FactCategoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E23683F253FB13A00BE17F3 /* FactCategoryViewModel.swift */; }; 1E32758F2532A2C0007E838A /* EmptyListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E32758E2532A2C0007E838A /* EmptyListView.swift */; }; 1E3275922532A2CD007E838A /* empty-box.json in Resources */ = {isa = PBXBuildFile; fileRef = 1E3275912532A2CD007E838A /* empty-box.json */; }; 1E463803253636160079D8E9 /* SearchFactsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E463802253636160079D8E9 /* SearchFactsViewController.swift */; }; 1E463805253636D80079D8E9 /* SearchFactsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E463804253636D80079D8E9 /* SearchFactsCoordinator.swift */; }; 1E46380B25363FF40079D8E9 /* SearchFactsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E46380A25363FF40079D8E9 /* SearchFactsViewModel.swift */; }; + 1E5617242540F43F00BF26A0 /* FactsServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E5617232540F43F00BF26A0 /* FactsServiceTests.swift */; }; + 1E5617282540FAF200BF26A0 /* get-categories.json in Resources */ = {isa = PBXBuildFile; fileRef = 1E5617272540FAF200BF26A0 /* get-categories.json */; }; + 1E56172C2541007500BF26A0 /* Data+Stub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E56172B2541007500BF26A0 /* Data+Stub.swift */; }; + 1E56172E2541039B00BF26A0 /* SearchFactsViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E56172D2541039B00BF26A0 /* SearchFactsViewControllerTests.swift */; }; 1E7F15BE253324CF0006887B /* FactsListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7F15BD253324CF0006887B /* FactsListViewModelTests.swift */; }; 1E7F15C0253324FD0006887B /* FactsListViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7F15BF253324FD0006887B /* FactsListViewControllerTests.swift */; }; 1E7F15C6253329780006887B /* FactViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7F15C5253329780006887B /* FactViewModelTests.swift */; }; @@ -20,6 +26,10 @@ 1E92112F253F7A0B00DB340B /* SearchFactsScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E92112E253F7A0B00DB340B /* SearchFactsScene.swift */; }; 1E921131253F7AAA00DB340B /* SearchFactsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E921130253F7AAA00DB340B /* SearchFactsUITests.swift */; }; 1E921134253F84F100DB340B /* SearchFactsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E921133253F84F100DB340B /* SearchFactsViewModelTests.swift */; }; + 1E921139253F909700DB340B /* FactsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E921138253F909700DB340B /* FactsStorage.swift */; }; + 1E92113B253F90BF00DB340B /* FactCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E92113A253F90BF00DB340B /* FactCategory.swift */; }; + 1E92113E253F915100DB340B /* FactCategoryEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E92113D253F915100DB340B /* FactCategoryEntity.swift */; }; + 1EAB20AF2540BEC400633382 /* FactCategoryViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAB20AE2540BEC400633382 /* FactCategoryViewFlowLayout.swift */; }; 1EACEC99253649BD0006B36D /* loading.json in Resources */ = {isa = PBXBuildFile; fileRef = 1EACEC98253649BD0006B36D /* loading.json */; }; 1EACEC9E25364B6C0006B36D /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EACEC9D25364B6C0006B36D /* ActivityIndicator.swift */; }; 1ED5D18D25348FD50035046C /* XCTestCase+Stub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED5D18C25348FD50035046C /* XCTestCase+Stub.swift */; }; @@ -30,6 +40,7 @@ 1ED5D19C2534AAE40035046C /* short-fact.json in Resources */ = {isa = PBXBuildFile; fileRef = 1ED5D19B2534AAE40035046C /* short-fact.json */; }; 1ED5D19F2534B0E30035046C /* FactsListScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED5D19E2534B0E30035046C /* FactsListScene.swift */; }; 1ED5D1A22534B0F40035046C /* FactsListUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED5D1A12534B0F40035046C /* FactsListUITests.swift */; }; + 1EDF0B372541C851001931AA /* get-categories.json in Resources */ = {isa = PBXBuildFile; fileRef = 1EDF0B362541C851001931AA /* get-categories.json */; }; 1EE0713D25314AF500F6BF6D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE0713C25314AF500F6BF6D /* AppDelegate.swift */; }; 1EE0713F25314AF500F6BF6D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE0713E25314AF500F6BF6D /* SceneDelegate.swift */; }; 1EE0714625314AF600F6BF6D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1EE0714525314AF600F6BF6D /* Assets.xcassets */; }; @@ -69,11 +80,17 @@ /* Begin PBXFileReference section */ 02186229E02F0A408D2EE0C2 /* Pods_Chuck_Norris_Facts.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Chuck_Norris_Facts.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1E23683D253FB07200BE17F3 /* FactCategoryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactCategoryCell.swift; sourceTree = ""; }; + 1E23683F253FB13A00BE17F3 /* FactCategoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactCategoryViewModel.swift; sourceTree = ""; }; 1E32758E2532A2C0007E838A /* EmptyListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyListView.swift; sourceTree = ""; }; 1E3275912532A2CD007E838A /* empty-box.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "empty-box.json"; sourceTree = ""; }; 1E463802253636160079D8E9 /* SearchFactsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFactsViewController.swift; sourceTree = ""; }; 1E463804253636D80079D8E9 /* SearchFactsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFactsCoordinator.swift; sourceTree = ""; }; 1E46380A25363FF40079D8E9 /* SearchFactsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFactsViewModel.swift; sourceTree = ""; }; + 1E5617232540F43F00BF26A0 /* FactsServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsServiceTests.swift; sourceTree = ""; }; + 1E5617272540FAF200BF26A0 /* get-categories.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "get-categories.json"; sourceTree = ""; }; + 1E56172B2541007500BF26A0 /* Data+Stub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Stub.swift"; sourceTree = ""; }; + 1E56172D2541039B00BF26A0 /* SearchFactsViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFactsViewControllerTests.swift; sourceTree = ""; }; 1E7F15BD253324CF0006887B /* FactsListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListViewModelTests.swift; sourceTree = ""; }; 1E7F15BF253324FD0006887B /* FactsListViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListViewControllerTests.swift; sourceTree = ""; }; 1E7F15C5253329780006887B /* FactViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactViewModelTests.swift; sourceTree = ""; }; @@ -82,6 +99,10 @@ 1E92112E253F7A0B00DB340B /* SearchFactsScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFactsScene.swift; sourceTree = ""; }; 1E921130253F7AAA00DB340B /* SearchFactsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFactsUITests.swift; sourceTree = ""; }; 1E921133253F84F100DB340B /* SearchFactsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFactsViewModelTests.swift; sourceTree = ""; }; + 1E921138253F909700DB340B /* FactsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsStorage.swift; sourceTree = ""; }; + 1E92113A253F90BF00DB340B /* FactCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactCategory.swift; sourceTree = ""; }; + 1E92113D253F915100DB340B /* FactCategoryEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactCategoryEntity.swift; sourceTree = ""; }; + 1EAB20AE2540BEC400633382 /* FactCategoryViewFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactCategoryViewFlowLayout.swift; sourceTree = ""; }; 1EACEC98253649BD0006B36D /* loading.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = loading.json; sourceTree = ""; }; 1EACEC9D25364B6C0006B36D /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = ""; }; 1ED5D18C25348FD50035046C /* XCTestCase+Stub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+Stub.swift"; sourceTree = ""; }; @@ -92,6 +113,7 @@ 1ED5D19B2534AAE40035046C /* short-fact.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "short-fact.json"; sourceTree = ""; }; 1ED5D19E2534B0E30035046C /* FactsListScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListScene.swift; sourceTree = ""; }; 1ED5D1A12534B0F40035046C /* FactsListUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListUITests.swift; sourceTree = ""; }; + 1EDF0B362541C851001931AA /* get-categories.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "get-categories.json"; sourceTree = ""; }; 1EE0713925314AF500F6BF6D /* Chuck Norris Facts.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Chuck Norris Facts.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 1EE0713C25314AF500F6BF6D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 1EE0713E25314AF500F6BF6D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -151,6 +173,16 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 1E23683C253FB05100BE17F3 /* Cells */ = { + isa = PBXGroup; + children = ( + 1E23683D253FB07200BE17F3 /* FactCategoryCell.swift */, + 1E23683F253FB13A00BE17F3 /* FactCategoryViewModel.swift */, + 1EAB20AE2540BEC400633382 /* FactCategoryViewFlowLayout.swift */, + ); + path = Cells; + sourceTree = ""; + }; 1E32758D2532A2A3007E838A /* Views */ = { isa = PBXGroup; children = ( @@ -171,6 +203,7 @@ 1E463801253636050079D8E9 /* SearchFacts */ = { isa = PBXGroup; children = ( + 1E23683C253FB05100BE17F3 /* Cells */, 1E463802253636160079D8E9 /* SearchFactsViewController.swift */, 1E463804253636D80079D8E9 /* SearchFactsCoordinator.swift */, 1E46380A25363FF40079D8E9 /* SearchFactsViewModel.swift */, @@ -178,6 +211,22 @@ path = SearchFacts; sourceTree = ""; }; + 1E5617212540F42600BF26A0 /* Data */ = { + isa = PBXGroup; + children = ( + 1E5617222540F42E00BF26A0 /* Services */, + ); + path = Data; + sourceTree = ""; + }; + 1E5617222540F42E00BF26A0 /* Services */ = { + isa = PBXGroup; + children = ( + 1E5617232540F43F00BF26A0 /* FactsServiceTests.swift */, + ); + path = Services; + sourceTree = ""; + }; 1E7F15BA253324760006887B /* Scenes */ = { isa = PBXGroup; children = ( @@ -198,7 +247,6 @@ 1E7F15BC253324BD0006887B /* FactsList */ = { isa = PBXGroup; children = ( - 1ED5D1942534AA460035046C /* Stubs */, 1E7F15C4253329600006887B /* Cells */, 1E7F15BD253324CF0006887B /* FactsListViewModelTests.swift */, 1E7F15BF253324FD0006887B /* FactsListViewControllerTests.swift */, @@ -234,10 +282,28 @@ isa = PBXGroup; children = ( 1E921133253F84F100DB340B /* SearchFactsViewModelTests.swift */, + 1E56172D2541039B00BF26A0 /* SearchFactsViewControllerTests.swift */, ); path = SearchFacts; sourceTree = ""; }; + 1E921137253F908C00DB340B /* Storage */ = { + isa = PBXGroup; + children = ( + 1E92113C253F914400DB340B /* Entities */, + 1E921138253F909700DB340B /* FactsStorage.swift */, + ); + path = Storage; + sourceTree = ""; + }; + 1E92113C253F914400DB340B /* Entities */ = { + isa = PBXGroup; + children = ( + 1E92113D253F915100DB340B /* FactCategoryEntity.swift */, + ); + path = Entities; + sourceTree = ""; + }; 1ED5D18B25348FC40035046C /* Library */ = { isa = PBXGroup; children = ( @@ -250,6 +316,7 @@ isa = PBXGroup; children = ( 1ED5D18F25349E9D0035046C /* UIViewController+Rx.swift */, + 1E56172B2541007500BF26A0 /* Data+Stub.swift */, ); path = Extensions; sourceTree = ""; @@ -268,6 +335,7 @@ 1ED5D19B2534AAE40035046C /* short-fact.json */, 1ED5D1972534AA700035046C /* long-fact.json */, 1ED5D1992534AA7A0035046C /* facts-list.json */, + 1EDF0B362541C851001931AA /* get-categories.json */, ); path = Stubs; sourceTree = ""; @@ -328,7 +396,9 @@ 1EE0715225314AF600F6BF6D /* Chuck Norris FactsTests */ = { isa = PBXGroup; children = ( + 1E5617212540F42600BF26A0 /* Data */, 1ED5D1912534A55D0035046C /* Mocks */, + 1ED5D1942534AA460035046C /* Stubs */, 1ED5D18B25348FC40035046C /* Library */, 1E7F15BA253324760006887B /* Scenes */, 1EE0715525314AF600F6BF6D /* Info.plist */, @@ -390,6 +460,7 @@ isa = PBXGroup; children = ( 1EFE287D25321071008806B9 /* search-facts.json */, + 1E5617272540FAF200BF26A0 /* get-categories.json */, ); path = Stubs; sourceTree = ""; @@ -397,6 +468,7 @@ 1EFE2881253210A4008806B9 /* Data */ = { isa = PBXGroup; children = ( + 1E921137253F908C00DB340B /* Storage */, 1E92112B253F6CF500DB340B /* Services */, 1EFE288525321111008806B9 /* Models */, 1EFE2882253210A8008806B9 /* Networking */, @@ -417,6 +489,7 @@ isa = PBXGroup; children = ( 1EFE288625321119008806B9 /* Fact.swift */, + 1E92113A253F90BF00DB340B /* FactCategory.swift */, ); path = Models; sourceTree = ""; @@ -587,6 +660,7 @@ files = ( 1EFE287E25321071008806B9 /* search-facts.json in Resources */, 1EE0714925314AF600F6BF6D /* LaunchScreen.storyboard in Resources */, + 1E5617282540FAF200BF26A0 /* get-categories.json in Resources */, 1EE0714625314AF600F6BF6D /* Assets.xcassets in Resources */, 1E3275922532A2CD007E838A /* empty-box.json in Resources */, 1EACEC99253649BD0006B36D /* loading.json in Resources */, @@ -599,6 +673,7 @@ files = ( 1ED5D19C2534AAE40035046C /* short-fact.json in Resources */, 1ED5D19A2534AA7A0035046C /* facts-list.json in Resources */, + 1EDF0B372541C851001931AA /* get-categories.json in Resources */, 1ED5D1982534AA700035046C /* long-fact.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -761,8 +836,10 @@ 1EFE288925321123008806B9 /* JSON.swift in Sources */, 1EFE288725321119008806B9 /* Fact.swift in Sources */, 1EFE289725321CE2008806B9 /* FactTableViewCell.swift in Sources */, + 1E56172C2541007500BF26A0 /* Data+Stub.swift in Sources */, 1EE0713D25314AF500F6BF6D /* AppDelegate.swift in Sources */, 1ED5D19025349E9D0035046C /* UIViewController+Rx.swift in Sources */, + 1E236840253FB13A00BE17F3 /* FactCategoryViewModel.swift in Sources */, 1EFE2869253207B2008806B9 /* AppCoordinator.swift in Sources */, 1E92112A253F6BB700DB340B /* SearchFactsResponse.swift in Sources */, 1EE0713F25314AF500F6BF6D /* SceneDelegate.swift in Sources */, @@ -771,10 +848,15 @@ 1E463803253636160079D8E9 /* SearchFactsViewController.swift in Sources */, 1EFE289525321CD2008806B9 /* FactViewModel.swift in Sources */, 1E46380B25363FF40079D8E9 /* SearchFactsViewModel.swift in Sources */, + 1E921139253F909700DB340B /* FactsStorage.swift in Sources */, + 1E23683E253FB07200BE17F3 /* FactCategoryCell.swift in Sources */, + 1E92113E253F915100DB340B /* FactCategoryEntity.swift in Sources */, + 1EAB20AF2540BEC400633382 /* FactCategoryViewFlowLayout.swift in Sources */, 1E92112D253F6D0000DB340B /* FactsService.swift in Sources */, 1EFE2892253214DB008806B9 /* FactsListViewModel.swift in Sources */, 1EACEC9E25364B6C0006B36D /* ActivityIndicator.swift in Sources */, 1EFE2867253206D3008806B9 /* BaseCoordinator.swift in Sources */, + 1E92113B253F90BF00DB340B /* FactCategory.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -782,8 +864,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1E56172E2541039B00BF26A0 /* SearchFactsViewControllerTests.swift in Sources */, 1E921134253F84F100DB340B /* SearchFactsViewModelTests.swift in Sources */, 1E7F15C6253329780006887B /* FactViewModelTests.swift in Sources */, + 1E5617242540F43F00BF26A0 /* FactsServiceTests.swift in Sources */, 1ED5D18D25348FD50035046C /* XCTestCase+Stub.swift in Sources */, 1ED5D1932534A56F0035046C /* FactsServiceMock.swift in Sources */, 1E7F15BE253324CF0006887B /* FactsListViewModelTests.swift in Sources */, diff --git a/Chuck Norris Facts/Data/Models/FactCategory.swift b/Chuck Norris Facts/Data/Models/FactCategory.swift new file mode 100644 index 0000000..98e516a --- /dev/null +++ b/Chuck Norris Facts/Data/Models/FactCategory.swift @@ -0,0 +1,21 @@ +// +// FactCategory.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/20/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation + +struct FactCategory: Decodable { + let text: String + + init(text: String) { + self.text = text + } + + init(from decoder: Decoder) throws { + self.text = try decoder.singleValueContainer().decode(String.self) + } +} diff --git a/Chuck Norris Facts/Data/Networking/FactsAPI.swift b/Chuck Norris Facts/Data/Networking/FactsAPI.swift index 35cf1b6..3d40571 100644 --- a/Chuck Norris Facts/Data/Networking/FactsAPI.swift +++ b/Chuck Norris Facts/Data/Networking/FactsAPI.swift @@ -10,6 +10,7 @@ import Moya enum FactsAPI { case searchFacts(searchTerm: String) + case getCategories } extension FactsAPI: TargetType { @@ -22,12 +23,14 @@ extension FactsAPI: TargetType { switch self { case .searchFacts: return "/search" + case .getCategories: + return "/categories" } } var method: Method { switch self { - case .searchFacts: + case .searchFacts, .getCategories: return .get } } @@ -36,6 +39,8 @@ extension FactsAPI: TargetType { switch self { case .searchFacts(let searchTerm): return .requestParameters(parameters: ["query": searchTerm], encoding: URLEncoding.queryString) + case .getCategories: + return .requestPlain } } @@ -44,6 +49,15 @@ extension FactsAPI: TargetType { } var sampleData: Data { + switch self { + case .getCategories: + if let data = try? Data.stub("get-categories") { + return data + } + default: + break + } + return Data() } diff --git a/Chuck Norris Facts/Data/Services/FactsService.swift b/Chuck Norris Facts/Data/Services/FactsService.swift index 8639d98..8f9dbff 100644 --- a/Chuck Norris Facts/Data/Services/FactsService.swift +++ b/Chuck Norris Facts/Data/Services/FactsService.swift @@ -11,11 +11,19 @@ import Moya protocol FactsServiceType { func searchFacts(searchTerm: String) -> Observable<[Fact]> + func syncCategories() -> Observable + func retrieveCategories() -> Observable<[FactCategory]> } struct FactsService: FactsServiceType { - private let provider = MoyaProvider() + private var provider: MoyaProvider + private var storage: FactsStorageType + + init(provider: MoyaProvider = MoyaProvider(), storage: FactsStorageType = FactsStorage()) { + self.provider = provider + self.storage = storage + } func searchFacts(searchTerm: String) -> Observable<[Fact]> { provider.rx @@ -25,4 +33,17 @@ struct FactsService: FactsServiceType { .map { $0.facts } } + func syncCategories() -> Observable { + provider.rx + .request(.getCategories) + .asObservable() + .map([FactCategory].self, using: JSON.decoder) + .map { self.storage.storeCategories($0) } + .map { () } + } + + func retrieveCategories() -> Observable<[FactCategory]> { + storage.retrieveCategories() + } + } diff --git a/Chuck Norris Facts/Data/Storage/Entities/FactCategoryEntity.swift b/Chuck Norris Facts/Data/Storage/Entities/FactCategoryEntity.swift new file mode 100644 index 0000000..d0156d6 --- /dev/null +++ b/Chuck Norris Facts/Data/Storage/Entities/FactCategoryEntity.swift @@ -0,0 +1,26 @@ +// +// FactCategoryEntity.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/20/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation +import RealmSwift + +class FactCategoryEntity: Object { + @objc dynamic var text = "" + + override static func primaryKey() -> String? { + "text" + } + + convenience init(category: FactCategory) { + self.init(value: ["text": category.text]) + } + + var item: FactCategory { + FactCategory(text: text) + } +} diff --git a/Chuck Norris Facts/Data/Storage/FactsStorage.swift b/Chuck Norris Facts/Data/Storage/FactsStorage.swift new file mode 100644 index 0000000..16a4b83 --- /dev/null +++ b/Chuck Norris Facts/Data/Storage/FactsStorage.swift @@ -0,0 +1,38 @@ +// +// FactsStorage.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/20/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation +import RxSwift +import RealmSwift +import RxRealm + +protocol FactsStorageType { + func storeCategories(_ categories: [FactCategory]) + + func retrieveCategories() -> Observable<[FactCategory]> +} + +final class FactsStorage: FactsStorageType { + private let realm: Realm! + + init(realm: Realm? = nil) { + self.realm = realm ?? (try? Realm()) + } + + func storeCategories(_ categories: [FactCategory]) { + try? realm.write { + let entities = categories.map(FactCategoryEntity.init) + self.realm.add(entities, update: .modified) + } + } + + func retrieveCategories() -> Observable<[FactCategory]> { + let entities = realm.objects(FactCategoryEntity.self) + return Observable.collection(from: entities).map { $0.map { $0.item } } + } +} diff --git a/Chuck Norris Facts/Extensions/Data+Stub.swift b/Chuck Norris Facts/Extensions/Data+Stub.swift new file mode 100644 index 0000000..b15c63e --- /dev/null +++ b/Chuck Norris Facts/Extensions/Data+Stub.swift @@ -0,0 +1,21 @@ +// +// Data+Stub.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/21/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation + +extension Data { + + static func stub(_ resource: String) throws -> Data? { + guard let url = Bundle.main.url(forResource: resource, withExtension: ".json") else { + return nil + } + + let data = try Data(contentsOf: url) + return data + } +} diff --git a/Chuck Norris Facts/Resources/Stubs/get-categories.json b/Chuck Norris Facts/Resources/Stubs/get-categories.json new file mode 100644 index 0000000..7fd08b8 --- /dev/null +++ b/Chuck Norris Facts/Resources/Stubs/get-categories.json @@ -0,0 +1 @@ +["animal","career","celebrity","dev","explicit","fashion","food","history","money","movie","music","political","religion","science","sport","travel"] diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift index 88479a5..6944601 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift @@ -136,6 +136,11 @@ class FactsListViewController: UIViewController { searchButton.rx.tap .bind(to: viewModel.startSearchFacts) .disposed(by: disposeBag) + + viewModel.syncCategories + .asDriver(onErrorJustReturn: ()) + .drive() + .disposed(by: disposeBag) } private func showEmptyView(_ isEmpty: Bool) { diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift index 457b68c..86684ff 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift @@ -36,6 +36,8 @@ final class FactsListViewModel { let isLoading: ActivityIndicator + let syncCategories: Observable + init(factsService: FactsServiceType = FactsService()) { let loadingIndicator = ActivityIndicator() @@ -56,6 +58,11 @@ final class FactsListViewModel { self.setSearchTerm = searchTermSubject.asObserver() self.searchTerm = searchTermSubject.asObservable() + self.syncCategories = viewDidAppearSubject.asObservable() + .flatMapLatest { _ -> Observable in + factsService.syncCategories() + } + self.facts = Observable.combineLatest(viewDidAppearSubject, searchTermSubject) .flatMapLatest { _, searchTerm -> Observable<[Fact]> in if CommandLine.arguments.contains("--search-facts") { diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/Cells/FactCategoryCell.swift b/Chuck Norris Facts/Scenes/Facts/SearchFacts/Cells/FactCategoryCell.swift new file mode 100644 index 0000000..57e18d0 --- /dev/null +++ b/Chuck Norris Facts/Scenes/Facts/SearchFacts/Cells/FactCategoryCell.swift @@ -0,0 +1,52 @@ +// +// FactCategoryCell.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/20/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import UIKit + +class FactCategoryCell: UICollectionViewCell { + + static let cellIdentifier = "FactCategoryCell" + + private lazy var bodyLabel: UILabel = { + let label = UILabel() + + label.translatesAutoresizingMaskIntoConstraints = false + label.lineBreakMode = .byWordWrapping + label.textColor = .white + label.numberOfLines = 0 + + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + setupView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setup(_ factCategory: FactCategoryViewModel) { + bodyLabel.text = factCategory.text + bodyLabel.font = .systemFont(ofSize: 16, weight: .bold) + } + + func setupView() { + layer.cornerRadius = 4 + + backgroundColor = .systemBlue + + contentView.addSubview(bodyLabel) + bodyLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 4).isActive = true + bodyLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -4).isActive = true + bodyLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 4).isActive = true + bodyLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4).isActive = true + } +} diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/Cells/FactCategoryViewFlowLayout.swift b/Chuck Norris Facts/Scenes/Facts/SearchFacts/Cells/FactCategoryViewFlowLayout.swift new file mode 100644 index 0000000..890b6c1 --- /dev/null +++ b/Chuck Norris Facts/Scenes/Facts/SearchFacts/Cells/FactCategoryViewFlowLayout.swift @@ -0,0 +1,32 @@ +// +// FactCategoryViewFlowLayout.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/21/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import UIKit + +final class FactCategoryViewFlowLayout: UICollectionViewFlowLayout { + + override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { + let attributes = super.layoutAttributesForElements(in: rect) + + var leftMargin = sectionInset.left + var maxY: CGFloat = -1.0 + attributes?.forEach { layoutAttribute in + if layoutAttribute.representedElementCategory == .cell { + if layoutAttribute.frame.origin.y >= maxY { + leftMargin = sectionInset.left + } + layoutAttribute.frame.origin.x = leftMargin + leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing + maxY = max(layoutAttribute.frame.maxY, maxY) + } + } + + return attributes + } + +} diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/Cells/FactCategoryViewModel.swift b/Chuck Norris Facts/Scenes/Facts/SearchFacts/Cells/FactCategoryViewModel.swift new file mode 100644 index 0000000..8d2b9a2 --- /dev/null +++ b/Chuck Norris Facts/Scenes/Facts/SearchFacts/Cells/FactCategoryViewModel.swift @@ -0,0 +1,30 @@ +// +// FactCategoryViewModel.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/20/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation +import RxDataSources + +class FactCategoryViewModel { + let text: String + + init(category: FactCategory) { + self.text = category.text + } +} + +extension FactCategoryViewModel: IdentifiableType { + var identity: String { + text + } +} + +extension FactCategoryViewModel: Equatable { + static func == (lhs: FactCategoryViewModel, rhs: FactCategoryViewModel) -> Bool { + return lhs.text == rhs.text + } +} diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewController.swift b/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewController.swift index 92e5e4d..fdfe9a5 100644 --- a/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewController.swift +++ b/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewController.swift @@ -8,6 +8,7 @@ import UIKit import RxSwift +import RxDataSources final class SearchFactsViewController: UIViewController { @@ -15,6 +16,29 @@ final class SearchFactsViewController: UIViewController { let disposeBag = DisposeBag() + lazy var collectionView: UICollectionView = { + let layout = FactCategoryViewFlowLayout() + + layout.scrollDirection = .vertical + layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize + layout.sectionInset = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) + + return UICollectionView(frame: .zero, collectionViewLayout: layout) + }() + + private lazy var categoriesDataSource = RxCollectionViewSectionedReloadDataSource( + configureCell: { _, collectionView, indexPath, category -> UICollectionViewCell in + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: FactCategoryCell.cellIdentifier, + for: indexPath + ) + if let cell = cell as? FactCategoryCell { + cell.setup(category) + } + return cell + } + ) + lazy var cancelButton: UIBarButtonItem = { let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: nil, action: nil) cancelButton.accessibilityIdentifier = "cancelButton" @@ -37,6 +61,7 @@ final class SearchFactsViewController: UIViewController { setupView() setupNavigationBar() setupBindings() + setupCollectionView() } private func setupView() { @@ -44,6 +69,22 @@ final class SearchFactsViewController: UIViewController { view.accessibilityIdentifier = "searchFactsView" } + private func setupCollectionView() { + view.addSubview(collectionView) + + collectionView.backgroundColor = .systemBackground + + collectionView.translatesAutoresizingMaskIntoConstraints = false + collectionView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true + collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + + collectionView.register(FactCategoryCell.self, forCellWithReuseIdentifier: FactCategoryCell.cellIdentifier) + + collectionView.accessibilityIdentifier = "factCategoriesCollectionView" + } + private func setupNavigationBar() { navigationItem.searchController = searchController navigationItem.leftBarButtonItem = cancelButton @@ -51,6 +92,10 @@ final class SearchFactsViewController: UIViewController { } private func setupBindings() { + rx.viewWillAppear + .bind(to: viewModel.viewWillAppear) + .disposed(by: disposeBag) + cancelButton.rx.tap .bind(to: viewModel.cancel) .disposed(by: disposeBag) @@ -63,5 +108,24 @@ final class SearchFactsViewController: UIViewController { searchController.searchBar.rx.textDidEndEditing .bind(to: viewModel.searchAction) .disposed(by: disposeBag) + + viewModel.categories + .observeOn(MainScheduler.instance) + .bind(to: collectionView.rx.items(dataSource: categoriesDataSource)) + .disposed(by: disposeBag) + + let categorySelected = collectionView.rx + .modelSelected(FactCategoryViewModel.self) + .asObservable() + + categorySelected + .compactMap { $0.text } + .bind(to: viewModel.searchTerm) + .disposed(by: disposeBag) + + categorySelected + .map { _ in () } + .bind(to: viewModel.searchAction) + .disposed(by: disposeBag) } } diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewModel.swift b/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewModel.swift index b5d3be6..409f04a 100644 --- a/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewModel.swift +++ b/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewModel.swift @@ -6,8 +6,12 @@ // Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. // +import Foundation +import RxDataSources import RxSwift +typealias FactCategoriesSectionModel = AnimatableSectionModel + class SearchFactsViewModel { // MARK: - Inputs @@ -18,13 +22,17 @@ class SearchFactsViewModel { let searchAction: AnyObserver + let viewWillAppear: AnyObserver + // MARK: - Outputs + let categories: Observable<[FactCategoriesSectionModel]> + let didCancel: Observable let didSearchFacts: Observable - init() { + init(factsService: FactsServiceType = FactsService()) { let cancelSubject = PublishSubject() self.cancel = cancelSubject.asObserver() self.didCancel = cancelSubject.asObservable() @@ -38,5 +46,14 @@ class SearchFactsViewModel { self.didSearchFacts = searchActionSubject .withLatestFrom(searchTermSubject) .filter { !$0.isEmpty } + + let viewWillAppearSubject = PublishSubject() + self.viewWillAppear = viewWillAppearSubject.asObserver() + + self.categories = viewWillAppearSubject + .flatMapLatest { factsService.retrieveCategories() } + .map { Array($0.shuffled().prefix(8)) } + .map { $0.map { FactCategoryViewModel(category: $0) } } + .map { [FactCategoriesSectionModel(model: "", items: $0)] } } } diff --git a/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift b/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift new file mode 100644 index 0000000..5ee342a --- /dev/null +++ b/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift @@ -0,0 +1,72 @@ +// +// FactsServiceTests.swift +// Chuck Norris FactsTests +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/21/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import XCTest +import RxSwift +import RxBlocking +import RxTest +import RealmSwift +import Moya + +@testable import Chuck_Norris_Facts + +final class FactsServiceTests: XCTestCase { + + var factsService: FactsServiceType! + var factsStorage: FactsStorageType! + var factsProvider: MoyaProvider! + var realm: Realm! + + var disposeBag: DisposeBag! + var testScheduler: TestScheduler! + + override func setUpWithError() throws { + testScheduler = TestScheduler(initialClock: 0) + disposeBag = DisposeBag() + realm = try Realm(configuration: .init(inMemoryIdentifier: self.name)) + factsStorage = FactsStorage(realm: realm) + factsProvider = MoyaProvider(stubClosure: MoyaProvider.immediatelyStub) + factsService = FactsService(provider: factsProvider, storage: factsStorage) + } + + override func tearDown() { + testScheduler = nil + disposeBag = nil + factsService = nil + factsStorage = nil + + try? realm.write { + realm.deleteAll() + } + } + + func test_syncCategoriesShouldSaveCategoriesOnStorage() throws { + let storedCategories = factsStorage.retrieveCategories() + let categories = try storedCategories.toBlocking().first() ?? [] + XCTAssertTrue(categories.isEmpty) + + factsService.syncCategories() + .subscribe() + .disposed(by: disposeBag) + + let savedCategories = try storedCategories.toBlocking().first() + XCTAssertEqual(savedCategories?.count, 16) + } + + func test_retrieveCategoriesShouldReturnCategoriesOnStorage() throws { + let storedCategories = factsStorage.retrieveCategories() + let categories = try storedCategories.toBlocking().first() ?? [] + XCTAssertTrue(categories.isEmpty) + + let stubCategories = try stub("get-categories", type: [FactCategory].self) ?? [] + factsStorage.storeCategories(stubCategories) + + let savedCategories = try storedCategories.toBlocking().first() + XCTAssertEqual(savedCategories?.count, stubCategories.count) + } +} diff --git a/Chuck Norris FactsTests/Mocks/FactsServiceMock.swift b/Chuck Norris FactsTests/Mocks/FactsServiceMock.swift index c4bc8a7..c255905 100644 --- a/Chuck Norris FactsTests/Mocks/FactsServiceMock.swift +++ b/Chuck Norris FactsTests/Mocks/FactsServiceMock.swift @@ -13,6 +13,16 @@ import RxSwift final class FactsServiceMock: FactsServiceType { + var syncCategoriesReturnValue: Observable = .just(()) + func syncCategories() -> Observable { + return syncCategoriesReturnValue + } + + var retrieveCategoriesReturnValue: Observable<[FactCategory]> = .just([]) + func retrieveCategories() -> Observable<[FactCategory]> { + return retrieveCategoriesReturnValue + } + var searchFactsReturnValue: Observable<[Fact]> = .just([]) func searchFacts(searchTerm: String) -> Observable<[Fact]> { return searchFactsReturnValue diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift index d2e8b88..e464cab 100644 --- a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift +++ b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift @@ -72,4 +72,22 @@ class FactsListViewModelTests: XCTestCase { let shareFact = factObserver.events.compactMap { $0.value.element }.first XCTAssertEqual(fact.value, shareFact?.text) } + + func test_categoriesShouldSyncWhenViewDidAppear() throws { + let stubCategories = try stub("get-categories", type: [FactCategory].self) ?? [] + let categories = try XCTUnwrap(stubCategories, "looks like get-categories.json doesn't exists") + factsServiceMock.retrieveCategoriesReturnValue = .just(categories) + + let syncCategoriesObserver = testScheduler.createObserver(Void.self) + + factsListViewModel.syncCategories + .subscribe(syncCategoriesObserver) + .disposed(by: disposeBag) + + factsListViewModel.viewDidAppear.onNext(()) + + testScheduler.start() + + XCTAssertEqual(syncCategoriesObserver.events.count, 1) + } } diff --git a/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewControllerTests.swift b/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewControllerTests.swift new file mode 100644 index 0000000..53c5243 --- /dev/null +++ b/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewControllerTests.swift @@ -0,0 +1,51 @@ +// +// SearchFactsViewControllerTests.swift +// Chuck Norris FactsTests +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/21/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import XCTest +import RxSwift +import RxBlocking +import RxTest + +@testable import Chuck_Norris_Facts + +class SearchFactsViewControllerTests: XCTestCase { + + var searchFactsViewController: SearchFactsViewController! + var searchFactsViewModel: SearchFactsViewModel! + var factsServiceMock: FactsServiceMock! + var testScheduler: TestScheduler! + var disposeBag: DisposeBag! + + override func setUp() { + disposeBag = DisposeBag() + testScheduler = TestScheduler(initialClock: 0) + factsServiceMock = FactsServiceMock() + searchFactsViewModel = SearchFactsViewModel(factsService: factsServiceMock) + searchFactsViewController = SearchFactsViewController() + searchFactsViewController.viewModel = searchFactsViewModel + + searchFactsViewController.loadView() + searchFactsViewController.viewDidLoad() + } + + override func tearDown() { + disposeBag = nil + testScheduler = nil + searchFactsViewModel = nil + factsServiceMock = nil + } + + func test_factCategoriesViewShouldShow8Categories() throws { + let stubFactCategories = try stub("get-categories", type: [FactCategory].self) ?? [] + factsServiceMock.retrieveCategoriesReturnValue = .just(stubFactCategories) + + searchFactsViewModel.viewWillAppear.onNext(()) + + XCTAssertEqual(searchFactsViewController.collectionView.numberOfItems(inSection: 0), 8) + } +} diff --git a/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewModelTests.swift b/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewModelTests.swift index 18021c7..b9ff58e 100644 --- a/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewModelTests.swift +++ b/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewModelTests.swift @@ -16,19 +16,22 @@ import RxTest class SearchFactsViewModelTests: XCTestCase { var searchFactsViewModel: SearchFactsViewModel! + var factsServiceMock: FactsServiceMock! var testScheduler: TestScheduler! var disposeBag: DisposeBag! override func setUp() { disposeBag = DisposeBag() testScheduler = TestScheduler(initialClock: 0) - searchFactsViewModel = SearchFactsViewModel() + factsServiceMock = FactsServiceMock() + searchFactsViewModel = SearchFactsViewModel(factsService: factsServiceMock) } override func tearDown() { disposeBag = nil testScheduler = nil searchFactsViewModel = nil + factsServiceMock = nil } func test_searchFactsWhenSearchShouldSearchFacts() { @@ -61,4 +64,22 @@ class SearchFactsViewModelTests: XCTestCase { let cancelCount = cancelObserver.events.compactMap { $0.value.element }.count XCTAssertEqual(cancelCount, 1) } + + func test_shouldLoad8RandomFactCategories() throws { + let factCategoriesObserver = testScheduler.createObserver([FactCategoriesSectionModel].self) + + let testCategories = try stub("get-categories", type: [FactCategory].self) ?? [] + factsServiceMock.retrieveCategoriesReturnValue = .just(testCategories) + + searchFactsViewModel.categories + .subscribe(factCategoriesObserver) + .disposed(by: disposeBag) + + searchFactsViewModel.viewWillAppear.onNext(()) + + testScheduler.start() + + let factCategoriesViewModel = factCategoriesObserver.events.compactMap { $0.value.element }.first + XCTAssertEqual(factCategoriesViewModel?.first?.items.count, 8) + } } diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/Stubs/facts-list.json b/Chuck Norris FactsTests/Stubs/facts-list.json similarity index 100% rename from Chuck Norris FactsTests/Scenes/Facts/FactsList/Stubs/facts-list.json rename to Chuck Norris FactsTests/Stubs/facts-list.json diff --git a/Chuck Norris FactsTests/Stubs/get-categories.json b/Chuck Norris FactsTests/Stubs/get-categories.json new file mode 100644 index 0000000..7fd08b8 --- /dev/null +++ b/Chuck Norris FactsTests/Stubs/get-categories.json @@ -0,0 +1 @@ +["animal","career","celebrity","dev","explicit","fashion","food","history","money","movie","music","political","religion","science","sport","travel"] diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/Stubs/long-fact.json b/Chuck Norris FactsTests/Stubs/long-fact.json similarity index 100% rename from Chuck Norris FactsTests/Scenes/Facts/FactsList/Stubs/long-fact.json rename to Chuck Norris FactsTests/Stubs/long-fact.json diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/Stubs/short-fact.json b/Chuck Norris FactsTests/Stubs/short-fact.json similarity index 100% rename from Chuck Norris FactsTests/Scenes/Facts/FactsList/Stubs/short-fact.json rename to Chuck Norris FactsTests/Stubs/short-fact.json diff --git a/Chuck Norris FactsUITests/Scenes/SearchFactsScene.swift b/Chuck Norris FactsUITests/Scenes/SearchFactsScene.swift index 206aeb4..3b2d64d 100644 --- a/Chuck Norris FactsUITests/Scenes/SearchFactsScene.swift +++ b/Chuck Norris FactsUITests/Scenes/SearchFactsScene.swift @@ -14,6 +14,7 @@ struct SearchFactsScene { let searchFactsView: XCUIElement let searchBarField: XCUIElement let cancelButton: XCUIElement + let factsCategoriesCollection: XCUIElement init() { let app = XCUIApplication() @@ -21,6 +22,7 @@ struct SearchFactsScene { searchFactsView = app.otherElements["searchFactsView"] searchBarField = app.searchFields["Search"] cancelButton = app.navigationBars.buttons["cancelButton"] + factsCategoriesCollection = app.collectionViews["factCategoriesCollectionView"] } } diff --git a/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift b/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift index 727f67c..c3a2c8d 100644 --- a/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift +++ b/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift @@ -46,4 +46,36 @@ final class SearchFactsUITests: XCTestCase { XCTAssertFalse(searchFactsScene.searchFactsView.exists) } + + func test_shouldShow8FactCategories() { + app.launch() + + let factsListScene = FactsListScene() + factsListScene.searchButton.tap() + + let searchFactsScene = SearchFactsScene() + XCTAssertTrue(searchFactsScene.searchFactsView.exists) + + XCTAssertEqual(searchFactsScene.factsCategoriesCollection.cells.count, 8) + } + + func test_tapFactCategoryShouldSearchByTerm() { + app.launch() + + let factsListScene = FactsListScene() + factsListScene.searchButton.tap() + + let searchFactsScene = SearchFactsScene() + XCTAssertTrue(searchFactsScene.searchFactsView.exists) + + let firstFactCategory = searchFactsScene.factsCategoriesCollection.cells.firstMatch + XCTAssertTrue(firstFactCategory.exists) + + firstFactCategory.tap() + XCTAssertFalse(searchFactsScene.searchFactsView.exists) + + sleep(5) + + XCTAssertGreaterThan(factsListScene.factsTableView.cells.count, 0) + } } diff --git a/Podfile b/Podfile index 92e640d..177d3b2 100644 --- a/Podfile +++ b/Podfile @@ -15,6 +15,7 @@ target 'Chuck Norris Facts' do pod 'RxSwift' pod 'RxCocoa' pod 'RxDataSources' + pod 'RxRealm' # Tools pod 'SwiftLint' @@ -25,6 +26,9 @@ target 'Chuck Norris Facts' do # Networking pod 'Moya/RxSwift' + # Storage + pod 'RealmSwift' + target 'Chuck Norris FactsTests' do inherit! :search_paths test_pods diff --git a/Podfile.lock b/Podfile.lock index f91ebe1..1cfac24 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -7,6 +7,11 @@ PODS: - Moya/RxSwift (14.0.0): - Moya/Core - RxSwift (~> 5.0) + - Realm (5.4.6): + - Realm/Headers (= 5.4.6) + - Realm/Headers (5.4.6) + - RealmSwift (5.4.6): + - Realm (= 5.4.6) - RxBlocking (5.1.1): - RxSwift (~> 5) - RxCocoa (5.1.1): @@ -16,6 +21,9 @@ PODS: - Differentiator (~> 4.0) - RxCocoa (~> 5.0) - RxSwift (~> 5.0) + - RxRealm (3.1.0): + - RealmSwift (~> 5.2) + - RxSwift (~> 5.0) - RxRelay (5.1.1): - RxSwift (~> 5) - RxSwift (5.1.1) @@ -26,9 +34,11 @@ PODS: DEPENDENCIES: - lottie-ios - Moya/RxSwift + - RealmSwift - RxBlocking - RxCocoa - RxDataSources + - RxRealm - RxSwift - RxTest - SwiftLint @@ -39,9 +49,12 @@ SPEC REPOS: - Differentiator - lottie-ios - Moya + - Realm + - RealmSwift - RxBlocking - RxCocoa - RxDataSources + - RxRealm - RxRelay - RxSwift - RxTest @@ -52,14 +65,17 @@ SPEC CHECKSUMS: Differentiator: 886080237d9f87f322641dedbc5be257061b0602 lottie-ios: 48fac6be217c76937e36e340e2d09cf7b10b7f5f Moya: 5b45dacb75adb009f97fde91c204c1e565d31916 + Realm: bb8d7be40d0bc92f139c47095124513c489c0baf + RealmSwift: c469118d55feccd985f1de12973c6ef5587213ca RxBlocking: 5f700a78cad61ce253ebd37c9a39b5ccc76477b4 RxCocoa: 32065309a38d29b5b0db858819b5bf9ef038b601 RxDataSources: efee07fa4de48477eca0a4611e6d11e2da9c1114 + RxRealm: 50e5fe5c1f22518205afbb313fbc5580d73bc586 RxRelay: d77f7d771495f43c556cbc43eebd1bb54d01e8e9 RxSwift: 81470a2074fa8780320ea5fe4102807cb7118178 RxTest: 711632d5644dffbeb62c936a521b5b008a1e1faa SwiftLint: dfd554ff0dff17288ee574814ccdd5cea85d76f7 -PODFILE CHECKSUM: 3f9595b8539a5aeb69b06a4e9b182bffa2122f96 +PODFILE CHECKSUM: 880153bdb3f2b2ce7a5e293d253fab39cc855c10 COCOAPODS: 1.9.3 From cc16724770c59d7206576d141f45636685cb0307 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Sat, 24 Oct 2020 15:32:19 -0300 Subject: [PATCH 07/18] Store Facts (#17) * Store Facts using Realm * Adjust current tests * Show Fact Category * Loading behind EmptyView --- Chuck Norris Facts.xcodeproj/project.pbxproj | 8 ++++ Chuck Norris Facts/Data/Models/Fact.swift | 9 ++++ .../Data/Networking/FactsAPI.swift | 6 ++- .../Data/Services/FactsService.swift | 18 ++++++- .../Data/Storage/Entities/FactEntity.swift | 37 +++++++++++++++ .../Data/Storage/FactsStorage.swift | 21 +++++++++ .../FactsList/Cells/FactTableViewCell.swift | 11 ++++- .../Facts/FactsList/Cells/FactViewModel.swift | 9 ++-- .../FactsList/FactsListViewController.swift | 11 +++-- .../Facts/FactsList/FactsListViewModel.swift | 25 +++++++--- .../Facts/FactsList/Views/CategoryView.swift | 47 +++++++++++++++++++ .../Data/Services/FactsServiceTests.swift | 25 ++++++++++ .../Mocks/FactsServiceMock.swift | 9 +++- .../FactsListViewControllerTests.swift | 10 ++-- .../FactsList/FactsListViewModelTests.swift | 3 +- Chuck Norris FactsTests/Stubs/long-fact.json | 1 + Chuck Norris FactsTests/Stubs/short-fact.json | 1 + .../Tests/SearchFactsUITests.swift | 2 +- 18 files changed, 223 insertions(+), 30 deletions(-) create mode 100644 Chuck Norris Facts/Data/Storage/Entities/FactEntity.swift create mode 100644 Chuck Norris Facts/Scenes/Facts/FactsList/Views/CategoryView.swift diff --git a/Chuck Norris Facts.xcodeproj/project.pbxproj b/Chuck Norris Facts.xcodeproj/project.pbxproj index 0732ba9..53635d8 100644 --- a/Chuck Norris Facts.xcodeproj/project.pbxproj +++ b/Chuck Norris Facts.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 1E0C7B402543A4E8002D5C47 /* FactEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E0C7B3F2543A4E8002D5C47 /* FactEntity.swift */; }; 1E23683E253FB07200BE17F3 /* FactCategoryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E23683D253FB07200BE17F3 /* FactCategoryCell.swift */; }; 1E236840253FB13A00BE17F3 /* FactCategoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E23683F253FB13A00BE17F3 /* FactCategoryViewModel.swift */; }; 1E32758F2532A2C0007E838A /* EmptyListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E32758E2532A2C0007E838A /* EmptyListView.swift */; }; @@ -45,6 +46,7 @@ 1EE0713F25314AF500F6BF6D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE0713E25314AF500F6BF6D /* SceneDelegate.swift */; }; 1EE0714625314AF600F6BF6D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1EE0714525314AF600F6BF6D /* Assets.xcassets */; }; 1EE0714925314AF600F6BF6D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1EE0714725314AF600F6BF6D /* LaunchScreen.storyboard */; }; + 1EF0DA1425449898005CF7E2 /* CategoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EF0DA1325449898005CF7E2 /* CategoryView.swift */; }; 1EFE2867253206D3008806B9 /* BaseCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE2866253206D3008806B9 /* BaseCoordinator.swift */; }; 1EFE2869253207B2008806B9 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE2868253207B2008806B9 /* AppCoordinator.swift */; }; 1EFE287E25321071008806B9 /* search-facts.json in Resources */ = {isa = PBXBuildFile; fileRef = 1EFE287D25321071008806B9 /* search-facts.json */; }; @@ -80,6 +82,7 @@ /* Begin PBXFileReference section */ 02186229E02F0A408D2EE0C2 /* Pods_Chuck_Norris_Facts.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Chuck_Norris_Facts.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1E0C7B3F2543A4E8002D5C47 /* FactEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactEntity.swift; sourceTree = ""; }; 1E23683D253FB07200BE17F3 /* FactCategoryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactCategoryCell.swift; sourceTree = ""; }; 1E23683F253FB13A00BE17F3 /* FactCategoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactCategoryViewModel.swift; sourceTree = ""; }; 1E32758E2532A2C0007E838A /* EmptyListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyListView.swift; sourceTree = ""; }; @@ -124,6 +127,7 @@ 1EE0715525314AF600F6BF6D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 1EE0715A25314AF600F6BF6D /* Chuck Norris FactsUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Chuck Norris FactsUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 1EE0716025314AF600F6BF6D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 1EF0DA1325449898005CF7E2 /* CategoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryView.swift; sourceTree = ""; }; 1EFE2866253206D3008806B9 /* BaseCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseCoordinator.swift; sourceTree = ""; }; 1EFE2868253207B2008806B9 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; 1EFE287D25321071008806B9 /* search-facts.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "search-facts.json"; sourceTree = ""; }; @@ -187,6 +191,7 @@ isa = PBXGroup; children = ( 1E32758E2532A2C0007E838A /* EmptyListView.swift */, + 1EF0DA1325449898005CF7E2 /* CategoryView.swift */, ); path = Views; sourceTree = ""; @@ -300,6 +305,7 @@ isa = PBXGroup; children = ( 1E92113D253F915100DB340B /* FactCategoryEntity.swift */, + 1E0C7B3F2543A4E8002D5C47 /* FactEntity.swift */, ); path = Entities; sourceTree = ""; @@ -848,9 +854,11 @@ 1E463803253636160079D8E9 /* SearchFactsViewController.swift in Sources */, 1EFE289525321CD2008806B9 /* FactViewModel.swift in Sources */, 1E46380B25363FF40079D8E9 /* SearchFactsViewModel.swift in Sources */, + 1E0C7B402543A4E8002D5C47 /* FactEntity.swift in Sources */, 1E921139253F909700DB340B /* FactsStorage.swift in Sources */, 1E23683E253FB07200BE17F3 /* FactCategoryCell.swift in Sources */, 1E92113E253F915100DB340B /* FactCategoryEntity.swift in Sources */, + 1EF0DA1425449898005CF7E2 /* CategoryView.swift in Sources */, 1EAB20AF2540BEC400633382 /* FactCategoryViewFlowLayout.swift in Sources */, 1E92112D253F6D0000DB340B /* FactsService.swift in Sources */, 1EFE2892253214DB008806B9 /* FactsListViewModel.swift in Sources */, diff --git a/Chuck Norris Facts/Data/Models/Fact.swift b/Chuck Norris Facts/Data/Models/Fact.swift index 4618f76..0d114b9 100644 --- a/Chuck Norris Facts/Data/Models/Fact.swift +++ b/Chuck Norris Facts/Data/Models/Fact.swift @@ -13,4 +13,13 @@ struct Fact: Decodable { let value: String let url: String? let iconUrl: String + let categories: [FactCategory] + + init(id: String, value: String, url: String?, iconUrl: String, categories: [FactCategory]) { + self.id = id + self.value = value + self.url = url + self.iconUrl = iconUrl + self.categories = categories + } } diff --git a/Chuck Norris Facts/Data/Networking/FactsAPI.swift b/Chuck Norris Facts/Data/Networking/FactsAPI.swift index 3d40571..09be7df 100644 --- a/Chuck Norris Facts/Data/Networking/FactsAPI.swift +++ b/Chuck Norris Facts/Data/Networking/FactsAPI.swift @@ -54,8 +54,10 @@ extension FactsAPI: TargetType { if let data = try? Data.stub("get-categories") { return data } - default: - break + case .searchFacts: + if let data = try? Data.stub("search-facts") { + return data + } } return Data() diff --git a/Chuck Norris Facts/Data/Services/FactsService.swift b/Chuck Norris Facts/Data/Services/FactsService.swift index 8f9dbff..aac2015 100644 --- a/Chuck Norris Facts/Data/Services/FactsService.swift +++ b/Chuck Norris Facts/Data/Services/FactsService.swift @@ -10,9 +10,18 @@ import RxSwift import Moya protocol FactsServiceType { - func searchFacts(searchTerm: String) -> Observable<[Fact]> + + // Search Facts on Chuck Norris API + func searchFacts(searchTerm: String) -> Observable + + // Sync local stored Categories with Chuck Norris API Categories func syncCategories() -> Observable + + // Retrieve local stored Categories func retrieveCategories() -> Observable<[FactCategory]> + + // Retrieve local stored Facts + func retrieveFacts(searchTerm: String) -> Observable<[Fact]> } struct FactsService: FactsServiceType { @@ -25,12 +34,14 @@ struct FactsService: FactsServiceType { self.storage = storage } - func searchFacts(searchTerm: String) -> Observable<[Fact]> { + func searchFacts(searchTerm: String) -> Observable { provider.rx .request(.searchFacts(searchTerm: searchTerm)) .asObservable() .map(SearchFactsResponse.self, using: JSON.decoder) .map { $0.facts } + .map { self.storage.storeFacts($0) } + .map { () } } func syncCategories() -> Observable { @@ -46,4 +57,7 @@ struct FactsService: FactsServiceType { storage.retrieveCategories() } + func retrieveFacts(searchTerm: String) -> Observable<[Fact]> { + storage.retrieveFacts(searchTerm: searchTerm) + } } diff --git a/Chuck Norris Facts/Data/Storage/Entities/FactEntity.swift b/Chuck Norris Facts/Data/Storage/Entities/FactEntity.swift new file mode 100644 index 0000000..580858d --- /dev/null +++ b/Chuck Norris Facts/Data/Storage/Entities/FactEntity.swift @@ -0,0 +1,37 @@ +// +// FactEntity.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/23/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation +import RealmSwift + +class FactEntity: Object { + @objc dynamic var id = "" + @objc dynamic var url = "" + @objc dynamic var value = "" + @objc dynamic var iconUrl = "" + + let categories = List() + + override static func primaryKey() -> String? { + "id" + } + + convenience init(fact: Fact) { + self.init(value: [ + "id": fact.id, + "url": fact.url ?? "", + "value": fact.value, + "iconUrl": fact.iconUrl, + "categories": fact.categories.map(FactCategoryEntity.init) + ]) + } + + var item: Fact { + Fact(id: id, value: value, url: url, iconUrl: iconUrl, categories: categories.map { $0.item }) + } +} diff --git a/Chuck Norris Facts/Data/Storage/FactsStorage.swift b/Chuck Norris Facts/Data/Storage/FactsStorage.swift index 16a4b83..1c44729 100644 --- a/Chuck Norris Facts/Data/Storage/FactsStorage.swift +++ b/Chuck Norris Facts/Data/Storage/FactsStorage.swift @@ -15,6 +15,10 @@ protocol FactsStorageType { func storeCategories(_ categories: [FactCategory]) func retrieveCategories() -> Observable<[FactCategory]> + + func storeFacts(_ facts: [Fact]) + + func retrieveFacts(searchTerm: String) -> Observable<[Fact]> } final class FactsStorage: FactsStorageType { @@ -35,4 +39,21 @@ final class FactsStorage: FactsStorageType { let entities = realm.objects(FactCategoryEntity.self) return Observable.collection(from: entities).map { $0.map { $0.item } } } + + func storeFacts(_ facts: [Fact]) { + try? realm.write { + let entities = facts.map(FactEntity.init) + self.realm.add(entities, update: .modified) + } + } + + func retrieveFacts(searchTerm: String) -> Observable<[Fact]> { + var entities = realm.objects(FactEntity.self) + + if !searchTerm.isEmpty { + entities = entities.filter("value CONTAINS %@", searchTerm) + } + + return Observable.collection(from: entities).map { $0.map { $0.item }} + } } diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/Cells/FactTableViewCell.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/Cells/FactTableViewCell.swift index 41142af..824b7f5 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/Cells/FactTableViewCell.swift +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/Cells/FactTableViewCell.swift @@ -12,6 +12,8 @@ class FactTableViewCell: UITableViewCell { static let cellIdentifier = "FactTableViewCell" + private let categoryView = CategoryView() + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupView() @@ -65,6 +67,7 @@ class FactTableViewCell: UITableViewCell { shadowView.addSubview(bodyLabel) shadowView.addSubview(shareButton) + shadowView.addSubview(categoryView) shadowView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16).isActive = true shadowView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16).isActive = true @@ -75,9 +78,13 @@ class FactTableViewCell: UITableViewCell { bodyLabel.leadingAnchor.constraint(equalTo: shadowView.leadingAnchor, constant: 16).isActive = true bodyLabel.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor, constant: -16).isActive = true - shareButton.topAnchor.constraint(equalTo: bodyLabel.bottomAnchor).isActive = true + shareButton.topAnchor.constraint(equalTo: bodyLabel.bottomAnchor, constant: 16).isActive = true shareButton.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor, constant: -16).isActive = true shareButton.bottomAnchor.constraint(equalTo: shadowView.bottomAnchor, constant: -16).isActive = true + + categoryView.translatesAutoresizingMaskIntoConstraints = false + categoryView.centerYAnchor.constraint(equalTo: shareButton.centerYAnchor).isActive = true + categoryView.leftAnchor.constraint(equalTo: shadowView.leftAnchor, constant: 16).isActive = true } func setup(_ fact: FactViewModel) { @@ -85,5 +92,7 @@ class FactTableViewCell: UITableViewCell { let fontSize = fact.text.count > 80 ? 16 : 24 bodyLabel.font = .systemFont(ofSize: CGFloat(fontSize), weight: .bold) + + categoryView.label.text = fact.category } } diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/Cells/FactViewModel.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/Cells/FactViewModel.swift index b00ebd7..973d651 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/Cells/FactViewModel.swift +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/Cells/FactViewModel.swift @@ -12,12 +12,14 @@ import RxDataSources final class FactViewModel { let text: String var url: URL? + let category: String init(fact: Fact) { self.text = fact.value if let factUrl = fact.url { self.url = URL(string: factUrl) } + self.category = fact.categories.first?.text.uppercased() ?? "UNCATEGORIZED" } } @@ -27,7 +29,8 @@ extension FactViewModel: IdentifiableType { } } -extension FactViewModel: Equatable { } -func == (lhs: FactViewModel, rhs: FactViewModel) -> Bool { - return lhs.text == rhs.text +extension FactViewModel: Equatable { + static func == (lhs: FactViewModel, rhs: FactViewModel) -> Bool { + return lhs.text == rhs.text + } } diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift index 6944601..933b06f 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift @@ -43,6 +43,7 @@ class FactsListViewController: UIViewController { private lazy var loadingView: AnimationView = { let loading = AnimationView() + loading.backgroundColor = .systemBackground loading.animation = Animation.named("loading") loading.loopMode = .loop @@ -55,8 +56,8 @@ class FactsListViewController: UIViewController { setupView() setupBindings() setupTableView() - setupLoadingView() setupEmptyListView() + setupLoadingView() setupNavigationBar() } @@ -84,10 +85,10 @@ class FactsListViewController: UIViewController { view.addSubview(loadingView) loadingView.translatesAutoresizingMaskIntoConstraints = false - loadingView.widthAnchor.constraint(equalToConstant: 50).isActive = true - loadingView.heightAnchor.constraint(equalToConstant: 50).isActive = true - loadingView.centerXAnchor.constraint(equalTo: tableView.centerXAnchor).isActive = true - loadingView.centerYAnchor.constraint(equalTo: tableView.centerYAnchor).isActive = true + loadingView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true + loadingView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true + loadingView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + loadingView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true } private func setupEmptyListView() { diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift index 86684ff..97c92b1 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift @@ -63,6 +63,14 @@ final class FactsListViewModel { factsService.syncCategories() } + _ = searchTermSubject.asObservable() + .filter { !$0.isEmpty } + .flatMapLatest { searchTerm -> Observable in + return factsService.searchFacts(searchTerm: searchTerm) + .trackActivity(loadingIndicator) + } + .subscribe(onNext: {}) + self.facts = Observable.combineLatest(viewDidAppearSubject, searchTermSubject) .flatMapLatest { _, searchTerm -> Observable<[Fact]> in if CommandLine.arguments.contains("--search-facts") { @@ -74,15 +82,20 @@ final class FactsListViewModel { let data = try Data(contentsOf: url) let stub = try JSON.decoder.decode(SearchFactsResponse.self, from: data) - return .just(stub.facts) + let facts = stub.facts.shuffled().prefix(10) + return .just(Array(facts)) } - if !searchTerm.isEmpty { - return factsService.searchFacts(searchTerm: searchTerm) - .trackActivity(loadingIndicator) + + if CommandLine.arguments.contains("--empty-facts") { + return .just([]) + } + + let facts = factsService.retrieveFacts(searchTerm: searchTerm) + if searchTerm.isEmpty { + return facts.map { Array($0.shuffled().prefix(10)) } } - return .just([]) + return facts } - .map { Array($0.shuffled().prefix(10)) } .map { $0.map { FactViewModel(fact: $0) } } .map { [FactsSectionModel(model: "", items: $0)] } } diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/Views/CategoryView.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/Views/CategoryView.swift new file mode 100644 index 0000000..cd3593e --- /dev/null +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/Views/CategoryView.swift @@ -0,0 +1,47 @@ +// +// CategoryView.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/24/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import UIKit + +class CategoryView: UIView { + + lazy var label: UILabel = { + let label = UILabel() + + label.translatesAutoresizingMaskIntoConstraints = false + label.lineBreakMode = .byTruncatingTail + label.font = .systemFont(ofSize: 16, weight: .bold) + label.textColor = .white + label.numberOfLines = 1 + + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + setupView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setupView() { + layer.cornerRadius = 4 + backgroundColor = .systemBlue + + addSubview(label) + + label.translatesAutoresizingMaskIntoConstraints = false + label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 4).isActive = true + label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -4).isActive = true + label.topAnchor.constraint(equalTo: topAnchor, constant: 4).isActive = true + label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4).isActive = true + } +} diff --git a/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift b/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift index 5ee342a..3554f9d 100644 --- a/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift +++ b/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift @@ -69,4 +69,29 @@ final class FactsServiceTests: XCTestCase { let savedCategories = try storedCategories.toBlocking().first() XCTAssertEqual(savedCategories?.count, stubCategories.count) } + + func test_searchFactsShouldSaveFactsOnStorage() throws { + let storedFacts = factsStorage.retrieveFacts(searchTerm: "") + let facts = try storedFacts.toBlocking().first() ?? [] + XCTAssertTrue(facts.isEmpty) + + factsService.searchFacts(searchTerm: "") + .subscribe() + .disposed(by: disposeBag) + + let savedFacts = try storedFacts.toBlocking().first() + XCTAssertEqual(savedFacts?.count, 16) + } + + func test_retrieveFactsShouldReturnFactsOnStorage() throws { + let storedFacts = factsStorage.retrieveFacts(searchTerm: "") + let facts = try storedFacts.toBlocking().first() ?? [] + XCTAssertTrue(facts.isEmpty) + + let stubFacts = try stub("facts-list", type: [Fact].self) ?? [] + factsStorage.storeFacts(stubFacts) + + let savedFacts = try storedFacts.toBlocking().first() + XCTAssertEqual(savedFacts?.count, stubFacts.count) + } } diff --git a/Chuck Norris FactsTests/Mocks/FactsServiceMock.swift b/Chuck Norris FactsTests/Mocks/FactsServiceMock.swift index c255905..ac07089 100644 --- a/Chuck Norris FactsTests/Mocks/FactsServiceMock.swift +++ b/Chuck Norris FactsTests/Mocks/FactsServiceMock.swift @@ -23,8 +23,13 @@ final class FactsServiceMock: FactsServiceType { return retrieveCategoriesReturnValue } - var searchFactsReturnValue: Observable<[Fact]> = .just([]) - func searchFacts(searchTerm: String) -> Observable<[Fact]> { + var searchFactsReturnValue: Observable = .just(()) + func searchFacts(searchTerm: String) -> Observable { return searchFactsReturnValue } + + var retrieveFactsReturnValue: Observable<[Fact]> = .just([]) + func retrieveFacts(searchTerm: String) -> Observable<[Fact]> { + return retrieveFactsReturnValue + } } diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift index e7156a5..7f9e3e4 100644 --- a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift +++ b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift @@ -38,7 +38,7 @@ class FactsListViewControllerTests: XCTestCase { } func test_factsListEmptyShouldShowEmptyList() { - factsServiceMock.searchFactsReturnValue = .just([]) + factsServiceMock.retrieveFactsReturnValue = .just([]) factsListViewModel.viewDidAppear.onNext(()) @@ -50,9 +50,8 @@ class FactsListViewControllerTests: XCTestCase { let factStub = try stub("short-fact", type: Fact.self) let fact = try XCTUnwrap(factStub, "looks like short-fact.json doesn't exists") - factsServiceMock.searchFactsReturnValue = .just([fact]) + factsServiceMock.retrieveFactsReturnValue = .just([fact]) - factsListViewModel.setSearchTerm.onNext("games") factsListViewModel.viewDidAppear.onNext(()) let factCell = factsListFirstCell() @@ -64,9 +63,8 @@ class FactsListViewControllerTests: XCTestCase { let factStub = try stub("long-fact", type: Fact.self) let fact = try XCTUnwrap(factStub, "looks like long-fact.json doesn't exists") - factsServiceMock.searchFactsReturnValue = .just([fact]) + factsServiceMock.retrieveFactsReturnValue = .just([fact]) - factsListViewModel.setSearchTerm.onNext("games") factsListViewModel.viewDidAppear.onNext(()) let factCell = factsListFirstCell() @@ -78,7 +76,7 @@ class FactsListViewControllerTests: XCTestCase { let factStub = try stub("short-fact", type: Fact.self) let fact = try XCTUnwrap(factStub, "looks like short-fact.json doesn't exists") - factsServiceMock.searchFactsReturnValue = .just([fact]) + factsServiceMock.retrieveFactsReturnValue = .just([fact]) let testScheduler = TestScheduler(initialClock: 0) let shareFactObserver = testScheduler.createObserver(FactViewModel.self) diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift index e464cab..6ae4fc3 100644 --- a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift +++ b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift @@ -37,7 +37,7 @@ class FactsListViewModelTests: XCTestCase { func test_load10RandomFacts() throws { let factsListStub = try stub("facts-list", type: [Fact].self) let factsList = try XCTUnwrap(factsListStub, "looks like facts-list.json doesn't exists") - factsServiceMock.searchFactsReturnValue = .just(factsList) + factsServiceMock.retrieveFactsReturnValue = .just(factsList) let factsObserver = testScheduler.createObserver([FactsSectionModel].self) @@ -45,7 +45,6 @@ class FactsListViewModelTests: XCTestCase { .subscribe(factsObserver) .disposed(by: disposeBag) - factsListViewModel.setSearchTerm.onNext("games") factsListViewModel.viewDidAppear.onNext(()) testScheduler.start() diff --git a/Chuck Norris FactsTests/Stubs/long-fact.json b/Chuck Norris FactsTests/Stubs/long-fact.json index 9c93886..1f72c25 100644 --- a/Chuck Norris FactsTests/Stubs/long-fact.json +++ b/Chuck Norris FactsTests/Stubs/long-fact.json @@ -1,4 +1,5 @@ { + "categories": [], "icon_url" : "https://assets.chucknorris.host/img/avatar/chuck-norris.png", "id" : "irY3YudqS1qXxhfWxw12NQ", "url" : "", diff --git a/Chuck Norris FactsTests/Stubs/short-fact.json b/Chuck Norris FactsTests/Stubs/short-fact.json index 70c6d93..aa3f8bc 100644 --- a/Chuck Norris FactsTests/Stubs/short-fact.json +++ b/Chuck Norris FactsTests/Stubs/short-fact.json @@ -1,4 +1,5 @@ { + "categories": [], "icon_url" : "https://assets.chucknorris.host/img/avatar/chuck-norris.png", "id" : "gusqVaYoSMKnJ3KKzca3GQ", "url" : "", diff --git a/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift b/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift index c3a2c8d..01a6a13 100644 --- a/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift +++ b/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift @@ -32,7 +32,7 @@ final class SearchFactsUITests: XCTestCase { sleep(5) - XCTAssertEqual(factsListScene.factsTableView.cells.count, 10) + XCTAssertEqual(factsListScene.factsTableView.cells.count, 15) } func test_cancelSearchFacts() throws { From fb2e71952bcf54603945f378aa50d02960a3d230 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Tue, 27 Oct 2020 22:27:52 -0300 Subject: [PATCH 08/18] Store past searches (#19) * Past Searches List * Store Past Searches * Fact Categories as a TableView cell in section * Move datasource to controller * onSelect category * Rename FactCategories -> Suggestions * Improve SuggestionsCell DataSource * Fix Suggestions Glitch * Don't hide searchBar when scrolling * Hide suggestions if there is none * Store searches together facts as related models * Fix Loading Size * Set Debug Scheme mode * Adjust current tests * Unit and UI tests of Store Past Searches * Set delegate by Rx * Setup delegate as nil before set * Don't reuse suggestions cell --- Chuck Norris Facts.xcodeproj/project.pbxproj | 80 ++++++++++--- .../xcschemes/Chuck Norris Facts.xcscheme | 2 +- Chuck Norris Facts/App/AppDelegate.swift | 10 +- .../Data/Services/FactsService.swift | 9 +- .../Data/Storage/Entities/FactEntity.swift | 2 + .../Data/Storage/Entities/SearchEntity.swift | 28 +++++ .../Data/Storage/FactsStorage.swift | 24 +++- .../Library/DynamicHeightCollectionView.swift | 23 ++++ .../FactCell.swift} | 6 +- .../{Cells => Fact}/FactViewModel.swift | 0 .../FactsList/FactsListViewController.swift | 17 +-- .../Facts/FactsList/Views/LoadingView.swift | 51 +++++++++ .../PastSearch/PastSearchCell.swift | 19 ++++ .../PastSearch/PastSearchViewModel.swift | 26 +++++ .../SearchFactsTableViewSection.swift | 56 ++++++++++ .../SearchFactsViewController.swift | 101 ++++++++++------- .../SearchFacts/SearchFactsViewModel.swift | 29 ++++- .../FactCategory}/FactCategoryCell.swift | 4 +- .../FactCategory}/FactCategoryViewModel.swift | 0 .../Suggestions/SuggestionsCell.swift | 105 ++++++++++++++++++ .../SuggestionsViewFlowLayout.swift} | 4 +- .../Suggestions/SuggestionsViewModel.swift | 41 +++++++ .../Data/Services/FactsServiceTests.swift | 20 ++++ .../Mocks/FactsServiceMock.swift | 5 + .../FactsListViewControllerTests.swift | 7 +- .../SearchFactsViewControllerTests.swift | 34 +++++- .../SearchFactsViewModelTests.swift | 29 ++++- .../Scenes/SearchFactsScene.swift | 12 +- .../Tests/SearchFactsUITests.swift | 73 +++++++++++- 29 files changed, 714 insertions(+), 103 deletions(-) create mode 100644 Chuck Norris Facts/Data/Storage/Entities/SearchEntity.swift create mode 100644 Chuck Norris Facts/Library/DynamicHeightCollectionView.swift rename Chuck Norris Facts/Scenes/Facts/FactsList/{Cells/FactTableViewCell.swift => Fact/FactCell.swift} (97%) rename Chuck Norris Facts/Scenes/Facts/FactsList/{Cells => Fact}/FactViewModel.swift (100%) create mode 100644 Chuck Norris Facts/Scenes/Facts/FactsList/Views/LoadingView.swift create mode 100644 Chuck Norris Facts/Scenes/Facts/SearchFacts/PastSearch/PastSearchCell.swift create mode 100644 Chuck Norris Facts/Scenes/Facts/SearchFacts/PastSearch/PastSearchViewModel.swift create mode 100644 Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsTableViewSection.swift rename Chuck Norris Facts/Scenes/Facts/SearchFacts/{Cells => Suggestions/FactCategory}/FactCategoryCell.swift (94%) rename Chuck Norris Facts/Scenes/Facts/SearchFacts/{Cells => Suggestions/FactCategory}/FactCategoryViewModel.swift (100%) create mode 100644 Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/SuggestionsCell.swift rename Chuck Norris Facts/Scenes/Facts/SearchFacts/{Cells/FactCategoryViewFlowLayout.swift => Suggestions/SuggestionsViewFlowLayout.swift} (89%) create mode 100644 Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/SuggestionsViewModel.swift diff --git a/Chuck Norris Facts.xcodeproj/project.pbxproj b/Chuck Norris Facts.xcodeproj/project.pbxproj index 53635d8..83e5166 100644 --- a/Chuck Norris Facts.xcodeproj/project.pbxproj +++ b/Chuck Norris Facts.xcodeproj/project.pbxproj @@ -22,6 +22,11 @@ 1E7F15BE253324CF0006887B /* FactsListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7F15BD253324CF0006887B /* FactsListViewModelTests.swift */; }; 1E7F15C0253324FD0006887B /* FactsListViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7F15BF253324FD0006887B /* FactsListViewControllerTests.swift */; }; 1E7F15C6253329780006887B /* FactViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7F15C5253329780006887B /* FactViewModelTests.swift */; }; + 1E8A0FEC25475B0800565A86 /* SearchFactsTableViewSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E8A0FEB25475B0800565A86 /* SearchFactsTableViewSection.swift */; }; + 1E8A0FEE2547603700565A86 /* SuggestionsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E8A0FED2547603700565A86 /* SuggestionsCell.swift */; }; + 1E8A0FF0254760D400565A86 /* SuggestionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E8A0FEF254760D400565A86 /* SuggestionsViewModel.swift */; }; + 1E8A0FF42547768500565A86 /* DynamicHeightCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E8A0FF32547768500565A86 /* DynamicHeightCollectionView.swift */; }; + 1E8AF33A254793D800BBB808 /* PastSearchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E8AF339254793D800BBB808 /* PastSearchCell.swift */; }; 1E92112A253F6BB700DB340B /* SearchFactsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E921129253F6BB700DB340B /* SearchFactsResponse.swift */; }; 1E92112D253F6D0000DB340B /* FactsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E92112C253F6D0000DB340B /* FactsService.swift */; }; 1E92112F253F7A0B00DB340B /* SearchFactsScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E92112E253F7A0B00DB340B /* SearchFactsScene.swift */; }; @@ -30,9 +35,10 @@ 1E921139253F909700DB340B /* FactsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E921138253F909700DB340B /* FactsStorage.swift */; }; 1E92113B253F90BF00DB340B /* FactCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E92113A253F90BF00DB340B /* FactCategory.swift */; }; 1E92113E253F915100DB340B /* FactCategoryEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E92113D253F915100DB340B /* FactCategoryEntity.swift */; }; - 1EAB20AF2540BEC400633382 /* FactCategoryViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAB20AE2540BEC400633382 /* FactCategoryViewFlowLayout.swift */; }; + 1EAB20AF2540BEC400633382 /* SuggestionsViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAB20AE2540BEC400633382 /* SuggestionsViewFlowLayout.swift */; }; 1EACEC99253649BD0006B36D /* loading.json in Resources */ = {isa = PBXBuildFile; fileRef = 1EACEC98253649BD0006B36D /* loading.json */; }; 1EACEC9E25364B6C0006B36D /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EACEC9D25364B6C0006B36D /* ActivityIndicator.swift */; }; + 1ED06C952548AAD300139151 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED06C942548AAD300139151 /* LoadingView.swift */; }; 1ED5D18D25348FD50035046C /* XCTestCase+Stub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED5D18C25348FD50035046C /* XCTestCase+Stub.swift */; }; 1ED5D19025349E9D0035046C /* UIViewController+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED5D18F25349E9D0035046C /* UIViewController+Rx.swift */; }; 1ED5D1932534A56F0035046C /* FactsServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED5D1922534A56F0035046C /* FactsServiceMock.swift */; }; @@ -46,6 +52,8 @@ 1EE0713F25314AF500F6BF6D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE0713E25314AF500F6BF6D /* SceneDelegate.swift */; }; 1EE0714625314AF600F6BF6D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1EE0714525314AF600F6BF6D /* Assets.xcassets */; }; 1EE0714925314AF600F6BF6D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1EE0714725314AF600F6BF6D /* LaunchScreen.storyboard */; }; + 1EF066E32545CE1D00ECF611 /* PastSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EF066E22545CE1D00ECF611 /* PastSearchViewModel.swift */; }; + 1EF066E52545CEC200ECF611 /* SearchEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EF066E42545CEC200ECF611 /* SearchEntity.swift */; }; 1EF0DA1425449898005CF7E2 /* CategoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EF0DA1325449898005CF7E2 /* CategoryView.swift */; }; 1EFE2867253206D3008806B9 /* BaseCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE2866253206D3008806B9 /* BaseCoordinator.swift */; }; 1EFE2869253207B2008806B9 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE2868253207B2008806B9 /* AppCoordinator.swift */; }; @@ -57,7 +65,7 @@ 1EFE28902532137C008806B9 /* FactsListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE288F2532137C008806B9 /* FactsListCoordinator.swift */; }; 1EFE2892253214DB008806B9 /* FactsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE2891253214DB008806B9 /* FactsListViewModel.swift */; }; 1EFE289525321CD2008806B9 /* FactViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE289425321CD2008806B9 /* FactViewModel.swift */; }; - 1EFE289725321CE2008806B9 /* FactTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE289625321CE2008806B9 /* FactTableViewCell.swift */; }; + 1EFE289725321CE2008806B9 /* FactCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EFE289625321CE2008806B9 /* FactCell.swift */; }; 54AACBFAFA5E5798DDE49218 /* Pods_Chuck_Norris_Facts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 02186229E02F0A408D2EE0C2 /* Pods_Chuck_Norris_Facts.framework */; }; D5197BBE6E28E81FC84E5C48 /* Pods_Chuck_Norris_FactsTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EF9A8C577644798596588861 /* Pods_Chuck_Norris_FactsTests.framework */; }; E08677EA93CEAFF961473756 /* Pods_Chuck_Norris_Facts_Chuck_Norris_FactsUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7C1F7C697FAAC85D6A1F91F4 /* Pods_Chuck_Norris_Facts_Chuck_Norris_FactsUITests.framework */; }; @@ -97,6 +105,11 @@ 1E7F15BD253324CF0006887B /* FactsListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListViewModelTests.swift; sourceTree = ""; }; 1E7F15BF253324FD0006887B /* FactsListViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListViewControllerTests.swift; sourceTree = ""; }; 1E7F15C5253329780006887B /* FactViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactViewModelTests.swift; sourceTree = ""; }; + 1E8A0FEB25475B0800565A86 /* SearchFactsTableViewSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFactsTableViewSection.swift; sourceTree = ""; }; + 1E8A0FED2547603700565A86 /* SuggestionsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionsCell.swift; sourceTree = ""; }; + 1E8A0FEF254760D400565A86 /* SuggestionsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionsViewModel.swift; sourceTree = ""; }; + 1E8A0FF32547768500565A86 /* DynamicHeightCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicHeightCollectionView.swift; sourceTree = ""; }; + 1E8AF339254793D800BBB808 /* PastSearchCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PastSearchCell.swift; sourceTree = ""; }; 1E921129253F6BB700DB340B /* SearchFactsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFactsResponse.swift; sourceTree = ""; }; 1E92112C253F6D0000DB340B /* FactsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsService.swift; sourceTree = ""; }; 1E92112E253F7A0B00DB340B /* SearchFactsScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFactsScene.swift; sourceTree = ""; }; @@ -105,9 +118,10 @@ 1E921138253F909700DB340B /* FactsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsStorage.swift; sourceTree = ""; }; 1E92113A253F90BF00DB340B /* FactCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactCategory.swift; sourceTree = ""; }; 1E92113D253F915100DB340B /* FactCategoryEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactCategoryEntity.swift; sourceTree = ""; }; - 1EAB20AE2540BEC400633382 /* FactCategoryViewFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactCategoryViewFlowLayout.swift; sourceTree = ""; }; + 1EAB20AE2540BEC400633382 /* SuggestionsViewFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionsViewFlowLayout.swift; sourceTree = ""; }; 1EACEC98253649BD0006B36D /* loading.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = loading.json; sourceTree = ""; }; 1EACEC9D25364B6C0006B36D /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = ""; }; + 1ED06C942548AAD300139151 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; 1ED5D18C25348FD50035046C /* XCTestCase+Stub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+Stub.swift"; sourceTree = ""; }; 1ED5D18F25349E9D0035046C /* UIViewController+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Rx.swift"; sourceTree = ""; }; 1ED5D1922534A56F0035046C /* FactsServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsServiceMock.swift; sourceTree = ""; }; @@ -127,6 +141,8 @@ 1EE0715525314AF600F6BF6D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 1EE0715A25314AF600F6BF6D /* Chuck Norris FactsUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Chuck Norris FactsUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 1EE0716025314AF600F6BF6D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 1EF066E22545CE1D00ECF611 /* PastSearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PastSearchViewModel.swift; sourceTree = ""; }; + 1EF066E42545CEC200ECF611 /* SearchEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchEntity.swift; sourceTree = ""; }; 1EF0DA1325449898005CF7E2 /* CategoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryView.swift; sourceTree = ""; }; 1EFE2866253206D3008806B9 /* BaseCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseCoordinator.swift; sourceTree = ""; }; 1EFE2868253207B2008806B9 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; @@ -138,7 +154,7 @@ 1EFE288F2532137C008806B9 /* FactsListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListCoordinator.swift; sourceTree = ""; }; 1EFE2891253214DB008806B9 /* FactsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListViewModel.swift; sourceTree = ""; }; 1EFE289425321CD2008806B9 /* FactViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactViewModel.swift; sourceTree = ""; }; - 1EFE289625321CE2008806B9 /* FactTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactTableViewCell.swift; sourceTree = ""; }; + 1EFE289625321CE2008806B9 /* FactCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactCell.swift; sourceTree = ""; }; 21EE31E3903E76B09B8FBA96 /* Pods-Chuck Norris Facts-Chuck Norris FactsUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Chuck Norris Facts-Chuck Norris FactsUITests.debug.xcconfig"; path = "Target Support Files/Pods-Chuck Norris Facts-Chuck Norris FactsUITests/Pods-Chuck Norris Facts-Chuck Norris FactsUITests.debug.xcconfig"; sourceTree = ""; }; 5BFEC696AC24CC9BF87C5195 /* Pods-Chuck Norris FactsTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Chuck Norris FactsTests.release.xcconfig"; path = "Target Support Files/Pods-Chuck Norris FactsTests/Pods-Chuck Norris FactsTests.release.xcconfig"; sourceTree = ""; }; 74304E6C0D317767335DC2AB /* Pods-Chuck Norris Facts.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Chuck Norris Facts.release.xcconfig"; path = "Target Support Files/Pods-Chuck Norris Facts/Pods-Chuck Norris Facts.release.xcconfig"; sourceTree = ""; }; @@ -177,14 +193,13 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 1E23683C253FB05100BE17F3 /* Cells */ = { + 1E23683C253FB05100BE17F3 /* PastSearch */ = { isa = PBXGroup; children = ( - 1E23683D253FB07200BE17F3 /* FactCategoryCell.swift */, - 1E23683F253FB13A00BE17F3 /* FactCategoryViewModel.swift */, - 1EAB20AE2540BEC400633382 /* FactCategoryViewFlowLayout.swift */, + 1E8AF339254793D800BBB808 /* PastSearchCell.swift */, + 1EF066E22545CE1D00ECF611 /* PastSearchViewModel.swift */, ); - path = Cells; + path = PastSearch; sourceTree = ""; }; 1E32758D2532A2A3007E838A /* Views */ = { @@ -192,6 +207,7 @@ children = ( 1E32758E2532A2C0007E838A /* EmptyListView.swift */, 1EF0DA1325449898005CF7E2 /* CategoryView.swift */, + 1ED06C942548AAD300139151 /* LoadingView.swift */, ); path = Views; sourceTree = ""; @@ -208,10 +224,12 @@ 1E463801253636050079D8E9 /* SearchFacts */ = { isa = PBXGroup; children = ( - 1E23683C253FB05100BE17F3 /* Cells */, + 1E8AF338254792E500BBB808 /* Suggestions */, + 1E23683C253FB05100BE17F3 /* PastSearch */, 1E463802253636160079D8E9 /* SearchFactsViewController.swift */, 1E463804253636D80079D8E9 /* SearchFactsCoordinator.swift */, 1E46380A25363FF40079D8E9 /* SearchFactsViewModel.swift */, + 1E8A0FEB25475B0800565A86 /* SearchFactsTableViewSection.swift */, ); path = SearchFacts; sourceTree = ""; @@ -267,6 +285,26 @@ path = Cells; sourceTree = ""; }; + 1E8AF337254792DB00BBB808 /* FactCategory */ = { + isa = PBXGroup; + children = ( + 1E23683D253FB07200BE17F3 /* FactCategoryCell.swift */, + 1E23683F253FB13A00BE17F3 /* FactCategoryViewModel.swift */, + ); + path = FactCategory; + sourceTree = ""; + }; + 1E8AF338254792E500BBB808 /* Suggestions */ = { + isa = PBXGroup; + children = ( + 1E8A0FED2547603700565A86 /* SuggestionsCell.swift */, + 1E8A0FEF254760D400565A86 /* SuggestionsViewModel.swift */, + 1EAB20AE2540BEC400633382 /* SuggestionsViewFlowLayout.swift */, + 1E8AF337254792DB00BBB808 /* FactCategory */, + ); + path = Suggestions; + sourceTree = ""; + }; 1E921128253F6BA500DB340B /* Responses */ = { isa = PBXGroup; children = ( @@ -306,6 +344,7 @@ children = ( 1E92113D253F915100DB340B /* FactCategoryEntity.swift */, 1E0C7B3F2543A4E8002D5C47 /* FactEntity.swift */, + 1EF066E42545CEC200ECF611 /* SearchEntity.swift */, ); path = Entities; sourceTree = ""; @@ -458,6 +497,7 @@ 1EFE2866253206D3008806B9 /* BaseCoordinator.swift */, 1EFE288825321123008806B9 /* JSON.swift */, 1EACEC9D25364B6C0006B36D /* ActivityIndicator.swift */, + 1E8A0FF32547768500565A86 /* DynamicHeightCollectionView.swift */, ); path = Library; sourceTree = ""; @@ -513,7 +553,7 @@ isa = PBXGroup; children = ( 1E32758D2532A2A3007E838A /* Views */, - 1EFE289325321CB4008806B9 /* Cells */, + 1EFE289325321CB4008806B9 /* Fact */, 1EFE288D2532135B008806B9 /* FactsListViewController.swift */, 1EFE288F2532137C008806B9 /* FactsListCoordinator.swift */, 1EFE2891253214DB008806B9 /* FactsListViewModel.swift */, @@ -521,13 +561,13 @@ path = FactsList; sourceTree = ""; }; - 1EFE289325321CB4008806B9 /* Cells */ = { + 1EFE289325321CB4008806B9 /* Fact */ = { isa = PBXGroup; children = ( + 1EFE289625321CE2008806B9 /* FactCell.swift */, 1EFE289425321CD2008806B9 /* FactViewModel.swift */, - 1EFE289625321CE2008806B9 /* FactTableViewCell.swift */, ); - path = Cells; + path = Fact; sourceTree = ""; }; 4AEC7A1E4DDCD345F1E9B6DA /* Pods */ = { @@ -838,18 +878,26 @@ files = ( 1EFE2884253210B2008806B9 /* FactsAPI.swift in Sources */, 1E32758F2532A2C0007E838A /* EmptyListView.swift in Sources */, + 1E8A0FF0254760D400565A86 /* SuggestionsViewModel.swift in Sources */, 1E463805253636D80079D8E9 /* SearchFactsCoordinator.swift in Sources */, 1EFE288925321123008806B9 /* JSON.swift in Sources */, + 1ED06C952548AAD300139151 /* LoadingView.swift in Sources */, 1EFE288725321119008806B9 /* Fact.swift in Sources */, - 1EFE289725321CE2008806B9 /* FactTableViewCell.swift in Sources */, + 1EFE289725321CE2008806B9 /* FactCell.swift in Sources */, 1E56172C2541007500BF26A0 /* Data+Stub.swift in Sources */, + 1E8A0FEE2547603700565A86 /* SuggestionsCell.swift in Sources */, + 1E8AF33A254793D800BBB808 /* PastSearchCell.swift in Sources */, 1EE0713D25314AF500F6BF6D /* AppDelegate.swift in Sources */, + 1E8A0FEC25475B0800565A86 /* SearchFactsTableViewSection.swift in Sources */, + 1EF066E52545CEC200ECF611 /* SearchEntity.swift in Sources */, 1ED5D19025349E9D0035046C /* UIViewController+Rx.swift in Sources */, 1E236840253FB13A00BE17F3 /* FactCategoryViewModel.swift in Sources */, 1EFE2869253207B2008806B9 /* AppCoordinator.swift in Sources */, 1E92112A253F6BB700DB340B /* SearchFactsResponse.swift in Sources */, 1EE0713F25314AF500F6BF6D /* SceneDelegate.swift in Sources */, + 1E8A0FF42547768500565A86 /* DynamicHeightCollectionView.swift in Sources */, 1EFE288E2532135B008806B9 /* FactsListViewController.swift in Sources */, + 1EF066E32545CE1D00ECF611 /* PastSearchViewModel.swift in Sources */, 1EFE28902532137C008806B9 /* FactsListCoordinator.swift in Sources */, 1E463803253636160079D8E9 /* SearchFactsViewController.swift in Sources */, 1EFE289525321CD2008806B9 /* FactViewModel.swift in Sources */, @@ -859,7 +907,7 @@ 1E23683E253FB07200BE17F3 /* FactCategoryCell.swift in Sources */, 1E92113E253F915100DB340B /* FactCategoryEntity.swift in Sources */, 1EF0DA1425449898005CF7E2 /* CategoryView.swift in Sources */, - 1EAB20AF2540BEC400633382 /* FactCategoryViewFlowLayout.swift in Sources */, + 1EAB20AF2540BEC400633382 /* SuggestionsViewFlowLayout.swift in Sources */, 1E92112D253F6D0000DB340B /* FactsService.swift in Sources */, 1EFE2892253214DB008806B9 /* FactsListViewModel.swift in Sources */, 1EACEC9E25364B6C0006B36D /* ActivityIndicator.swift in Sources */, diff --git a/Chuck Norris Facts.xcodeproj/xcshareddata/xcschemes/Chuck Norris Facts.xcscheme b/Chuck Norris Facts.xcodeproj/xcshareddata/xcschemes/Chuck Norris Facts.xcscheme index 235e7c1..8604fd7 100644 --- a/Chuck Norris Facts.xcodeproj/xcshareddata/xcschemes/Chuck Norris Facts.xcscheme +++ b/Chuck Norris Facts.xcodeproj/xcshareddata/xcschemes/Chuck Norris Facts.xcscheme @@ -51,7 +51,7 @@ Bool { - // Override point for customization after application launch. + + if CommandLine.arguments.contains("--reset-storage") { + let realm = try? Realm() + try? realm?.write { + realm?.deleteAll() + } + } + return true } diff --git a/Chuck Norris Facts/Data/Services/FactsService.swift b/Chuck Norris Facts/Data/Services/FactsService.swift index aac2015..f72b367 100644 --- a/Chuck Norris Facts/Data/Services/FactsService.swift +++ b/Chuck Norris Facts/Data/Services/FactsService.swift @@ -22,6 +22,9 @@ protocol FactsServiceType { // Retrieve local stored Facts func retrieveFacts(searchTerm: String) -> Observable<[Fact]> + + // Retrieve local stored Past Searches + func retrievePastSearches() -> Observable<[String]> } struct FactsService: FactsServiceType { @@ -40,7 +43,7 @@ struct FactsService: FactsServiceType { .asObservable() .map(SearchFactsResponse.self, using: JSON.decoder) .map { $0.facts } - .map { self.storage.storeFacts($0) } + .map { self.storage.storeSearch(searchTerm: searchTerm, facts: $0) } .map { () } } @@ -60,4 +63,8 @@ struct FactsService: FactsServiceType { func retrieveFacts(searchTerm: String) -> Observable<[Fact]> { storage.retrieveFacts(searchTerm: searchTerm) } + + func retrievePastSearches() -> Observable<[String]> { + storage.retrieveSearches() + } } diff --git a/Chuck Norris Facts/Data/Storage/Entities/FactEntity.swift b/Chuck Norris Facts/Data/Storage/Entities/FactEntity.swift index 580858d..16ec503 100644 --- a/Chuck Norris Facts/Data/Storage/Entities/FactEntity.swift +++ b/Chuck Norris Facts/Data/Storage/Entities/FactEntity.swift @@ -17,6 +17,8 @@ class FactEntity: Object { let categories = List() + let search = LinkingObjects(fromType: SearchEntity.self, property: "facts") + override static func primaryKey() -> String? { "id" } diff --git a/Chuck Norris Facts/Data/Storage/Entities/SearchEntity.swift b/Chuck Norris Facts/Data/Storage/Entities/SearchEntity.swift new file mode 100644 index 0000000..47fc135 --- /dev/null +++ b/Chuck Norris Facts/Data/Storage/Entities/SearchEntity.swift @@ -0,0 +1,28 @@ +// +// SearchEntity.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/25/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation +import RealmSwift + +class SearchEntity: Object { + @objc dynamic var searchTerm = "" + @objc dynamic var updatedAt = Date() + + let facts = List() + + override static func primaryKey() -> String? { + "searchTerm" + } + + convenience init(searchTerm: String, facts: [Fact]) { + self.init(value: [ + "searchTerm": searchTerm, + "facts": facts.map(FactEntity.init) + ]) + } +} diff --git a/Chuck Norris Facts/Data/Storage/FactsStorage.swift b/Chuck Norris Facts/Data/Storage/FactsStorage.swift index 1c44729..feacc3c 100644 --- a/Chuck Norris Facts/Data/Storage/FactsStorage.swift +++ b/Chuck Norris Facts/Data/Storage/FactsStorage.swift @@ -12,13 +12,23 @@ import RealmSwift import RxRealm protocol FactsStorageType { + // Store a list of categories func storeCategories(_ categories: [FactCategory]) + // Retrieve all local stored categories func retrieveCategories() -> Observable<[FactCategory]> + // Store a list of facts func storeFacts(_ facts: [Fact]) + // Retrieve local stored facts filtered by a search term func retrieveFacts(searchTerm: String) -> Observable<[Fact]> + + // Store a search and it's result + func storeSearch(searchTerm: String, facts: [Fact]) + + // Retrieve all past searches terms + func retrieveSearches() -> Observable<[String]> } final class FactsStorage: FactsStorageType { @@ -51,9 +61,21 @@ final class FactsStorage: FactsStorageType { var entities = realm.objects(FactEntity.self) if !searchTerm.isEmpty { - entities = entities.filter("value CONTAINS %@", searchTerm) + entities = entities.filter("ANY search.searchTerm = %@", searchTerm) } return Observable.collection(from: entities).map { $0.map { $0.item }} } + + func storeSearch(searchTerm: String, facts: [Fact]) { + try? realm.write { + let entity = SearchEntity(searchTerm: searchTerm, facts: facts) + self.realm.add(entity, update: .modified) + } + } + + func retrieveSearches() -> Observable<[String]> { + let entities = realm.objects(SearchEntity.self).sorted(byKeyPath: "updatedAt", ascending: false) + return Observable.collection(from: entities).map { $0.map { $0.searchTerm } } + } } diff --git a/Chuck Norris Facts/Library/DynamicHeightCollectionView.swift b/Chuck Norris Facts/Library/DynamicHeightCollectionView.swift new file mode 100644 index 0000000..619129c --- /dev/null +++ b/Chuck Norris Facts/Library/DynamicHeightCollectionView.swift @@ -0,0 +1,23 @@ +// +// DynamicHeightCollectionView.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/26/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import UIKit + +class DynamicHeightCollectionView: UICollectionView { + + override func layoutSubviews() { + super.layoutSubviews() + if bounds.size != intrinsicContentSize { + self.invalidateIntrinsicContentSize() + } + } + + override var intrinsicContentSize: CGSize { + return collectionViewLayout.collectionViewContentSize + } +} diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/Cells/FactTableViewCell.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/Fact/FactCell.swift similarity index 97% rename from Chuck Norris Facts/Scenes/Facts/FactsList/Cells/FactTableViewCell.swift rename to Chuck Norris Facts/Scenes/Facts/FactsList/Fact/FactCell.swift index 824b7f5..d687cf0 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/Cells/FactTableViewCell.swift +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/Fact/FactCell.swift @@ -1,6 +1,6 @@ // -// FactTableViewCell.swift -// Chuck +// FactCell.swift +// Chuck Norris Facts // // Created by Djorkaeff Alexandre Vilela Pereira on 10/10/20. // Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. @@ -8,7 +8,7 @@ import UIKit -class FactTableViewCell: UITableViewCell { +class FactCell: UITableViewCell { static let cellIdentifier = "FactTableViewCell" diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/Cells/FactViewModel.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/Fact/FactViewModel.swift similarity index 100% rename from Chuck Norris Facts/Scenes/Facts/FactsList/Cells/FactViewModel.swift rename to Chuck Norris Facts/Scenes/Facts/FactsList/Fact/FactViewModel.swift diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift index 933b06f..6a99f8a 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift @@ -19,6 +19,7 @@ class FactsListViewController: UIViewController { private let disposeBag = DisposeBag() let tableView = UITableView() + let loadingView = LoadingView() let emptyListView = EmptyListView() let searchButton = UIBarButtonItem(barButtonSystemItem: .search, target: nil, action: nil) @@ -26,9 +27,9 @@ class FactsListViewController: UIViewController { configureCell: { [weak self] _, tableView, indexPath, fact -> UITableViewCell in guard let viewModel = self?.viewModel, let disposeBag = self?.disposeBag else { return UITableViewCell() } - let cell = tableView.dequeueReusableCell(withIdentifier: FactTableViewCell.cellIdentifier, for: indexPath) + let cell = tableView.dequeueReusableCell(withIdentifier: FactCell.cellIdentifier, for: indexPath) - if let cell = cell as? FactTableViewCell { + if let cell = cell as? FactCell { cell.setup(fact) cell.shareButton.rx.tap .map { fact } @@ -40,16 +41,6 @@ class FactsListViewController: UIViewController { } ) - private lazy var loadingView: AnimationView = { - let loading = AnimationView() - - loading.backgroundColor = .systemBackground - loading.animation = Animation.named("loading") - loading.loopMode = .loop - - return loading - }() - override func viewDidLoad() { super.viewDidLoad() @@ -76,7 +67,7 @@ class FactsListViewController: UIViewController { tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true - tableView.register(FactTableViewCell.self, forCellReuseIdentifier: FactTableViewCell.cellIdentifier) + tableView.register(FactCell.self, forCellReuseIdentifier: FactCell.cellIdentifier) tableView.accessibilityIdentifier = "factsTableView" } diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/Views/LoadingView.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/Views/LoadingView.swift new file mode 100644 index 0000000..36e5b83 --- /dev/null +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/Views/LoadingView.swift @@ -0,0 +1,51 @@ +// +// LoadingView.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/27/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import UIKit +import Lottie + +final class LoadingView: UIView { + + private lazy var animation: AnimationView = { + let loading = AnimationView() + + loading.animation = Animation.named("loading") + loading.loopMode = .loop + + return loading + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + setupView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView() { + backgroundColor = .systemBackground + + addSubview(animation) + animation.translatesAutoresizingMaskIntoConstraints = false + animation.widthAnchor.constraint(equalToConstant: 48).isActive = true + animation.heightAnchor.constraint(equalToConstant: 48).isActive = true + animation.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true + animation.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true + } + + func play() { + animation.play() + } + + func stop() { + animation.stop() + } +} diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/PastSearch/PastSearchCell.swift b/Chuck Norris Facts/Scenes/Facts/SearchFacts/PastSearch/PastSearchCell.swift new file mode 100644 index 0000000..636b8db --- /dev/null +++ b/Chuck Norris Facts/Scenes/Facts/SearchFacts/PastSearch/PastSearchCell.swift @@ -0,0 +1,19 @@ +// +// PastSearchCell.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/26/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import UIKit + +class PastSearchCell: UITableViewCell { + + static let identifier = "PastSearchCell" + + func setup(_ pastSearch: PastSearchViewModel) { + textLabel?.text = pastSearch.text + imageView?.image = UIImage(systemName: "magnifyingglass") + } +} diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/PastSearch/PastSearchViewModel.swift b/Chuck Norris Facts/Scenes/Facts/SearchFacts/PastSearch/PastSearchViewModel.swift new file mode 100644 index 0000000..2c35a32 --- /dev/null +++ b/Chuck Norris Facts/Scenes/Facts/SearchFacts/PastSearch/PastSearchViewModel.swift @@ -0,0 +1,26 @@ +// +// PastSearchViewModel.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/25/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation +import RxDataSources + +struct PastSearchViewModel { + let text: String +} + +extension PastSearchViewModel: IdentifiableType { + var identity: String { + text + } +} + +extension PastSearchViewModel: Equatable { + static func == (lhs: PastSearchViewModel, rhs: PastSearchViewModel) -> Bool { + return lhs.text == rhs.text + } +} diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsTableViewSection.swift b/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsTableViewSection.swift new file mode 100644 index 0000000..fbaf7ce --- /dev/null +++ b/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsTableViewSection.swift @@ -0,0 +1,56 @@ +// +// SearchFactsTableViewSection.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/26/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import RxDataSources + +enum SearchFactsTableViewItem { + case SuggestionsTableViewItem(suggestions: [SuggestionsSectionModel]) + case PastSearchTableViewItem(model: PastSearchViewModel) +} + +extension SearchFactsTableViewItem { + var quantity: Int { + switch self { + case .SuggestionsTableViewItem(let suggestions): + return suggestions.first?.items.count ?? 0 + default: + return 0 + } + } +} + +enum SearchFactsTableViewSection { + case SuggestionsSection(items: [SearchFactsTableViewItem]) + case PastSearchesSection(items: [SearchFactsTableViewItem]) +} + +extension SearchFactsTableViewSection: SectionModelType { + typealias Item = SearchFactsTableViewItem + + var header: String { + switch self { + case .SuggestionsSection: + return "Suggestions" + case .PastSearchesSection: + return "Past Searches" + } + } + + var items: [SearchFactsTableViewItem] { + switch self { + case .SuggestionsSection(let items): + return items + case .PastSearchesSection(let items): + return items + } + } + + init(original: Self, items: [Self.Item]) { + self = original + } +} diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewController.swift b/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewController.swift index fdfe9a5..9a93067 100644 --- a/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewController.swift +++ b/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewController.swift @@ -16,29 +16,44 @@ final class SearchFactsViewController: UIViewController { let disposeBag = DisposeBag() - lazy var collectionView: UICollectionView = { - let layout = FactCategoryViewFlowLayout() - - layout.scrollDirection = .vertical - layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize - layout.sectionInset = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) - - return UICollectionView(frame: .zero, collectionViewLayout: layout) - }() - - private lazy var categoriesDataSource = RxCollectionViewSectionedReloadDataSource( - configureCell: { _, collectionView, indexPath, category -> UICollectionViewCell in - let cell = collectionView.dequeueReusableCell( - withReuseIdentifier: FactCategoryCell.cellIdentifier, - for: indexPath - ) - if let cell = cell as? FactCategoryCell { - cell.setup(category) + private lazy var itemsDataSource = RxTableViewSectionedReloadDataSource( + configureCell: { dataSource, tableView, indexPath, _ -> UITableViewCell in + + switch dataSource[indexPath] { + case .SuggestionsTableViewItem(let suggestions): + let cell = SuggestionsCell(style: .default, reuseIdentifier: SuggestionsCell.identifier) + let viewModel = SuggestionsViewModel(suggestions: suggestions) + cell.viewModel = viewModel + viewModel.didSelectSuggestion + .bind(to: self.viewModel.searchTerm) + .disposed(by: self.disposeBag) + viewModel.didSelectSuggestion + .map { _ in () } + .bind(to: self.viewModel.searchAction) + .disposed(by: self.disposeBag) + return cell + case .PastSearchTableViewItem(let model): + let cell = tableView.dequeueReusableCell(withIdentifier: PastSearchCell.identifier) as? PastSearchCell + ?? PastSearchCell(style: .default, reuseIdentifier: PastSearchCell.identifier) + cell.setup(model) + return cell } - return cell + + }, titleForHeaderInSection: { dataSource, index in + return dataSource.sectionModels[index].header } ) + lazy var tableView: UITableView = { + let tableView = UITableView() + + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.accessibilityIdentifier = "itemsTableView" + tableView.tableFooterView = UIView() + + return tableView + }() + lazy var cancelButton: UIBarButtonItem = { let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: nil, action: nil) cancelButton.accessibilityIdentifier = "cancelButton" @@ -61,7 +76,7 @@ final class SearchFactsViewController: UIViewController { setupView() setupNavigationBar() setupBindings() - setupCollectionView() + setupTableView() } private func setupView() { @@ -69,23 +84,23 @@ final class SearchFactsViewController: UIViewController { view.accessibilityIdentifier = "searchFactsView" } - private func setupCollectionView() { - view.addSubview(collectionView) + private func setupTableView() { + view.addSubview(tableView) - collectionView.backgroundColor = .systemBackground + tableView.backgroundColor = .systemBackground + tableView.rowHeight = UITableView.automaticDimension - collectionView.translatesAutoresizingMaskIntoConstraints = false - collectionView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true - collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true - collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true - collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true - collectionView.register(FactCategoryCell.self, forCellWithReuseIdentifier: FactCategoryCell.cellIdentifier) - - collectionView.accessibilityIdentifier = "factCategoriesCollectionView" + tableView.register(SuggestionsCell.self, forCellReuseIdentifier: SuggestionsCell.identifier) + tableView.register(PastSearchCell.self, forCellReuseIdentifier: PastSearchCell.identifier) } private func setupNavigationBar() { + navigationItem.hidesSearchBarWhenScrolling = false navigationItem.searchController = searchController navigationItem.leftBarButtonItem = cancelButton navigationItem.title = "Search" @@ -109,21 +124,29 @@ final class SearchFactsViewController: UIViewController { .bind(to: viewModel.searchAction) .disposed(by: disposeBag) - viewModel.categories - .observeOn(MainScheduler.instance) - .bind(to: collectionView.rx.items(dataSource: categoriesDataSource)) + viewModel.items + .bind(to: tableView.rx.items(dataSource: itemsDataSource)) .disposed(by: disposeBag) - let categorySelected = collectionView.rx - .modelSelected(FactCategoryViewModel.self) + let pastSearchSelected = tableView.rx + .modelSelected(SearchFactsTableViewItem.self) .asObservable() - categorySelected - .compactMap { $0.text } + pastSearchSelected + .compactMap { + switch $0 { + case .PastSearchTableViewItem(let model): + return model.text + default: + break + } + return "" + } + .filter { !$0.isEmpty } .bind(to: viewModel.searchTerm) .disposed(by: disposeBag) - categorySelected + pastSearchSelected .map { _ in () } .bind(to: viewModel.searchAction) .disposed(by: disposeBag) diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewModel.swift b/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewModel.swift index 409f04a..60896bf 100644 --- a/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewModel.swift +++ b/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewModel.swift @@ -10,7 +10,9 @@ import Foundation import RxDataSources import RxSwift -typealias FactCategoriesSectionModel = AnimatableSectionModel +typealias SuggestionsSectionModel = AnimatableSectionModel + +typealias PastSearchesSectionModel = AnimatableSectionModel class SearchFactsViewModel { @@ -26,12 +28,12 @@ class SearchFactsViewModel { // MARK: - Outputs - let categories: Observable<[FactCategoriesSectionModel]> - let didCancel: Observable let didSearchFacts: Observable + let items: Observable<[SearchFactsTableViewSection]> + init(factsService: FactsServiceType = FactsService()) { let cancelSubject = PublishSubject() self.cancel = cancelSubject.asObserver() @@ -50,10 +52,27 @@ class SearchFactsViewModel { let viewWillAppearSubject = PublishSubject() self.viewWillAppear = viewWillAppearSubject.asObserver() - self.categories = viewWillAppearSubject + let categories = viewWillAppearSubject .flatMapLatest { factsService.retrieveCategories() } .map { Array($0.shuffled().prefix(8)) } .map { $0.map { FactCategoryViewModel(category: $0) } } - .map { [FactCategoriesSectionModel(model: "", items: $0)] } + .map { [SuggestionsSectionModel(model: "", items: $0)] } + .map { suggestions -> [SearchFactsTableViewItem] in + if let firstSection = suggestions.first, firstSection.items.isEmpty { + return [] + } + return [SearchFactsTableViewItem.SuggestionsTableViewItem(suggestions: suggestions)] + } + + let pastSearches = viewWillAppearSubject + .flatMapLatest { factsService.retrievePastSearches() } + .map { $0.map { PastSearchViewModel(text: $0) } } + .map { $0.map { SearchFactsTableViewItem.PastSearchTableViewItem(model: $0) } } + + self.items = Observable.combineLatest(categories, pastSearches) + .map { categories, pastSearches -> [SearchFactsTableViewSection] in + [.SuggestionsSection(items: categories), .PastSearchesSection(items: pastSearches)] + } + .map { $0.filter { !$0.items.isEmpty } } } } diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/Cells/FactCategoryCell.swift b/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryCell.swift similarity index 94% rename from Chuck Norris Facts/Scenes/Facts/SearchFacts/Cells/FactCategoryCell.swift rename to Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryCell.swift index 57e18d0..5f6e0f9 100644 --- a/Chuck Norris Facts/Scenes/Facts/SearchFacts/Cells/FactCategoryCell.swift +++ b/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryCell.swift @@ -18,7 +18,6 @@ class FactCategoryCell: UICollectionViewCell { label.translatesAutoresizingMaskIntoConstraints = false label.lineBreakMode = .byWordWrapping label.textColor = .white - label.numberOfLines = 0 return label }() @@ -27,6 +26,9 @@ class FactCategoryCell: UICollectionViewCell { super.init(frame: frame) setupView() + + isAccessibilityElement = true + accessibilityIdentifier = "factCategoryCell" } required init?(coder: NSCoder) { diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/Cells/FactCategoryViewModel.swift b/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryViewModel.swift similarity index 100% rename from Chuck Norris Facts/Scenes/Facts/SearchFacts/Cells/FactCategoryViewModel.swift rename to Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryViewModel.swift diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/SuggestionsCell.swift b/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/SuggestionsCell.swift new file mode 100644 index 0000000..61f6879 --- /dev/null +++ b/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/SuggestionsCell.swift @@ -0,0 +1,105 @@ +// +// SuggestionsCell.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/26/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import UIKit +import RxSwift +import RxCocoa +import RxDataSources + +class SuggestionsCell: UITableViewCell { + + static let identifier = "SuggestionsCell" + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private let disposeBag = DisposeBag() + + private lazy var suggestionsDataSource = RxCollectionViewSectionedReloadDataSource( + configureCell: { _, collectionView, indexPath, category -> UICollectionViewCell in + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: FactCategoryCell.cellIdentifier, + for: indexPath + ) as? FactCategoryCell ?? FactCategoryCell() + + cell.setup(category) + return cell + } + ) + + var viewModel: SuggestionsViewModel! { + didSet { + self.setupBindings() + } + } + + lazy var collectionView: DynamicHeightCollectionView = { + let layout = SuggestionsViewFlowLayout() + let collectionView = DynamicHeightCollectionView(frame: .zero, collectionViewLayout: layout) + + layout.scrollDirection = .vertical + layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize + layout.sectionInset = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) + + collectionView.isScrollEnabled = false + collectionView.translatesAutoresizingMaskIntoConstraints = false + collectionView.register(FactCategoryCell.self, forCellWithReuseIdentifier: FactCategoryCell.cellIdentifier) + + return collectionView + }() + + private func setupView() { + contentView.addSubview(collectionView) + + collectionView.backgroundColor = .systemBackground + collectionView.widthAnchor.constraint(equalTo: contentView.widthAnchor).isActive = true + collectionView.heightAnchor.constraint(equalTo: contentView.heightAnchor).isActive = true + } + + private func setupBindings() { + collectionView.rx + .setDelegate(self) + .disposed(by: disposeBag) + + viewModel.suggestions + .observeOn(MainScheduler.instance) + .bind(to: collectionView.rx.items(dataSource: suggestionsDataSource)) + .disposed(by: disposeBag) + + let categorySelected = collectionView.rx + .modelSelected(FactCategoryViewModel.self) + .asObservable() + + categorySelected + .compactMap { $0.text } + .bind(to: viewModel.suggestion) + .disposed(by: disposeBag) + + categorySelected + .map { _ in () } + .bind(to: viewModel.selectAction) + .disposed(by: disposeBag) + } +} + +extension SuggestionsCell: UICollectionViewDelegateFlowLayout { + func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath) -> CGSize { + let cell = FactCategoryCell() + let item = suggestionsDataSource.sectionModels[indexPath.section].items[indexPath.row] + cell.setup(item) + return cell.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + } +} diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/Cells/FactCategoryViewFlowLayout.swift b/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/SuggestionsViewFlowLayout.swift similarity index 89% rename from Chuck Norris Facts/Scenes/Facts/SearchFacts/Cells/FactCategoryViewFlowLayout.swift rename to Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/SuggestionsViewFlowLayout.swift index 890b6c1..aa6b5ca 100644 --- a/Chuck Norris Facts/Scenes/Facts/SearchFacts/Cells/FactCategoryViewFlowLayout.swift +++ b/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/SuggestionsViewFlowLayout.swift @@ -1,5 +1,5 @@ // -// FactCategoryViewFlowLayout.swift +// SuggestionsViewFlowLayout.swift // Chuck Norris Facts // // Created by Djorkaeff Alexandre Vilela Pereira on 10/21/20. @@ -8,7 +8,7 @@ import UIKit -final class FactCategoryViewFlowLayout: UICollectionViewFlowLayout { +final class SuggestionsViewFlowLayout: UICollectionViewFlowLayout { override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { let attributes = super.layoutAttributesForElements(in: rect) diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/SuggestionsViewModel.swift b/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/SuggestionsViewModel.swift new file mode 100644 index 0000000..1de8910 --- /dev/null +++ b/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/SuggestionsViewModel.swift @@ -0,0 +1,41 @@ +// +// SuggestionsViewModel.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/26/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import RxSwift + +struct SuggestionsViewModel { + + // MARK: - Inputs + + let suggestion: AnyObserver + + let selectAction: AnyObserver + + // MARK: - Outputs + + let suggestions: Observable<[SuggestionsSectionModel]> + + let didSelectSuggestion: Observable + + init(suggestions: [SuggestionsSectionModel]) { + let suggestionsSubject = BehaviorSubject<[SuggestionsSectionModel]>(value: []) + self.suggestions = suggestionsSubject.asObserver() + + suggestionsSubject.onNext(suggestions) + + let suggestionSubject = BehaviorSubject(value: "") + self.suggestion = suggestionSubject.asObserver() + + let selectActionSubject = PublishSubject() + self.selectAction = selectActionSubject.asObserver() + + self.didSelectSuggestion = selectActionSubject + .withLatestFrom(suggestionSubject) + .filter { !$0.isEmpty } + } +} diff --git a/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift b/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift index 3554f9d..64ccbe3 100644 --- a/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift +++ b/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift @@ -94,4 +94,24 @@ final class FactsServiceTests: XCTestCase { let savedFacts = try storedFacts.toBlocking().first() XCTAssertEqual(savedFacts?.count, stubFacts.count) } + + func test_retrievePastSearchesShouldReturnDistinctSortedByDateSearchesOnStorage() throws { + let storedSearches = factsStorage.retrieveSearches() + let searches = try storedSearches.toBlocking().first() ?? [] + XCTAssertTrue(searches.isEmpty) + + let stubShortFact = try stub("short-fact", type: Fact.self) + let shortFact = try XCTUnwrap(stubShortFact) + + let stubLongFact = try stub("long-fact", type: Fact.self) + let longFact = try XCTUnwrap(stubLongFact) + + factsStorage.storeSearch(searchTerm: "games", facts: [shortFact]) + factsStorage.storeSearch(searchTerm: "explicit", facts: [longFact]) + factsStorage.storeSearch(searchTerm: "explicit", facts: [longFact]) + factsStorage.storeSearch(searchTerm: "fashion", facts: [shortFact]) + + let savedSearches = try storedSearches.toBlocking().first() + XCTAssertEqual(savedSearches, ["fashion", "explicit", "games"]) + } } diff --git a/Chuck Norris FactsTests/Mocks/FactsServiceMock.swift b/Chuck Norris FactsTests/Mocks/FactsServiceMock.swift index ac07089..aa976be 100644 --- a/Chuck Norris FactsTests/Mocks/FactsServiceMock.swift +++ b/Chuck Norris FactsTests/Mocks/FactsServiceMock.swift @@ -32,4 +32,9 @@ final class FactsServiceMock: FactsServiceType { func retrieveFacts(searchTerm: String) -> Observable<[Fact]> { return retrieveFactsReturnValue } + + var retrievePastSearchesReturnValue: Observable<[String]> = .just([]) + func retrievePastSearches() -> Observable<[String]> { + return retrievePastSearchesReturnValue + } } diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift index 7f9e3e4..793af8a 100644 --- a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift +++ b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift @@ -27,8 +27,7 @@ class FactsListViewControllerTests: XCTestCase { factsListViewController = FactsListViewController() factsListViewController.viewModel = factsListViewModel - factsListViewController.loadView() - factsListViewController.viewDidLoad() + factsListViewController.loadViewIfNeeded() } override func tearDown() { @@ -100,8 +99,8 @@ class FactsListViewControllerTests: XCTestCase { } extension FactsListViewControllerTests { - func factsListFirstCell() -> FactTableViewCell? { + func factsListFirstCell() -> FactCell? { let indexPath = IndexPath(row: 0, section: 0) - return factsListViewController.tableView.cellForRow(at: indexPath) as? FactTableViewCell + return factsListViewController.tableView.cellForRow(at: indexPath) as? FactCell } } diff --git a/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewControllerTests.swift b/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewControllerTests.swift index 53c5243..57ed533 100644 --- a/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewControllerTests.swift +++ b/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewControllerTests.swift @@ -29,8 +29,7 @@ class SearchFactsViewControllerTests: XCTestCase { searchFactsViewController = SearchFactsViewController() searchFactsViewController.viewModel = searchFactsViewModel - searchFactsViewController.loadView() - searchFactsViewController.viewDidLoad() + searchFactsViewController.loadViewIfNeeded() } override func tearDown() { @@ -40,12 +39,39 @@ class SearchFactsViewControllerTests: XCTestCase { factsServiceMock = nil } - func test_factCategoriesViewShouldShow8Categories() throws { + func test_searchFactsViewShouldShow8Categories() throws { let stubFactCategories = try stub("get-categories", type: [FactCategory].self) ?? [] factsServiceMock.retrieveCategoriesReturnValue = .just(stubFactCategories) searchFactsViewModel.viewWillAppear.onNext(()) - XCTAssertEqual(searchFactsViewController.collectionView.numberOfItems(inSection: 0), 8) + let tableView = searchFactsViewController.tableView + let searchFactsDataSource = tableView.dataSource + let indexPath = IndexPath(row: 0, section: 0) + let suggestionsCell = searchFactsDataSource?.tableView(tableView, cellForRowAt: indexPath) as? SuggestionsCell + + XCTAssertEqual(searchFactsDataSource?.numberOfSections?(in: tableView), 1) + XCTAssertEqual(suggestionsCell?.collectionView.numberOfItems(inSection: 0), 8) + } + + func test_searchFactsViewShouldShowOnlyPastSearches() { + factsServiceMock.retrievePastSearchesReturnValue = .just(["fashion", "games", "explicit"]) + + searchFactsViewModel.viewWillAppear.onNext(()) + + let tableView = searchFactsViewController.tableView + let searchFactsDataSource = tableView.dataSource + + XCTAssertEqual(searchFactsDataSource?.numberOfSections?(in: tableView), 1) + XCTAssertEqual(searchFactsDataSource?.tableView(tableView, numberOfRowsInSection: 0), 3) + } + + func test_searchFactsViewShouldBeEmptyWhenThereIsNoSuggestionsOrPastSearches() { + searchFactsViewModel.viewWillAppear.onNext(()) + + let tableView = searchFactsViewController.tableView + let searchFactsDataSource = tableView.dataSource + + XCTAssertEqual(searchFactsDataSource?.numberOfSections?(in: tableView), 0) } } diff --git a/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewModelTests.swift b/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewModelTests.swift index b9ff58e..9dea832 100644 --- a/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewModelTests.swift +++ b/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewModelTests.swift @@ -66,20 +66,39 @@ class SearchFactsViewModelTests: XCTestCase { } func test_shouldLoad8RandomFactCategories() throws { - let factCategoriesObserver = testScheduler.createObserver([FactCategoriesSectionModel].self) + let searchFactsItemsObserver = testScheduler.createObserver([SearchFactsTableViewSection].self) let testCategories = try stub("get-categories", type: [FactCategory].self) ?? [] factsServiceMock.retrieveCategoriesReturnValue = .just(testCategories) - searchFactsViewModel.categories - .subscribe(factCategoriesObserver) + searchFactsViewModel.items + .subscribe(searchFactsItemsObserver) .disposed(by: disposeBag) searchFactsViewModel.viewWillAppear.onNext(()) testScheduler.start() - let factCategoriesViewModel = factCategoriesObserver.events.compactMap { $0.value.element }.first - XCTAssertEqual(factCategoriesViewModel?.first?.items.count, 8) + let searchFactsViewModelEvents = searchFactsItemsObserver.events.compactMap { $0.value.element }.first + XCTAssertEqual(searchFactsViewModelEvents?.first?.items.first?.quantity, 8) + } + + func test_shouldLoadPastSearches() { + let searchFactsItemsObserver = testScheduler.createObserver([SearchFactsTableViewSection].self) + + let pastSearches = ["fashion", "games", "food"] + factsServiceMock.retrievePastSearchesReturnValue = .just(pastSearches) + + searchFactsViewModel.items + .subscribe(searchFactsItemsObserver) + .disposed(by: disposeBag) + + searchFactsViewModel.viewWillAppear.onNext(()) + + testScheduler.start() + + let searchFactsViewModelEvents = searchFactsItemsObserver.events.compactMap { $0.value.element }.first + XCTAssertEqual(searchFactsViewModelEvents?.count, 1) + XCTAssertEqual(searchFactsViewModelEvents?.first?.items.count, 3) } } diff --git a/Chuck Norris FactsUITests/Scenes/SearchFactsScene.swift b/Chuck Norris FactsUITests/Scenes/SearchFactsScene.swift index 3b2d64d..a71e9c9 100644 --- a/Chuck Norris FactsUITests/Scenes/SearchFactsScene.swift +++ b/Chuck Norris FactsUITests/Scenes/SearchFactsScene.swift @@ -14,7 +14,7 @@ struct SearchFactsScene { let searchFactsView: XCUIElement let searchBarField: XCUIElement let cancelButton: XCUIElement - let factsCategoriesCollection: XCUIElement + let itemsTableView: XCUIElement init() { let app = XCUIApplication() @@ -22,7 +22,15 @@ struct SearchFactsScene { searchFactsView = app.otherElements["searchFactsView"] searchBarField = app.searchFields["Search"] cancelButton = app.navigationBars.buttons["cancelButton"] - factsCategoriesCollection = app.collectionViews["factCategoriesCollectionView"] + itemsTableView = app.tables["itemsTableView"] } } + +extension SearchFactsScene { + var factCategoryCells: XCUIElementQuery { + itemsTableView.cells + .children(matching: .other) + .matching(identifier: "factCategoryCell") + } +} diff --git a/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift b/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift index 01a6a13..c4e159f 100644 --- a/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift +++ b/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift @@ -32,7 +32,7 @@ final class SearchFactsUITests: XCTestCase { sleep(5) - XCTAssertEqual(factsListScene.factsTableView.cells.count, 15) + XCTAssertEqual(factsListScene.factsTableView.cells.count, 16) } func test_cancelSearchFacts() throws { @@ -56,7 +56,9 @@ final class SearchFactsUITests: XCTestCase { let searchFactsScene = SearchFactsScene() XCTAssertTrue(searchFactsScene.searchFactsView.exists) - XCTAssertEqual(searchFactsScene.factsCategoriesCollection.cells.count, 8) + let suggestionsCells = searchFactsScene.factCategoryCells + + XCTAssertEqual(suggestionsCells.count, 8) } func test_tapFactCategoryShouldSearchByTerm() { @@ -68,14 +70,75 @@ final class SearchFactsUITests: XCTestCase { let searchFactsScene = SearchFactsScene() XCTAssertTrue(searchFactsScene.searchFactsView.exists) - let firstFactCategory = searchFactsScene.factsCategoriesCollection.cells.firstMatch - XCTAssertTrue(firstFactCategory.exists) + let suggestionsCells = searchFactsScene.factCategoryCells + + let suggestion = suggestionsCells.firstMatch + XCTAssertTrue(suggestion.exists) + + suggestion.tap() + + sleep(5) - firstFactCategory.tap() XCTAssertFalse(searchFactsScene.searchFactsView.exists) + XCTAssertGreaterThan(factsListScene.factsTableView.cells.count, 0) + } + + func test_tapPastSearchShouldSearchByTerm() { + app.launch() + + let factsListScene = FactsListScene() + factsListScene.searchButton.tap() + + let searchFactsScene = SearchFactsScene() + XCTAssertTrue(searchFactsScene.searchFactsView.exists) + + let searchFactsCells = searchFactsScene.itemsTableView.cells + + let pastSearchCell = searchFactsCells.element(boundBy: 1) + XCTAssertTrue(pastSearchCell.exists) + + pastSearchCell.tap() sleep(5) + XCTAssertFalse(searchFactsScene.searchFactsView.exists) XCTAssertGreaterThan(factsListScene.factsTableView.cells.count, 0) } + + func test_tapPastSearchShouldOrderByDate() { + app.launch() + + let factsListScene = FactsListScene() + factsListScene.searchButton.tap() + + let searchFactsScene = SearchFactsScene() + XCTAssertTrue(searchFactsScene.searchFactsView.exists) + + let searchFactsCells = searchFactsScene.itemsTableView.cells + + let secondItem = searchFactsCells.element(boundBy: 2) + XCTAssertTrue(secondItem.exists) + + secondItem.tap() + + let firstItem = searchFactsCells.element(boundBy: 1) + XCTAssertTrue(firstItem.exists) + + XCTAssertEqual(firstItem.label, secondItem.label) + } + + func test_pastSearchShouldBeHiddenOnFirstAccess() { + app.launchArguments = ["--reset-storage"] + app.launch() + + let factsListScene = FactsListScene() + factsListScene.searchButton.tap() + + let searchFactsScene = SearchFactsScene() + XCTAssertTrue(searchFactsScene.searchFactsView.exists) + + let searchFactsCells = searchFactsScene.itemsTableView.cells + + XCTAssertEqual(searchFactsCells.count, 1) + } } From 2c161c9afe5e4d8c373df85702b771c6153d802a Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Tue, 27 Oct 2020 22:57:04 -0300 Subject: [PATCH 09/18] Set App icon (#21) * Set App icon * Set App Icon iPad --- .../AppIcon.appiconset/Contents.json | 24 ++++++++++++++++++ .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 614 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 1352 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 2127 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 939 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 2015 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 3103 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 1352 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 2900 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 4443 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 4443 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 6890 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 2733 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 5788 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 6338 bytes .../AppIcon.appiconset/ItunesArtwork@2x.png | Bin 0 -> 40073 bytes 16 files changed, 24 insertions(+) create mode 100644 Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png diff --git a/Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json index 9221b9b..1e2d708 100644 --- a/Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,91 +1,115 @@ { "images" : [ { + "filename" : "Icon-App-20x20@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "20x20" }, { + "filename" : "Icon-App-20x20@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "20x20" }, { + "filename" : "Icon-App-29x29@1x.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "Icon-App-29x29@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "29x29" }, { + "filename" : "Icon-App-29x29@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "29x29" }, { + "filename" : "Icon-App-40x40@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "40x40" }, { + "filename" : "Icon-App-40x40@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "40x40" }, { + "filename" : "Icon-App-60x60@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "60x60" }, { + "filename" : "Icon-App-60x60@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "60x60" }, { + "filename" : "Icon-App-20x20@1x.png", "idiom" : "ipad", "scale" : "1x", "size" : "20x20" }, { + "filename" : "Icon-App-20x20@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "20x20" }, { + "filename" : "Icon-App-29x29@1x.png", "idiom" : "ipad", "scale" : "1x", "size" : "29x29" }, { + "filename" : "Icon-App-29x29@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "29x29" }, { + "filename" : "Icon-App-40x40@1x.png", "idiom" : "ipad", "scale" : "1x", "size" : "40x40" }, { + "filename" : "Icon-App-40x40@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "40x40" }, { + "filename" : "Icon-App-76x76@1x.png", "idiom" : "ipad", "scale" : "1x", "size" : "76x76" }, { + "filename" : "Icon-App-76x76@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "76x76" }, { + "filename" : "Icon-App-83.5x83.5@2x.png", "idiom" : "ipad", "scale" : "2x", "size" : "83.5x83.5" }, { + "filename" : "ItunesArtwork@2x.png", "idiom" : "ios-marketing", "scale" : "1x", "size" : "1024x1024" diff --git a/Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..6cd3392fa85d2624627cb3bc526b7c66f7a411bf GIT binary patch literal 614 zcmV-s0-61ZP)zGlENm>av=I>tn^;7! z@&Q^1f?_3#77;NY@Q zW$4jx_!jy-XgP^p0oII5foN@FA)h&$>zKf&YuL#5{K6 zHVy>XFy7%t@HQBUpW|>)(p=O84yre(cuZANbkwud3E(l@jH+jG1eYWDY$SPr3pjIs+)O6G#kinQW!Xax`b;V>CNqXGH}`>-?ohSWQq zKWRN5jeeZNSM_+z1lVR=ipyw*zWaS#(Q+e|`U0LtdlmNLw0h6ySE;1x=5Q%XfDFd} zq#l{y(Q3=84{T~DQq{vqO?D&AiWQbOq+1f{KNjn?uk5RRW&i*H07*qoM6N<$f;O}l As{jB1 literal 0 HcmV?d00001 diff --git a/Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1da8f00fa398732ad50090477cc84fe726dd6f15 GIT binary patch literal 1352 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 FV1n|^7!Uve literal 0 HcmV?d00001 diff --git a/Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..d3a69234a03fee6d39e96fb1238b36b00b320665 GIT binary patch literal 939 zcmV;c162HpP)nBD|Ml07P=D@B`>2+#!P0W`?@%%=HBk~OwTMVD7w4qRDE^6^PN-G)p7b$RJH%V zRp?W9r>MGH1g$Y>ZBW)sZ37g#buOL+TBC#61YScgPHrjZw$f^Jw`#Zp|KdaZh_jf( zL%4UFz!O1hLEj<{;ir-L&u|2f;2&F~dg9>**p2rRu$LQm^28O6;#~e-%QXWVhinLR zAYxSdT5ftCUs_k6z9z06T)?~7gPH8pd5Ce~>-5&?QMYAO<7#||Gp&QQTR#u;DGuPq zw3)?C`FjPIHPo+z?!ZZ$!e6;?8n@zCje5sXRwwvtBjdBU3oj)2a*kKW8O%~0#m8ed z+)iu9s-1>=@s3dYCF;&j{Dx07;+Pug9YNvfc?g6gk=M=P{4NB0t-Q zZF@T<*y^KGO578;O~jz>&F(Z@F z>>Dm11^8@w({5$$!IlG^GI1H#i+s8ptC}VwvR_;XS7`}pl!}!EI)s-+ zHTg47|3%XHJ{}ika~{74)667fn^V7NU2coFI zA@0(y5YsXIjr&EIE{%A%;U!+^8Q`1rVhOK`nBR%-@l4w96N$enibzL<;0ii71Fdp{ z{h|ULY0Z7q`rb>s`-H$QXv*bAFN6(W4wZ=S93E}~l$S&4tAf%6%{ysGq1yp1lS_C} z6Q5PXuZuhIwqT>A*`!vTV6fS{x0JA5g4UBcz8Cl4fdpEwG@F58jd~(z0VZ;=Tz$P9 zzi7B-Zw0hlnM_H!ITl;Cm-2H`{6_bw(Cw!qTm3IHYPZ?NJ`S``{U6$*|80-u|8W2S N002ovPDHLkV1jp@zQq6l literal 0 HcmV?d00001 diff --git a/Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..6d27928183179ef4bf832fbbfcf5e03da9129634 GIT binary patch literal 2015 zcmV<52O#)~P)Qv)CYZ#L?9ZCNgp)&fDsc# zNi@ZZ`XXLlG%Ar)6GbD&2d!X?QLEyeNa-DCdd~Rptu=eio^xi-nR8BS`z1T)?7h}r z>-(?u-Pdk>m-#803(ihzV2*Bkisso46xs{O*V#%7Xr68+P8GFJ#U%Ztrs34B^hAMn z)9aX!$$#nn}T?4idMKHdgD{hwi`$<2OoU0lFYu#v}M;x*t~jx&quE@l|2a z<}B6gx{eP+tGbtk+HXmXqHP+##=-P_Ee0hA4qw8{@C>2zbqVQWUM$4P^v-G$)E8(c zz1&V$eXy=-b>~vsA5z;e`dLz5oMX5Z&sPCyip4w+Pr)p7Rvu@L?jq1Ku-+}qe0}2jWT%_* zVq`UWJw7NzzYcV9(Q;ICmjJm=rI&e!>giT)Yg!rsohn+e)Rue`ckAtk7lCH?@U8Ul zq$*77w|luQ8>Z8>%NcMJd+_$|ruqMd=ZZjr1T>jo+wf#j`dEuE%nS5PT!qU; zxnzUTnYLP#AQUS*NX z@umg%^F&m0t*A=uFH$C__1lUY@Gqeg4WX}Pz|rg7M$wlDAGhLWmA(#3`JMA? z7#4wMJ>81e8Ei#!`$Z~4Gs-Tk6S_5v3)eVa&TB6alC=zIzB!7wi9o$m@2~=c<0@5j zh4I>+?uQ90GY=OEpgDak+ZG3%7oxknXdEP|9Y@Ae^0Be_KPcGT?|CAHEDh4KjcAG7 z^F{c8O3qiBY}|#Xu5kJ}<{oqba~OE7rMpQV9$Uf^z7g+JMKb*WjazJgk6)<_-L?VV zZKeJ%#udV_mVEUsrL=^Lcnh}RW2*C!x{vC$<9Tt)KC3NABaMBBu--d`R85IS~hj*Wo>S7YSCJJA-|>aO9V%3eF|4I>?R)2F@+6O;)ZB<^tQF@j!ZZg|O&tQ7pT- z$9#DKySN2AEZ^0?!@c)Q4@`&9x5wj+7>4&z4LDSTD7${it| zcj9w;8?T=V9lBLiGtRM8lo#;5^nX^^gpH!Kb3m0}W?s%0r*Q8OJGl~=p$ujg*GGjY zelh(X625nzC_WPgp@kO;qq3zK=K&Gs9TB@Ymzcg+oTXBUT0`Y(=2iLZF6_lig=(#K zgt4ZB;C5niBW}m16El0ns$V0blU*W{Ay#}p9>Rn8MSAx(A?^=~-N_bobNYV-*NX++ zm)?I3UL_W2C2<`Ys5JO-k!GzIx_dx~(fftLnN!7^Q(|YY5xaA{IAeLGSg;SNa>$mD zfLx{TilV@?Rd(S>(RUCU>1aP&y_v)`l{=VE03J#VU#`kc+d@x&DEwsh%h|v@DP3hy zenKp4&)9VU8u=TBt`O6lR_zteev7Co-YpouUhGI_Fgx|Sdurf0LUc>995wnH31~JD zm#TE@Vc~1*^tBbnDNpPcgl>p6f;VdA+h<#>zKa?qA12BLmIJ`gJ19IB7V2$mN!fs5|Wx7IoaI z-^k--CfDPms&m!V#4d|(-r-yDPJ{c!VQDN2mkVn`sG+o&!$DZyH|f*yF}>qbs|O{_ZMTCmK_9-^Pnf!eD;gD_F!t;C{6+{5s}U& zq2Ggu`dVpXKr?kk#57fVp3hz$S5+A&d4QaOPV6(*thdK{w3Vmir-~-}Ve+I5%UbCF xiI<5|dzb?8nMfI5doFo^Gwqv&K`9%f{{dp6BgqCHRa^i7002ovPDHLkV1nGm@^b(H literal 0 HcmV?d00001 diff --git a/Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..1f966ce5dd0d629cea32109b406a38c0bf024c0e GIT binary patch literal 3103 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 yhy? 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 GdHH_^6bFI; literal 0 HcmV?d00001 diff --git a/Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..41f709c235f88eb8abf791bfea8bdd3c46f07d39 GIT binary patch literal 2733 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 T6d(WRI%8<4=z?36Y@q)Ge>&aF literal 0 HcmV?d00001 diff --git a/Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/Chuck Norris Facts/Resources/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..8830d6e3a0ea279af179ee38db29fe2c66965427 GIT binary patch literal 6338 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 W_cYt>onaURe2ANa|5f2f Date: Thu, 29 Oct 2020 16:44:16 -0300 Subject: [PATCH 10/18] Setup documentation (#23) --- .github/images/screenshot.png | Bin 0 -> 646870 bytes README.md | 55 ++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 .github/images/screenshot.png create mode 100644 README.md diff --git a/.github/images/screenshot.png b/.github/images/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..d209c5ff47ecb6a81e0cca7dfddd18373899254b GIT binary patch literal 646870 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 m~aNueb2y1N^ie<2--6GIq&WUbTP$32-=6S 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

+ +

+ +## Getting started + +### Requirements +This application was developed using these tools and no have warranty that it runs on older versions. +- Xcode 11.7 +- Swift 5 +- Cocoapods 1.9.3 + +### How to run +1. You should clone the repository +`$ git clone git@github.com:djorkaeffalexandre/chuck-norris-facts.git` +2. Enter the project folder +`$ cd chuck-norris-facts` +3. Install dependencies +`$ pod install` +4. Open the project on Xcode +`$ open -a Xcode Chuck\ Norris\ Facts.xcworkspace` + +## Architecture +This application conforms to [MVVM-C (Model-View-ViewModel-Coordinators)](https://stevenpcurtis.medium.com/mvvm-c-architecture-with-dependency-injection-testing-3b7197eb2e4d) pattern, +that helps with separation of concerns and allows testing and implementation to be better than [MVC](https://medium.com/swift-coding/mvc-in-swift-a9b1121ab6f0). +This application uses [RxSwift](https://github.com/ReactiveX/RxSwift) which allows reacting to changes even with multiple threads and with lot less code, complexity and bugs. +Using [RxSwift](https://github.com/ReactiveX/RxSwift) and [RxRealm](https://github.com/RxSwiftCommunity/RxRealm) we can listen to database changes and binding them to Views, View Models and View Controllers. + +### Dependencies +- [RxSwift/RxCocoa](https://github.com/ReactiveX/RxSwift) Reactive Programming in Swift. +- [RxDataSources](https://github.com/RxSwiftCommunity/RxDataSources) UITableView and UICollectionView Data Sources for RxSwift. +- [RxRealm](https://github.com/RxSwiftCommunity/RxRealm) RxSwift extension for RealmSwift's types. +- [SwiftLint](https://github.com/realm/SwiftLint) A tool to enforce Swift style and conventions. +- [Lottie](https://github.com/airbnb/lottie-ios) An iOS library to natively render After Effects vector animations. +- [Moya](https://github.com/Moya/Moya) Network abstraction layer written in Swift. +- [RealmSwift](https://github.com/realm/realm-cocoa) Realm is a mobile database: a replacement for Core Data & SQLite. + +## Fastlane +We use [Fastlane](https://github.com/fastlane/fastlane) to provide CLI commands to easily build, test and deploy the application on Continuous Integration and Delivery platforms. +You should install [Fastlane](https://github.com/fastlane/fastlane) and it's plugins using `$ bundle install`. + +### Commands +- Run Unit and UI tests (Running on Github Actions) +`$ fastlane ios tests` + +- Release the app to Test Flight (Beta Release) +`$ fastlane ios beta` + +## Contact +Email: djorkaeff7@icloud.com From f9bc52b58a3da3cc776f54fe54263bcd3baa4902 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Thu, 29 Oct 2020 20:20:37 -0300 Subject: [PATCH 11/18] Improve Launch arguments (#25) * Launch Arguments as a Enumerable * Fix UI Tests --- Chuck Norris Facts.xcodeproj/project.pbxproj | 20 +++ Chuck Norris Facts/App/AppDelegate.swift | 27 +++- .../Data/Networking/FactsAPI.swift | 10 +- Chuck Norris Facts/Extensions/Data+Stub.swift | 24 ++- .../Library/LaunchArgument.swift | 23 +++ Chuck Norris Facts/Resources/Stubs/facts.json | 150 ++++++++++++++++++ .../Facts/FactsList/FactsListViewModel.swift | 17 -- .../XCUIApplication+LaunchArgument.swift | 28 ++++ .../Tests/FactsListUITests.swift | 7 +- .../Tests/SearchFactsUITests.swift | 12 +- 10 files changed, 283 insertions(+), 35 deletions(-) create mode 100644 Chuck Norris Facts/Library/LaunchArgument.swift create mode 100644 Chuck Norris Facts/Resources/Stubs/facts.json create mode 100644 Chuck Norris FactsUITests/Library/XCUIApplication+LaunchArgument.swift diff --git a/Chuck Norris Facts.xcodeproj/project.pbxproj b/Chuck Norris Facts.xcodeproj/project.pbxproj index 83e5166..3d91b82 100644 --- a/Chuck Norris Facts.xcodeproj/project.pbxproj +++ b/Chuck Norris Facts.xcodeproj/project.pbxproj @@ -8,6 +8,9 @@ /* Begin PBXBuildFile section */ 1E0C7B402543A4E8002D5C47 /* FactEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E0C7B3F2543A4E8002D5C47 /* FactEntity.swift */; }; + 1E135FAA254B4E66009D18AF /* LaunchArgument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E135FA9254B4E66009D18AF /* LaunchArgument.swift */; }; + 1E135FAD254B4E9F009D18AF /* XCUIApplication+LaunchArgument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E135FAC254B4E9F009D18AF /* XCUIApplication+LaunchArgument.swift */; }; + 1E135FAF254B52E0009D18AF /* facts.json in Resources */ = {isa = PBXBuildFile; fileRef = 1E135FAE254B52E0009D18AF /* facts.json */; }; 1E23683E253FB07200BE17F3 /* FactCategoryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E23683D253FB07200BE17F3 /* FactCategoryCell.swift */; }; 1E236840253FB13A00BE17F3 /* FactCategoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E23683F253FB13A00BE17F3 /* FactCategoryViewModel.swift */; }; 1E32758F2532A2C0007E838A /* EmptyListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E32758E2532A2C0007E838A /* EmptyListView.swift */; }; @@ -91,6 +94,9 @@ /* Begin PBXFileReference section */ 02186229E02F0A408D2EE0C2 /* Pods_Chuck_Norris_Facts.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Chuck_Norris_Facts.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1E0C7B3F2543A4E8002D5C47 /* FactEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactEntity.swift; sourceTree = ""; }; + 1E135FA9254B4E66009D18AF /* LaunchArgument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchArgument.swift; sourceTree = ""; }; + 1E135FAC254B4E9F009D18AF /* XCUIApplication+LaunchArgument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCUIApplication+LaunchArgument.swift"; sourceTree = ""; }; + 1E135FAE254B52E0009D18AF /* facts.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = facts.json; sourceTree = ""; }; 1E23683D253FB07200BE17F3 /* FactCategoryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactCategoryCell.swift; sourceTree = ""; }; 1E23683F253FB13A00BE17F3 /* FactCategoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactCategoryViewModel.swift; sourceTree = ""; }; 1E32758E2532A2C0007E838A /* EmptyListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyListView.swift; sourceTree = ""; }; @@ -193,6 +199,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 1E135FAB254B4E92009D18AF /* Library */ = { + isa = PBXGroup; + children = ( + 1E135FAC254B4E9F009D18AF /* XCUIApplication+LaunchArgument.swift */, + ); + path = Library; + sourceTree = ""; + }; 1E23683C253FB05100BE17F3 /* PastSearch */ = { isa = PBXGroup; children = ( @@ -454,6 +468,7 @@ 1EE0715D25314AF600F6BF6D /* Chuck Norris FactsUITests */ = { isa = PBXGroup; children = ( + 1E135FAB254B4E92009D18AF /* Library */, 1ED5D1A02534B0E80035046C /* Tests */, 1ED5D19D2534B0D60035046C /* Scenes */, 1EE0716025314AF600F6BF6D /* Info.plist */, @@ -498,6 +513,7 @@ 1EFE288825321123008806B9 /* JSON.swift */, 1EACEC9D25364B6C0006B36D /* ActivityIndicator.swift */, 1E8A0FF32547768500565A86 /* DynamicHeightCollectionView.swift */, + 1E135FA9254B4E66009D18AF /* LaunchArgument.swift */, ); path = Library; sourceTree = ""; @@ -507,6 +523,7 @@ children = ( 1EFE287D25321071008806B9 /* search-facts.json */, 1E5617272540FAF200BF26A0 /* get-categories.json */, + 1E135FAE254B52E0009D18AF /* facts.json */, ); path = Stubs; sourceTree = ""; @@ -704,6 +721,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1E135FAF254B52E0009D18AF /* facts.json in Resources */, 1EFE287E25321071008806B9 /* search-facts.json in Resources */, 1EE0714925314AF600F6BF6D /* LaunchScreen.storyboard in Resources */, 1E5617282540FAF200BF26A0 /* get-categories.json in Resources */, @@ -911,6 +929,7 @@ 1E92112D253F6D0000DB340B /* FactsService.swift in Sources */, 1EFE2892253214DB008806B9 /* FactsListViewModel.swift in Sources */, 1EACEC9E25364B6C0006B36D /* ActivityIndicator.swift in Sources */, + 1E135FAA254B4E66009D18AF /* LaunchArgument.swift in Sources */, 1EFE2867253206D3008806B9 /* BaseCoordinator.swift in Sources */, 1E92113B253F90BF00DB340B /* FactCategory.swift in Sources */, ); @@ -937,6 +956,7 @@ files = ( 1ED5D1A22534B0F40035046C /* FactsListUITests.swift in Sources */, 1ED5D19F2534B0E30035046C /* FactsListScene.swift in Sources */, + 1E135FAD254B4E9F009D18AF /* XCUIApplication+LaunchArgument.swift in Sources */, 1E92112F253F7A0B00DB340B /* SearchFactsScene.swift in Sources */, 1E921131253F7AAA00DB340B /* SearchFactsUITests.swift in Sources */, ); diff --git a/Chuck Norris Facts/App/AppDelegate.swift b/Chuck Norris Facts/App/AppDelegate.swift index eafb22e..c99a550 100644 --- a/Chuck Norris Facts/App/AppDelegate.swift +++ b/Chuck Norris Facts/App/AppDelegate.swift @@ -17,14 +17,35 @@ class AppDelegate: UIResponder, UIApplicationDelegate { didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - if CommandLine.arguments.contains("--reset-storage") { + processArguments() + + return true + } + + private func processArguments() { + if LaunchArgument.check(.uiTest) { + UIView.setAnimationsEnabled(false) + } + + if LaunchArgument.check(.resetData) { let realm = try? Realm() try? realm?.write { realm?.deleteAll() } } - return true - } + if LaunchArgument.check(.mockStorage) { + let facts = Data.stub("facts", type: [Fact].self) ?? [] + + let entities = [ + SearchEntity(searchTerm: "games", facts: facts), + SearchEntity(searchTerm: "fashion", facts: facts) + ] + let realm = try? Realm() + try? realm?.write { + realm?.add(entities, update: .modified) + } + } + } } diff --git a/Chuck Norris Facts/Data/Networking/FactsAPI.swift b/Chuck Norris Facts/Data/Networking/FactsAPI.swift index 09be7df..719fbcd 100644 --- a/Chuck Norris Facts/Data/Networking/FactsAPI.swift +++ b/Chuck Norris Facts/Data/Networking/FactsAPI.swift @@ -51,16 +51,10 @@ extension FactsAPI: TargetType { var sampleData: Data { switch self { case .getCategories: - if let data = try? Data.stub("get-categories") { - return data - } + return Data.stub("get-categories") ?? Data() case .searchFacts: - if let data = try? Data.stub("search-facts") { - return data - } + return Data.stub("search-facts") ?? Data() } - - return Data() } } diff --git a/Chuck Norris Facts/Extensions/Data+Stub.swift b/Chuck Norris Facts/Extensions/Data+Stub.swift index b15c63e..c3ddfa8 100644 --- a/Chuck Norris Facts/Extensions/Data+Stub.swift +++ b/Chuck Norris Facts/Extensions/Data+Stub.swift @@ -10,12 +10,30 @@ import Foundation extension Data { - static func stub(_ resource: String) throws -> Data? { + static func stub(_ resource: String) -> Data? { guard let url = Bundle.main.url(forResource: resource, withExtension: ".json") else { return nil } - let data = try Data(contentsOf: url) - return data + do { + let data = try Data(contentsOf: url) + return data + } catch { + return nil + } + } + + static func stub(_ resource: String, type: T.Type, decoder: JSONDecoder = JSON.decoder) -> T? { + guard let url = Bundle.main.url(forResource: resource, withExtension: ".json") else { + return nil + } + + do { + let data = try Data(contentsOf: url) + let stub = try decoder.decode(type, from: data) + return stub + } catch { + return nil + } } } diff --git a/Chuck Norris Facts/Library/LaunchArgument.swift b/Chuck Norris Facts/Library/LaunchArgument.swift new file mode 100644 index 0000000..03f72de --- /dev/null +++ b/Chuck Norris Facts/Library/LaunchArgument.swift @@ -0,0 +1,23 @@ +// +// LaunchArgument.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/29/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +enum LaunchArgument: String { + // UI Testing + case uiTest = "--ui-test" + + // Reset storage + case resetData = "--reset-data" + + // Mock storage data + case mockStorage = "--mock-storage" + + // Check if there is an argument on CommandLine arguments + static func check(_ argument: LaunchArgument) -> Bool { + CommandLine.arguments.contains(argument.rawValue) + } +} diff --git a/Chuck Norris Facts/Resources/Stubs/facts.json b/Chuck Norris Facts/Resources/Stubs/facts.json new file mode 100644 index 0000000..1b7c6d5 --- /dev/null +++ b/Chuck Norris Facts/Resources/Stubs/facts.json @@ -0,0 +1,150 @@ +[ + { + "categories": [ + "movie" + ], + "created_at": "2020-01-05 13:42:19.576875", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "sudkgw_tr_ejehjag7cqwq", + "updated_at": "2020-01-05 13:42:19.576875", + "url": "https:\/\/api.chucknorris.io\/jokes\/sudkgw_tr_ejehjag7cqwq", + "value": "The opening scene of the movie \"Saving Private Ryan\" is loosely based on games of dodgeball Chuck Norris played in second grade." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:19.576875", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "H7lHICEVSsW25ffciJEjxw", + "updated_at": "2020-01-05 13:42:19.576875", + "url": "https:\/\/api.chucknorris.io\/jokes\/H7lHICEVSsW25ffciJEjxw", + "value": "Chuck Norris can play Xbox Kinect games on his PlayStation4 and PlayStation Move games on his Xbox 720." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:20.568859", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "0fvCgPtrRqe3BzC8jxEkUA", + "updated_at": "2020-01-05 13:42:20.568859", + "url": "https:\/\/api.chucknorris.io\/jokes\/0fvCgPtrRqe3BzC8jxEkUA", + "value": "Chuck Norris doesn't need to play games against people to beat their high scores. He just plays with himself and beats every highscore on every game on every console in the whole entire universe." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:21.795084", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "2COz4ZY4SJaM7WKJUmSZ3Q", + "updated_at": "2020-01-05 13:42:21.795084", + "url": "https:\/\/api.chucknorris.io\/jokes\/2COz4ZY4SJaM7WKJUmSZ3Q", + "value": "Michael Phelps currently holds the record for most Olympic gold medals in a single Games with 8. That record will be broken in 2012, when Chuck Norris wins 22." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:23.484083", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "eOcHK252SCmv6T5MsJiexA", + "updated_at": "2020-01-05 13:42:23.484083", + "url": "https:\/\/api.chucknorris.io\/jokes\/eOcHK252SCmv6T5MsJiexA", + "value": "Why did Chuck Norris hasn't appeared on any mortal kombat games. Simple, the name says it all. \"mortal\". Also there won't be any fatality tha will work on him, he will just roundhouse kick anyone either he wins or loose." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:23.484083", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "BUBK6qDSRqWevu0YGEEZvw", + "updated_at": "2020-01-05 13:42:23.484083", + "url": "https:\/\/api.chucknorris.io\/jokes\/BUBK6qDSRqWevu0YGEEZvw", + "value": "Chuck Norris can fight better than all fighting video games. How? He instantly wins." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:25.099703", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "pYt9of-uQPqyPhk85Z-zUA", + "updated_at": "2020-01-05 13:42:25.099703", + "url": "https:\/\/api.chucknorris.io\/jokes\/pYt9of-uQPqyPhk85Z-zUA", + "value": "If Chuck Norris were a PC or Mac he'd be a Mac because you can't play games with Chuck Norris" + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:25.628594", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "x6hL23bhTEK03DUlagsIUQ", + "updated_at": "2020-01-05 13:42:25.628594", + "url": "https:\/\/api.chucknorris.io\/jokes\/x6hL23bhTEK03DUlagsIUQ", + "value": "Chuck Norris enjoys playing backyard games with his grandchildren. They often play badminton. But instead of using little sissy racquets & a plastic birdie, they use boat oars & dead chickens." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:25.905626", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "-j_jS99eTIi7hrDpRQ9qLw", + "updated_at": "2020-01-05 13:42:25.905626", + "url": "https:\/\/api.chucknorris.io\/jokes\/-j_jS99eTIi7hrDpRQ9qLw", + "value": "Chuck Norris is forbidden from competing in paintball games... for very fucking obvious reasons." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:26.194739", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "20QgKwidT1-ySGoHJCpwSw", + "updated_at": "2020-01-05 13:42:26.194739", + "url": "https:\/\/api.chucknorris.io\/jokes\/20QgKwidT1-ySGoHJCpwSw", + "value": "It's all fun and games until Chuck Norris pulls your eyes out with a socket wrench." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:26.766831", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "RB2hbqTzTd2ORXy53ITqqQ", + "updated_at": "2020-01-05 13:42:26.766831", + "url": "https:\/\/api.chucknorris.io\/jokes\/RB2hbqTzTd2ORXy53ITqqQ", + "value": "Chuck Norris finished every Call of Duty games in less than 15 minutes..........without shooting a single bullet." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:26.766831", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "1Iy7_hYKT5GgOfxkYuTK3A", + "updated_at": "2020-01-05 13:42:26.766831", + "url": "https:\/\/api.chucknorris.io\/jokes\/1Iy7_hYKT5GgOfxkYuTK3A", + "value": "Chuck Norris is unstoppable in all games of Call of Duty" + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:27.496799", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "4QsnKWP-QFar62XWvYTTsw", + "updated_at": "2020-01-05 13:42:27.496799", + "url": "https:\/\/api.chucknorris.io\/jokes\/4QsnKWP-QFar62XWvYTTsw", + "value": "Chuck Norris invented the olympic games. with his left pinky." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:28.664997", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "R3xVlG1FR7qySlLqsK5Yjw", + "updated_at": "2020-01-05 13:42:28.664997", + "url": "https:\/\/api.chucknorris.io\/jokes\/R3xVlG1FR7qySlLqsK5Yjw", + "value": "Chuck Norris' last birthday party was held at the La Brea Tar Pits where he enjoyed all of the party games and easily won the 'dunking for dinosaurs' event." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:29.296379", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "baXxcGBqQG6an7udXMTQWA", + "updated_at": "2020-01-05 13:42:29.296379", + "url": "https:\/\/api.chucknorris.io\/jokes\/baXxcGBqQG6an7udXMTQWA", + "value": "When Chuck Norris plays a game, every minute is potentially \"Sudden Death\" for his opponents...including cards and board games." + }, + { + "categories": [ + "celebrity" + ], + "created_at": "2020-01-05 13:42:29.855523", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "l7QlUREJQzOIJVB88DY9jg", + "updated_at": "2020-01-05 13:42:29.855523", + "url": "https:\/\/api.chucknorris.io\/jokes\/l7QlUREJQzOIJVB88DY9jg", + "value": "Chuck Norris was at the X-games getting ready for competition when he got a message from Paris Hilton saying that she had sent him a friend request on MySpace. An infuriated Chuck Norris logged on to MySpace using his skateboard and rejected the request immediately." + } +] diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift index 97c92b1..6b2b962 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift @@ -73,23 +73,6 @@ final class FactsListViewModel { self.facts = Observable.combineLatest(viewDidAppearSubject, searchTermSubject) .flatMapLatest { _, searchTerm -> Observable<[Fact]> in - if CommandLine.arguments.contains("--search-facts") { - let bundle = Bundle(for: Self.self) - - guard let url = bundle.url(forResource: "search-facts", withExtension: ".json") else { - return .just([]) - } - - let data = try Data(contentsOf: url) - let stub = try JSON.decoder.decode(SearchFactsResponse.self, from: data) - let facts = stub.facts.shuffled().prefix(10) - return .just(Array(facts)) - } - - if CommandLine.arguments.contains("--empty-facts") { - return .just([]) - } - let facts = factsService.retrieveFacts(searchTerm: searchTerm) if searchTerm.isEmpty { return facts.map { Array($0.shuffled().prefix(10)) } diff --git a/Chuck Norris FactsUITests/Library/XCUIApplication+LaunchArgument.swift b/Chuck Norris FactsUITests/Library/XCUIApplication+LaunchArgument.swift new file mode 100644 index 0000000..4e560d8 --- /dev/null +++ b/Chuck Norris FactsUITests/Library/XCUIApplication+LaunchArgument.swift @@ -0,0 +1,28 @@ +// +// XCUIApplication+LaunchArgument.swift +// Chuck Norris FactsUITests +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/29/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation +import XCTest + +enum LaunchArgument: String { + // UI Testing + case uiTest = "--ui-test" + + // Reset storage + case resetData = "--reset-data" + + // Mock storage data + case mockStorage = "--mock-storage" +} + +extension XCUIApplication { + // Set Launch Arguments to App Command Line arguments + func setLaunchArguments(_ arguments: [LaunchArgument]) { + launchArguments = arguments.map { $0.rawValue } + } +} diff --git a/Chuck Norris FactsUITests/Tests/FactsListUITests.swift b/Chuck Norris FactsUITests/Tests/FactsListUITests.swift index b93f04b..7cd6cda 100644 --- a/Chuck Norris FactsUITests/Tests/FactsListUITests.swift +++ b/Chuck Norris FactsUITests/Tests/FactsListUITests.swift @@ -19,7 +19,7 @@ final class FactsListUITests: XCTestCase { } func test_showEmptyView() throws { - app.launchArguments = ["--empty-facts"] + app.setLaunchArguments([.uiTest, .resetData]) app.launch() let factsListScene = FactsListScene() @@ -29,7 +29,7 @@ final class FactsListUITests: XCTestCase { } func test_show10RandomFacts() { - app.launchArguments = ["--search-facts"] + app.setLaunchArguments([.uiTest, .mockStorage]) app.launch() let factsListScene = FactsListScene() @@ -38,7 +38,7 @@ final class FactsListUITests: XCTestCase { } func test_shareFact() { - app.launchArguments = ["--search-facts"] + app.setLaunchArguments([.uiTest, .mockStorage]) app.launch() let factsListScene = FactsListScene() @@ -58,6 +58,7 @@ final class FactsListUITests: XCTestCase { } func test_searchFacts() { + app.setLaunchArguments([.uiTest, .mockStorage]) app.launch() let factsListScene = FactsListScene() diff --git a/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift b/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift index c4e159f..3911b2c 100644 --- a/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift +++ b/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift @@ -19,6 +19,7 @@ final class SearchFactsUITests: XCTestCase { } func test_searchFactsUsingSearchBar() throws { + app.setLaunchArguments([.uiTest, .mockStorage]) app.launch() let factsListScene = FactsListScene() @@ -36,6 +37,7 @@ final class SearchFactsUITests: XCTestCase { } func test_cancelSearchFacts() throws { + app.setLaunchArguments([.uiTest, .mockStorage]) app.launch() let factsListScene = FactsListScene() @@ -48,6 +50,7 @@ final class SearchFactsUITests: XCTestCase { } func test_shouldShow8FactCategories() { + app.setLaunchArguments([.uiTest, .mockStorage]) app.launch() let factsListScene = FactsListScene() @@ -62,6 +65,7 @@ final class SearchFactsUITests: XCTestCase { } func test_tapFactCategoryShouldSearchByTerm() { + app.setLaunchArguments([.uiTest, .mockStorage]) app.launch() let factsListScene = FactsListScene() @@ -84,6 +88,7 @@ final class SearchFactsUITests: XCTestCase { } func test_tapPastSearchShouldSearchByTerm() { + app.setLaunchArguments([.uiTest, .mockStorage]) app.launch() let factsListScene = FactsListScene() @@ -106,6 +111,7 @@ final class SearchFactsUITests: XCTestCase { } func test_tapPastSearchShouldOrderByDate() { + app.setLaunchArguments([.uiTest, .mockStorage]) app.launch() let factsListScene = FactsListScene() @@ -121,6 +127,8 @@ final class SearchFactsUITests: XCTestCase { secondItem.tap() + factsListScene.searchButton.tap() + let firstItem = searchFactsCells.element(boundBy: 1) XCTAssertTrue(firstItem.exists) @@ -128,9 +136,11 @@ final class SearchFactsUITests: XCTestCase { } func test_pastSearchShouldBeHiddenOnFirstAccess() { - app.launchArguments = ["--reset-storage"] + app.setLaunchArguments([.uiTest, .resetData]) app.launch() + sleep(3) + let factsListScene = FactsListScene() factsListScene.searchButton.tap() From f4b25012dea6f3836daa92e9608ad85cd90bbab9 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Thu, 29 Oct 2020 21:45:43 -0300 Subject: [PATCH 12/18] Localize Strings (#28) * Localize Strings * Add SwiftGen to documentation --- Chuck Norris Facts.xcodeproj/project.pbxproj | 35 +++++++++++ .../Resources/Generated/Strings.swift | 62 +++++++++++++++++++ .../Resources/Localizable.strings | 17 +++++ .../Facts/FactsList/Fact/FactViewModel.swift | 2 +- .../FactsList/FactsListViewController.swift | 2 +- .../Facts/FactsList/Views/EmptyListView.swift | 2 +- .../SearchFactsTableViewSection.swift | 4 +- .../SearchFactsViewController.swift | 2 +- Podfile | 1 + Podfile.lock | 6 +- README.md | 1 + swiftgen.yml | 5 ++ 12 files changed, 132 insertions(+), 7 deletions(-) create mode 100644 Chuck Norris Facts/Resources/Generated/Strings.swift create mode 100644 Chuck Norris Facts/Resources/Localizable.strings create mode 100644 swiftgen.yml diff --git a/Chuck Norris Facts.xcodeproj/project.pbxproj b/Chuck Norris Facts.xcodeproj/project.pbxproj index 3d91b82..ce80239 100644 --- a/Chuck Norris Facts.xcodeproj/project.pbxproj +++ b/Chuck Norris Facts.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 1E5617282540FAF200BF26A0 /* get-categories.json in Resources */ = {isa = PBXBuildFile; fileRef = 1E5617272540FAF200BF26A0 /* get-categories.json */; }; 1E56172C2541007500BF26A0 /* Data+Stub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E56172B2541007500BF26A0 /* Data+Stub.swift */; }; 1E56172E2541039B00BF26A0 /* SearchFactsViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E56172D2541039B00BF26A0 /* SearchFactsViewControllerTests.swift */; }; + 1E580BC8254B92E600886A2E /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1E580BC7254B92E600886A2E /* Localizable.strings */; }; 1E7F15BE253324CF0006887B /* FactsListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7F15BD253324CF0006887B /* FactsListViewModelTests.swift */; }; 1E7F15C0253324FD0006887B /* FactsListViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7F15BF253324FD0006887B /* FactsListViewControllerTests.swift */; }; 1E7F15C6253329780006887B /* FactViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7F15C5253329780006887B /* FactViewModelTests.swift */; }; @@ -38,6 +39,7 @@ 1E921139253F909700DB340B /* FactsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E921138253F909700DB340B /* FactsStorage.swift */; }; 1E92113B253F90BF00DB340B /* FactCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E92113A253F90BF00DB340B /* FactCategory.swift */; }; 1E92113E253F915100DB340B /* FactCategoryEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E92113D253F915100DB340B /* FactCategoryEntity.swift */; }; + 1EA3AB02254B956C004A877B /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA3AB01254B956C004A877B /* Strings.swift */; }; 1EAB20AF2540BEC400633382 /* SuggestionsViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAB20AE2540BEC400633382 /* SuggestionsViewFlowLayout.swift */; }; 1EACEC99253649BD0006B36D /* loading.json in Resources */ = {isa = PBXBuildFile; fileRef = 1EACEC98253649BD0006B36D /* loading.json */; }; 1EACEC9E25364B6C0006B36D /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EACEC9D25364B6C0006B36D /* ActivityIndicator.swift */; }; @@ -108,6 +110,7 @@ 1E5617272540FAF200BF26A0 /* get-categories.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "get-categories.json"; sourceTree = ""; }; 1E56172B2541007500BF26A0 /* Data+Stub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Stub.swift"; sourceTree = ""; }; 1E56172D2541039B00BF26A0 /* SearchFactsViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFactsViewControllerTests.swift; sourceTree = ""; }; + 1E580BC7254B92E600886A2E /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = ""; }; 1E7F15BD253324CF0006887B /* FactsListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListViewModelTests.swift; sourceTree = ""; }; 1E7F15BF253324FD0006887B /* FactsListViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListViewControllerTests.swift; sourceTree = ""; }; 1E7F15C5253329780006887B /* FactViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactViewModelTests.swift; sourceTree = ""; }; @@ -124,6 +127,7 @@ 1E921138253F909700DB340B /* FactsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsStorage.swift; sourceTree = ""; }; 1E92113A253F90BF00DB340B /* FactCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactCategory.swift; sourceTree = ""; }; 1E92113D253F915100DB340B /* FactCategoryEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactCategoryEntity.swift; sourceTree = ""; }; + 1EA3AB01254B956C004A877B /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; 1EAB20AE2540BEC400633382 /* SuggestionsViewFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionsViewFlowLayout.swift; sourceTree = ""; }; 1EACEC98253649BD0006B36D /* loading.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = loading.json; sourceTree = ""; }; 1EACEC9D25364B6C0006B36D /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = ""; }; @@ -363,6 +367,14 @@ path = Entities; sourceTree = ""; }; + 1EA3AB00254B955F004A877B /* Generated */ = { + isa = PBXGroup; + children = ( + 1EA3AB01254B956C004A877B /* Strings.swift */, + ); + path = Generated; + sourceTree = ""; + }; 1ED5D18B25348FC40035046C /* Library */ = { isa = PBXGroup; children = ( @@ -479,11 +491,13 @@ 1EFE286025320614008806B9 /* Resources */ = { isa = PBXGroup; children = ( + 1EA3AB00254B955F004A877B /* Generated */, 1E3275902532A2C4007E838A /* Animations */, 1EFE287C2532105D008806B9 /* Stubs */, 1EE0714525314AF600F6BF6D /* Assets.xcassets */, 1EE0714725314AF600F6BF6D /* LaunchScreen.storyboard */, 1EE0714A25314AF600F6BF6D /* Info.plist */, + 1E580BC7254B92E600886A2E /* Localizable.strings */, ); path = Resources; sourceTree = ""; @@ -622,6 +636,7 @@ 1EE0713625314AF500F6BF6D /* Frameworks */, 1EE0713725314AF500F6BF6D /* Resources */, 1EFBC28C2531F8FB00594676 /* SwiftLint */, + 1E580BC6254B919900886A2E /* SwiftGen */, 735DB507838707E07E18E902 /* [CP] Embed Pods Frameworks */, ); buildRules = ( @@ -726,6 +741,7 @@ 1EE0714925314AF600F6BF6D /* LaunchScreen.storyboard in Resources */, 1E5617282540FAF200BF26A0 /* get-categories.json in Resources */, 1EE0714625314AF600F6BF6D /* Assets.xcassets in Resources */, + 1E580BC8254B92E600886A2E /* Localizable.strings in Resources */, 1E3275922532A2CD007E838A /* empty-box.json in Resources */, 1EACEC99253649BD0006B36D /* loading.json in Resources */, ); @@ -774,6 +790,24 @@ 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; }; + 1E580BC6254B919900886A2E /* SwiftGen */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = SwiftGen; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [[ -f \"${PODS_ROOT}/SwiftGen/bin/swiftgen\" ]]; then\n \"${PODS_ROOT}/SwiftGen/bin/swiftgen\"\nelse\n echo \"warning: SwiftGen is not installed. Run 'pod install --repo-update' to install it.\"\nfi\n"; + }; 1EFBC28C2531F8FB00594676 /* SwiftLint */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -901,6 +935,7 @@ 1EFE288925321123008806B9 /* JSON.swift in Sources */, 1ED06C952548AAD300139151 /* LoadingView.swift in Sources */, 1EFE288725321119008806B9 /* Fact.swift in Sources */, + 1EA3AB02254B956C004A877B /* Strings.swift in Sources */, 1EFE289725321CE2008806B9 /* FactCell.swift in Sources */, 1E56172C2541007500BF26A0 /* Data+Stub.swift in Sources */, 1E8A0FEE2547603700565A86 /* SuggestionsCell.swift in Sources */, diff --git a/Chuck Norris Facts/Resources/Generated/Strings.swift b/Chuck Norris Facts/Resources/Generated/Strings.swift new file mode 100644 index 0000000..de360ca --- /dev/null +++ b/Chuck Norris Facts/Resources/Generated/Strings.swift @@ -0,0 +1,62 @@ +// swiftlint:disable all +// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen + +import Foundation + +// swiftlint:disable superfluous_disable_command file_length implicit_return + +// MARK: - Strings + +// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length +// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces +internal enum L10n { + + internal enum EmptyView { + /// Looks like there are no Facts + internal static let text = L10n.tr("Localizable", "EmptyView.text") + } + + internal enum FactCategory { + /// UNCATEGORIZED + internal static let uncategorized = L10n.tr("Localizable", "FactCategory.uncategorized") + } + + internal enum FactsList { + /// Chuck Norris Facts + internal static let title = L10n.tr("Localizable", "FactsList.title") + } + + internal enum SearchFacts { + /// Search + internal static let title = L10n.tr("Localizable", "SearchFacts.title") + internal enum Sections { + /// Past Searches + internal static let pastSearches = L10n.tr("Localizable", "SearchFacts.sections.pastSearches") + /// Suggestions + internal static let suggestions = L10n.tr("Localizable", "SearchFacts.sections.suggestions") + } + } +} +// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length +// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces + +// MARK: - Implementation Details + +extension L10n { + private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String { + let format = BundleToken.bundle.localizedString(forKey: key, value: nil, table: table) + return String(format: format, locale: Locale.current, arguments: args) + } +} + +// swiftlint:disable convenience_type +private final class BundleToken { + static let bundle: Bundle = { + #if SWIFT_PACKAGE + return Bundle.module + #else + return Bundle(for: BundleToken.self) + #endif + }() +} +// swiftlint:enable convenience_type diff --git a/Chuck Norris Facts/Resources/Localizable.strings b/Chuck Norris Facts/Resources/Localizable.strings new file mode 100644 index 0000000..412a7f7 --- /dev/null +++ b/Chuck Norris Facts/Resources/Localizable.strings @@ -0,0 +1,17 @@ +/* + Localizable.strings + Chuck Norris Facts + + Created by Djorkaeff Alexandre Vilela Pereira on 10/29/20. + Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +*/ + +"FactsList.title" = "Chuck Norris Facts"; + +"SearchFacts.title" = "Search"; +"SearchFacts.sections.suggestions" = "Suggestions"; +"SearchFacts.sections.pastSearches" = "Past Searches"; + +"EmptyView.text" = "Looks like there are no Facts"; + +"FactCategory.uncategorized" = "UNCATEGORIZED"; diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/Fact/FactViewModel.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/Fact/FactViewModel.swift index 973d651..b92ec9e 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/Fact/FactViewModel.swift +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/Fact/FactViewModel.swift @@ -19,7 +19,7 @@ final class FactViewModel { if let factUrl = fact.url { self.url = URL(string: factUrl) } - self.category = fact.categories.first?.text.uppercased() ?? "UNCATEGORIZED" + self.category = fact.categories.first?.text.uppercased() ?? L10n.FactCategory.uncategorized } } diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift index 6a99f8a..4a5d358 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift @@ -93,7 +93,7 @@ class FactsListViewController: UIViewController { } private func setupNavigationBar() { - navigationItem.title = "Chuck Norris Facts" + navigationItem.title = L10n.FactsList.title navigationItem.rightBarButtonItem = searchButton navigationController?.navigationBar.prefersLargeTitles = true diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/Views/EmptyListView.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/Views/EmptyListView.swift index 1b44f36..cce6fbb 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/Views/EmptyListView.swift +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/Views/EmptyListView.swift @@ -27,7 +27,7 @@ final class EmptyListView: UIView { label.lineBreakMode = .byWordWrapping label.numberOfLines = 0 - label.text = "Looks like there are no Facts" + label.text = L10n.EmptyView.text label.font = UIFont.systemFont(ofSize: 18, weight: .semibold) return label diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsTableViewSection.swift b/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsTableViewSection.swift index fbaf7ce..6db825d 100644 --- a/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsTableViewSection.swift +++ b/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsTableViewSection.swift @@ -35,9 +35,9 @@ extension SearchFactsTableViewSection: SectionModelType { var header: String { switch self { case .SuggestionsSection: - return "Suggestions" + return L10n.SearchFacts.Sections.suggestions case .PastSearchesSection: - return "Past Searches" + return L10n.SearchFacts.Sections.pastSearches } } diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewController.swift b/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewController.swift index 9a93067..a6a9f28 100644 --- a/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewController.swift +++ b/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewController.swift @@ -103,7 +103,7 @@ final class SearchFactsViewController: UIViewController { navigationItem.hidesSearchBarWhenScrolling = false navigationItem.searchController = searchController navigationItem.leftBarButtonItem = cancelButton - navigationItem.title = "Search" + navigationItem.title = L10n.SearchFacts.title } private func setupBindings() { diff --git a/Podfile b/Podfile index 177d3b2..dc5a739 100644 --- a/Podfile +++ b/Podfile @@ -19,6 +19,7 @@ target 'Chuck Norris Facts' do # Tools pod 'SwiftLint' + pod 'SwiftGen' # UI pod 'lottie-ios' diff --git a/Podfile.lock b/Podfile.lock index 1cfac24..8f69904 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -29,6 +29,7 @@ PODS: - RxSwift (5.1.1) - RxTest (5.1.1): - RxSwift (~> 5) + - SwiftGen (6.4.0) - SwiftLint (0.40.3) DEPENDENCIES: @@ -41,6 +42,7 @@ DEPENDENCIES: - RxRealm - RxSwift - RxTest + - SwiftGen - SwiftLint SPEC REPOS: @@ -58,6 +60,7 @@ SPEC REPOS: - RxRelay - RxSwift - RxTest + - SwiftGen - SwiftLint SPEC CHECKSUMS: @@ -74,8 +77,9 @@ SPEC CHECKSUMS: RxRelay: d77f7d771495f43c556cbc43eebd1bb54d01e8e9 RxSwift: 81470a2074fa8780320ea5fe4102807cb7118178 RxTest: 711632d5644dffbeb62c936a521b5b008a1e1faa + SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108 SwiftLint: dfd554ff0dff17288ee574814ccdd5cea85d76f7 -PODFILE CHECKSUM: 880153bdb3f2b2ce7a5e293d253fab39cc855c10 +PODFILE CHECKSUM: 002646d5407246e73467489d3ab8bd3ae6552551 COCOAPODS: 1.9.3 diff --git a/README.md b/README.md index b3ae18e..5cf80ba 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ Using [RxSwift](https://github.com/ReactiveX/RxSwift) and [RxRealm](https://gith - [RxDataSources](https://github.com/RxSwiftCommunity/RxDataSources) UITableView and UICollectionView Data Sources for RxSwift. - [RxRealm](https://github.com/RxSwiftCommunity/RxRealm) RxSwift extension for RealmSwift's types. - [SwiftLint](https://github.com/realm/SwiftLint) A tool to enforce Swift style and conventions. +- [SwiftGen](https://github.com/SwiftGen/SwiftGen) The Swift code generator for Localizable.strings. - [Lottie](https://github.com/airbnb/lottie-ios) An iOS library to natively render After Effects vector animations. - [Moya](https://github.com/Moya/Moya) Network abstraction layer written in Swift. - [RealmSwift](https://github.com/realm/realm-cocoa) Realm is a mobile database: a replacement for Core Data & SQLite. diff --git a/swiftgen.yml b/swiftgen.yml new file mode 100644 index 0000000..a9b1e46 --- /dev/null +++ b/swiftgen.yml @@ -0,0 +1,5 @@ +strings: + inputs: Chuck Norris Facts/Resources/Localizable.strings + outputs: + - templateName: structured-swift5 + output: Chuck Norris Facts/Resources/Generated/Strings.swift \ No newline at end of file From bd30b6eee8436bafadf999b8028c4c44aa99e8fa Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Thu, 29 Oct 2020 22:22:33 -0300 Subject: [PATCH 13/18] Handle errors (#26) * Add Animation * Add a search button to EmptyList View * Create ErrorView of FactsList * FactsListError * Customize EmptyList View * Improve dequeueReusableCell * Improve dequeueReusableCell * Handle no connection states * Check if there is some categories before request it * Adjust current tests * Remove moya imports of Scenes folder * Improve FactsListError * Don't hide tableView when EmptyView is showed * Write tests to ErrorView * Fix tests * Fix categories test --- Chuck Norris Facts.xcodeproj/project.pbxproj | 28 ++++++ .../Data/Networking/HTTPError.swift | 12 +++ .../Data/Services/FactsService.swift | 62 +++++++++++-- .../Extensions/MoyaError+Code.swift | 30 +++++++ .../UICollectionView+Extensions.swift | 19 ++++ .../Extensions/UITableView+Extensions.swift | 19 ++++ .../Library/LaunchArgument.swift | 6 ++ .../Resources/Animations/error.json | 1 + .../Resources/Generated/Strings.swift | 11 ++- .../Resources/Localizable.strings | 6 +- .../Facts/FactsList/Fact/FactCell.swift | 10 ++- .../Facts/FactsList/FactsListError.swift | 44 ++++++++++ .../FactsList/FactsListViewController.swift | 86 ++++++++++++++----- .../Facts/FactsList/FactsListViewModel.swift | 36 ++++++-- .../Facts/FactsList/Views/EmptyListView.swift | 23 ++++- .../Facts/FactsList/Views/ErrorView.swift | 81 +++++++++++++++++ .../SearchFactsViewController.swift | 13 ++- .../FactCategory/FactCategoryCell.swift | 2 - .../Suggestions/SuggestionsCell.swift | 19 ++-- .../Data/Services/FactsServiceTests.swift | 2 +- .../FactsListViewControllerTests.swift | 1 - .../FactsList/FactsListViewModelTests.swift | 11 +-- .../XCUIApplication+LaunchArgument.swift | 6 ++ .../Scenes/FactsListScene.swift | 4 + .../Tests/FactsListUITests.swift | 22 +++++ .../Tests/SearchFactsUITests.swift | 18 ++-- 26 files changed, 486 insertions(+), 86 deletions(-) create mode 100644 Chuck Norris Facts/Data/Networking/HTTPError.swift create mode 100644 Chuck Norris Facts/Extensions/MoyaError+Code.swift create mode 100644 Chuck Norris Facts/Extensions/UICollectionView+Extensions.swift create mode 100644 Chuck Norris Facts/Extensions/UITableView+Extensions.swift create mode 100644 Chuck Norris Facts/Resources/Animations/error.json create mode 100644 Chuck Norris Facts/Scenes/Facts/FactsList/FactsListError.swift create mode 100644 Chuck Norris Facts/Scenes/Facts/FactsList/Views/ErrorView.swift diff --git a/Chuck Norris Facts.xcodeproj/project.pbxproj b/Chuck Norris Facts.xcodeproj/project.pbxproj index ce80239..ecbf67c 100644 --- a/Chuck Norris Facts.xcodeproj/project.pbxproj +++ b/Chuck Norris Facts.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* Begin PBXBuildFile section */ 1E0C7B402543A4E8002D5C47 /* FactEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E0C7B3F2543A4E8002D5C47 /* FactEntity.swift */; }; + 1E0E4BA22549F7E30030BC49 /* error.json in Resources */ = {isa = PBXBuildFile; fileRef = 1E0E4BA12549F7E30030BC49 /* error.json */; }; + 1E15408F2549FA6200675DC4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E15408E2549FA6200675DC4 /* ErrorView.swift */; }; 1E135FAA254B4E66009D18AF /* LaunchArgument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E135FA9254B4E66009D18AF /* LaunchArgument.swift */; }; 1E135FAD254B4E9F009D18AF /* XCUIApplication+LaunchArgument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E135FAC254B4E9F009D18AF /* XCUIApplication+LaunchArgument.swift */; }; 1E135FAF254B52E0009D18AF /* facts.json in Resources */ = {isa = PBXBuildFile; fileRef = 1E135FAE254B52E0009D18AF /* facts.json */; }; @@ -43,6 +45,7 @@ 1EAB20AF2540BEC400633382 /* SuggestionsViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAB20AE2540BEC400633382 /* SuggestionsViewFlowLayout.swift */; }; 1EACEC99253649BD0006B36D /* loading.json in Resources */ = {isa = PBXBuildFile; fileRef = 1EACEC98253649BD0006B36D /* loading.json */; }; 1EACEC9E25364B6C0006B36D /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EACEC9D25364B6C0006B36D /* ActivityIndicator.swift */; }; + 1ECF1443254A514D007D9487 /* HTTPError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ECF1442254A514D007D9487 /* HTTPError.swift */; }; 1ED06C952548AAD300139151 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED06C942548AAD300139151 /* LoadingView.swift */; }; 1ED5D18D25348FD50035046C /* XCTestCase+Stub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED5D18C25348FD50035046C /* XCTestCase+Stub.swift */; }; 1ED5D19025349E9D0035046C /* UIViewController+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED5D18F25349E9D0035046C /* UIViewController+Rx.swift */; }; @@ -57,6 +60,10 @@ 1EE0713F25314AF500F6BF6D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE0713E25314AF500F6BF6D /* SceneDelegate.swift */; }; 1EE0714625314AF600F6BF6D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1EE0714525314AF600F6BF6D /* Assets.xcassets */; }; 1EE0714925314AF600F6BF6D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1EE0714725314AF600F6BF6D /* LaunchScreen.storyboard */; }; + 1EEDC69F254A301D00D75F3E /* UITableView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EEDC69E254A301D00D75F3E /* UITableView+Extensions.swift */; }; + 1EEDC6A1254A331500D75F3E /* UICollectionView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EEDC6A0254A331500D75F3E /* UICollectionView+Extensions.swift */; }; + 1EEDC6A3254A38B800D75F3E /* MoyaError+Code.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EEDC6A2254A38B800D75F3E /* MoyaError+Code.swift */; }; + 1EEDC6A5254A408B00D75F3E /* FactsListError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EEDC6A4254A408B00D75F3E /* FactsListError.swift */; }; 1EF066E32545CE1D00ECF611 /* PastSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EF066E22545CE1D00ECF611 /* PastSearchViewModel.swift */; }; 1EF066E52545CEC200ECF611 /* SearchEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EF066E42545CEC200ECF611 /* SearchEntity.swift */; }; 1EF0DA1425449898005CF7E2 /* CategoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EF0DA1325449898005CF7E2 /* CategoryView.swift */; }; @@ -96,6 +103,8 @@ /* Begin PBXFileReference section */ 02186229E02F0A408D2EE0C2 /* Pods_Chuck_Norris_Facts.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Chuck_Norris_Facts.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1E0C7B3F2543A4E8002D5C47 /* FactEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactEntity.swift; sourceTree = ""; }; + 1E0E4BA12549F7E30030BC49 /* error.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = error.json; sourceTree = ""; }; + 1E15408E2549FA6200675DC4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; 1E135FA9254B4E66009D18AF /* LaunchArgument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchArgument.swift; sourceTree = ""; }; 1E135FAC254B4E9F009D18AF /* XCUIApplication+LaunchArgument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCUIApplication+LaunchArgument.swift"; sourceTree = ""; }; 1E135FAE254B52E0009D18AF /* facts.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = facts.json; sourceTree = ""; }; @@ -131,6 +140,7 @@ 1EAB20AE2540BEC400633382 /* SuggestionsViewFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionsViewFlowLayout.swift; sourceTree = ""; }; 1EACEC98253649BD0006B36D /* loading.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = loading.json; sourceTree = ""; }; 1EACEC9D25364B6C0006B36D /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = ""; }; + 1ECF1442254A514D007D9487 /* HTTPError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPError.swift; sourceTree = ""; }; 1ED06C942548AAD300139151 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; 1ED5D18C25348FD50035046C /* XCTestCase+Stub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+Stub.swift"; sourceTree = ""; }; 1ED5D18F25349E9D0035046C /* UIViewController+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Rx.swift"; sourceTree = ""; }; @@ -151,6 +161,10 @@ 1EE0715525314AF600F6BF6D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 1EE0715A25314AF600F6BF6D /* Chuck Norris FactsUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Chuck Norris FactsUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 1EE0716025314AF600F6BF6D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 1EEDC69E254A301D00D75F3E /* UITableView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+Extensions.swift"; sourceTree = ""; }; + 1EEDC6A0254A331500D75F3E /* UICollectionView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICollectionView+Extensions.swift"; sourceTree = ""; }; + 1EEDC6A2254A38B800D75F3E /* MoyaError+Code.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MoyaError+Code.swift"; sourceTree = ""; }; + 1EEDC6A4254A408B00D75F3E /* FactsListError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListError.swift; sourceTree = ""; }; 1EF066E22545CE1D00ECF611 /* PastSearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PastSearchViewModel.swift; sourceTree = ""; }; 1EF066E42545CEC200ECF611 /* SearchEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchEntity.swift; sourceTree = ""; }; 1EF0DA1325449898005CF7E2 /* CategoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryView.swift; sourceTree = ""; }; @@ -226,6 +240,7 @@ 1E32758E2532A2C0007E838A /* EmptyListView.swift */, 1EF0DA1325449898005CF7E2 /* CategoryView.swift */, 1ED06C942548AAD300139151 /* LoadingView.swift */, + 1E15408E2549FA6200675DC4 /* ErrorView.swift */, ); path = Views; sourceTree = ""; @@ -233,6 +248,7 @@ 1E3275902532A2C4007E838A /* Animations */ = { isa = PBXGroup; children = ( + 1E0E4BA12549F7E30030BC49 /* error.json */, 1EACEC98253649BD0006B36D /* loading.json */, 1E3275912532A2CD007E838A /* empty-box.json */, ); @@ -388,6 +404,9 @@ children = ( 1ED5D18F25349E9D0035046C /* UIViewController+Rx.swift */, 1E56172B2541007500BF26A0 /* Data+Stub.swift */, + 1EEDC69E254A301D00D75F3E /* UITableView+Extensions.swift */, + 1EEDC6A0254A331500D75F3E /* UICollectionView+Extensions.swift */, + 1EEDC6A2254A38B800D75F3E /* MoyaError+Code.swift */, ); path = Extensions; sourceTree = ""; @@ -558,6 +577,7 @@ children = ( 1E921128253F6BA500DB340B /* Responses */, 1EFE2883253210B2008806B9 /* FactsAPI.swift */, + 1ECF1442254A514D007D9487 /* HTTPError.swift */, ); path = Networking; sourceTree = ""; @@ -588,6 +608,7 @@ 1EFE288D2532135B008806B9 /* FactsListViewController.swift */, 1EFE288F2532137C008806B9 /* FactsListCoordinator.swift */, 1EFE2891253214DB008806B9 /* FactsListViewModel.swift */, + 1EEDC6A4254A408B00D75F3E /* FactsListError.swift */, ); path = FactsList; sourceTree = ""; @@ -738,6 +759,7 @@ files = ( 1E135FAF254B52E0009D18AF /* facts.json in Resources */, 1EFE287E25321071008806B9 /* search-facts.json in Resources */, + 1E0E4BA22549F7E30030BC49 /* error.json in Resources */, 1EE0714925314AF600F6BF6D /* LaunchScreen.storyboard in Resources */, 1E5617282540FAF200BF26A0 /* get-categories.json in Resources */, 1EE0714625314AF600F6BF6D /* Assets.xcassets in Resources */, @@ -928,6 +950,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1ECF1443254A514D007D9487 /* HTTPError.swift in Sources */, 1EFE2884253210B2008806B9 /* FactsAPI.swift in Sources */, 1E32758F2532A2C0007E838A /* EmptyListView.swift in Sources */, 1E8A0FF0254760D400565A86 /* SuggestionsViewModel.swift in Sources */, @@ -947,14 +970,19 @@ 1E236840253FB13A00BE17F3 /* FactCategoryViewModel.swift in Sources */, 1EFE2869253207B2008806B9 /* AppCoordinator.swift in Sources */, 1E92112A253F6BB700DB340B /* SearchFactsResponse.swift in Sources */, + 1EEDC6A5254A408B00D75F3E /* FactsListError.swift in Sources */, 1EE0713F25314AF500F6BF6D /* SceneDelegate.swift in Sources */, 1E8A0FF42547768500565A86 /* DynamicHeightCollectionView.swift in Sources */, 1EFE288E2532135B008806B9 /* FactsListViewController.swift in Sources */, + 1E15408F2549FA6200675DC4 /* ErrorView.swift in Sources */, + 1EEDC6A1254A331500D75F3E /* UICollectionView+Extensions.swift in Sources */, 1EF066E32545CE1D00ECF611 /* PastSearchViewModel.swift in Sources */, 1EFE28902532137C008806B9 /* FactsListCoordinator.swift in Sources */, 1E463803253636160079D8E9 /* SearchFactsViewController.swift in Sources */, + 1EEDC69F254A301D00D75F3E /* UITableView+Extensions.swift in Sources */, 1EFE289525321CD2008806B9 /* FactViewModel.swift in Sources */, 1E46380B25363FF40079D8E9 /* SearchFactsViewModel.swift in Sources */, + 1EEDC6A3254A38B800D75F3E /* MoyaError+Code.swift in Sources */, 1E0C7B402543A4E8002D5C47 /* FactEntity.swift in Sources */, 1E921139253F909700DB340B /* FactsStorage.swift in Sources */, 1E23683E253FB07200BE17F3 /* FactCategoryCell.swift in Sources */, diff --git a/Chuck Norris Facts/Data/Networking/HTTPError.swift b/Chuck Norris Facts/Data/Networking/HTTPError.swift new file mode 100644 index 0000000..22e15a3 --- /dev/null +++ b/Chuck Norris Facts/Data/Networking/HTTPError.swift @@ -0,0 +1,12 @@ +// +// HTTPError.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/28/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation +import Moya + +typealias HTTPError = MoyaError diff --git a/Chuck Norris Facts/Data/Services/FactsService.swift b/Chuck Norris Facts/Data/Services/FactsService.swift index f72b367..0db4559 100644 --- a/Chuck Norris Facts/Data/Services/FactsService.swift +++ b/Chuck Norris Facts/Data/Services/FactsService.swift @@ -27,20 +27,60 @@ protocol FactsServiceType { func retrievePastSearches() -> Observable<[String]> } +let errorEndpointClosure = { (target: FactsAPI) -> Endpoint in + Endpoint( + url: URL(target: target).absoluteString, + sampleResponseClosure: { .networkResponse(500, Data()) }, + method: target.method, + task: target.task, + httpHeaderFields: target.headers + ) +} + +let mockEndpointClosure = { (target: FactsAPI) -> Endpoint in + Endpoint( + url: URL(target: target).absoluteString, + sampleResponseClosure: { .networkResponse(200, target.sampleData) }, + method: target.method, + task: target.task, + httpHeaderFields: target.headers + ) +} + struct FactsService: FactsServiceType { private var provider: MoyaProvider private var storage: FactsStorageType + private var scheduler: SchedulerType? + + init( + provider: MoyaProvider = MoyaProvider(), + storage: FactsStorageType = FactsStorage(), + scheduler: SchedulerType? = nil + ) { + if LaunchArgument.check(.mockHttpError) { + self.provider = MoyaProvider( + endpointClosure: errorEndpointClosure, + stubClosure: MoyaProvider.immediatelyStub + ) + } else if LaunchArgument.check(.mockHttp) { + self.provider = MoyaProvider( + endpointClosure: mockEndpointClosure, + stubClosure: MoyaProvider.immediatelyStub + ) + } else { + self.provider = provider + } - init(provider: MoyaProvider = MoyaProvider(), storage: FactsStorageType = FactsStorage()) { - self.provider = provider self.storage = storage + self.scheduler = scheduler } func searchFacts(searchTerm: String) -> Observable { provider.rx .request(.searchFacts(searchTerm: searchTerm)) .asObservable() + .observeOn(scheduler ?? MainScheduler.asyncInstance) .map(SearchFactsResponse.self, using: JSON.decoder) .map { $0.facts } .map { self.storage.storeSearch(searchTerm: searchTerm, facts: $0) } @@ -48,12 +88,18 @@ struct FactsService: FactsServiceType { } func syncCategories() -> Observable { - provider.rx - .request(.getCategories) - .asObservable() - .map([FactCategory].self, using: JSON.decoder) - .map { self.storage.storeCategories($0) } - .map { () } + storage.retrieveCategories() + .flatMapLatest { categories -> Observable in + guard categories.isEmpty else { return Observable.just(()) } + + return self.provider.rx + .request(.getCategories) + .asObservable() + .observeOn(self.scheduler ?? MainScheduler.asyncInstance) + .map([FactCategory].self, using: JSON.decoder) + .map { self.storage.storeCategories($0) } + .map { () } + } } func retrieveCategories() -> Observable<[FactCategory]> { diff --git a/Chuck Norris Facts/Extensions/MoyaError+Code.swift b/Chuck Norris Facts/Extensions/MoyaError+Code.swift new file mode 100644 index 0000000..385b499 --- /dev/null +++ b/Chuck Norris Facts/Extensions/MoyaError+Code.swift @@ -0,0 +1,30 @@ +// +// MoyaError+Code.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/28/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Moya +import Alamofire + +enum HTTPErrorCode: Int { + case unknown = 0 + case noConnection = 1 +} + +extension MoyaError { + + var code: HTTPErrorCode { + let alamofireError = errorUserInfo["NSUnderlyingError"] as? Alamofire.AFError + let error = alamofireError?.underlyingError as NSError? + + switch error?.code { + case NSURLErrorNotConnectedToInternet: + return .noConnection + default: + return .unknown + } + } +} diff --git a/Chuck Norris Facts/Extensions/UICollectionView+Extensions.swift b/Chuck Norris Facts/Extensions/UICollectionView+Extensions.swift new file mode 100644 index 0000000..3a36910 --- /dev/null +++ b/Chuck Norris Facts/Extensions/UICollectionView+Extensions.swift @@ -0,0 +1,19 @@ +// +// UICollectionView+Extensions.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/28/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import UIKit + +extension UICollectionView { + func register(_ cell: UICollectionViewCell.Type) { + register(cell, forCellWithReuseIdentifier: String(describing: cell.self)) + } + + func dequeueReusableCell(cell: T.Type, indexPath: IndexPath) -> T { + dequeueReusableCell(withReuseIdentifier: String(describing: T.self), for: indexPath) as? T ?? T() + } +} diff --git a/Chuck Norris Facts/Extensions/UITableView+Extensions.swift b/Chuck Norris Facts/Extensions/UITableView+Extensions.swift new file mode 100644 index 0000000..c7223a7 --- /dev/null +++ b/Chuck Norris Facts/Extensions/UITableView+Extensions.swift @@ -0,0 +1,19 @@ +// +// UITableView+Extensions.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/28/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import UIKit + +extension UITableView { + func register(_ cell: UITableViewCell.Type) { + register(cell, forCellReuseIdentifier: String(describing: cell.self)) + } + + func dequeueReusableCell(cell: T.Type, indexPath: IndexPath) -> T { + dequeueReusableCell(withIdentifier: String(describing: T.self), for: indexPath) as? T ?? T() + } +} diff --git a/Chuck Norris Facts/Library/LaunchArgument.swift b/Chuck Norris Facts/Library/LaunchArgument.swift index 03f72de..2582e4f 100644 --- a/Chuck Norris Facts/Library/LaunchArgument.swift +++ b/Chuck Norris Facts/Library/LaunchArgument.swift @@ -16,6 +16,12 @@ enum LaunchArgument: String { // Mock storage data case mockStorage = "--mock-storage" + // Mock Http Result + case mockHttp = "--mock-http" + + // Mock Http Error Result + case mockHttpError = "--mock-http-error" + // Check if there is an argument on CommandLine arguments static func check(_ argument: LaunchArgument) -> Bool { CommandLine.arguments.contains(argument.rawValue) diff --git a/Chuck Norris Facts/Resources/Animations/error.json b/Chuck Norris Facts/Resources/Animations/error.json new file mode 100644 index 0000000..83d5793 --- /dev/null +++ b/Chuck Norris Facts/Resources/Animations/error.json @@ -0,0 +1 @@ +{"v":"5.0.1","fr":60,"ip":0,"op":67.98,"w":120,"h":140,"ddd":0,"assets":[],"layers":[{"ind":2,"nm":"Layer 2","ks":{"p":{"a":0,"k":[59.997,60.37]},"a":{"a":0,"k":[8.95,-21.138,0]},"s":{"a":1,"k":[{"t":14,"s":[0,0,100],"i":{"x":[0.07],"y":[1]},"o":{"x":[0.86],"y":[0]},"e":[92,92,100]},{"t":63,"s":[92,92,100]}]},"r":{"a":0,"k":0},"o":{"a":1,"k":[{"t":14,"s":[0],"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"e":[100]},{"t":63,"s":[100]}]}},"ao":0,"ip":0,"op":68,"st":0,"bm":0,"sr":1,"ty":4,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"i":[[0,0],[-0.059,2.139],[0,0],[0,0.147],[2.197,0],[0,-2.285],[0,-0.205],[0,0],[-2.08,0]],"o":[[2.08,0],[0,0],[0,-0.205],[0,-2.285],[-2.197,0],[0,0.147],[0,0],[0.058,2.139],[0,0]],"v":[[8.936,-13.535],[12.217,-16.963],[12.598,-38.613],[12.627,-39.17],[8.965,-42.861],[5.273,-39.17],[5.303,-38.613],[5.684,-16.963],[8.936,-13.535]],"c":true},"hd":false}},{"ty":"sh","d":1,"ks":{"a":0,"k":{"i":[[0,0],[0,2.314],[2.402,0],[0,-2.314],[-2.373,0]],"o":[[2.402,0],[0,-2.315],[-2.374,0],[0,2.314],[0,0]],"v":[[8.936,0.586],[13.213,-3.574],[8.936,-7.705],[4.687,-3.574],[8.936,0.586]],"c":true},"hd":false}},{"ty":"fl","c":{"a":0,"k":[0.8,0.8196078431372549,0.8509803921568627,1]},"hd":false,"o":{"a":0,"k":100},"r":1},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"Object","hd":false}]},{"ind":1,"nm":"Layer 1","ks":{"p":{"a":0,"k":[60,60.18]},"a":{"a":0,"k":[51,51,0]},"s":{"a":0,"k":[104,104,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}},"ao":0,"ip":0,"op":68,"st":0,"bm":0,"sr":1,"ty":4,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"i":[[0,0],[28.167,0],[0,28.167],[-28.166,0],[0,-28.166]],"o":[[0,28.167],[-28.166,0],[0,-28.166],[29,0],[0,0]],"v":[[51,0],[0,51],[-51,0],[0,-51],[51,0]],"c":true},"hd":false}},{"ty":"st","c":{"a":0,"k":[0.8,0.8196078431372549,0.8509803921568627,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":5},"lc":1,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[51,51]},"a":{"a":0,"k":[0,0]},"s":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[0.07],"y":[1]},"o":{"x":[0.86],"y":[0]},"e":[105,105]},{"t":49,"s":[105,105],"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"e":[92,92]},{"t":68,"s":[92,92]}]},"r":{"a":0,"k":0},"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"e":[100]},{"t":49,"s":[100]}]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"Object","hd":false}]}],"markers":[]} \ No newline at end of file diff --git a/Chuck Norris Facts/Resources/Generated/Strings.swift b/Chuck Norris Facts/Resources/Generated/Strings.swift index de360ca..101e3ad 100644 --- a/Chuck Norris Facts/Resources/Generated/Strings.swift +++ b/Chuck Norris Facts/Resources/Generated/Strings.swift @@ -13,7 +13,9 @@ internal enum L10n { internal enum EmptyView { /// Looks like there are no Facts - internal static let text = L10n.tr("Localizable", "EmptyView.text") + internal static let empty = L10n.tr("Localizable", "EmptyView.empty") + /// There are no facts to your search + internal static let emptySearch = L10n.tr("Localizable", "EmptyView.emptySearch") } internal enum FactCategory { @@ -21,6 +23,13 @@ internal enum L10n { internal static let uncategorized = L10n.tr("Localizable", "FactCategory.uncategorized") } + internal enum FactListError { + /// Internet Connection appears to be offline + internal static let noConnection = L10n.tr("Localizable", "FactListError.noConnection") + /// Looks like the Chuck Norris Service is unavailable + internal static let serviceUnavailable = L10n.tr("Localizable", "FactListError.serviceUnavailable") + } + internal enum FactsList { /// Chuck Norris Facts internal static let title = L10n.tr("Localizable", "FactsList.title") diff --git a/Chuck Norris Facts/Resources/Localizable.strings b/Chuck Norris Facts/Resources/Localizable.strings index 412a7f7..ff20991 100644 --- a/Chuck Norris Facts/Resources/Localizable.strings +++ b/Chuck Norris Facts/Resources/Localizable.strings @@ -12,6 +12,10 @@ "SearchFacts.sections.suggestions" = "Suggestions"; "SearchFacts.sections.pastSearches" = "Past Searches"; -"EmptyView.text" = "Looks like there are no Facts"; +"EmptyView.empty" = "Looks like there are no Facts"; +"EmptyView.emptySearch" = "There are no facts to your search"; "FactCategory.uncategorized" = "UNCATEGORIZED"; + +"FactListError.noConnection" = "Internet Connection appears to be offline"; +"FactListError.serviceUnavailable" = "Looks like the Chuck Norris Service is unavailable"; diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/Fact/FactCell.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/Fact/FactCell.swift index d687cf0..717cb39 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/Fact/FactCell.swift +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/Fact/FactCell.swift @@ -7,13 +7,14 @@ // import UIKit +import RxSwift class FactCell: UITableViewCell { - static let cellIdentifier = "FactTableViewCell" - private let categoryView = CategoryView() + var disposeBag = DisposeBag() + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupView() @@ -23,6 +24,11 @@ class FactCell: UITableViewCell { fatalError("init(coder:) has not been implemented") } + override func prepareForReuse() { + disposeBag = DisposeBag() + super.prepareForReuse() + } + private lazy var shadowView: UIView = { let view = UIView() diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListError.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListError.swift new file mode 100644 index 0000000..c0879e5 --- /dev/null +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListError.swift @@ -0,0 +1,44 @@ +// +// FactsListError.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/28/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation + +struct FactsListError { + + enum ErrorType { + case syncCategories + case searchFacts + } + + let error: HTTPError? + + let type: ErrorType + + var message: String { + switch error?.code { + case .noConnection: + return L10n.FactListError.noConnection + default: + return L10n.FactListError.serviceUnavailable + } + } + + var retryEnabled: Bool { + switch error?.code { + case .noConnection: + return false + default: + return true + } + } + + init(_ error: Error, type: ErrorType) { + self.error = error as? HTTPError + self.type = type + } +} diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift index 4a5d358..248d1cc 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift @@ -19,6 +19,7 @@ class FactsListViewController: UIViewController { private let disposeBag = DisposeBag() let tableView = UITableView() + let errorView = ErrorView() let loadingView = LoadingView() let emptyListView = EmptyListView() let searchButton = UIBarButtonItem(barButtonSystemItem: .search, target: nil, action: nil) @@ -27,15 +28,13 @@ class FactsListViewController: UIViewController { configureCell: { [weak self] _, tableView, indexPath, fact -> UITableViewCell in guard let viewModel = self?.viewModel, let disposeBag = self?.disposeBag else { return UITableViewCell() } - let cell = tableView.dequeueReusableCell(withIdentifier: FactCell.cellIdentifier, for: indexPath) + let cell = tableView.dequeueReusableCell(cell: FactCell.self, indexPath: indexPath) - if let cell = cell as? FactCell { - cell.setup(fact) - cell.shareButton.rx.tap - .map { fact } - .bind(to: viewModel.startShareFact) - .disposed(by: disposeBag) - } + cell.setup(fact) + cell.shareButton.rx.tap + .map { fact } + .bind(to: viewModel.startShareFact) + .disposed(by: cell.disposeBag) return cell } @@ -47,6 +46,7 @@ class FactsListViewController: UIViewController { setupView() setupBindings() setupTableView() + setupErrorView() setupEmptyListView() setupLoadingView() setupNavigationBar() @@ -61,14 +61,13 @@ class FactsListViewController: UIViewController { tableView.separatorStyle = .none + tableView.register(FactCell.self) tableView.translatesAutoresizingMaskIntoConstraints = false tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true - tableView.register(FactCell.self, forCellReuseIdentifier: FactCell.cellIdentifier) - tableView.accessibilityIdentifier = "factsTableView" } @@ -82,6 +81,17 @@ class FactsListViewController: UIViewController { loadingView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true } + private func setupErrorView() { + view.addSubview(errorView) + + errorView.isHidden = true + errorView.translatesAutoresizingMaskIntoConstraints = false + errorView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true + errorView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true + errorView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true + errorView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + } + private func setupEmptyListView() { view.addSubview(emptyListView) @@ -111,12 +121,19 @@ class FactsListViewController: UIViewController { }) .disposed(by: disposeBag) - viewModel.facts + let factsIsEmpty = viewModel.facts .map { $0.flatMap { $0.items } } .map { $0.isEmpty } - .asDriver(onErrorJustReturn: true) - .drive(onNext: { [weak self] isEmpty in - self?.showEmptyView(isEmpty) + .share() + + let searchIsEmpty = viewModel.searchTerm + .map { $0.isEmpty } + .share() + + Observable.combineLatest(factsIsEmpty, searchIsEmpty) + .asDriver(onErrorJustReturn: (true, true)) + .drive(onNext: { [weak self] listEmpty, searchEmpty in + self?.showEmptyView(listEmpty, searchEmpty) }) .disposed(by: disposeBag) @@ -129,17 +146,33 @@ class FactsListViewController: UIViewController { .bind(to: viewModel.startSearchFacts) .disposed(by: disposeBag) - viewModel.syncCategories - .asDriver(onErrorJustReturn: ()) - .drive() + emptyListView.searchButton.rx.tap + .bind(to: viewModel.startSearchFacts) + .disposed(by: disposeBag) + + errorView.retryButton.rx.tap + .bind(to: viewModel.retryAction) + .disposed(by: disposeBag) + + viewModel.errors + .bind(onNext: { [weak self] error in + self?.showErrorView(error) + }) .disposed(by: disposeBag) } - private func showEmptyView(_ isEmpty: Bool) { - tableView.isHidden = isEmpty - emptyListView.isHidden = !isEmpty + private func showEmptyView(_ listEmpty: Bool, _ searchEmpty: Bool) { + emptyListView.isHidden = !listEmpty + + if searchEmpty { + emptyListView.label.text = L10n.EmptyView.empty + emptyListView.searchButton.isHidden = false + } else { + emptyListView.label.text = L10n.EmptyView.emptySearch + emptyListView.searchButton.isHidden = true + } - if isEmpty { + if listEmpty { emptyListView.play() } else { emptyListView.stop() @@ -147,14 +180,21 @@ class FactsListViewController: UIViewController { } private func showLoadingView(_ isLoading: Bool) { - tableView.isHidden = isLoading loadingView.isHidden = !isLoading if isLoading { - emptyListView.isHidden = isLoading loadingView.play() } else { loadingView.stop() } } + + private func showErrorView(_ error: FactsListError) { + emptyListView.isHidden = true + + errorView.label.text = error.message + errorView.retryButton.isHidden = !error.retryEnabled + errorView.isHidden = false + errorView.play() + } } diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift index 6b2b962..5f3f0ac 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift @@ -24,6 +24,8 @@ final class FactsListViewModel { let setSearchTerm: AnyObserver + let retryAction: AnyObserver + // MARK: - Outputs let facts: Observable<[FactsSectionModel]> @@ -36,10 +38,9 @@ final class FactsListViewModel { let isLoading: ActivityIndicator - let syncCategories: Observable + let errors: Observable init(factsService: FactsServiceType = FactsService()) { - let loadingIndicator = ActivityIndicator() self.isLoading = loadingIndicator @@ -58,18 +59,34 @@ final class FactsListViewModel { self.setSearchTerm = searchTermSubject.asObserver() self.searchTerm = searchTermSubject.asObservable() - self.syncCategories = viewDidAppearSubject.asObservable() - .flatMapLatest { _ -> Observable in + let retryActionSubject = PublishSubject() + self.retryAction = retryActionSubject.asObserver() + + let currentErrorSubject = BehaviorSubject(value: nil) + + let retrySyncCategories = retryActionSubject + .withLatestFrom(currentErrorSubject) + .compactMap { $0 } + .filter { $0.type == .syncCategories } + .map { _ in () } + + let syncCategoriesError = Observable.merge(viewDidAppearSubject, retrySyncCategories) + .flatMapLatest { _ in factsService.syncCategories() + .materialize() } + .compactMap { $0.event.error } + .map { FactsListError($0, type: .syncCategories) } - _ = searchTermSubject.asObservable() + let searchFactsError = Observable.combineLatest(viewDidAppearSubject, searchTerm) { _, term in term } .filter { !$0.isEmpty } - .flatMapLatest { searchTerm -> Observable in - return factsService.searchFacts(searchTerm: searchTerm) + .flatMapLatest { searchTerm in + factsService.searchFacts(searchTerm: searchTerm) .trackActivity(loadingIndicator) + .materialize() } - .subscribe(onNext: {}) + .compactMap { $0.event.error } + .map { FactsListError($0, type: .searchFacts) } self.facts = Observable.combineLatest(viewDidAppearSubject, searchTermSubject) .flatMapLatest { _, searchTerm -> Observable<[Fact]> in @@ -81,5 +98,8 @@ final class FactsListViewModel { } .map { $0.map { FactViewModel(fact: $0) } } .map { [FactsSectionModel(model: "", items: $0)] } + + self.errors = Observable.merge(searchFactsError, syncCategoriesError) + .do(onNext: currentErrorSubject.onNext) } } diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/Views/EmptyListView.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/Views/EmptyListView.swift index cce6fbb..fdcd5a4 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/Views/EmptyListView.swift +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/Views/EmptyListView.swift @@ -20,19 +20,29 @@ final class EmptyListView: UIView { return animation }() - private lazy var label: UILabel = { + lazy var label: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.systemFont(ofSize: 16, weight: .semibold) label.lineBreakMode = .byWordWrapping label.numberOfLines = 0 - label.text = L10n.EmptyView.text - label.font = UIFont.systemFont(ofSize: 18, weight: .semibold) - return label }() + lazy var searchButton: UIButton = { + let button = UIButton(type: .system) + + button.translatesAutoresizingMaskIntoConstraints = false + button.accessibilityLabel = "Search" + button.setTitle("Search", for: .normal) + button.titleLabel?.font = .systemFont(ofSize: 16, weight: .regular) + button.accessibilityIdentifier = "searchButton" + + return button + }() + override init(frame: CGRect) { super.init(frame: frame) @@ -60,6 +70,11 @@ final class EmptyListView: UIView { label.topAnchor.constraint(equalTo: animation.bottomAnchor).isActive = true label.centerXAnchor.constraint(equalTo: animation.centerXAnchor).isActive = true + addSubview(searchButton) + searchButton.translatesAutoresizingMaskIntoConstraints = false + searchButton.topAnchor.constraint(equalTo: label.bottomAnchor).isActive = true + searchButton.centerXAnchor.constraint(equalTo: label.centerXAnchor).isActive = true + accessibilityIdentifier = "emptyListView" label.accessibilityIdentifier = "emptyListLabelView" } diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/Views/ErrorView.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/Views/ErrorView.swift new file mode 100644 index 0000000..1d51522 --- /dev/null +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/Views/ErrorView.swift @@ -0,0 +1,81 @@ +// +// ErrorView.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/28/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import UIKit +import Lottie + +final class ErrorView: UIView { + + private lazy var animation: AnimationView = { + let loading = AnimationView() + + loading.translatesAutoresizingMaskIntoConstraints = false + loading.animation = Animation.named("error") + + return loading + }() + + lazy var label: UILabel = { + let label = UILabel() + + label.translatesAutoresizingMaskIntoConstraints = false + label.lineBreakMode = .byWordWrapping + label.numberOfLines = 0 + label.textAlignment = .center + label.font = .systemFont(ofSize: 16, weight: .semibold) + + return label + }() + + lazy var retryButton: UIButton = { + let button = UIButton(type: .system) + + button.translatesAutoresizingMaskIntoConstraints = false + button.accessibilityLabel = "Retry" + button.setTitle("Retry", for: .normal) + button.titleLabel?.font = .systemFont(ofSize: 16, weight: .regular) + button.accessibilityIdentifier = "retryButton" + + return button + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + setupView() + + accessibilityIdentifier = "errorView" + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView() { + backgroundColor = .systemBackground + + addSubview(animation) + animation.widthAnchor.constraint(equalToConstant: 200).isActive = true + animation.heightAnchor.constraint(equalToConstant: 200).isActive = true + animation.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true + animation.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true + + addSubview(label) + label.topAnchor.constraint(equalTo: animation.bottomAnchor).isActive = true + label.widthAnchor.constraint(equalTo: widthAnchor, constant: -16).isActive = true + label.centerXAnchor.constraint(equalTo: animation.centerXAnchor).isActive = true + + addSubview(retryButton) + retryButton.topAnchor.constraint(equalTo: label.bottomAnchor).isActive = true + retryButton.centerXAnchor.constraint(equalTo: label.centerXAnchor).isActive = true + } + + func play() { + animation.play() + } +} diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewController.swift b/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewController.swift index a6a9f28..2118935 100644 --- a/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewController.swift +++ b/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewController.swift @@ -21,20 +21,19 @@ final class SearchFactsViewController: UIViewController { switch dataSource[indexPath] { case .SuggestionsTableViewItem(let suggestions): - let cell = SuggestionsCell(style: .default, reuseIdentifier: SuggestionsCell.identifier) + let cell = tableView.dequeueReusableCell(cell: SuggestionsCell.self, indexPath: indexPath) let viewModel = SuggestionsViewModel(suggestions: suggestions) cell.viewModel = viewModel viewModel.didSelectSuggestion .bind(to: self.viewModel.searchTerm) - .disposed(by: self.disposeBag) + .disposed(by: cell.disposeBag) viewModel.didSelectSuggestion .map { _ in () } .bind(to: self.viewModel.searchAction) - .disposed(by: self.disposeBag) + .disposed(by: cell.disposeBag) return cell case .PastSearchTableViewItem(let model): - let cell = tableView.dequeueReusableCell(withIdentifier: PastSearchCell.identifier) as? PastSearchCell - ?? PastSearchCell(style: .default, reuseIdentifier: PastSearchCell.identifier) + let cell = tableView.dequeueReusableCell(cell: PastSearchCell.self, indexPath: indexPath) cell.setup(model) return cell } @@ -95,8 +94,8 @@ final class SearchFactsViewController: UIViewController { tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true - tableView.register(SuggestionsCell.self, forCellReuseIdentifier: SuggestionsCell.identifier) - tableView.register(PastSearchCell.self, forCellReuseIdentifier: PastSearchCell.identifier) + tableView.register(SuggestionsCell.self) + tableView.register(PastSearchCell.self) } private func setupNavigationBar() { diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryCell.swift b/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryCell.swift index 5f6e0f9..3d2c050 100644 --- a/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryCell.swift +++ b/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryCell.swift @@ -10,8 +10,6 @@ import UIKit class FactCategoryCell: UICollectionViewCell { - static let cellIdentifier = "FactCategoryCell" - private lazy var bodyLabel: UILabel = { let label = UILabel() diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/SuggestionsCell.swift b/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/SuggestionsCell.swift index 61f6879..d5f6812 100644 --- a/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/SuggestionsCell.swift +++ b/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/SuggestionsCell.swift @@ -13,8 +13,6 @@ import RxDataSources class SuggestionsCell: UITableViewCell { - static let identifier = "SuggestionsCell" - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupView() @@ -24,18 +22,19 @@ class SuggestionsCell: UITableViewCell { fatalError("init(coder:) has not been implemented") } - private let disposeBag = DisposeBag() + var disposeBag = DisposeBag() + + override func prepareForReuse() { + disposeBag = DisposeBag() + super.prepareForReuse() + } private lazy var suggestionsDataSource = RxCollectionViewSectionedReloadDataSource( configureCell: { _, collectionView, indexPath, category -> UICollectionViewCell in - let cell = collectionView.dequeueReusableCell( - withReuseIdentifier: FactCategoryCell.cellIdentifier, - for: indexPath - ) as? FactCategoryCell ?? FactCategoryCell() - + let cell = collectionView.dequeueReusableCell(cell: FactCategoryCell.self, indexPath: indexPath) cell.setup(category) return cell - } + } ) var viewModel: SuggestionsViewModel! { @@ -54,7 +53,7 @@ class SuggestionsCell: UITableViewCell { collectionView.isScrollEnabled = false collectionView.translatesAutoresizingMaskIntoConstraints = false - collectionView.register(FactCategoryCell.self, forCellWithReuseIdentifier: FactCategoryCell.cellIdentifier) + collectionView.register(FactCategoryCell.self) return collectionView }() diff --git a/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift b/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift index 64ccbe3..423b693 100644 --- a/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift +++ b/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift @@ -31,7 +31,7 @@ final class FactsServiceTests: XCTestCase { realm = try Realm(configuration: .init(inMemoryIdentifier: self.name)) factsStorage = FactsStorage(realm: realm) factsProvider = MoyaProvider(stubClosure: MoyaProvider.immediatelyStub) - factsService = FactsService(provider: factsProvider, storage: factsStorage) + factsService = FactsService(provider: factsProvider, storage: factsStorage, scheduler: MainScheduler.instance) } override func tearDown() { diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift index 793af8a..e37300c 100644 --- a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift +++ b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift @@ -42,7 +42,6 @@ class FactsListViewControllerTests: XCTestCase { factsListViewModel.viewDidAppear.onNext(()) XCTAssertFalse(factsListViewController.emptyListView.isHidden) - XCTAssertTrue(factsListViewController.tableView.isHidden) } func test_factCellFontSizeShouldBe24ForShortContent() throws { diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift index 6ae4fc3..f3b215c 100644 --- a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift +++ b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift @@ -72,21 +72,22 @@ class FactsListViewModelTests: XCTestCase { XCTAssertEqual(fact.value, shareFact?.text) } - func test_categoriesShouldSyncWhenViewDidAppear() throws { + func test_categoriesShouldSyncWhenViewDidAppearWithNoErrors() throws { let stubCategories = try stub("get-categories", type: [FactCategory].self) ?? [] let categories = try XCTUnwrap(stubCategories, "looks like get-categories.json doesn't exists") factsServiceMock.retrieveCategoriesReturnValue = .just(categories) - let syncCategoriesObserver = testScheduler.createObserver(Void.self) + let errorObserver = testScheduler.createObserver(FactsListError.self) - factsListViewModel.syncCategories - .subscribe(syncCategoriesObserver) + factsListViewModel.errors + .subscribe(errorObserver) .disposed(by: disposeBag) factsListViewModel.viewDidAppear.onNext(()) testScheduler.start() - XCTAssertEqual(syncCategoriesObserver.events.count, 1) + let error = errorObserver.events.compactMap { $0.value.element }.first + XCTAssertNil(error) } } diff --git a/Chuck Norris FactsUITests/Library/XCUIApplication+LaunchArgument.swift b/Chuck Norris FactsUITests/Library/XCUIApplication+LaunchArgument.swift index 4e560d8..abfbffe 100644 --- a/Chuck Norris FactsUITests/Library/XCUIApplication+LaunchArgument.swift +++ b/Chuck Norris FactsUITests/Library/XCUIApplication+LaunchArgument.swift @@ -18,6 +18,12 @@ enum LaunchArgument: String { // Mock storage data case mockStorage = "--mock-storage" + + // Mock Http Result + case mockHttp = "--mock-http" + + // Mock Http Error Result + case mockHttpError = "--mock-http-error" } extension XCUIApplication { diff --git a/Chuck Norris FactsUITests/Scenes/FactsListScene.swift b/Chuck Norris FactsUITests/Scenes/FactsListScene.swift index 808972f..ba3452d 100644 --- a/Chuck Norris FactsUITests/Scenes/FactsListScene.swift +++ b/Chuck Norris FactsUITests/Scenes/FactsListScene.swift @@ -15,6 +15,8 @@ struct FactsListScene { let emptyListView: XCUIElement let emptyListLabelView: XCUIElement let searchButton: XCUIElement + let errorView: XCUIElement + let retryButton: XCUIElement init() { let app = XCUIApplication() @@ -23,6 +25,8 @@ struct FactsListScene { emptyListView = app.otherElements["emptyListView"] emptyListLabelView = app.staticTexts["emptyListLabelView"] searchButton = app.navigationBars.buttons["searchButton"] + errorView = app.otherElements["errorView"] + retryButton = app.buttons["retryButton"] } } diff --git a/Chuck Norris FactsUITests/Tests/FactsListUITests.swift b/Chuck Norris FactsUITests/Tests/FactsListUITests.swift index 7cd6cda..65f8e78 100644 --- a/Chuck Norris FactsUITests/Tests/FactsListUITests.swift +++ b/Chuck Norris FactsUITests/Tests/FactsListUITests.swift @@ -73,4 +73,26 @@ final class FactsListUITests: XCTestCase { XCTAssertTrue(searchFactsView.exists) } + + func test_shouldShowErrorViewWhenSearchRaisesAnError() { + app.setLaunchArguments([.uiTest, .mockHttpError]) + app.launch() + + let factsListScene = FactsListScene() + let searchFactsButton = factsListScene.searchButton + + let searchFactsScene = SearchFactsScene() + + XCTAssertTrue(searchFactsButton.exists) + + searchFactsButton.firstMatch.tap() + + searchFactsScene.searchBarField.tap() + searchFactsScene.searchBarField.typeText("games") + + app.keyboards.buttons["Search"].tap() + + XCTAssertTrue(factsListScene.errorView.exists) + XCTAssertTrue(factsListScene.retryButton.exists) + } } diff --git a/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift b/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift index 3911b2c..6b76af1 100644 --- a/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift +++ b/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift @@ -19,7 +19,7 @@ final class SearchFactsUITests: XCTestCase { } func test_searchFactsUsingSearchBar() throws { - app.setLaunchArguments([.uiTest, .mockStorage]) + app.setLaunchArguments([.uiTest, .resetData, .mockStorage, .mockHttp]) app.launch() let factsListScene = FactsListScene() @@ -31,8 +31,6 @@ final class SearchFactsUITests: XCTestCase { app.keyboards.buttons["Search"].tap() - sleep(5) - XCTAssertEqual(factsListScene.factsTableView.cells.count, 16) } @@ -50,7 +48,7 @@ final class SearchFactsUITests: XCTestCase { } func test_shouldShow8FactCategories() { - app.setLaunchArguments([.uiTest, .mockStorage]) + app.setLaunchArguments([.uiTest, .resetData, .mockHttp]) app.launch() let factsListScene = FactsListScene() @@ -65,7 +63,7 @@ final class SearchFactsUITests: XCTestCase { } func test_tapFactCategoryShouldSearchByTerm() { - app.setLaunchArguments([.uiTest, .mockStorage]) + app.setLaunchArguments([.uiTest, .mockStorage, .mockHttp]) app.launch() let factsListScene = FactsListScene() @@ -81,14 +79,12 @@ final class SearchFactsUITests: XCTestCase { suggestion.tap() - sleep(5) - XCTAssertFalse(searchFactsScene.searchFactsView.exists) XCTAssertGreaterThan(factsListScene.factsTableView.cells.count, 0) } func test_tapPastSearchShouldSearchByTerm() { - app.setLaunchArguments([.uiTest, .mockStorage]) + app.setLaunchArguments([.uiTest, .mockStorage, .mockHttp]) app.launch() let factsListScene = FactsListScene() @@ -104,8 +100,6 @@ final class SearchFactsUITests: XCTestCase { pastSearchCell.tap() - sleep(5) - XCTAssertFalse(searchFactsScene.searchFactsView.exists) XCTAssertGreaterThan(factsListScene.factsTableView.cells.count, 0) } @@ -136,11 +130,9 @@ final class SearchFactsUITests: XCTestCase { } func test_pastSearchShouldBeHiddenOnFirstAccess() { - app.setLaunchArguments([.uiTest, .resetData]) + app.setLaunchArguments([.uiTest, .resetData, .mockHttp]) app.launch() - sleep(3) - let factsListScene = FactsListScene() factsListScene.searchButton.tap() From 668c2fd503ae974f11b30449a07db58fa14bce9c Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Fri, 30 Oct 2020 21:10:52 -0300 Subject: [PATCH 14/18] Refactor Networking Layer (#30) * Impl own networking layer * Dependency Injection on APIProvider * Fix Tests * Moya -> API --- Chuck Norris Facts.xcodeproj/project.pbxproj | 68 ++++++-- Chuck Norris Facts/API/APIError.swift | 110 +++++++++++++ Chuck Norris Facts/API/APIProvider.swift | 96 +++++++++++ Chuck Norris Facts/API/APIResponse.swift | 14 ++ Chuck Norris Facts/API/APITarget.swift | 62 +++++++ Chuck Norris Facts/API/APITask.swift | 19 +++ Chuck Norris Facts/API/HTTP/HTTPMethod.swift | 14 ++ .../Data/Networking/FactsAPI.swift | 32 ++-- .../Data/Networking/HTTPError.swift | 12 -- .../Data/Services/FactsService.swift | 40 +---- Chuck Norris Facts/Extensions/API+Rx.swift | 48 ++++++ .../Extensions/MoyaError+Code.swift | 30 ---- .../Extensions/URLRequest+Encoded.swift | 25 +++ .../Facts/FactsList/FactsListError.swift | 43 +++-- .../FactsList/FactsListViewController.swift | 6 +- .../Facts/FactsList/FactsListViewModel.swift | 6 +- .../Data/Services/FactsServiceTests.swift | 24 +-- .../Library/XCTestCase+Stub.swift | 15 ++ Chuck Norris FactsTests/Mocks/APIMock.swift | 28 ++++ .../Stubs/search-facts.json | 153 ++++++++++++++++++ Podfile | 3 - Podfile.lock | 13 +- README.md | 1 - 23 files changed, 705 insertions(+), 157 deletions(-) create mode 100644 Chuck Norris Facts/API/APIError.swift create mode 100644 Chuck Norris Facts/API/APIProvider.swift create mode 100644 Chuck Norris Facts/API/APIResponse.swift create mode 100644 Chuck Norris Facts/API/APITarget.swift create mode 100644 Chuck Norris Facts/API/APITask.swift create mode 100644 Chuck Norris Facts/API/HTTP/HTTPMethod.swift delete mode 100644 Chuck Norris Facts/Data/Networking/HTTPError.swift create mode 100644 Chuck Norris Facts/Extensions/API+Rx.swift delete mode 100644 Chuck Norris Facts/Extensions/MoyaError+Code.swift create mode 100644 Chuck Norris Facts/Extensions/URLRequest+Encoded.swift create mode 100644 Chuck Norris FactsTests/Mocks/APIMock.swift create mode 100644 Chuck Norris FactsTests/Stubs/search-facts.json diff --git a/Chuck Norris Facts.xcodeproj/project.pbxproj b/Chuck Norris Facts.xcodeproj/project.pbxproj index ecbf67c..9db8585 100644 --- a/Chuck Norris Facts.xcodeproj/project.pbxproj +++ b/Chuck Norris Facts.xcodeproj/project.pbxproj @@ -9,12 +9,15 @@ /* Begin PBXBuildFile section */ 1E0C7B402543A4E8002D5C47 /* FactEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E0C7B3F2543A4E8002D5C47 /* FactEntity.swift */; }; 1E0E4BA22549F7E30030BC49 /* error.json in Resources */ = {isa = PBXBuildFile; fileRef = 1E0E4BA12549F7E30030BC49 /* error.json */; }; - 1E15408F2549FA6200675DC4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E15408E2549FA6200675DC4 /* ErrorView.swift */; }; 1E135FAA254B4E66009D18AF /* LaunchArgument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E135FA9254B4E66009D18AF /* LaunchArgument.swift */; }; 1E135FAD254B4E9F009D18AF /* XCUIApplication+LaunchArgument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E135FAC254B4E9F009D18AF /* XCUIApplication+LaunchArgument.swift */; }; 1E135FAF254B52E0009D18AF /* facts.json in Resources */ = {isa = PBXBuildFile; fileRef = 1E135FAE254B52E0009D18AF /* facts.json */; }; + 1E15408F2549FA6200675DC4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E15408E2549FA6200675DC4 /* ErrorView.swift */; }; 1E23683E253FB07200BE17F3 /* FactCategoryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E23683D253FB07200BE17F3 /* FactCategoryCell.swift */; }; 1E236840253FB13A00BE17F3 /* FactCategoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E23683F253FB13A00BE17F3 /* FactCategoryViewModel.swift */; }; + 1E3075C2254C9D0B0082A194 /* APITarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E3075C1254C9D0B0082A194 /* APITarget.swift */; }; + 1E3075C5254C9D710082A194 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E3075C4254C9D710082A194 /* HTTPMethod.swift */; }; + 1E3075C9254CA5F70082A194 /* APIProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E3075C8254CA5F70082A194 /* APIProvider.swift */; }; 1E32758F2532A2C0007E838A /* EmptyListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E32758E2532A2C0007E838A /* EmptyListView.swift */; }; 1E3275922532A2CD007E838A /* empty-box.json in Resources */ = {isa = PBXBuildFile; fileRef = 1E3275912532A2CD007E838A /* empty-box.json */; }; 1E463803253636160079D8E9 /* SearchFactsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E463802253636160079D8E9 /* SearchFactsViewController.swift */; }; @@ -25,9 +28,16 @@ 1E56172C2541007500BF26A0 /* Data+Stub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E56172B2541007500BF26A0 /* Data+Stub.swift */; }; 1E56172E2541039B00BF26A0 /* SearchFactsViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E56172D2541039B00BF26A0 /* SearchFactsViewControllerTests.swift */; }; 1E580BC8254B92E600886A2E /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1E580BC7254B92E600886A2E /* Localizable.strings */; }; + 1E5A87C4254CD8E30039DE07 /* search-facts.json in Resources */ = {isa = PBXBuildFile; fileRef = 1E5A87C3254CD8E30039DE07 /* search-facts.json */; }; + 1E655786254CB0FF00950706 /* APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E655785254CB0FF00950706 /* APIError.swift */; }; + 1E655788254CB13B00950706 /* APIResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E655787254CB13B00950706 /* APIResponse.swift */; }; + 1E65578A254CB1BB00950706 /* APITask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E655789254CB1BB00950706 /* APITask.swift */; }; + 1E65578C254CB20D00950706 /* API+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E65578B254CB20D00950706 /* API+Rx.swift */; }; + 1E65578E254CB22800950706 /* URLRequest+Encoded.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E65578D254CB22800950706 /* URLRequest+Encoded.swift */; }; 1E7F15BE253324CF0006887B /* FactsListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7F15BD253324CF0006887B /* FactsListViewModelTests.swift */; }; 1E7F15C0253324FD0006887B /* FactsListViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7F15BF253324FD0006887B /* FactsListViewControllerTests.swift */; }; 1E7F15C6253329780006887B /* FactViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7F15C5253329780006887B /* FactViewModelTests.swift */; }; + 1E7F9F9B254CD5110062613C /* APIMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7F9F9A254CD5110062613C /* APIMock.swift */; }; 1E8A0FEC25475B0800565A86 /* SearchFactsTableViewSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E8A0FEB25475B0800565A86 /* SearchFactsTableViewSection.swift */; }; 1E8A0FEE2547603700565A86 /* SuggestionsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E8A0FED2547603700565A86 /* SuggestionsCell.swift */; }; 1E8A0FF0254760D400565A86 /* SuggestionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E8A0FEF254760D400565A86 /* SuggestionsViewModel.swift */; }; @@ -45,7 +55,6 @@ 1EAB20AF2540BEC400633382 /* SuggestionsViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAB20AE2540BEC400633382 /* SuggestionsViewFlowLayout.swift */; }; 1EACEC99253649BD0006B36D /* loading.json in Resources */ = {isa = PBXBuildFile; fileRef = 1EACEC98253649BD0006B36D /* loading.json */; }; 1EACEC9E25364B6C0006B36D /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EACEC9D25364B6C0006B36D /* ActivityIndicator.swift */; }; - 1ECF1443254A514D007D9487 /* HTTPError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ECF1442254A514D007D9487 /* HTTPError.swift */; }; 1ED06C952548AAD300139151 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED06C942548AAD300139151 /* LoadingView.swift */; }; 1ED5D18D25348FD50035046C /* XCTestCase+Stub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED5D18C25348FD50035046C /* XCTestCase+Stub.swift */; }; 1ED5D19025349E9D0035046C /* UIViewController+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED5D18F25349E9D0035046C /* UIViewController+Rx.swift */; }; @@ -62,7 +71,6 @@ 1EE0714925314AF600F6BF6D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1EE0714725314AF600F6BF6D /* LaunchScreen.storyboard */; }; 1EEDC69F254A301D00D75F3E /* UITableView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EEDC69E254A301D00D75F3E /* UITableView+Extensions.swift */; }; 1EEDC6A1254A331500D75F3E /* UICollectionView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EEDC6A0254A331500D75F3E /* UICollectionView+Extensions.swift */; }; - 1EEDC6A3254A38B800D75F3E /* MoyaError+Code.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EEDC6A2254A38B800D75F3E /* MoyaError+Code.swift */; }; 1EEDC6A5254A408B00D75F3E /* FactsListError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EEDC6A4254A408B00D75F3E /* FactsListError.swift */; }; 1EF066E32545CE1D00ECF611 /* PastSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EF066E22545CE1D00ECF611 /* PastSearchViewModel.swift */; }; 1EF066E52545CEC200ECF611 /* SearchEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EF066E42545CEC200ECF611 /* SearchEntity.swift */; }; @@ -104,12 +112,15 @@ 02186229E02F0A408D2EE0C2 /* Pods_Chuck_Norris_Facts.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Chuck_Norris_Facts.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1E0C7B3F2543A4E8002D5C47 /* FactEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactEntity.swift; sourceTree = ""; }; 1E0E4BA12549F7E30030BC49 /* error.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = error.json; sourceTree = ""; }; - 1E15408E2549FA6200675DC4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; 1E135FA9254B4E66009D18AF /* LaunchArgument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchArgument.swift; sourceTree = ""; }; 1E135FAC254B4E9F009D18AF /* XCUIApplication+LaunchArgument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCUIApplication+LaunchArgument.swift"; sourceTree = ""; }; 1E135FAE254B52E0009D18AF /* facts.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = facts.json; sourceTree = ""; }; + 1E15408E2549FA6200675DC4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; 1E23683D253FB07200BE17F3 /* FactCategoryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactCategoryCell.swift; sourceTree = ""; }; 1E23683F253FB13A00BE17F3 /* FactCategoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactCategoryViewModel.swift; sourceTree = ""; }; + 1E3075C1254C9D0B0082A194 /* APITarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APITarget.swift; sourceTree = ""; }; + 1E3075C4254C9D710082A194 /* HTTPMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; + 1E3075C8254CA5F70082A194 /* APIProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIProvider.swift; sourceTree = ""; }; 1E32758E2532A2C0007E838A /* EmptyListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyListView.swift; sourceTree = ""; }; 1E3275912532A2CD007E838A /* empty-box.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "empty-box.json"; sourceTree = ""; }; 1E463802253636160079D8E9 /* SearchFactsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFactsViewController.swift; sourceTree = ""; }; @@ -120,9 +131,16 @@ 1E56172B2541007500BF26A0 /* Data+Stub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Stub.swift"; sourceTree = ""; }; 1E56172D2541039B00BF26A0 /* SearchFactsViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFactsViewControllerTests.swift; sourceTree = ""; }; 1E580BC7254B92E600886A2E /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = ""; }; + 1E5A87C3254CD8E30039DE07 /* search-facts.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "search-facts.json"; sourceTree = ""; }; + 1E655785254CB0FF00950706 /* APIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIError.swift; sourceTree = ""; }; + 1E655787254CB13B00950706 /* APIResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIResponse.swift; sourceTree = ""; }; + 1E655789254CB1BB00950706 /* APITask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APITask.swift; sourceTree = ""; }; + 1E65578B254CB20D00950706 /* API+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "API+Rx.swift"; sourceTree = ""; }; + 1E65578D254CB22800950706 /* URLRequest+Encoded.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+Encoded.swift"; sourceTree = ""; }; 1E7F15BD253324CF0006887B /* FactsListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListViewModelTests.swift; sourceTree = ""; }; 1E7F15BF253324FD0006887B /* FactsListViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListViewControllerTests.swift; sourceTree = ""; }; 1E7F15C5253329780006887B /* FactViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactViewModelTests.swift; sourceTree = ""; }; + 1E7F9F9A254CD5110062613C /* APIMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIMock.swift; sourceTree = ""; }; 1E8A0FEB25475B0800565A86 /* SearchFactsTableViewSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFactsTableViewSection.swift; sourceTree = ""; }; 1E8A0FED2547603700565A86 /* SuggestionsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionsCell.swift; sourceTree = ""; }; 1E8A0FEF254760D400565A86 /* SuggestionsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionsViewModel.swift; sourceTree = ""; }; @@ -140,7 +158,6 @@ 1EAB20AE2540BEC400633382 /* SuggestionsViewFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionsViewFlowLayout.swift; sourceTree = ""; }; 1EACEC98253649BD0006B36D /* loading.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = loading.json; sourceTree = ""; }; 1EACEC9D25364B6C0006B36D /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = ""; }; - 1ECF1442254A514D007D9487 /* HTTPError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPError.swift; sourceTree = ""; }; 1ED06C942548AAD300139151 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; 1ED5D18C25348FD50035046C /* XCTestCase+Stub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+Stub.swift"; sourceTree = ""; }; 1ED5D18F25349E9D0035046C /* UIViewController+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Rx.swift"; sourceTree = ""; }; @@ -163,7 +180,6 @@ 1EE0716025314AF600F6BF6D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 1EEDC69E254A301D00D75F3E /* UITableView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+Extensions.swift"; sourceTree = ""; }; 1EEDC6A0254A331500D75F3E /* UICollectionView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICollectionView+Extensions.swift"; sourceTree = ""; }; - 1EEDC6A2254A38B800D75F3E /* MoyaError+Code.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MoyaError+Code.swift"; sourceTree = ""; }; 1EEDC6A4254A408B00D75F3E /* FactsListError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListError.swift; sourceTree = ""; }; 1EF066E22545CE1D00ECF611 /* PastSearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PastSearchViewModel.swift; sourceTree = ""; }; 1EF066E42545CEC200ECF611 /* SearchEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchEntity.swift; sourceTree = ""; }; @@ -234,6 +250,27 @@ path = PastSearch; sourceTree = ""; }; + 1E3075C0254C9CFC0082A194 /* API */ = { + isa = PBXGroup; + children = ( + 1E3075C3254C9D680082A194 /* HTTP */, + 1E3075C1254C9D0B0082A194 /* APITarget.swift */, + 1E3075C8254CA5F70082A194 /* APIProvider.swift */, + 1E655785254CB0FF00950706 /* APIError.swift */, + 1E655787254CB13B00950706 /* APIResponse.swift */, + 1E655789254CB1BB00950706 /* APITask.swift */, + ); + path = API; + sourceTree = ""; + }; + 1E3075C3254C9D680082A194 /* HTTP */ = { + isa = PBXGroup; + children = ( + 1E3075C4254C9D710082A194 /* HTTPMethod.swift */, + ); + path = HTTP; + sourceTree = ""; + }; 1E32758D2532A2A3007E838A /* Views */ = { isa = PBXGroup; children = ( @@ -406,7 +443,8 @@ 1E56172B2541007500BF26A0 /* Data+Stub.swift */, 1EEDC69E254A301D00D75F3E /* UITableView+Extensions.swift */, 1EEDC6A0254A331500D75F3E /* UICollectionView+Extensions.swift */, - 1EEDC6A2254A38B800D75F3E /* MoyaError+Code.swift */, + 1E65578B254CB20D00950706 /* API+Rx.swift */, + 1E65578D254CB22800950706 /* URLRequest+Encoded.swift */, ); path = Extensions; sourceTree = ""; @@ -415,6 +453,7 @@ isa = PBXGroup; children = ( 1ED5D1922534A56F0035046C /* FactsServiceMock.swift */, + 1E7F9F9A254CD5110062613C /* APIMock.swift */, ); path = Mocks; sourceTree = ""; @@ -426,6 +465,7 @@ 1ED5D1972534AA700035046C /* long-fact.json */, 1ED5D1992534AA7A0035046C /* facts-list.json */, 1EDF0B362541C851001931AA /* get-categories.json */, + 1E5A87C3254CD8E30039DE07 /* search-facts.json */, ); path = Stubs; sourceTree = ""; @@ -473,6 +513,7 @@ 1EE0713B25314AF500F6BF6D /* Chuck Norris Facts */ = { isa = PBXGroup; children = ( + 1E3075C0254C9CFC0082A194 /* API */, 1ED5D18E25349E8D0035046C /* Extensions */, 1EFE2881253210A4008806B9 /* Data */, 1EFE2865253206C8008806B9 /* Library */, @@ -577,7 +618,6 @@ children = ( 1E921128253F6BA500DB340B /* Responses */, 1EFE2883253210B2008806B9 /* FactsAPI.swift */, - 1ECF1442254A514D007D9487 /* HTTPError.swift */, ); path = Networking; sourceTree = ""; @@ -776,6 +816,7 @@ 1ED5D19C2534AAE40035046C /* short-fact.json in Resources */, 1ED5D19A2534AA7A0035046C /* facts-list.json in Resources */, 1EDF0B372541C851001931AA /* get-categories.json in Resources */, + 1E5A87C4254CD8E30039DE07 /* search-facts.json in Resources */, 1ED5D1982534AA700035046C /* long-fact.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -950,10 +991,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 1ECF1443254A514D007D9487 /* HTTPError.swift in Sources */, 1EFE2884253210B2008806B9 /* FactsAPI.swift in Sources */, + 1E3075C2254C9D0B0082A194 /* APITarget.swift in Sources */, + 1E65578C254CB20D00950706 /* API+Rx.swift in Sources */, 1E32758F2532A2C0007E838A /* EmptyListView.swift in Sources */, 1E8A0FF0254760D400565A86 /* SuggestionsViewModel.swift in Sources */, + 1E3075C5254C9D710082A194 /* HTTPMethod.swift in Sources */, 1E463805253636D80079D8E9 /* SearchFactsCoordinator.swift in Sources */, 1EFE288925321123008806B9 /* JSON.swift in Sources */, 1ED06C952548AAD300139151 /* LoadingView.swift in Sources */, @@ -961,13 +1004,16 @@ 1EA3AB02254B956C004A877B /* Strings.swift in Sources */, 1EFE289725321CE2008806B9 /* FactCell.swift in Sources */, 1E56172C2541007500BF26A0 /* Data+Stub.swift in Sources */, + 1E65578A254CB1BB00950706 /* APITask.swift in Sources */, 1E8A0FEE2547603700565A86 /* SuggestionsCell.swift in Sources */, 1E8AF33A254793D800BBB808 /* PastSearchCell.swift in Sources */, 1EE0713D25314AF500F6BF6D /* AppDelegate.swift in Sources */, + 1E3075C9254CA5F70082A194 /* APIProvider.swift in Sources */, 1E8A0FEC25475B0800565A86 /* SearchFactsTableViewSection.swift in Sources */, 1EF066E52545CEC200ECF611 /* SearchEntity.swift in Sources */, 1ED5D19025349E9D0035046C /* UIViewController+Rx.swift in Sources */, 1E236840253FB13A00BE17F3 /* FactCategoryViewModel.swift in Sources */, + 1E65578E254CB22800950706 /* URLRequest+Encoded.swift in Sources */, 1EFE2869253207B2008806B9 /* AppCoordinator.swift in Sources */, 1E92112A253F6BB700DB340B /* SearchFactsResponse.swift in Sources */, 1EEDC6A5254A408B00D75F3E /* FactsListError.swift in Sources */, @@ -976,13 +1022,14 @@ 1EFE288E2532135B008806B9 /* FactsListViewController.swift in Sources */, 1E15408F2549FA6200675DC4 /* ErrorView.swift in Sources */, 1EEDC6A1254A331500D75F3E /* UICollectionView+Extensions.swift in Sources */, + 1E655788254CB13B00950706 /* APIResponse.swift in Sources */, 1EF066E32545CE1D00ECF611 /* PastSearchViewModel.swift in Sources */, + 1E655786254CB0FF00950706 /* APIError.swift in Sources */, 1EFE28902532137C008806B9 /* FactsListCoordinator.swift in Sources */, 1E463803253636160079D8E9 /* SearchFactsViewController.swift in Sources */, 1EEDC69F254A301D00D75F3E /* UITableView+Extensions.swift in Sources */, 1EFE289525321CD2008806B9 /* FactViewModel.swift in Sources */, 1E46380B25363FF40079D8E9 /* SearchFactsViewModel.swift in Sources */, - 1EEDC6A3254A38B800D75F3E /* MoyaError+Code.swift in Sources */, 1E0C7B402543A4E8002D5C47 /* FactEntity.swift in Sources */, 1E921139253F909700DB340B /* FactsStorage.swift in Sources */, 1E23683E253FB07200BE17F3 /* FactCategoryCell.swift in Sources */, @@ -1008,6 +1055,7 @@ 1E5617242540F43F00BF26A0 /* FactsServiceTests.swift in Sources */, 1ED5D18D25348FD50035046C /* XCTestCase+Stub.swift in Sources */, 1ED5D1932534A56F0035046C /* FactsServiceMock.swift in Sources */, + 1E7F9F9B254CD5110062613C /* APIMock.swift in Sources */, 1E7F15BE253324CF0006887B /* FactsListViewModelTests.swift in Sources */, 1E7F15C0253324FD0006887B /* FactsListViewControllerTests.swift in Sources */, ); diff --git a/Chuck Norris Facts/API/APIError.swift b/Chuck Norris Facts/API/APIError.swift new file mode 100644 index 0000000..4224a6e --- /dev/null +++ b/Chuck Norris Facts/API/APIError.swift @@ -0,0 +1,110 @@ +// +// APIError.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/30/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation + +/// A type representing possible errors API can throw. +enum APIError: Swift.Error { + + // Indicates a response failed to map to a Decodable object. + case objectMapping(Swift.Error, APIResponse) + + // Indicated data was not received. + case dataMapping(Swift.Error?) + + // Indicates a response failed with an invalid HTTP status code. + case statusCode(APIResponse) + + // Indicates a response failed due to an underlying `Error`. + case underlying(Swift.Error, APIResponse?) + + // Indicates that an `Endpoint` failed to map to a `URLRequest`. + case requestMapping(String) + + // Indicates that an `Endpoint` failed to encode the parameters for the `URLRequest`. + case parameterEncoding(Swift.Error) + + // Indicates that an Unknown error happened + case unknown(Error?) +} + +extension APIError { + + // Code for each error type. + var code: Int { + switch self { + case .objectMapping: + return 1 + case .dataMapping: + return 2 + case .statusCode: + return 3 + case .underlying: + return 4 + case .requestMapping: + return 5 + case .parameterEncoding: + return 6 + case .unknown: + return 7 + } + } + + // Depending on error type, returns a `Response` object. + var response: APIResponse? { + switch self { + case .objectMapping: return nil + case .requestMapping: return nil + case .parameterEncoding: return nil + case .statusCode: return nil + case .underlying: return nil + case .dataMapping: return nil + case .unknown: return nil + } + } + + // Depending on error type, returns an underlying `Error`. + var underlyingError: Swift.Error? { + switch self { + case .objectMapping(let error, _): return error + case .statusCode: return nil + case .underlying(let error, _): return error + case .requestMapping: return nil + case .parameterEncoding(let error): return error + case .dataMapping: return nil + case .unknown: return nil + } + } +} + +extension APIError: LocalizedError { + public var errorDescription: String? { + switch self { + case .dataMapping: + return "Failed to read data from request." + case .objectMapping: + return "Failed to map data to a Decodable object." + case .statusCode: + return "Status code didn't fall within the given range." + case .underlying(let error, _): + return error.localizedDescription + case .requestMapping: + return "Failed to map Endpoint to a URLRequest." + case .parameterEncoding(let error): + return "Failed to encode parameters for URLRequest. \(error.localizedDescription)" + case .unknown: + return "Something unexpected happened." + } + } +} + +extension APIError: Equatable { + static func == (lhs: APIError, rhs: APIError) -> Bool { + return lhs.code == rhs.code + } +} diff --git a/Chuck Norris Facts/API/APIProvider.swift b/Chuck Norris Facts/API/APIProvider.swift new file mode 100644 index 0000000..9d61380 --- /dev/null +++ b/Chuck Norris Facts/API/APIProvider.swift @@ -0,0 +1,96 @@ +// +// APIProvider.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/30/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation + +// A protocol representing a minimal interface for a APIProvider. +protocol APIProviderType: AnyObject { + + // Completion of a request make by a provider. + typealias Completion = (_ result: Result) -> Void + + // Associated type of an APITarget. + associatedtype Target: APITarget + + // Designated request-making method. + func request(_ target: Target, completion: @escaping Completion) -> URLSessionDataTask? +} + +class APIProvider: APIProviderType { + + // Closure that defines the urlRequest for the provider. + typealias RequestClosure = (Target) -> URLRequest + + // A closure responsible for mapping a `APITarget` to an `URLRequest`. + let requestClosure: RequestClosure + + private let urlSession: URLSession + + init( + urlSession: URLSession = URLSession.shared, + requestClosure: @escaping RequestClosure = { $0.urlRequest() } + ) { + self.urlSession = urlSession + self.requestClosure = requestClosure + } + + // Designated request-making method. + func request(_ target: Target, completion: @escaping Completion) -> URLSessionDataTask? { + + let request = requestClosure(target) + + if let sampleData = target.sampleData { + completion(.success(APIResponse(statusCode: 200, data: sampleData))) + return nil + } + + let task = urlSession.dataTask(with: request) { (data, response, error) in + let response = response as? HTTPURLResponse + + let result = self.convertResponseToResult( + response, + request: request, + data: data, + error: error + ) + + completion(result) + } + + task.resume() + + return task + } + + // A function responsible for converting the result of a `URLRequest` to a Result. + private func convertResponseToResult( + _ response: HTTPURLResponse?, + request: URLRequest?, + data: Data?, + error: Swift.Error? + ) -> Result { + switch (response, data, error) { + case let (.some(response), data, .none): + let response = APIResponse(statusCode: response.statusCode, data: data ?? Data()) + return .success(response) + case let (.some(response), _, .some(error)): + let response = APIResponse(statusCode: response.statusCode, data: data ?? Data()) + let error = APIError.underlying(error, response) + return .failure(error) + case let (_, _, .some(error)): + let error = APIError.underlying(error, nil) + return .failure(error) + default: + let error = APIError.underlying( + NSError(domain: NSURLErrorDomain, code: NSURLErrorUnknown, userInfo: nil), + nil + ) + return .failure(error) + } + } +} diff --git a/Chuck Norris Facts/API/APIResponse.swift b/Chuck Norris Facts/API/APIResponse.swift new file mode 100644 index 0000000..91d4bab --- /dev/null +++ b/Chuck Norris Facts/API/APIResponse.swift @@ -0,0 +1,14 @@ +// +// APIResponse.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/30/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation + +struct APIResponse { + let statusCode: Int + let data: Data? +} diff --git a/Chuck Norris Facts/API/APITarget.swift b/Chuck Norris Facts/API/APITarget.swift new file mode 100644 index 0000000..cc2a2be --- /dev/null +++ b/Chuck Norris Facts/API/APITarget.swift @@ -0,0 +1,62 @@ +// +// APITarget.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/30/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation + +protocol APITarget { + + // The target's base `URL`. + var baseURL: URL { get } + + // The path to be appended to `baseURL` to form the full `URL`. + var path: String { get } + + // The HTTP method used in the request. + var method: HTTPMethod { get } + + // The headers to be used in the request. + var headers: [String: String]? { get } + + // Provides stub data for use in testing. + var sampleData: Data? { get } + + // The type of HTTP task to be performed. + var task: APITask { get } +} + +extension APITarget { + + // Returns the `Endpoint` converted to a `URLRequest` if valid. Throws an error otherwise. + func urlRequest() -> URLRequest { + + var url: URL + let targetPath = self.path + if targetPath.isEmpty { + url = baseURL + } else { + url = baseURL.appendingPathComponent(targetPath) + } + + var request = URLRequest(url: url) + request.httpMethod = method.rawValue + request.allHTTPHeaderFields = headers + + switch task { + case .requestPlain: + return request + case .requestParameters(let parameters): + return request.encoded(parameters: parameters) + } + } +} + +extension APITarget { + + // Provides stub data for use in testing. Default is `Data()`. + var sampleData: Data? { Data() } +} diff --git a/Chuck Norris Facts/API/APITask.swift b/Chuck Norris Facts/API/APITask.swift new file mode 100644 index 0000000..0ba72ae --- /dev/null +++ b/Chuck Norris Facts/API/APITask.swift @@ -0,0 +1,19 @@ +// +// APITask.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/30/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation + +// Represents an HTTP task. +enum APITask { + + // A request with no additional data. + case requestPlain + + // A requests body set with encoded parameters. + case requestParameters(parameters: [String: Any]) +} diff --git a/Chuck Norris Facts/API/HTTP/HTTPMethod.swift b/Chuck Norris Facts/API/HTTP/HTTPMethod.swift new file mode 100644 index 0000000..1191d46 --- /dev/null +++ b/Chuck Norris Facts/API/HTTP/HTTPMethod.swift @@ -0,0 +1,14 @@ +// +// HTTPMethod.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/30/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation + +enum HTTPMethod: String { + case get = "GET" + case post = "POST" +} diff --git a/Chuck Norris Facts/Data/Networking/FactsAPI.swift b/Chuck Norris Facts/Data/Networking/FactsAPI.swift index 719fbcd..251c2e6 100644 --- a/Chuck Norris Facts/Data/Networking/FactsAPI.swift +++ b/Chuck Norris Facts/Data/Networking/FactsAPI.swift @@ -6,15 +6,14 @@ // Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. // -import Moya +import Foundation enum FactsAPI { case searchFacts(searchTerm: String) case getCategories } -extension FactsAPI: TargetType { - +extension FactsAPI: APITarget { var baseURL: URL { return URL(string: "https://api.chucknorris.io/jokes")! } @@ -28,17 +27,17 @@ extension FactsAPI: TargetType { } } - var method: Method { + var method: HTTPMethod { switch self { case .searchFacts, .getCategories: return .get } } - var task: Task { + var task: APITask { switch self { case .searchFacts(let searchTerm): - return .requestParameters(parameters: ["query": searchTerm], encoding: URLEncoding.queryString) + return .requestParameters(parameters: ["query": searchTerm]) case .getCategories: return .requestPlain } @@ -48,13 +47,20 @@ extension FactsAPI: TargetType { return ["Content-type": "application/json"] } - var sampleData: Data { - switch self { - case .getCategories: - return Data.stub("get-categories") ?? Data() - case .searchFacts: - return Data.stub("search-facts") ?? Data() + var sampleData: Data? { + if LaunchArgument.check(.mockHttp) { + switch self { + case .getCategories: + return Data.stub("get-categories") + case .searchFacts: + return Data.stub("search-facts") + } + } + + if LaunchArgument.check(.mockHttpError) { + return Data() } - } + return nil + } } diff --git a/Chuck Norris Facts/Data/Networking/HTTPError.swift b/Chuck Norris Facts/Data/Networking/HTTPError.swift deleted file mode 100644 index 22e15a3..0000000 --- a/Chuck Norris Facts/Data/Networking/HTTPError.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// HTTPError.swift -// Chuck Norris Facts -// -// Created by Djorkaeff Alexandre Vilela Pereira on 10/28/20. -// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. -// - -import Foundation -import Moya - -typealias HTTPError = MoyaError diff --git a/Chuck Norris Facts/Data/Services/FactsService.swift b/Chuck Norris Facts/Data/Services/FactsService.swift index 0db4559..5c4d5ec 100644 --- a/Chuck Norris Facts/Data/Services/FactsService.swift +++ b/Chuck Norris Facts/Data/Services/FactsService.swift @@ -7,7 +7,6 @@ // import RxSwift -import Moya protocol FactsServiceType { @@ -27,51 +26,18 @@ protocol FactsServiceType { func retrievePastSearches() -> Observable<[String]> } -let errorEndpointClosure = { (target: FactsAPI) -> Endpoint in - Endpoint( - url: URL(target: target).absoluteString, - sampleResponseClosure: { .networkResponse(500, Data()) }, - method: target.method, - task: target.task, - httpHeaderFields: target.headers - ) -} - -let mockEndpointClosure = { (target: FactsAPI) -> Endpoint in - Endpoint( - url: URL(target: target).absoluteString, - sampleResponseClosure: { .networkResponse(200, target.sampleData) }, - method: target.method, - task: target.task, - httpHeaderFields: target.headers - ) -} - struct FactsService: FactsServiceType { - private var provider: MoyaProvider + private var provider: APIProvider private var storage: FactsStorageType private var scheduler: SchedulerType? init( - provider: MoyaProvider = MoyaProvider(), + provider: APIProvider = APIProvider(), storage: FactsStorageType = FactsStorage(), scheduler: SchedulerType? = nil ) { - if LaunchArgument.check(.mockHttpError) { - self.provider = MoyaProvider( - endpointClosure: errorEndpointClosure, - stubClosure: MoyaProvider.immediatelyStub - ) - } else if LaunchArgument.check(.mockHttp) { - self.provider = MoyaProvider( - endpointClosure: mockEndpointClosure, - stubClosure: MoyaProvider.immediatelyStub - ) - } else { - self.provider = provider - } - + self.provider = provider self.storage = storage self.scheduler = scheduler } diff --git a/Chuck Norris Facts/Extensions/API+Rx.swift b/Chuck Norris Facts/Extensions/API+Rx.swift new file mode 100644 index 0000000..422c251 --- /dev/null +++ b/Chuck Norris Facts/Extensions/API+Rx.swift @@ -0,0 +1,48 @@ +// +// API+Rx.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/30/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation +import RxSwift + +extension APIProvider: ReactiveCompatible {} + +extension Reactive where Base: APIProviderType { + + func request(_ token: Base.Target, callbackQueue: DispatchQueue? = nil) -> Single { + return Single.create { [weak base] single in + let cancellableToken = base?.request(token) { result in + switch result { + case let .success(response): + single(.success(response)) + case let .failure(error): + single(.error(error)) + } + } + + return Disposables.create { + cancellableToken?.cancel() + } + } + } +} + +extension ObservableType where Element == APIResponse { + func map(_ type: D.Type, using decoder: JSONDecoder = JSON.decoder) -> Observable { + flatMap { response -> Observable in + do { + guard let data = response.data else { + throw APIError.dataMapping(nil) + } + + return Observable.just(try decoder.decode(D.self, from: data)) + } catch let error { + throw APIError.objectMapping(error, response) + } + } + } +} diff --git a/Chuck Norris Facts/Extensions/MoyaError+Code.swift b/Chuck Norris Facts/Extensions/MoyaError+Code.swift deleted file mode 100644 index 385b499..0000000 --- a/Chuck Norris Facts/Extensions/MoyaError+Code.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// MoyaError+Code.swift -// Chuck Norris Facts -// -// Created by Djorkaeff Alexandre Vilela Pereira on 10/28/20. -// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. -// - -import Moya -import Alamofire - -enum HTTPErrorCode: Int { - case unknown = 0 - case noConnection = 1 -} - -extension MoyaError { - - var code: HTTPErrorCode { - let alamofireError = errorUserInfo["NSUnderlyingError"] as? Alamofire.AFError - let error = alamofireError?.underlyingError as NSError? - - switch error?.code { - case NSURLErrorNotConnectedToInternet: - return .noConnection - default: - return .unknown - } - } -} diff --git a/Chuck Norris Facts/Extensions/URLRequest+Encoded.swift b/Chuck Norris Facts/Extensions/URLRequest+Encoded.swift new file mode 100644 index 0000000..3bd1573 --- /dev/null +++ b/Chuck Norris Facts/Extensions/URLRequest+Encoded.swift @@ -0,0 +1,25 @@ +// +// URLRequest+Encoded.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/30/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation + +extension URLRequest { + + // Encode an URL request into a new URLRequest with parameters + func encoded(parameters: [String: Any]?) -> URLRequest { + guard let url = url else { return self } + + var components = URLComponents(url: url, resolvingAgainstBaseURL: false) + components?.query = parameters? + .compactMap { "\($0.key)=\($0.value)" } + .joined(separator: "&") + .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) + + return URLRequest(url: components?.url ?? url) + } +} diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListError.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListError.swift index c0879e5..6e326e1 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListError.swift +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListError.swift @@ -8,37 +8,32 @@ import Foundation -struct FactsListError { +enum FactsListError { - enum ErrorType { - case syncCategories - case searchFacts - } - - let error: HTTPError? + case syncCategories(Error) + case searchFacts(Error) - let type: ErrorType - - var message: String { - switch error?.code { - case .noConnection: - return L10n.FactListError.noConnection - default: - return L10n.FactListError.serviceUnavailable + var error: APIError { + switch self { + case .syncCategories(let error): + return (error as? APIError) ?? APIError.unknown(error) + case .searchFacts(let error): + return (error as? APIError) ?? APIError.unknown(error) } } - var retryEnabled: Bool { - switch error?.code { - case .noConnection: - return false - default: - return true + var code: Int { + switch self { + case .syncCategories: + return -100 + case .searchFacts: + return -101 } } +} - init(_ error: Error, type: ErrorType) { - self.error = error as? HTTPError - self.type = type +extension FactsListError: Equatable { + static func == (lhs: FactsListError, rhs: FactsListError) -> Bool { + lhs.code == rhs.code } } diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift index 248d1cc..4c84332 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift @@ -189,11 +189,11 @@ class FactsListViewController: UIViewController { } } - private func showErrorView(_ error: FactsListError) { + private func showErrorView(_ factsListError: FactsListError) { emptyListView.isHidden = true - errorView.label.text = error.message - errorView.retryButton.isHidden = !error.retryEnabled + let localizedErrorDescription = factsListError.error.underlyingError?.localizedDescription + errorView.label.text = localizedErrorDescription ?? L10n.FactListError.serviceUnavailable errorView.isHidden = false errorView.play() } diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift index 5f3f0ac..16596f7 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift @@ -67,7 +67,7 @@ final class FactsListViewModel { let retrySyncCategories = retryActionSubject .withLatestFrom(currentErrorSubject) .compactMap { $0 } - .filter { $0.type == .syncCategories } + .filter { $0 == .syncCategories($0.error) } .map { _ in () } let syncCategoriesError = Observable.merge(viewDidAppearSubject, retrySyncCategories) @@ -76,7 +76,7 @@ final class FactsListViewModel { .materialize() } .compactMap { $0.event.error } - .map { FactsListError($0, type: .syncCategories) } + .map { FactsListError.syncCategories($0) } let searchFactsError = Observable.combineLatest(viewDidAppearSubject, searchTerm) { _, term in term } .filter { !$0.isEmpty } @@ -86,7 +86,7 @@ final class FactsListViewModel { .materialize() } .compactMap { $0.event.error } - .map { FactsListError($0, type: .searchFacts) } + .map { FactsListError.searchFacts($0) } self.facts = Observable.combineLatest(viewDidAppearSubject, searchTermSubject) .flatMapLatest { _, searchTerm -> Observable<[Fact]> in diff --git a/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift b/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift index 423b693..0f668ed 100644 --- a/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift +++ b/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift @@ -11,7 +11,6 @@ import RxSwift import RxBlocking import RxTest import RealmSwift -import Moya @testable import Chuck_Norris_Facts @@ -19,7 +18,7 @@ final class FactsServiceTests: XCTestCase { var factsService: FactsServiceType! var factsStorage: FactsStorageType! - var factsProvider: MoyaProvider! + var factsProvider: APIMock! var realm: Realm! var disposeBag: DisposeBag! @@ -30,7 +29,7 @@ final class FactsServiceTests: XCTestCase { disposeBag = DisposeBag() realm = try Realm(configuration: .init(inMemoryIdentifier: self.name)) factsStorage = FactsStorage(realm: realm) - factsProvider = MoyaProvider(stubClosure: MoyaProvider.immediatelyStub) + factsProvider = APIMock() factsService = FactsService(provider: factsProvider, storage: factsStorage, scheduler: MainScheduler.instance) } @@ -46,6 +45,8 @@ final class FactsServiceTests: XCTestCase { } func test_syncCategoriesShouldSaveCategoriesOnStorage() throws { + factsProvider.mockRequest(statusCode: 200, data: stub("get-categories")) + let storedCategories = factsStorage.retrieveCategories() let categories = try storedCategories.toBlocking().first() ?? [] XCTAssertTrue(categories.isEmpty) @@ -63,14 +64,17 @@ final class FactsServiceTests: XCTestCase { let categories = try storedCategories.toBlocking().first() ?? [] XCTAssertTrue(categories.isEmpty) - let stubCategories = try stub("get-categories", type: [FactCategory].self) ?? [] - factsStorage.storeCategories(stubCategories) + let stubCategories = try stub("get-categories", type: [FactCategory].self) + let mockCategories = try XCTUnwrap(stubCategories) + factsStorage.storeCategories(mockCategories) let savedCategories = try storedCategories.toBlocking().first() - XCTAssertEqual(savedCategories?.count, stubCategories.count) + XCTAssertEqual(savedCategories?.count, mockCategories.count) } func test_searchFactsShouldSaveFactsOnStorage() throws { + factsProvider.mockRequest(statusCode: 200, data: stub("search-facts")) + let storedFacts = factsStorage.retrieveFacts(searchTerm: "") let facts = try storedFacts.toBlocking().first() ?? [] XCTAssertTrue(facts.isEmpty) @@ -88,11 +92,13 @@ final class FactsServiceTests: XCTestCase { let facts = try storedFacts.toBlocking().first() ?? [] XCTAssertTrue(facts.isEmpty) - let stubFacts = try stub("facts-list", type: [Fact].self) ?? [] - factsStorage.storeFacts(stubFacts) + let stubFacts = try stub("facts-list", type: [Fact].self) + let mockFacts = try XCTUnwrap(stubFacts) + + factsStorage.storeFacts(mockFacts) let savedFacts = try storedFacts.toBlocking().first() - XCTAssertEqual(savedFacts?.count, stubFacts.count) + XCTAssertEqual(savedFacts?.count, mockFacts.count) } func test_retrievePastSearchesShouldReturnDistinctSortedByDateSearchesOnStorage() throws { diff --git a/Chuck Norris FactsTests/Library/XCTestCase+Stub.swift b/Chuck Norris FactsTests/Library/XCTestCase+Stub.swift index 50435dd..5736e7b 100644 --- a/Chuck Norris FactsTests/Library/XCTestCase+Stub.swift +++ b/Chuck Norris FactsTests/Library/XCTestCase+Stub.swift @@ -24,4 +24,19 @@ extension XCTestCase { let stub = try decoder.decode(type, from: data) return stub } + + func stub(_ resource: String) -> Data? { + let bundle = Bundle(for: Self.self) + + do { + guard let url = bundle.url(forResource: resource, withExtension: ".json") else { + return nil + } + + let data = try Data(contentsOf: url) + return data + } catch { + return nil + } + } } diff --git a/Chuck Norris FactsTests/Mocks/APIMock.swift b/Chuck Norris FactsTests/Mocks/APIMock.swift new file mode 100644 index 0000000..91a9340 --- /dev/null +++ b/Chuck Norris FactsTests/Mocks/APIMock.swift @@ -0,0 +1,28 @@ +// +// APIMock.swift +// Chuck Norris FactsTests +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/30/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation + +@testable import Chuck_Norris_Facts + +final class APIMock: APIProvider { + + var requestReturnValue: Result? = .success(APIResponse(statusCode: 200, data: Data())) + + override func request( + _ target: FactsAPI, + completion: @escaping APIProvider.Completion + ) -> URLSessionDataTask? { + completion(requestReturnValue ?? .failure(.unknown(nil))) + return nil + } + + func mockRequest(statusCode: Int, data: Data?) { + requestReturnValue = .success(APIResponse(statusCode: statusCode, data: data)) + } +} diff --git a/Chuck Norris FactsTests/Stubs/search-facts.json b/Chuck Norris FactsTests/Stubs/search-facts.json new file mode 100644 index 0000000..3cb93ab --- /dev/null +++ b/Chuck Norris FactsTests/Stubs/search-facts.json @@ -0,0 +1,153 @@ +{ + "total": 16, + "result": [ + { + "categories": [ + "movie" + ], + "created_at": "2020-01-05 13:42:19.576875", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "sudkgw_tr_ejehjag7cqwq", + "updated_at": "2020-01-05 13:42:19.576875", + "url": "https:\/\/api.chucknorris.io\/jokes\/sudkgw_tr_ejehjag7cqwq", + "value": "The opening scene of the movie \"Saving Private Ryan\" is loosely based on games of dodgeball Chuck Norris played in second grade." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:19.576875", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "H7lHICEVSsW25ffciJEjxw", + "updated_at": "2020-01-05 13:42:19.576875", + "url": "https:\/\/api.chucknorris.io\/jokes\/H7lHICEVSsW25ffciJEjxw", + "value": "Chuck Norris can play Xbox Kinect games on his PlayStation4 and PlayStation Move games on his Xbox 720." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:20.568859", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "0fvCgPtrRqe3BzC8jxEkUA", + "updated_at": "2020-01-05 13:42:20.568859", + "url": "https:\/\/api.chucknorris.io\/jokes\/0fvCgPtrRqe3BzC8jxEkUA", + "value": "Chuck Norris doesn't need to play games against people to beat their high scores. He just plays with himself and beats every highscore on every game on every console in the whole entire universe." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:21.795084", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "2COz4ZY4SJaM7WKJUmSZ3Q", + "updated_at": "2020-01-05 13:42:21.795084", + "url": "https:\/\/api.chucknorris.io\/jokes\/2COz4ZY4SJaM7WKJUmSZ3Q", + "value": "Michael Phelps currently holds the record for most Olympic gold medals in a single Games with 8. That record will be broken in 2012, when Chuck Norris wins 22." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:23.484083", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "eOcHK252SCmv6T5MsJiexA", + "updated_at": "2020-01-05 13:42:23.484083", + "url": "https:\/\/api.chucknorris.io\/jokes\/eOcHK252SCmv6T5MsJiexA", + "value": "Why did Chuck Norris hasn't appeared on any mortal kombat games. Simple, the name says it all. \"mortal\". Also there won't be any fatality tha will work on him, he will just roundhouse kick anyone either he wins or loose." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:23.484083", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "BUBK6qDSRqWevu0YGEEZvw", + "updated_at": "2020-01-05 13:42:23.484083", + "url": "https:\/\/api.chucknorris.io\/jokes\/BUBK6qDSRqWevu0YGEEZvw", + "value": "Chuck Norris can fight better than all fighting video games. How? He instantly wins." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:25.099703", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "pYt9of-uQPqyPhk85Z-zUA", + "updated_at": "2020-01-05 13:42:25.099703", + "url": "https:\/\/api.chucknorris.io\/jokes\/pYt9of-uQPqyPhk85Z-zUA", + "value": "If Chuck Norris were a PC or Mac he'd be a Mac because you can't play games with Chuck Norris" + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:25.628594", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "x6hL23bhTEK03DUlagsIUQ", + "updated_at": "2020-01-05 13:42:25.628594", + "url": "https:\/\/api.chucknorris.io\/jokes\/x6hL23bhTEK03DUlagsIUQ", + "value": "Chuck Norris enjoys playing backyard games with his grandchildren. They often play badminton. But instead of using little sissy racquets & a plastic birdie, they use boat oars & dead chickens." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:25.905626", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "-j_jS99eTIi7hrDpRQ9qLw", + "updated_at": "2020-01-05 13:42:25.905626", + "url": "https:\/\/api.chucknorris.io\/jokes\/-j_jS99eTIi7hrDpRQ9qLw", + "value": "Chuck Norris is forbidden from competing in paintball games... for very fucking obvious reasons." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:26.194739", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "20QgKwidT1-ySGoHJCpwSw", + "updated_at": "2020-01-05 13:42:26.194739", + "url": "https:\/\/api.chucknorris.io\/jokes\/20QgKwidT1-ySGoHJCpwSw", + "value": "It's all fun and games until Chuck Norris pulls your eyes out with a socket wrench." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:26.766831", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "RB2hbqTzTd2ORXy53ITqqQ", + "updated_at": "2020-01-05 13:42:26.766831", + "url": "https:\/\/api.chucknorris.io\/jokes\/RB2hbqTzTd2ORXy53ITqqQ", + "value": "Chuck Norris finished every Call of Duty games in less than 15 minutes..........without shooting a single bullet." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:26.766831", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "1Iy7_hYKT5GgOfxkYuTK3A", + "updated_at": "2020-01-05 13:42:26.766831", + "url": "https:\/\/api.chucknorris.io\/jokes\/1Iy7_hYKT5GgOfxkYuTK3A", + "value": "Chuck Norris is unstoppable in all games of Call of Duty" + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:27.496799", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "4QsnKWP-QFar62XWvYTTsw", + "updated_at": "2020-01-05 13:42:27.496799", + "url": "https:\/\/api.chucknorris.io\/jokes\/4QsnKWP-QFar62XWvYTTsw", + "value": "Chuck Norris invented the olympic games. with his left pinky." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:28.664997", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "R3xVlG1FR7qySlLqsK5Yjw", + "updated_at": "2020-01-05 13:42:28.664997", + "url": "https:\/\/api.chucknorris.io\/jokes\/R3xVlG1FR7qySlLqsK5Yjw", + "value": "Chuck Norris' last birthday party was held at the La Brea Tar Pits where he enjoyed all of the party games and easily won the 'dunking for dinosaurs' event." + }, + { + "categories": [], + "created_at": "2020-01-05 13:42:29.296379", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "baXxcGBqQG6an7udXMTQWA", + "updated_at": "2020-01-05 13:42:29.296379", + "url": "https:\/\/api.chucknorris.io\/jokes\/baXxcGBqQG6an7udXMTQWA", + "value": "When Chuck Norris plays a game, every minute is potentially \"Sudden Death\" for his opponents...including cards and board games." + }, + { + "categories": [ + "celebrity" + ], + "created_at": "2020-01-05 13:42:29.855523", + "icon_url": "https:\/\/assets.chucknorris.host\/img\/avatar\/chuck-norris.png", + "id": "l7QlUREJQzOIJVB88DY9jg", + "updated_at": "2020-01-05 13:42:29.855523", + "url": "https:\/\/api.chucknorris.io\/jokes\/l7QlUREJQzOIJVB88DY9jg", + "value": "Chuck Norris was at the X-games getting ready for competition when he got a message from Paris Hilton saying that she had sent him a friend request on MySpace. An infuriated Chuck Norris logged on to MySpace using his skateboard and rejected the request immediately." + } + ] +} diff --git a/Podfile b/Podfile index dc5a739..89d3d8b 100644 --- a/Podfile +++ b/Podfile @@ -24,9 +24,6 @@ target 'Chuck Norris Facts' do # UI pod 'lottie-ios' - # Networking - pod 'Moya/RxSwift' - # Storage pod 'RealmSwift' diff --git a/Podfile.lock b/Podfile.lock index 8f69904..e27aa0a 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,12 +1,6 @@ PODS: - - Alamofire (5.2.2) - Differentiator (4.0.1) - lottie-ios (3.1.8) - - Moya/Core (14.0.0): - - Alamofire (~> 5.0) - - Moya/RxSwift (14.0.0): - - Moya/Core - - RxSwift (~> 5.0) - Realm (5.4.6): - Realm/Headers (= 5.4.6) - Realm/Headers (5.4.6) @@ -34,7 +28,6 @@ PODS: DEPENDENCIES: - lottie-ios - - Moya/RxSwift - RealmSwift - RxBlocking - RxCocoa @@ -47,10 +40,8 @@ DEPENDENCIES: SPEC REPOS: trunk: - - Alamofire - Differentiator - lottie-ios - - Moya - Realm - RealmSwift - RxBlocking @@ -64,10 +55,8 @@ SPEC REPOS: - SwiftLint SPEC CHECKSUMS: - Alamofire: 814429acc853c6c54ff123fc3d2ef66803823ce0 Differentiator: 886080237d9f87f322641dedbc5be257061b0602 lottie-ios: 48fac6be217c76937e36e340e2d09cf7b10b7f5f - Moya: 5b45dacb75adb009f97fde91c204c1e565d31916 Realm: bb8d7be40d0bc92f139c47095124513c489c0baf RealmSwift: c469118d55feccd985f1de12973c6ef5587213ca RxBlocking: 5f700a78cad61ce253ebd37c9a39b5ccc76477b4 @@ -80,6 +69,6 @@ SPEC CHECKSUMS: SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108 SwiftLint: dfd554ff0dff17288ee574814ccdd5cea85d76f7 -PODFILE CHECKSUM: 002646d5407246e73467489d3ab8bd3ae6552551 +PODFILE CHECKSUM: 39b23e4d93b6e19f465020f3530517a02f6d6898 COCOAPODS: 1.9.3 diff --git a/README.md b/README.md index 5cf80ba..04bd4d2 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,6 @@ Using [RxSwift](https://github.com/ReactiveX/RxSwift) and [RxRealm](https://gith - [SwiftLint](https://github.com/realm/SwiftLint) A tool to enforce Swift style and conventions. - [SwiftGen](https://github.com/SwiftGen/SwiftGen) The Swift code generator for Localizable.strings. - [Lottie](https://github.com/airbnb/lottie-ios) An iOS library to natively render After Effects vector animations. -- [Moya](https://github.com/Moya/Moya) Network abstraction layer written in Swift. - [RealmSwift](https://github.com/realm/realm-cocoa) Realm is a mobile database: a replacement for Core Data & SQLite. ## Fastlane From 774b43358168d83ebf9b70e29e2a5f6fe11a435b Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Sat, 31 Oct 2020 11:49:42 -0300 Subject: [PATCH 15/18] Match Challenge requirements (#33) * APITask -> HTTPTask * Adjust unit tests * Use a more readable syntax * Adjust UI Tests * Adjust FactsList ViewModel Test --- Chuck Norris Facts.xcodeproj/project.pbxproj | 12 ++---- Chuck Norris Facts/API/APITarget.swift | 2 +- .../{APITask.swift => HTTP/HTTPTask.swift} | 6 +-- Chuck Norris Facts/App/AppDelegate.swift | 6 +-- .../Data/Networking/FactsAPI.swift | 2 +- .../Data/Services/FactsService.swift | 22 ++++------ .../Data/Storage/Entities/FactEntity.swift | 39 ----------------- .../Data/Storage/Entities/SearchEntity.swift | 9 +--- .../Data/Storage/FactsStorage.swift | 29 ++----------- .../Facts/FactsList/FactsListViewModel.swift | 20 ++++----- .../Data/Services/FactsServiceTests.swift | 42 +++++-------------- .../Mocks/FactsServiceMock.swift | 9 +--- .../FactsListViewControllerTests.swift | 8 ++-- .../FactsList/FactsListViewModelTests.swift | 8 ++-- .../Tests/FactsListUITests.swift | 19 +++++---- .../Tests/SearchFactsUITests.swift | 2 - 16 files changed, 63 insertions(+), 172 deletions(-) rename Chuck Norris Facts/API/{APITask.swift => HTTP/HTTPTask.swift} (77%) delete mode 100644 Chuck Norris Facts/Data/Storage/Entities/FactEntity.swift diff --git a/Chuck Norris Facts.xcodeproj/project.pbxproj b/Chuck Norris Facts.xcodeproj/project.pbxproj index 9db8585..9a62718 100644 --- a/Chuck Norris Facts.xcodeproj/project.pbxproj +++ b/Chuck Norris Facts.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 1E0C7B402543A4E8002D5C47 /* FactEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E0C7B3F2543A4E8002D5C47 /* FactEntity.swift */; }; 1E0E4BA22549F7E30030BC49 /* error.json in Resources */ = {isa = PBXBuildFile; fileRef = 1E0E4BA12549F7E30030BC49 /* error.json */; }; 1E135FAA254B4E66009D18AF /* LaunchArgument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E135FA9254B4E66009D18AF /* LaunchArgument.swift */; }; 1E135FAD254B4E9F009D18AF /* XCUIApplication+LaunchArgument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E135FAC254B4E9F009D18AF /* XCUIApplication+LaunchArgument.swift */; }; @@ -31,9 +30,9 @@ 1E5A87C4254CD8E30039DE07 /* search-facts.json in Resources */ = {isa = PBXBuildFile; fileRef = 1E5A87C3254CD8E30039DE07 /* search-facts.json */; }; 1E655786254CB0FF00950706 /* APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E655785254CB0FF00950706 /* APIError.swift */; }; 1E655788254CB13B00950706 /* APIResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E655787254CB13B00950706 /* APIResponse.swift */; }; - 1E65578A254CB1BB00950706 /* APITask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E655789254CB1BB00950706 /* APITask.swift */; }; 1E65578C254CB20D00950706 /* API+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E65578B254CB20D00950706 /* API+Rx.swift */; }; 1E65578E254CB22800950706 /* URLRequest+Encoded.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E65578D254CB22800950706 /* URLRequest+Encoded.swift */; }; + 1E7A6528254DA2B1006E493B /* HTTPTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7A6527254DA2B1006E493B /* HTTPTask.swift */; }; 1E7F15BE253324CF0006887B /* FactsListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7F15BD253324CF0006887B /* FactsListViewModelTests.swift */; }; 1E7F15C0253324FD0006887B /* FactsListViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7F15BF253324FD0006887B /* FactsListViewControllerTests.swift */; }; 1E7F15C6253329780006887B /* FactViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7F15C5253329780006887B /* FactViewModelTests.swift */; }; @@ -110,7 +109,6 @@ /* Begin PBXFileReference section */ 02186229E02F0A408D2EE0C2 /* Pods_Chuck_Norris_Facts.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Chuck_Norris_Facts.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 1E0C7B3F2543A4E8002D5C47 /* FactEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactEntity.swift; sourceTree = ""; }; 1E0E4BA12549F7E30030BC49 /* error.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = error.json; sourceTree = ""; }; 1E135FA9254B4E66009D18AF /* LaunchArgument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchArgument.swift; sourceTree = ""; }; 1E135FAC254B4E9F009D18AF /* XCUIApplication+LaunchArgument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCUIApplication+LaunchArgument.swift"; sourceTree = ""; }; @@ -134,9 +132,9 @@ 1E5A87C3254CD8E30039DE07 /* search-facts.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "search-facts.json"; sourceTree = ""; }; 1E655785254CB0FF00950706 /* APIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIError.swift; sourceTree = ""; }; 1E655787254CB13B00950706 /* APIResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIResponse.swift; sourceTree = ""; }; - 1E655789254CB1BB00950706 /* APITask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APITask.swift; sourceTree = ""; }; 1E65578B254CB20D00950706 /* API+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "API+Rx.swift"; sourceTree = ""; }; 1E65578D254CB22800950706 /* URLRequest+Encoded.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+Encoded.swift"; sourceTree = ""; }; + 1E7A6527254DA2B1006E493B /* HTTPTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPTask.swift; sourceTree = ""; }; 1E7F15BD253324CF0006887B /* FactsListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListViewModelTests.swift; sourceTree = ""; }; 1E7F15BF253324FD0006887B /* FactsListViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListViewControllerTests.swift; sourceTree = ""; }; 1E7F15C5253329780006887B /* FactViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactViewModelTests.swift; sourceTree = ""; }; @@ -258,7 +256,6 @@ 1E3075C8254CA5F70082A194 /* APIProvider.swift */, 1E655785254CB0FF00950706 /* APIError.swift */, 1E655787254CB13B00950706 /* APIResponse.swift */, - 1E655789254CB1BB00950706 /* APITask.swift */, ); path = API; sourceTree = ""; @@ -267,6 +264,7 @@ isa = PBXGroup; children = ( 1E3075C4254C9D710082A194 /* HTTPMethod.swift */, + 1E7A6527254DA2B1006E493B /* HTTPTask.swift */, ); path = HTTP; sourceTree = ""; @@ -414,7 +412,6 @@ isa = PBXGroup; children = ( 1E92113D253F915100DB340B /* FactCategoryEntity.swift */, - 1E0C7B3F2543A4E8002D5C47 /* FactEntity.swift */, 1EF066E42545CEC200ECF611 /* SearchEntity.swift */, ); path = Entities; @@ -1004,7 +1001,6 @@ 1EA3AB02254B956C004A877B /* Strings.swift in Sources */, 1EFE289725321CE2008806B9 /* FactCell.swift in Sources */, 1E56172C2541007500BF26A0 /* Data+Stub.swift in Sources */, - 1E65578A254CB1BB00950706 /* APITask.swift in Sources */, 1E8A0FEE2547603700565A86 /* SuggestionsCell.swift in Sources */, 1E8AF33A254793D800BBB808 /* PastSearchCell.swift in Sources */, 1EE0713D25314AF500F6BF6D /* AppDelegate.swift in Sources */, @@ -1030,11 +1026,11 @@ 1EEDC69F254A301D00D75F3E /* UITableView+Extensions.swift in Sources */, 1EFE289525321CD2008806B9 /* FactViewModel.swift in Sources */, 1E46380B25363FF40079D8E9 /* SearchFactsViewModel.swift in Sources */, - 1E0C7B402543A4E8002D5C47 /* FactEntity.swift in Sources */, 1E921139253F909700DB340B /* FactsStorage.swift in Sources */, 1E23683E253FB07200BE17F3 /* FactCategoryCell.swift in Sources */, 1E92113E253F915100DB340B /* FactCategoryEntity.swift in Sources */, 1EF0DA1425449898005CF7E2 /* CategoryView.swift in Sources */, + 1E7A6528254DA2B1006E493B /* HTTPTask.swift in Sources */, 1EAB20AF2540BEC400633382 /* SuggestionsViewFlowLayout.swift in Sources */, 1E92112D253F6D0000DB340B /* FactsService.swift in Sources */, 1EFE2892253214DB008806B9 /* FactsListViewModel.swift in Sources */, diff --git a/Chuck Norris Facts/API/APITarget.swift b/Chuck Norris Facts/API/APITarget.swift index cc2a2be..5459f8e 100644 --- a/Chuck Norris Facts/API/APITarget.swift +++ b/Chuck Norris Facts/API/APITarget.swift @@ -26,7 +26,7 @@ protocol APITarget { var sampleData: Data? { get } // The type of HTTP task to be performed. - var task: APITask { get } + var task: HTTPTask { get } } extension APITarget { diff --git a/Chuck Norris Facts/API/APITask.swift b/Chuck Norris Facts/API/HTTP/HTTPTask.swift similarity index 77% rename from Chuck Norris Facts/API/APITask.swift rename to Chuck Norris Facts/API/HTTP/HTTPTask.swift index 0ba72ae..a9d58a9 100644 --- a/Chuck Norris Facts/API/APITask.swift +++ b/Chuck Norris Facts/API/HTTP/HTTPTask.swift @@ -1,15 +1,15 @@ // -// APITask.swift +// HTTPTask.swift // Chuck Norris Facts // -// Created by Djorkaeff Alexandre Vilela Pereira on 10/30/20. +// Created by Djorkaeff Alexandre Vilela Pereira on 10/31/20. // Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. // import Foundation // Represents an HTTP task. -enum APITask { +enum HTTPTask { // A request with no additional data. case requestPlain diff --git a/Chuck Norris Facts/App/AppDelegate.swift b/Chuck Norris Facts/App/AppDelegate.swift index c99a550..62f35c0 100644 --- a/Chuck Norris Facts/App/AppDelegate.swift +++ b/Chuck Norris Facts/App/AppDelegate.swift @@ -35,11 +35,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } if LaunchArgument.check(.mockStorage) { - let facts = Data.stub("facts", type: [Fact].self) ?? [] - let entities = [ - SearchEntity(searchTerm: "games", facts: facts), - SearchEntity(searchTerm: "fashion", facts: facts) + SearchEntity(searchTerm: "games"), + SearchEntity(searchTerm: "fashion") ] let realm = try? Realm() diff --git a/Chuck Norris Facts/Data/Networking/FactsAPI.swift b/Chuck Norris Facts/Data/Networking/FactsAPI.swift index 251c2e6..a05ba11 100644 --- a/Chuck Norris Facts/Data/Networking/FactsAPI.swift +++ b/Chuck Norris Facts/Data/Networking/FactsAPI.swift @@ -34,7 +34,7 @@ extension FactsAPI: APITarget { } } - var task: APITask { + var task: HTTPTask { switch self { case .searchFacts(let searchTerm): return .requestParameters(parameters: ["query": searchTerm]) diff --git a/Chuck Norris Facts/Data/Services/FactsService.swift b/Chuck Norris Facts/Data/Services/FactsService.swift index 5c4d5ec..5b9be21 100644 --- a/Chuck Norris Facts/Data/Services/FactsService.swift +++ b/Chuck Norris Facts/Data/Services/FactsService.swift @@ -11,7 +11,7 @@ import RxSwift protocol FactsServiceType { // Search Facts on Chuck Norris API - func searchFacts(searchTerm: String) -> Observable + func searchFacts(searchTerm: String) -> Observable<[Fact]> // Sync local stored Categories with Chuck Norris API Categories func syncCategories() -> Observable @@ -19,9 +19,6 @@ protocol FactsServiceType { // Retrieve local stored Categories func retrieveCategories() -> Observable<[FactCategory]> - // Retrieve local stored Facts - func retrieveFacts(searchTerm: String) -> Observable<[Fact]> - // Retrieve local stored Past Searches func retrievePastSearches() -> Observable<[String]> } @@ -42,15 +39,18 @@ struct FactsService: FactsServiceType { self.scheduler = scheduler } - func searchFacts(searchTerm: String) -> Observable { - provider.rx + func searchFacts(searchTerm: String) -> Observable<[Fact]> { + guard !searchTerm.isEmpty else { return .just([]) } + + return provider.rx .request(.searchFacts(searchTerm: searchTerm)) .asObservable() - .observeOn(scheduler ?? MainScheduler.asyncInstance) + .observeOn(self.scheduler ?? MainScheduler.asyncInstance) .map(SearchFactsResponse.self, using: JSON.decoder) .map { $0.facts } - .map { self.storage.storeSearch(searchTerm: searchTerm, facts: $0) } - .map { () } + .do(onNext: { _ in + self.storage.storeSearch(searchTerm: searchTerm) + }) } func syncCategories() -> Observable { @@ -72,10 +72,6 @@ struct FactsService: FactsServiceType { storage.retrieveCategories() } - func retrieveFacts(searchTerm: String) -> Observable<[Fact]> { - storage.retrieveFacts(searchTerm: searchTerm) - } - func retrievePastSearches() -> Observable<[String]> { storage.retrieveSearches() } diff --git a/Chuck Norris Facts/Data/Storage/Entities/FactEntity.swift b/Chuck Norris Facts/Data/Storage/Entities/FactEntity.swift deleted file mode 100644 index 16ec503..0000000 --- a/Chuck Norris Facts/Data/Storage/Entities/FactEntity.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// FactEntity.swift -// Chuck Norris Facts -// -// Created by Djorkaeff Alexandre Vilela Pereira on 10/23/20. -// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. -// - -import Foundation -import RealmSwift - -class FactEntity: Object { - @objc dynamic var id = "" - @objc dynamic var url = "" - @objc dynamic var value = "" - @objc dynamic var iconUrl = "" - - let categories = List() - - let search = LinkingObjects(fromType: SearchEntity.self, property: "facts") - - override static func primaryKey() -> String? { - "id" - } - - convenience init(fact: Fact) { - self.init(value: [ - "id": fact.id, - "url": fact.url ?? "", - "value": fact.value, - "iconUrl": fact.iconUrl, - "categories": fact.categories.map(FactCategoryEntity.init) - ]) - } - - var item: Fact { - Fact(id: id, value: value, url: url, iconUrl: iconUrl, categories: categories.map { $0.item }) - } -} diff --git a/Chuck Norris Facts/Data/Storage/Entities/SearchEntity.swift b/Chuck Norris Facts/Data/Storage/Entities/SearchEntity.swift index 47fc135..9e03521 100644 --- a/Chuck Norris Facts/Data/Storage/Entities/SearchEntity.swift +++ b/Chuck Norris Facts/Data/Storage/Entities/SearchEntity.swift @@ -13,16 +13,11 @@ class SearchEntity: Object { @objc dynamic var searchTerm = "" @objc dynamic var updatedAt = Date() - let facts = List() - override static func primaryKey() -> String? { "searchTerm" } - convenience init(searchTerm: String, facts: [Fact]) { - self.init(value: [ - "searchTerm": searchTerm, - "facts": facts.map(FactEntity.init) - ]) + convenience init(searchTerm: String) { + self.init(value: ["searchTerm": searchTerm]) } } diff --git a/Chuck Norris Facts/Data/Storage/FactsStorage.swift b/Chuck Norris Facts/Data/Storage/FactsStorage.swift index feacc3c..b80f0a7 100644 --- a/Chuck Norris Facts/Data/Storage/FactsStorage.swift +++ b/Chuck Norris Facts/Data/Storage/FactsStorage.swift @@ -18,14 +18,8 @@ protocol FactsStorageType { // Retrieve all local stored categories func retrieveCategories() -> Observable<[FactCategory]> - // Store a list of facts - func storeFacts(_ facts: [Fact]) - - // Retrieve local stored facts filtered by a search term - func retrieveFacts(searchTerm: String) -> Observable<[Fact]> - // Store a search and it's result - func storeSearch(searchTerm: String, facts: [Fact]) + func storeSearch(searchTerm: String) // Retrieve all past searches terms func retrieveSearches() -> Observable<[String]> @@ -50,26 +44,9 @@ final class FactsStorage: FactsStorageType { return Observable.collection(from: entities).map { $0.map { $0.item } } } - func storeFacts(_ facts: [Fact]) { - try? realm.write { - let entities = facts.map(FactEntity.init) - self.realm.add(entities, update: .modified) - } - } - - func retrieveFacts(searchTerm: String) -> Observable<[Fact]> { - var entities = realm.objects(FactEntity.self) - - if !searchTerm.isEmpty { - entities = entities.filter("ANY search.searchTerm = %@", searchTerm) - } - - return Observable.collection(from: entities).map { $0.map { $0.item }} - } - - func storeSearch(searchTerm: String, facts: [Fact]) { + func storeSearch(searchTerm: String) { try? realm.write { - let entity = SearchEntity(searchTerm: searchTerm, facts: facts) + let entity = SearchEntity(searchTerm: searchTerm) self.realm.add(entity, update: .modified) } } diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift index 16596f7..c732638 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift +++ b/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift @@ -64,8 +64,7 @@ final class FactsListViewModel { let currentErrorSubject = BehaviorSubject(value: nil) - let retrySyncCategories = retryActionSubject - .withLatestFrom(currentErrorSubject) + let retrySyncCategories = retryActionSubject.withLatestFrom(currentErrorSubject) .compactMap { $0 } .filter { $0 == .syncCategories($0.error) } .map { _ in () } @@ -78,28 +77,23 @@ final class FactsListViewModel { .compactMap { $0.event.error } .map { FactsListError.syncCategories($0) } - let searchFactsError = Observable.combineLatest(viewDidAppearSubject, searchTerm) { _, term in term } - .filter { !$0.isEmpty } + let searchFacts = Observable.combineLatest(viewDidAppearSubject, searchTerm) { _, term in term } .flatMapLatest { searchTerm in factsService.searchFacts(searchTerm: searchTerm) .trackActivity(loadingIndicator) .materialize() } + + let searchFactsError = searchFacts .compactMap { $0.event.error } .map { FactsListError.searchFacts($0) } - self.facts = Observable.combineLatest(viewDidAppearSubject, searchTermSubject) - .flatMapLatest { _, searchTerm -> Observable<[Fact]> in - let facts = factsService.retrieveFacts(searchTerm: searchTerm) - if searchTerm.isEmpty { - return facts.map { Array($0.shuffled().prefix(10)) } - } - return facts - } + self.facts = searchFacts + .compactMap { $0.event.element } .map { $0.map { FactViewModel(fact: $0) } } .map { [FactsSectionModel(model: "", items: $0)] } - self.errors = Observable.merge(searchFactsError, syncCategoriesError) + self.errors = Observable.merge(syncCategoriesError, searchFactsError) .do(onNext: currentErrorSubject.onNext) } } diff --git a/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift b/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift index 0f668ed..fc88cfc 100644 --- a/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift +++ b/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift @@ -72,33 +72,19 @@ final class FactsServiceTests: XCTestCase { XCTAssertEqual(savedCategories?.count, mockCategories.count) } - func test_searchFactsShouldSaveFactsOnStorage() throws { + func test_searchFactsShouldSaveRetrieveAPIFacts() throws { factsProvider.mockRequest(statusCode: 200, data: stub("search-facts")) - let storedFacts = factsStorage.retrieveFacts(searchTerm: "") - let facts = try storedFacts.toBlocking().first() ?? [] - XCTAssertTrue(facts.isEmpty) + let factsObserver = testScheduler.createObserver([Fact].self) - factsService.searchFacts(searchTerm: "") - .subscribe() + factsService.searchFacts(searchTerm: "games") + .subscribe(factsObserver) .disposed(by: disposeBag) - let savedFacts = try storedFacts.toBlocking().first() - XCTAssertEqual(savedFacts?.count, 16) - } - - func test_retrieveFactsShouldReturnFactsOnStorage() throws { - let storedFacts = factsStorage.retrieveFacts(searchTerm: "") - let facts = try storedFacts.toBlocking().first() ?? [] - XCTAssertTrue(facts.isEmpty) - - let stubFacts = try stub("facts-list", type: [Fact].self) - let mockFacts = try XCTUnwrap(stubFacts) + testScheduler.start() - factsStorage.storeFacts(mockFacts) - - let savedFacts = try storedFacts.toBlocking().first() - XCTAssertEqual(savedFacts?.count, mockFacts.count) + let facts = factsObserver.events.compactMap { $0.value.element }.first + XCTAssertEqual(facts?.count, 16) } func test_retrievePastSearchesShouldReturnDistinctSortedByDateSearchesOnStorage() throws { @@ -106,16 +92,10 @@ final class FactsServiceTests: XCTestCase { let searches = try storedSearches.toBlocking().first() ?? [] XCTAssertTrue(searches.isEmpty) - let stubShortFact = try stub("short-fact", type: Fact.self) - let shortFact = try XCTUnwrap(stubShortFact) - - let stubLongFact = try stub("long-fact", type: Fact.self) - let longFact = try XCTUnwrap(stubLongFact) - - factsStorage.storeSearch(searchTerm: "games", facts: [shortFact]) - factsStorage.storeSearch(searchTerm: "explicit", facts: [longFact]) - factsStorage.storeSearch(searchTerm: "explicit", facts: [longFact]) - factsStorage.storeSearch(searchTerm: "fashion", facts: [shortFact]) + factsStorage.storeSearch(searchTerm: "games") + factsStorage.storeSearch(searchTerm: "explicit") + factsStorage.storeSearch(searchTerm: "explicit") + factsStorage.storeSearch(searchTerm: "fashion") let savedSearches = try storedSearches.toBlocking().first() XCTAssertEqual(savedSearches, ["fashion", "explicit", "games"]) diff --git a/Chuck Norris FactsTests/Mocks/FactsServiceMock.swift b/Chuck Norris FactsTests/Mocks/FactsServiceMock.swift index aa976be..511daed 100644 --- a/Chuck Norris FactsTests/Mocks/FactsServiceMock.swift +++ b/Chuck Norris FactsTests/Mocks/FactsServiceMock.swift @@ -23,16 +23,11 @@ final class FactsServiceMock: FactsServiceType { return retrieveCategoriesReturnValue } - var searchFactsReturnValue: Observable = .just(()) - func searchFacts(searchTerm: String) -> Observable { + var searchFactsReturnValue: Observable<[Fact]> = .just([]) + func searchFacts(searchTerm: String) -> Observable<[Fact]> { return searchFactsReturnValue } - var retrieveFactsReturnValue: Observable<[Fact]> = .just([]) - func retrieveFacts(searchTerm: String) -> Observable<[Fact]> { - return retrieveFactsReturnValue - } - var retrievePastSearchesReturnValue: Observable<[String]> = .just([]) func retrievePastSearches() -> Observable<[String]> { return retrievePastSearchesReturnValue diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift index e37300c..58e6789 100644 --- a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift +++ b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift @@ -37,7 +37,7 @@ class FactsListViewControllerTests: XCTestCase { } func test_factsListEmptyShouldShowEmptyList() { - factsServiceMock.retrieveFactsReturnValue = .just([]) + factsServiceMock.searchFactsReturnValue = .just([]) factsListViewModel.viewDidAppear.onNext(()) @@ -48,7 +48,7 @@ class FactsListViewControllerTests: XCTestCase { let factStub = try stub("short-fact", type: Fact.self) let fact = try XCTUnwrap(factStub, "looks like short-fact.json doesn't exists") - factsServiceMock.retrieveFactsReturnValue = .just([fact]) + factsServiceMock.searchFactsReturnValue = .just([fact]) factsListViewModel.viewDidAppear.onNext(()) @@ -61,7 +61,7 @@ class FactsListViewControllerTests: XCTestCase { let factStub = try stub("long-fact", type: Fact.self) let fact = try XCTUnwrap(factStub, "looks like long-fact.json doesn't exists") - factsServiceMock.retrieveFactsReturnValue = .just([fact]) + factsServiceMock.searchFactsReturnValue = .just([fact]) factsListViewModel.viewDidAppear.onNext(()) @@ -74,7 +74,7 @@ class FactsListViewControllerTests: XCTestCase { let factStub = try stub("short-fact", type: Fact.self) let fact = try XCTUnwrap(factStub, "looks like short-fact.json doesn't exists") - factsServiceMock.retrieveFactsReturnValue = .just([fact]) + factsServiceMock.searchFactsReturnValue = .just([fact]) let testScheduler = TestScheduler(initialClock: 0) let shareFactObserver = testScheduler.createObserver(FactViewModel.self) diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift index f3b215c..320ca30 100644 --- a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift +++ b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift @@ -34,10 +34,8 @@ class FactsListViewModelTests: XCTestCase { factsListViewModel = nil } - func test_load10RandomFacts() throws { - let factsListStub = try stub("facts-list", type: [Fact].self) - let factsList = try XCTUnwrap(factsListStub, "looks like facts-list.json doesn't exists") - factsServiceMock.retrieveFactsReturnValue = .just(factsList) + func test_loadEmptyFacts() throws { + factsServiceMock.searchFactsReturnValue = .just([]) let factsObserver = testScheduler.createObserver([FactsSectionModel].self) @@ -50,7 +48,7 @@ class FactsListViewModelTests: XCTestCase { testScheduler.start() let sectionModels = factsObserver.events.compactMap { $0.value.element }.first - XCTAssertEqual(sectionModels?.first?.items.count, 10) + XCTAssertEqual(sectionModels?.first?.items.count, 0) } func test_showShareFact() throws { diff --git a/Chuck Norris FactsUITests/Tests/FactsListUITests.swift b/Chuck Norris FactsUITests/Tests/FactsListUITests.swift index 65f8e78..f7c3a4c 100644 --- a/Chuck Norris FactsUITests/Tests/FactsListUITests.swift +++ b/Chuck Norris FactsUITests/Tests/FactsListUITests.swift @@ -28,20 +28,23 @@ final class FactsListUITests: XCTestCase { XCTAssertTrue(factsListScene.emptyListLabelView.exists) } - func test_show10RandomFacts() { - app.setLaunchArguments([.uiTest, .mockStorage]) + func test_shareFact() { + app.setLaunchArguments([.uiTest, .mockStorage, .mockHttp]) app.launch() let factsListScene = FactsListScene() - XCTAssertEqual(factsListScene.factsTableView.cells.count, 10) - } + let searchFactsScene = SearchFactsScene() + let searchFactsButton = factsListScene.searchButton + XCTAssertTrue(searchFactsButton.exists) - func test_shareFact() { - app.setLaunchArguments([.uiTest, .mockStorage]) - app.launch() + searchFactsButton.firstMatch.tap() + + searchFactsScene.searchBarField.tap() + searchFactsScene.searchBarField.typeText("games") + + app.keyboards.buttons["Search"].tap() - let factsListScene = FactsListScene() let firstFactCell = factsListScene.factsTableView.firstMatch let shareFactButton = firstFactCell.buttons["shareFactButton"] XCTAssertTrue(shareFactButton.exists) diff --git a/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift b/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift index 6b76af1..16b27d8 100644 --- a/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift +++ b/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift @@ -79,7 +79,6 @@ final class SearchFactsUITests: XCTestCase { suggestion.tap() - XCTAssertFalse(searchFactsScene.searchFactsView.exists) XCTAssertGreaterThan(factsListScene.factsTableView.cells.count, 0) } @@ -100,7 +99,6 @@ final class SearchFactsUITests: XCTestCase { pastSearchCell.tap() - XCTAssertFalse(searchFactsScene.searchFactsView.exists) XCTAssertGreaterThan(factsListScene.factsTableView.cells.count, 0) } From 6d1d6705a9c55d54c74976e8f7bcc7d9ffec0610 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Sat, 31 Oct 2020 16:17:16 -0300 Subject: [PATCH 16/18] Refactor Tests (#34) * Better naming tests * Increase coverage --- Chuck Norris Facts.xcodeproj/project.pbxproj | 30 ++++++++++-- .../Data/Services/FactsServiceTests.swift | 25 ++++++++-- .../{Cells => Fact}/FactViewModelTests.swift | 16 +++++-- .../FactsListViewControllerTests.swift | 37 +++++++++++--- .../FactsList/FactsListViewModelTests.swift | 48 +++++++++++++++++-- .../SearchFactsViewControllerTests.swift | 28 +++++++++-- .../SearchFactsViewModelTests.swift | 8 ++-- .../FactCategoryViewModelTests.swift | 34 +++++++++++++ .../Stubs/fact-category.json | 1 + .../Tests/FactsListUITests.swift | 8 ++-- .../Tests/SearchFactsUITests.swift | 14 +++--- 11 files changed, 208 insertions(+), 41 deletions(-) rename Chuck Norris FactsTests/Scenes/Facts/FactsList/{Cells => Fact}/FactViewModelTests.swift (57%) create mode 100644 Chuck Norris FactsTests/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryViewModelTests.swift create mode 100644 Chuck Norris FactsTests/Stubs/fact-category.json diff --git a/Chuck Norris Facts.xcodeproj/project.pbxproj b/Chuck Norris Facts.xcodeproj/project.pbxproj index 9a62718..4f80f1c 100644 --- a/Chuck Norris Facts.xcodeproj/project.pbxproj +++ b/Chuck Norris Facts.xcodeproj/project.pbxproj @@ -50,6 +50,8 @@ 1E921139253F909700DB340B /* FactsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E921138253F909700DB340B /* FactsStorage.swift */; }; 1E92113B253F90BF00DB340B /* FactCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E92113A253F90BF00DB340B /* FactCategory.swift */; }; 1E92113E253F915100DB340B /* FactCategoryEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E92113D253F915100DB340B /* FactCategoryEntity.swift */; }; + 1E9489F3254DEB2500A0002C /* FactCategoryViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9489F2254DEB2500A0002C /* FactCategoryViewModelTests.swift */; }; + 1E9489F5254DEBB200A0002C /* fact-category.json in Resources */ = {isa = PBXBuildFile; fileRef = 1E9489F4254DEBB200A0002C /* fact-category.json */; }; 1EA3AB02254B956C004A877B /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA3AB01254B956C004A877B /* Strings.swift */; }; 1EAB20AF2540BEC400633382 /* SuggestionsViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EAB20AE2540BEC400633382 /* SuggestionsViewFlowLayout.swift */; }; 1EACEC99253649BD0006B36D /* loading.json in Resources */ = {isa = PBXBuildFile; fileRef = 1EACEC98253649BD0006B36D /* loading.json */; }; @@ -152,6 +154,8 @@ 1E921138253F909700DB340B /* FactsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsStorage.swift; sourceTree = ""; }; 1E92113A253F90BF00DB340B /* FactCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactCategory.swift; sourceTree = ""; }; 1E92113D253F915100DB340B /* FactCategoryEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactCategoryEntity.swift; sourceTree = ""; }; + 1E9489F2254DEB2500A0002C /* FactCategoryViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactCategoryViewModelTests.swift; sourceTree = ""; }; + 1E9489F4254DEBB200A0002C /* fact-category.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "fact-category.json"; sourceTree = ""; }; 1EA3AB01254B956C004A877B /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; 1EAB20AE2540BEC400633382 /* SuggestionsViewFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionsViewFlowLayout.swift; sourceTree = ""; }; 1EACEC98253649BD0006B36D /* loading.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = loading.json; sourceTree = ""; }; @@ -339,19 +343,19 @@ 1E7F15BC253324BD0006887B /* FactsList */ = { isa = PBXGroup; children = ( - 1E7F15C4253329600006887B /* Cells */, + 1E7F15C4253329600006887B /* Fact */, 1E7F15BD253324CF0006887B /* FactsListViewModelTests.swift */, 1E7F15BF253324FD0006887B /* FactsListViewControllerTests.swift */, ); path = FactsList; sourceTree = ""; }; - 1E7F15C4253329600006887B /* Cells */ = { + 1E7F15C4253329600006887B /* Fact */ = { isa = PBXGroup; children = ( 1E7F15C5253329780006887B /* FactViewModelTests.swift */, ); - path = Cells; + path = Fact; sourceTree = ""; }; 1E8AF337254792DB00BBB808 /* FactCategory */ = { @@ -393,6 +397,7 @@ 1E921132253F84DE00DB340B /* SearchFacts */ = { isa = PBXGroup; children = ( + 1E9489F0254DEB0B00A0002C /* Suggestions */, 1E921133253F84F100DB340B /* SearchFactsViewModelTests.swift */, 1E56172D2541039B00BF26A0 /* SearchFactsViewControllerTests.swift */, ); @@ -417,6 +422,22 @@ path = Entities; sourceTree = ""; }; + 1E9489F0254DEB0B00A0002C /* Suggestions */ = { + isa = PBXGroup; + children = ( + 1E9489F1254DEB1300A0002C /* FactCategory */, + ); + path = Suggestions; + sourceTree = ""; + }; + 1E9489F1254DEB1300A0002C /* FactCategory */ = { + isa = PBXGroup; + children = ( + 1E9489F2254DEB2500A0002C /* FactCategoryViewModelTests.swift */, + ); + path = FactCategory; + sourceTree = ""; + }; 1EA3AB00254B955F004A877B /* Generated */ = { isa = PBXGroup; children = ( @@ -463,6 +484,7 @@ 1ED5D1992534AA7A0035046C /* facts-list.json */, 1EDF0B362541C851001931AA /* get-categories.json */, 1E5A87C3254CD8E30039DE07 /* search-facts.json */, + 1E9489F4254DEBB200A0002C /* fact-category.json */, ); path = Stubs; sourceTree = ""; @@ -813,6 +835,7 @@ 1ED5D19C2534AAE40035046C /* short-fact.json in Resources */, 1ED5D19A2534AA7A0035046C /* facts-list.json in Resources */, 1EDF0B372541C851001931AA /* get-categories.json in Resources */, + 1E9489F5254DEBB200A0002C /* fact-category.json in Resources */, 1E5A87C4254CD8E30039DE07 /* search-facts.json in Resources */, 1ED5D1982534AA700035046C /* long-fact.json in Resources */, ); @@ -1051,6 +1074,7 @@ 1E5617242540F43F00BF26A0 /* FactsServiceTests.swift in Sources */, 1ED5D18D25348FD50035046C /* XCTestCase+Stub.swift in Sources */, 1ED5D1932534A56F0035046C /* FactsServiceMock.swift in Sources */, + 1E9489F3254DEB2500A0002C /* FactCategoryViewModelTests.swift in Sources */, 1E7F9F9B254CD5110062613C /* APIMock.swift in Sources */, 1E7F15BE253324CF0006887B /* FactsListViewModelTests.swift in Sources */, 1E7F15C0253324FD0006887B /* FactsListViewControllerTests.swift in Sources */, diff --git a/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift b/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift index fc88cfc..339fa86 100644 --- a/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift +++ b/Chuck Norris FactsTests/Data/Services/FactsServiceTests.swift @@ -44,7 +44,7 @@ final class FactsServiceTests: XCTestCase { } } - func test_syncCategoriesShouldSaveCategoriesOnStorage() throws { + func test_FactsService_WhenSyncCategories_ShouldSaveCategoriesOnDatabase() throws { factsProvider.mockRequest(statusCode: 200, data: stub("get-categories")) let storedCategories = factsStorage.retrieveCategories() @@ -59,7 +59,24 @@ final class FactsServiceTests: XCTestCase { XCTAssertEqual(savedCategories?.count, 16) } - func test_retrieveCategoriesShouldReturnCategoriesOnStorage() throws { + func test_FactsService_WhenSearchFacts_ShouldSaveSearchTermOnDatabase() throws { + factsProvider.mockRequest(statusCode: 200, data: stub("search-facts")) + + let storedSearches = factsStorage.retrieveSearches() + let searches = try storedSearches.toBlocking().first() ?? [] + XCTAssertTrue(searches.isEmpty) + + factsService.searchFacts(searchTerm: "games") + .subscribe() + .disposed(by: disposeBag) + + testScheduler.start() + + let savedSearches = try storedSearches.toBlocking().first() + XCTAssertEqual(savedSearches?.count, 1) + } + + func test_FactsService_WhenRetrieveCategories_ShouldReturnCategoriesOnDatabase() throws { let storedCategories = factsStorage.retrieveCategories() let categories = try storedCategories.toBlocking().first() ?? [] XCTAssertTrue(categories.isEmpty) @@ -72,7 +89,7 @@ final class FactsServiceTests: XCTestCase { XCTAssertEqual(savedCategories?.count, mockCategories.count) } - func test_searchFactsShouldSaveRetrieveAPIFacts() throws { + func test_FactsService_WhenSearchFacts_ShouldReturnFacts() throws { factsProvider.mockRequest(statusCode: 200, data: stub("search-facts")) let factsObserver = testScheduler.createObserver([Fact].self) @@ -87,7 +104,7 @@ final class FactsServiceTests: XCTestCase { XCTAssertEqual(facts?.count, 16) } - func test_retrievePastSearchesShouldReturnDistinctSortedByDateSearchesOnStorage() throws { + func test_FactsService_WhenRetrievePastSearches_ShouldReturnDistinctSortedByDateSearches() throws { let storedSearches = factsStorage.retrieveSearches() let searches = try storedSearches.toBlocking().first() ?? [] XCTAssertTrue(searches.isEmpty) diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/Cells/FactViewModelTests.swift b/Chuck Norris FactsTests/Scenes/Facts/FactsList/Fact/FactViewModelTests.swift similarity index 57% rename from Chuck Norris FactsTests/Scenes/Facts/FactsList/Cells/FactViewModelTests.swift rename to Chuck Norris FactsTests/Scenes/Facts/FactsList/Fact/FactViewModelTests.swift index eb4af10..f4e2d8a 100644 --- a/Chuck Norris FactsTests/Scenes/Facts/FactsList/Cells/FactViewModelTests.swift +++ b/Chuck Norris FactsTests/Scenes/Facts/FactsList/Fact/FactViewModelTests.swift @@ -15,18 +15,26 @@ import RxTest class FactViewModelTests: XCTestCase { - func test_factViewModelIsEquatable() throws { + func test_FactViewModel_WhenWithoutCategory_ShouldHaveCategoryUncategorized() throws { let factStub = try stub("short-fact", type: Fact.self) - let fact = try XCTUnwrap(factStub, "looks like short-fact.json doesn't exists") + let fact = try XCTUnwrap(factStub) + + let factViewModel = FactViewModel(fact: fact) + XCTAssertEqual(factViewModel.category, L10n.FactCategory.uncategorized) + } + + func test_FactViewModel_WhenCompare_ShouldBeEquatable() throws { + let factStub = try stub("short-fact", type: Fact.self) + let fact = try XCTUnwrap(factStub) let factViewModelTest = FactViewModel(fact: fact) let factViewModel = FactViewModel(fact: fact) XCTAssertEqual(factViewModelTest, factViewModel) } - func test_factViewModelIsIdentifiable() throws { + func test_FactViewModel_WhenCompare_ShouldBeIdentifiable() throws { let factStub = try stub("short-fact", type: Fact.self) - let fact = try XCTUnwrap(factStub, "looks like short-fact.json doesn't exists") + let fact = try XCTUnwrap(factStub) let factViewModelTest = FactViewModel(fact: fact) XCTAssertEqual(factViewModelTest.identity, fact.value) diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift index 58e6789..15de823 100644 --- a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift +++ b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift @@ -36,17 +36,40 @@ class FactsListViewControllerTests: XCTestCase { factsListViewController = nil } - func test_factsListEmptyShouldShowEmptyList() { + func test_FactsListViewController_WhenFactsIsEmpty_WhenSearchTermIsEmpty_ShouldShowEmptyList() { factsServiceMock.searchFactsReturnValue = .just([]) factsListViewModel.viewDidAppear.onNext(()) XCTAssertFalse(factsListViewController.emptyListView.isHidden) + XCTAssertEqual(factsListViewController.emptyListView.label.text, L10n.EmptyView.empty) + XCTAssertFalse(factsListViewController.emptyListView.searchButton.isHidden) } - func test_factCellFontSizeShouldBe24ForShortContent() throws { + func test_FactsListViewController_WhenFactsIsEmpty_WhenSearchTermIsNotEmpty_ShouldShowEmptyList() { + factsServiceMock.searchFactsReturnValue = .just([]) + + factsListViewModel.setSearchTerm.onNext("games") + factsListViewModel.viewDidAppear.onNext(()) + + XCTAssertFalse(factsListViewController.emptyListView.isHidden) + XCTAssertEqual(factsListViewController.emptyListView.label.text, L10n.EmptyView.emptySearch) + XCTAssertTrue(factsListViewController.emptyListView.searchButton.isHidden) + } + + func test_FactsListViewController_WhenThereIsAnError_ShouldShowErrorView() { + let response = APIResponse(statusCode: 500, data: nil) + let apiError = APIError.statusCode(response) + factsServiceMock.searchFactsReturnValue = .error(apiError) + + factsListViewModel.viewDidAppear.onNext(()) + + XCTAssertFalse(factsListViewController.errorView.isHidden) + } + + func test_FactCell_WhenContentIsShort_FontSizeShouldBe24() throws { let factStub = try stub("short-fact", type: Fact.self) - let fact = try XCTUnwrap(factStub, "looks like short-fact.json doesn't exists") + let fact = try XCTUnwrap(factStub) factsServiceMock.searchFactsReturnValue = .just([fact]) @@ -57,9 +80,9 @@ class FactsListViewControllerTests: XCTestCase { XCTAssertEqual(factCell?.bodyLabel.font.pointSize, 24) } - func test_factCellFontSizeShouldBe16ForLongContent() throws { + func test_FactCell_WhenContentIsLong_FontSizeShouldBe16() throws { let factStub = try stub("long-fact", type: Fact.self) - let fact = try XCTUnwrap(factStub, "looks like long-fact.json doesn't exists") + let fact = try XCTUnwrap(factStub) factsServiceMock.searchFactsReturnValue = .just([fact]) @@ -70,9 +93,9 @@ class FactsListViewControllerTests: XCTestCase { XCTAssertEqual(factCell?.bodyLabel.font.pointSize, 16) } - func test_shareFactButtonTapShouldShowShareFact() throws { + func test_FactCell_WhenTapShareFact_ShouldShowShareActivity() throws { let factStub = try stub("short-fact", type: Fact.self) - let fact = try XCTUnwrap(factStub, "looks like short-fact.json doesn't exists") + let fact = try XCTUnwrap(factStub) factsServiceMock.searchFactsReturnValue = .just([fact]) diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift index 320ca30..4a86093 100644 --- a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift +++ b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift @@ -34,7 +34,7 @@ class FactsListViewModelTests: XCTestCase { factsListViewModel = nil } - func test_loadEmptyFacts() throws { + func test_FactsListViewModel_WhenViewDidAppear_ShouldLoadEmptyFacts() throws { factsServiceMock.searchFactsReturnValue = .just([]) let factsObserver = testScheduler.createObserver([FactsSectionModel].self) @@ -51,9 +51,9 @@ class FactsListViewModelTests: XCTestCase { XCTAssertEqual(sectionModels?.first?.items.count, 0) } - func test_showShareFact() throws { + func test_FactsListViewModel_WhenStartShareFact_ShouldShowShareFact() throws { let factStub = try stub("short-fact", type: Fact.self) - let fact = try XCTUnwrap(factStub, "looks like short-fact.json doesn't exists") + let fact = try XCTUnwrap(factStub) factsListViewModel.viewDidAppear.onNext(()) @@ -70,9 +70,9 @@ class FactsListViewModelTests: XCTestCase { XCTAssertEqual(fact.value, shareFact?.text) } - func test_categoriesShouldSyncWhenViewDidAppearWithNoErrors() throws { + func test_FactsListViewModel_WhenViewDidAppear_ShouldSyncCategoriesWithNoErrors() throws { let stubCategories = try stub("get-categories", type: [FactCategory].self) ?? [] - let categories = try XCTUnwrap(stubCategories, "looks like get-categories.json doesn't exists") + let categories = try XCTUnwrap(stubCategories) factsServiceMock.retrieveCategoriesReturnValue = .just(categories) let errorObserver = testScheduler.createObserver(FactsListError.self) @@ -88,4 +88,42 @@ class FactsListViewModelTests: XCTestCase { let error = errorObserver.events.compactMap { $0.value.element }.first XCTAssertNil(error) } + + func test_FactsListViewModel_WhenSearchFactsWithError_ShouldEmmitFactListError() throws { + let response = APIResponse(statusCode: 500, data: nil) + let apiError = APIError.statusCode(response) + factsServiceMock.searchFactsReturnValue = .error(apiError) + + let errorObserver = testScheduler.createObserver(FactsListError.self) + + factsListViewModel.errors + .subscribe(errorObserver) + .disposed(by: disposeBag) + + factsListViewModel.viewDidAppear.onNext(()) + + testScheduler.start() + + let error = errorObserver.events.compactMap { $0.value.element }.first + XCTAssertEqual(error?.code, FactsListError.searchFacts(apiError).code) + } + + func test_FactsListViewModel_WhenSyncCategoriesWithError_ShouldEmmitFactListError() throws { + let response = APIResponse(statusCode: 500, data: nil) + let apiError = APIError.statusCode(response) + factsServiceMock.syncCategoriesReturnValue = .error(apiError) + + let errorObserver = testScheduler.createObserver(FactsListError.self) + + factsListViewModel.errors + .subscribe(errorObserver) + .disposed(by: disposeBag) + + factsListViewModel.viewDidAppear.onNext(()) + + testScheduler.start() + + let error = errorObserver.events.compactMap { $0.value.element }.first + XCTAssertEqual(error?.code, FactsListError.syncCategories(apiError).code) + } } diff --git a/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewControllerTests.swift b/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewControllerTests.swift index 57ed533..2c97ab8 100644 --- a/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewControllerTests.swift +++ b/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewControllerTests.swift @@ -39,7 +39,7 @@ class SearchFactsViewControllerTests: XCTestCase { factsServiceMock = nil } - func test_searchFactsViewShouldShow8Categories() throws { + func test_SearchFactsViewController_WhenViewWillAppear_ShouldLoad8Categories() throws { let stubFactCategories = try stub("get-categories", type: [FactCategory].self) ?? [] factsServiceMock.retrieveCategoriesReturnValue = .just(stubFactCategories) @@ -54,7 +54,7 @@ class SearchFactsViewControllerTests: XCTestCase { XCTAssertEqual(suggestionsCell?.collectionView.numberOfItems(inSection: 0), 8) } - func test_searchFactsViewShouldShowOnlyPastSearches() { + func test_SearchFactsViewController_WhenViewWillAppear_ShouldLoadPastSearches() { factsServiceMock.retrievePastSearchesReturnValue = .just(["fashion", "games", "explicit"]) searchFactsViewModel.viewWillAppear.onNext(()) @@ -66,7 +66,29 @@ class SearchFactsViewControllerTests: XCTestCase { XCTAssertEqual(searchFactsDataSource?.tableView(tableView, numberOfRowsInSection: 0), 3) } - func test_searchFactsViewShouldBeEmptyWhenThereIsNoSuggestionsOrPastSearches() { + func test_SearchFactsViewController_WhenViewWillAppearWithoutData_ShouldBeEmpty() { + searchFactsViewModel.viewWillAppear.onNext(()) + + let tableView = searchFactsViewController.tableView + let searchFactsDataSource = tableView.dataSource + + XCTAssertEqual(searchFactsDataSource?.numberOfSections?(in: tableView), 0) + } + + func test_Suggestions_WhenEmpty_ShouldBeHidden() { + factsServiceMock.retrieveCategoriesReturnValue = .just([]) + + searchFactsViewModel.viewWillAppear.onNext(()) + + let tableView = searchFactsViewController.tableView + let searchFactsDataSource = tableView.dataSource + + XCTAssertEqual(searchFactsDataSource?.numberOfSections?(in: tableView), 0) + } + + func test_PastSearches_WhenEmpty_ShouldBeHidden() { + factsServiceMock.retrievePastSearchesReturnValue = .just([]) + searchFactsViewModel.viewWillAppear.onNext(()) let tableView = searchFactsViewController.tableView diff --git a/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewModelTests.swift b/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewModelTests.swift index 9dea832..05e970b 100644 --- a/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewModelTests.swift +++ b/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewModelTests.swift @@ -34,7 +34,7 @@ class SearchFactsViewModelTests: XCTestCase { factsServiceMock = nil } - func test_searchFactsWhenSearchShouldSearchFacts() { + func test_SeachFactsViewModel_WhenSearchFacts_ShouldSetSearchTerm() { let searchFactsObserver = testScheduler.createObserver(String.self) searchFactsViewModel.didSearchFacts @@ -50,7 +50,7 @@ class SearchFactsViewModelTests: XCTestCase { XCTAssertEqual(searchFactsTerm, "games") } - func test_cancelSearchShouldCallCancelSearch() { + func test_SearchFactsViewModel_WhenCancelSearch_ShouldCancelSearchScene() { let cancelObserver = testScheduler.createObserver(Void.self) searchFactsViewModel.didCancel @@ -65,7 +65,7 @@ class SearchFactsViewModelTests: XCTestCase { XCTAssertEqual(cancelCount, 1) } - func test_shouldLoad8RandomFactCategories() throws { + func test_SearchFactsViewModel_WhenViewWillAppear_ShouldLoad8RandomSuggestions() throws { let searchFactsItemsObserver = testScheduler.createObserver([SearchFactsTableViewSection].self) let testCategories = try stub("get-categories", type: [FactCategory].self) ?? [] @@ -83,7 +83,7 @@ class SearchFactsViewModelTests: XCTestCase { XCTAssertEqual(searchFactsViewModelEvents?.first?.items.first?.quantity, 8) } - func test_shouldLoadPastSearches() { + func test_SearchFactsViewModel_WhenViewWillAppear_ShouldLoadPastSearches() { let searchFactsItemsObserver = testScheduler.createObserver([SearchFactsTableViewSection].self) let pastSearches = ["fashion", "games", "food"] diff --git a/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryViewModelTests.swift b/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryViewModelTests.swift new file mode 100644 index 0000000..07be07a --- /dev/null +++ b/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryViewModelTests.swift @@ -0,0 +1,34 @@ +// +// FactCategoryViewModelTests.swift +// Chuck Norris FactsTests +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/31/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import XCTest +import RxSwift +import RxBlocking +import RxTest + +@testable import Chuck_Norris_Facts + +class FactCategoryViewModelTests: XCTestCase { + + func test_FactCategoryViewModel_WhenCompare_ShouldBeEquatable() throws { + let factCategoryStub = try stub("fact-category", type: FactCategory.self) + let factCategory = try XCTUnwrap(factCategoryStub) + + let factCategoryViewModelTest = FactCategoryViewModel(category: factCategory) + let factCategoryViewModel = FactCategoryViewModel(category: factCategory) + XCTAssertEqual(factCategoryViewModelTest, factCategoryViewModel) + } + + func test_FactCategoryViewModel_WhenCompare_ShouldBeIdentifiable() throws { + let factCategoryStub = try stub("fact-category", type: FactCategory.self) + let factCategory = try XCTUnwrap(factCategoryStub) + + let factCategoryViewModelTest = FactCategoryViewModel(category: factCategory) + XCTAssertEqual(factCategoryViewModelTest.identity, factCategory.text) + } +} diff --git a/Chuck Norris FactsTests/Stubs/fact-category.json b/Chuck Norris FactsTests/Stubs/fact-category.json new file mode 100644 index 0000000..fc9fd2d --- /dev/null +++ b/Chuck Norris FactsTests/Stubs/fact-category.json @@ -0,0 +1 @@ +"games" diff --git a/Chuck Norris FactsUITests/Tests/FactsListUITests.swift b/Chuck Norris FactsUITests/Tests/FactsListUITests.swift index f7c3a4c..9700977 100644 --- a/Chuck Norris FactsUITests/Tests/FactsListUITests.swift +++ b/Chuck Norris FactsUITests/Tests/FactsListUITests.swift @@ -18,7 +18,7 @@ final class FactsListUITests: XCTestCase { continueAfterFailure = false } - func test_showEmptyView() throws { + func test_FactsList_WhenFirstAccess_ShouldShowEmptyView() throws { app.setLaunchArguments([.uiTest, .resetData]) app.launch() @@ -28,7 +28,7 @@ final class FactsListUITests: XCTestCase { XCTAssertTrue(factsListScene.emptyListLabelView.exists) } - func test_shareFact() { + func test_FactsList_WhenShareFact_ShouldShowShareActivity() { app.setLaunchArguments([.uiTest, .mockStorage, .mockHttp]) app.launch() @@ -60,7 +60,7 @@ final class FactsListUITests: XCTestCase { XCTAssertFalse(shareActivity.waitForExistence(timeout: 1)) } - func test_searchFacts() { + func test_FactsList_WhenTapSearch_ShouldShowSearchFacts() { app.setLaunchArguments([.uiTest, .mockStorage]) app.launch() @@ -77,7 +77,7 @@ final class FactsListUITests: XCTestCase { XCTAssertTrue(searchFactsView.exists) } - func test_shouldShowErrorViewWhenSearchRaisesAnError() { + func test_FactsList_WhenSearchFails_ShouldShowErrorView() { app.setLaunchArguments([.uiTest, .mockHttpError]) app.launch() diff --git a/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift b/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift index 16b27d8..0f5be7e 100644 --- a/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift +++ b/Chuck Norris FactsUITests/Tests/SearchFactsUITests.swift @@ -18,7 +18,7 @@ final class SearchFactsUITests: XCTestCase { continueAfterFailure = false } - func test_searchFactsUsingSearchBar() throws { + func test_SearchFacts_WhenSearchByTerm_ShouldLoadFacts() throws { app.setLaunchArguments([.uiTest, .resetData, .mockStorage, .mockHttp]) app.launch() @@ -34,7 +34,7 @@ final class SearchFactsUITests: XCTestCase { XCTAssertEqual(factsListScene.factsTableView.cells.count, 16) } - func test_cancelSearchFacts() throws { + func test_SearchFacts_WhenCancelSearch_ShouldCancelSearchFacts() throws { app.setLaunchArguments([.uiTest, .mockStorage]) app.launch() @@ -47,7 +47,7 @@ final class SearchFactsUITests: XCTestCase { XCTAssertFalse(searchFactsScene.searchFactsView.exists) } - func test_shouldShow8FactCategories() { + func test_SearchFacts_WhenViewAppear_ShouldShow8RandomSuggestions() { app.setLaunchArguments([.uiTest, .resetData, .mockHttp]) app.launch() @@ -62,7 +62,7 @@ final class SearchFactsUITests: XCTestCase { XCTAssertEqual(suggestionsCells.count, 8) } - func test_tapFactCategoryShouldSearchByTerm() { + func test_SearchFacts_WhenTapSuggestion_ShouldSearchBySuggestion() { app.setLaunchArguments([.uiTest, .mockStorage, .mockHttp]) app.launch() @@ -82,7 +82,7 @@ final class SearchFactsUITests: XCTestCase { XCTAssertGreaterThan(factsListScene.factsTableView.cells.count, 0) } - func test_tapPastSearchShouldSearchByTerm() { + func test_SearchFacts_WhenTapPastSearch_ShouldSearchByPastSearch() { app.setLaunchArguments([.uiTest, .mockStorage, .mockHttp]) app.launch() @@ -102,7 +102,7 @@ final class SearchFactsUITests: XCTestCase { XCTAssertGreaterThan(factsListScene.factsTableView.cells.count, 0) } - func test_tapPastSearchShouldOrderByDate() { + func test_SearchFacts_WhenTapPastSearch_ShouldOrderPastSearch() { app.setLaunchArguments([.uiTest, .mockStorage]) app.launch() @@ -127,7 +127,7 @@ final class SearchFactsUITests: XCTestCase { XCTAssertEqual(firstItem.label, secondItem.label) } - func test_pastSearchShouldBeHiddenOnFirstAccess() { + func test_SearchFacts_WhenFirstAccess_ShouldNoHavePastSearches() { app.setLaunchArguments([.uiTest, .resetData, .mockHttp]) app.launch() From c70c9e8466a89443cac964a2059134af8c940a44 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Sun, 1 Nov 2020 23:12:14 -0300 Subject: [PATCH 17/18] Improve Architecture Implementation (#36) * WIP Improve code readability * Improve ViewModel Outputs/Inputs * RxSwift Extensions * Resizable font * Transform SectionModel into SuggestionsViewModel * Translate layout metrics into constants * Format FactCategory * Trailing lines to improve readability * Use fact id as equatable and identifiable * Improve folder structure * selectItem SearchFactsViewModel * Move API Folder * Move sectionModels typealiases * Add ViewModels descriptions * Improve FactCategoryCell * Improve variables name * Check font on cell label tests --- Chuck Norris Facts.xcodeproj/project.pbxproj | 34 +++-- .../Facts/FactsList/Fact/FactCell.swift | 29 +++-- .../Facts/FactsList/Fact/FactViewModel.swift | 16 ++- .../FactsList/FactsListCoordinator.swift | 6 +- .../Facts/FactsList/FactsListError.swift | 0 .../FactsList/FactsListViewController.swift | 20 +-- .../Facts/FactsList/FactsListViewModel.swift | 75 ++++++++--- .../Facts/FactsList/Views/EmptyListView.swift | 10 +- .../Facts/FactsList/Views/ErrorView.swift | 13 +- .../Facts/FactsList/Views/LoadingView.swift | 6 +- .../PastSearch/PastSearchCell.swift | 0 .../PastSearch/PastSearchViewModel.swift | 0 .../SearchFacts/SearchFactsCoordinator.swift | 7 +- .../SearchFactsTableViewSection.swift | 42 +++++-- .../SearchFactsViewController.swift | 42 +++---- .../SearchFacts/SearchFactsViewModel.swift | 118 ++++++++++++++++++ .../FactCategory/FactCategoryCell.swift | 39 ++++++ .../FactCategory/FactCategoryViewModel.swift | 6 +- .../Suggestions/SuggestionsCell.swift | 25 ++-- .../SuggestionsViewFlowLayout.swift | 0 .../Suggestions/SuggestionsViewModel.swift | 56 +++++++++ .../Views/CategoryView.swift | 15 ++- .../{ => Core}/API/APIError.swift | 0 .../{ => Core}/API/APIProvider.swift | 0 .../{ => Core}/API/APIResponse.swift | 0 .../{ => Core}/API/APITarget.swift | 0 .../{ => Core}/API/HTTP/HTTPMethod.swift | 0 .../{ => Core}/API/HTTP/HTTPTask.swift | 0 .../{ => Core}/Data/Models/Fact.swift | 6 + .../{ => Core}/Data/Models/FactCategory.swift | 6 + .../{ => Core}/Data/Networking/FactsAPI.swift | 0 .../Responses/SearchFactsResponse.swift | 0 .../Data/Services/FactsService.swift | 2 +- .../Storage/Entities/FactCategoryEntity.swift | 0 .../Data/Storage/Entities/SearchEntity.swift | 0 .../Data/Storage/FactsStorage.swift | 0 .../{ => Core}/Extensions/API+Rx.swift | 0 .../{ => Core}/Extensions/Data+Stub.swift | 0 .../Core/Extensions/RxSwift+Extensions.swift | 26 ++++ .../UICollectionView+Extensions.swift | 0 .../Extensions/UITableView+Extensions.swift | 0 .../Extensions/UIViewController+Rx.swift | 4 +- .../Extensions/URLRequest+Encoded.swift | 0 .../Library/ActivityIndicator.swift | 0 .../{ => Core}/Library/BaseCoordinator.swift | 0 .../Library/DynamicHeightCollectionView.swift | 0 .../{ => Core}/Library/JSON.swift | 0 .../{ => Core}/Library/LaunchArgument.swift | 0 .../SearchFacts/SearchFactsViewModel.swift | 78 ------------ .../FactCategory/FactCategoryCell.swift | 52 -------- .../Suggestions/SuggestionsViewModel.swift | 41 ------ .../FactsList/Fact/FactViewModelTests.swift | 2 +- .../FactsListViewControllerTests.swift | 26 ++-- .../FactsList/FactsListViewModelTests.swift | 22 ++-- .../SearchFactsViewControllerTests.swift | 10 +- .../SearchFactsViewModelTests.swift | 18 +-- .../FactCategoryViewModelTests.swift | 8 ++ 57 files changed, 522 insertions(+), 338 deletions(-) rename Chuck Norris Facts/{ => App}/Scenes/Facts/FactsList/Fact/FactCell.swift (80%) rename Chuck Norris Facts/{ => App}/Scenes/Facts/FactsList/Fact/FactViewModel.swift (80%) rename Chuck Norris Facts/{ => App}/Scenes/Facts/FactsList/FactsListCoordinator.swift (92%) rename Chuck Norris Facts/{ => App}/Scenes/Facts/FactsList/FactsListError.swift (100%) rename Chuck Norris Facts/{ => App}/Scenes/Facts/FactsList/FactsListViewController.swift (93%) rename Chuck Norris Facts/{ => App}/Scenes/Facts/FactsList/FactsListViewModel.swift (55%) rename Chuck Norris Facts/{ => App}/Scenes/Facts/FactsList/Views/EmptyListView.swift (87%) rename Chuck Norris Facts/{ => App}/Scenes/Facts/FactsList/Views/ErrorView.swift (83%) rename Chuck Norris Facts/{ => App}/Scenes/Facts/FactsList/Views/LoadingView.swift (83%) rename Chuck Norris Facts/{ => App}/Scenes/Facts/SearchFacts/PastSearch/PastSearchCell.swift (100%) rename Chuck Norris Facts/{ => App}/Scenes/Facts/SearchFacts/PastSearch/PastSearchViewModel.swift (100%) rename Chuck Norris Facts/{ => App}/Scenes/Facts/SearchFacts/SearchFactsCoordinator.swift (73%) rename Chuck Norris Facts/{ => App}/Scenes/Facts/SearchFacts/SearchFactsTableViewSection.swift (52%) rename Chuck Norris Facts/{ => App}/Scenes/Facts/SearchFacts/SearchFactsViewController.swift (80%) create mode 100644 Chuck Norris Facts/App/Scenes/Facts/SearchFacts/SearchFactsViewModel.swift create mode 100644 Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryCell.swift rename Chuck Norris Facts/{ => App}/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryViewModel.swift (82%) rename Chuck Norris Facts/{ => App}/Scenes/Facts/SearchFacts/Suggestions/SuggestionsCell.swift (82%) rename Chuck Norris Facts/{ => App}/Scenes/Facts/SearchFacts/Suggestions/SuggestionsViewFlowLayout.swift (100%) create mode 100644 Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/SuggestionsViewModel.swift rename Chuck Norris Facts/{Scenes/Facts/FactsList => App}/Views/CategoryView.swift (78%) rename Chuck Norris Facts/{ => Core}/API/APIError.swift (100%) rename Chuck Norris Facts/{ => Core}/API/APIProvider.swift (100%) rename Chuck Norris Facts/{ => Core}/API/APIResponse.swift (100%) rename Chuck Norris Facts/{ => Core}/API/APITarget.swift (100%) rename Chuck Norris Facts/{ => Core}/API/HTTP/HTTPMethod.swift (100%) rename Chuck Norris Facts/{ => Core}/API/HTTP/HTTPTask.swift (100%) rename Chuck Norris Facts/{ => Core}/Data/Models/Fact.swift (84%) rename Chuck Norris Facts/{ => Core}/Data/Models/FactCategory.swift (76%) rename Chuck Norris Facts/{ => Core}/Data/Networking/FactsAPI.swift (100%) rename Chuck Norris Facts/{ => Core}/Data/Networking/Responses/SearchFactsResponse.swift (100%) rename Chuck Norris Facts/{ => Core}/Data/Services/FactsService.swift (98%) rename Chuck Norris Facts/{ => Core}/Data/Storage/Entities/FactCategoryEntity.swift (100%) rename Chuck Norris Facts/{ => Core}/Data/Storage/Entities/SearchEntity.swift (100%) rename Chuck Norris Facts/{ => Core}/Data/Storage/FactsStorage.swift (100%) rename Chuck Norris Facts/{ => Core}/Extensions/API+Rx.swift (100%) rename Chuck Norris Facts/{ => Core}/Extensions/Data+Stub.swift (100%) create mode 100644 Chuck Norris Facts/Core/Extensions/RxSwift+Extensions.swift rename Chuck Norris Facts/{ => Core}/Extensions/UICollectionView+Extensions.swift (100%) rename Chuck Norris Facts/{ => Core}/Extensions/UITableView+Extensions.swift (100%) rename Chuck Norris Facts/{ => Core}/Extensions/UIViewController+Rx.swift (72%) rename Chuck Norris Facts/{ => Core}/Extensions/URLRequest+Encoded.swift (100%) rename Chuck Norris Facts/{ => Core}/Library/ActivityIndicator.swift (100%) rename Chuck Norris Facts/{ => Core}/Library/BaseCoordinator.swift (100%) rename Chuck Norris Facts/{ => Core}/Library/DynamicHeightCollectionView.swift (100%) rename Chuck Norris Facts/{ => Core}/Library/JSON.swift (100%) rename Chuck Norris Facts/{ => Core}/Library/LaunchArgument.swift (100%) delete mode 100644 Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewModel.swift delete mode 100644 Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryCell.swift delete mode 100644 Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/SuggestionsViewModel.swift diff --git a/Chuck Norris Facts.xcodeproj/project.pbxproj b/Chuck Norris Facts.xcodeproj/project.pbxproj index 4f80f1c..4428b46 100644 --- a/Chuck Norris Facts.xcodeproj/project.pbxproj +++ b/Chuck Norris Facts.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 1E049E13254F5A5300226E0B /* RxSwift+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E049E12254F5A5300226E0B /* RxSwift+Extensions.swift */; }; 1E0E4BA22549F7E30030BC49 /* error.json in Resources */ = {isa = PBXBuildFile; fileRef = 1E0E4BA12549F7E30030BC49 /* error.json */; }; 1E135FAA254B4E66009D18AF /* LaunchArgument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E135FA9254B4E66009D18AF /* LaunchArgument.swift */; }; 1E135FAD254B4E9F009D18AF /* XCUIApplication+LaunchArgument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E135FAC254B4E9F009D18AF /* XCUIApplication+LaunchArgument.swift */; }; @@ -111,6 +112,7 @@ /* Begin PBXFileReference section */ 02186229E02F0A408D2EE0C2 /* Pods_Chuck_Norris_Facts.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Chuck_Norris_Facts.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1E049E12254F5A5300226E0B /* RxSwift+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RxSwift+Extensions.swift"; sourceTree = ""; }; 1E0E4BA12549F7E30030BC49 /* error.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = error.json; sourceTree = ""; }; 1E135FA9254B4E66009D18AF /* LaunchArgument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchArgument.swift; sourceTree = ""; }; 1E135FAC254B4E9F009D18AF /* XCUIApplication+LaunchArgument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCUIApplication+LaunchArgument.swift"; sourceTree = ""; }; @@ -277,7 +279,6 @@ isa = PBXGroup; children = ( 1E32758E2532A2C0007E838A /* EmptyListView.swift */, - 1EF0DA1325449898005CF7E2 /* CategoryView.swift */, 1ED06C942548AAD300139151 /* LoadingView.swift */, 1E15408E2549FA6200675DC4 /* ErrorView.swift */, ); @@ -307,6 +308,25 @@ path = SearchFacts; sourceTree = ""; }; + 1E49C420254F8FC3005BB6B4 /* Views */ = { + isa = PBXGroup; + children = ( + 1EF0DA1325449898005CF7E2 /* CategoryView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 1E49C421254F9043005BB6B4 /* Core */ = { + isa = PBXGroup; + children = ( + 1E3075C0254C9CFC0082A194 /* API */, + 1ED5D18E25349E8D0035046C /* Extensions */, + 1EFE2881253210A4008806B9 /* Data */, + 1EFE2865253206C8008806B9 /* Library */, + ); + path = Core; + sourceTree = ""; + }; 1E5617212540F42600BF26A0 /* Data */ = { isa = PBXGroup; children = ( @@ -463,6 +483,7 @@ 1EEDC6A0254A331500D75F3E /* UICollectionView+Extensions.swift */, 1E65578B254CB20D00950706 /* API+Rx.swift */, 1E65578D254CB22800950706 /* URLRequest+Encoded.swift */, + 1E049E12254F5A5300226E0B /* RxSwift+Extensions.swift */, ); path = Extensions; sourceTree = ""; @@ -532,13 +553,9 @@ 1EE0713B25314AF500F6BF6D /* Chuck Norris Facts */ = { isa = PBXGroup; children = ( - 1E3075C0254C9CFC0082A194 /* API */, - 1ED5D18E25349E8D0035046C /* Extensions */, - 1EFE2881253210A4008806B9 /* Data */, - 1EFE2865253206C8008806B9 /* Library */, - 1EFE28632532062A008806B9 /* Scenes */, - 1EFE286125320620008806B9 /* App */, + 1E49C421254F9043005BB6B4 /* Core */, 1EFE286025320614008806B9 /* Resources */, + 1EFE286125320620008806B9 /* App */, ); path = "Chuck Norris Facts"; sourceTree = ""; @@ -584,6 +601,8 @@ 1EFE286125320620008806B9 /* App */ = { isa = PBXGroup; children = ( + 1E49C420254F8FC3005BB6B4 /* Views */, + 1EFE28632532062A008806B9 /* Scenes */, 1EE0713C25314AF500F6BF6D /* AppDelegate.swift */, 1EE0713E25314AF500F6BF6D /* SceneDelegate.swift */, 1EFE2868253207B2008806B9 /* AppCoordinator.swift */, @@ -1018,6 +1037,7 @@ 1E8A0FF0254760D400565A86 /* SuggestionsViewModel.swift in Sources */, 1E3075C5254C9D710082A194 /* HTTPMethod.swift in Sources */, 1E463805253636D80079D8E9 /* SearchFactsCoordinator.swift in Sources */, + 1E049E13254F5A5300226E0B /* RxSwift+Extensions.swift in Sources */, 1EFE288925321123008806B9 /* JSON.swift in Sources */, 1ED06C952548AAD300139151 /* LoadingView.swift in Sources */, 1EFE288725321119008806B9 /* Fact.swift in Sources */, diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/Fact/FactCell.swift b/Chuck Norris Facts/App/Scenes/Facts/FactsList/Fact/FactCell.swift similarity index 80% rename from Chuck Norris Facts/Scenes/Facts/FactsList/Fact/FactCell.swift rename to Chuck Norris Facts/App/Scenes/Facts/FactsList/Fact/FactCell.swift index 717cb39..af645fb 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/Fact/FactCell.swift +++ b/Chuck Norris Facts/App/Scenes/Facts/FactsList/Fact/FactCell.swift @@ -65,6 +65,8 @@ class FactCell: UITableViewCell { }() private func setupView() { + let padding: CGFloat = 16 + clipsToBounds = false selectionStyle = .none @@ -75,29 +77,30 @@ class FactCell: UITableViewCell { shadowView.addSubview(shareButton) shadowView.addSubview(categoryView) - shadowView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16).isActive = true - shadowView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16).isActive = true - shadowView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16 / 2).isActive = true - shadowView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16 / 2).isActive = true + shadowView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: padding).isActive = true + shadowView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -padding).isActive = true + shadowView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding / 2).isActive = true + shadowView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -padding / 2).isActive = true - bodyLabel.topAnchor.constraint(equalTo: shadowView.topAnchor, constant: 16).isActive = true - bodyLabel.leadingAnchor.constraint(equalTo: shadowView.leadingAnchor, constant: 16).isActive = true - bodyLabel.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor, constant: -16).isActive = true + bodyLabel.topAnchor.constraint(equalTo: shadowView.topAnchor, constant: padding).isActive = true + bodyLabel.leadingAnchor.constraint(equalTo: shadowView.leadingAnchor, constant: padding).isActive = true + bodyLabel.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor, constant: -padding).isActive = true - shareButton.topAnchor.constraint(equalTo: bodyLabel.bottomAnchor, constant: 16).isActive = true - shareButton.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor, constant: -16).isActive = true - shareButton.bottomAnchor.constraint(equalTo: shadowView.bottomAnchor, constant: -16).isActive = true + shareButton.topAnchor.constraint(equalTo: bodyLabel.bottomAnchor, constant: padding).isActive = true + shareButton.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor, constant: -padding).isActive = true + shareButton.bottomAnchor.constraint(equalTo: shadowView.bottomAnchor, constant: -padding).isActive = true categoryView.translatesAutoresizingMaskIntoConstraints = false categoryView.centerYAnchor.constraint(equalTo: shareButton.centerYAnchor).isActive = true - categoryView.leftAnchor.constraint(equalTo: shadowView.leftAnchor, constant: 16).isActive = true + categoryView.leftAnchor.constraint(equalTo: shadowView.leftAnchor, constant: padding).isActive = true } func setup(_ fact: FactViewModel) { bodyLabel.text = fact.text - let fontSize = fact.text.count > 80 ? 16 : 24 - bodyLabel.font = .systemFont(ofSize: CGFloat(fontSize), weight: .bold) + bodyLabel.font = fact.text.count > 80 + ? UIFont.preferredFont(forTextStyle: .title3) + : UIFont.preferredFont(forTextStyle: .title1) categoryView.label.text = fact.category } diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/Fact/FactViewModel.swift b/Chuck Norris Facts/App/Scenes/Facts/FactsList/Fact/FactViewModel.swift similarity index 80% rename from Chuck Norris Facts/Scenes/Facts/FactsList/Fact/FactViewModel.swift rename to Chuck Norris Facts/App/Scenes/Facts/FactsList/Fact/FactViewModel.swift index b92ec9e..86cd859 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/Fact/FactViewModel.swift +++ b/Chuck Norris Facts/App/Scenes/Facts/FactsList/Fact/FactViewModel.swift @@ -10,27 +10,31 @@ import Foundation import RxDataSources final class FactViewModel { + let fact: Fact + let text: String - var url: URL? let category: String + var url: URL? init(fact: Fact) { + self.fact = fact self.text = fact.value - if let factUrl = fact.url { - self.url = URL(string: factUrl) - } self.category = fact.categories.first?.text.uppercased() ?? L10n.FactCategory.uncategorized + + if let url = fact.url { + self.url = URL(string: url) + } } } extension FactViewModel: IdentifiableType { var identity: String { - text + fact.id } } extension FactViewModel: Equatable { static func == (lhs: FactViewModel, rhs: FactViewModel) -> Bool { - return lhs.text == rhs.text + return lhs.fact == rhs.fact } } diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListCoordinator.swift b/Chuck Norris Facts/App/Scenes/Facts/FactsList/FactsListCoordinator.swift similarity index 92% rename from Chuck Norris Facts/Scenes/Facts/FactsList/FactsListCoordinator.swift rename to Chuck Norris Facts/App/Scenes/Facts/FactsList/FactsListCoordinator.swift index 2604ad1..2a7b855 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListCoordinator.swift +++ b/Chuck Norris Facts/App/Scenes/Facts/FactsList/FactsListCoordinator.swift @@ -24,18 +24,18 @@ final class FactsListCoordinator: BaseCoordinator { let navigationController = UINavigationController(rootViewController: factsListViewController) - factsListViewModel.showShareFact + factsListViewModel.outputs.showShareFact .bind(onNext: { [weak self] in self?.showShareFact(fact: $0, in: navigationController) }) .disposed(by: disposeBag) - factsListViewModel.showSearchFacts + factsListViewModel.outputs.showSearchFacts .flatMap { [weak self] _ -> Observable in self?.showSearchFacts(on: factsListViewController) ?? .empty() } .compactMap { $0 } - .bind(to: factsListViewModel.setSearchTerm) + .bind(to: factsListViewModel.inputs.setSearchTerm) .disposed(by: disposeBag) window.rootViewController = navigationController diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListError.swift b/Chuck Norris Facts/App/Scenes/Facts/FactsList/FactsListError.swift similarity index 100% rename from Chuck Norris Facts/Scenes/Facts/FactsList/FactsListError.swift rename to Chuck Norris Facts/App/Scenes/Facts/FactsList/FactsListError.swift diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift b/Chuck Norris Facts/App/Scenes/Facts/FactsList/FactsListViewController.swift similarity index 93% rename from Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift rename to Chuck Norris Facts/App/Scenes/Facts/FactsList/FactsListViewController.swift index 4c84332..965942a 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewController.swift +++ b/Chuck Norris Facts/App/Scenes/Facts/FactsList/FactsListViewController.swift @@ -33,7 +33,7 @@ class FactsListViewController: UIViewController { cell.setup(fact) cell.shareButton.rx.tap .map { fact } - .bind(to: viewModel.startShareFact) + .bind(to: viewModel.inputs.startShareFact) .disposed(by: cell.disposeBag) return cell @@ -112,21 +112,21 @@ class FactsListViewController: UIViewController { private func setupBindings() { rx.viewDidAppear - .bind(to: viewModel.viewDidAppear) + .bind(to: viewModel.inputs.viewDidAppear) .disposed(by: disposeBag) - viewModel.isLoading + viewModel.outputs.isLoading .drive(onNext: { [weak self] isLoading in self?.showLoadingView(isLoading) }) .disposed(by: disposeBag) - let factsIsEmpty = viewModel.facts + let factsIsEmpty = viewModel.outputs.facts .map { $0.flatMap { $0.items } } .map { $0.isEmpty } .share() - let searchIsEmpty = viewModel.searchTerm + let searchIsEmpty = viewModel.outputs.searchTerm .map { $0.isEmpty } .share() @@ -137,24 +137,24 @@ class FactsListViewController: UIViewController { }) .disposed(by: disposeBag) - viewModel.facts + viewModel.outputs.facts .observeOn(MainScheduler.instance) .bind(to: tableView.rx.items(dataSource: factsDataSource)) .disposed(by: disposeBag) searchButton.rx.tap - .bind(to: viewModel.startSearchFacts) + .bind(to: viewModel.inputs.startSearchFacts) .disposed(by: disposeBag) emptyListView.searchButton.rx.tap - .bind(to: viewModel.startSearchFacts) + .bind(to: viewModel.inputs.startSearchFacts) .disposed(by: disposeBag) errorView.retryButton.rx.tap - .bind(to: viewModel.retryAction) + .bind(to: viewModel.inputs.retryAction) .disposed(by: disposeBag) - viewModel.errors + viewModel.outputs.errors .bind(onNext: { [weak self] error in self?.showErrorView(error) }) diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift b/Chuck Norris Facts/App/Scenes/Facts/FactsList/FactsListViewModel.swift similarity index 55% rename from Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift rename to Chuck Norris Facts/App/Scenes/Facts/FactsList/FactsListViewModel.swift index c732638..0fa2478 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/FactsListViewModel.swift +++ b/Chuck Norris Facts/App/Scenes/Facts/FactsList/FactsListViewModel.swift @@ -12,33 +12,74 @@ import RxDataSources typealias FactsSectionModel = AnimatableSectionModel -final class FactsListViewModel { +protocol FactsListViewModelInputs { + // Call when view did appear to syncCategories + var viewDidAppear: AnyObserver { get } + + // Call when user start to share a fact + var startShareFact: AnyObserver { get } + + // Call to show SearchFacts scene + var startSearchFacts: AnyObserver { get } + + // Call to set SearchTerm to be used on search + var setSearchTerm: AnyObserver { get } + + // Call to retry a syncCategories action + var retryAction: AnyObserver { get } +} + +protocol FactsListViewModelOutputs { + // Emmits an array of FactsSectionModel to bind on tableView + var facts: Observable<[FactsSectionModel]> { get } + + // Emmits an FactViewModel to be shared + var showShareFact: Observable { get } + + // Emmits an event to show SearchFacts scene + var showSearchFacts: Observable { get } + + // Emmits an string to be used as a search query and check empty state + var searchTerm: Observable { get } + + // Emmits an ActivityIndicator to check if there is a facts search happening + var isLoading: ActivityIndicator { get } + + // Emmits an FactsListError to be shown + var errors: Observable { get } +} + +final class FactsListViewModel: FactsListViewModelInputs, FactsListViewModelOutputs { + + var inputs: FactsListViewModelInputs { self } + + var outputs: FactsListViewModelOutputs { self } // MARK: - Inputs - let viewDidAppear: AnyObserver + var viewDidAppear: AnyObserver - let startShareFact: AnyObserver + var startShareFact: AnyObserver - let startSearchFacts: AnyObserver + var startSearchFacts: AnyObserver - let setSearchTerm: AnyObserver + var setSearchTerm: AnyObserver - let retryAction: AnyObserver + var retryAction: AnyObserver // MARK: - Outputs - let facts: Observable<[FactsSectionModel]> + var facts: Observable<[FactsSectionModel]> - let showShareFact: Observable + var showShareFact: Observable - let showSearchFacts: Observable + var showSearchFacts: Observable - let searchTerm: Observable + var searchTerm: Observable - let isLoading: ActivityIndicator + var isLoading: ActivityIndicator - let errors: Observable + var errors: Observable init(factsService: FactsServiceType = FactsService()) { let loadingIndicator = ActivityIndicator() @@ -67,17 +108,17 @@ final class FactsListViewModel { let retrySyncCategories = retryActionSubject.withLatestFrom(currentErrorSubject) .compactMap { $0 } .filter { $0 == .syncCategories($0.error) } - .map { _ in () } + .mapToVoid() let syncCategoriesError = Observable.merge(viewDidAppearSubject, retrySyncCategories) .flatMapLatest { _ in factsService.syncCategories() .materialize() } - .compactMap { $0.event.error } + .errors() .map { FactsListError.syncCategories($0) } - let searchFacts = Observable.combineLatest(viewDidAppearSubject, searchTerm) { _, term in term } + let searchFacts = searchTermSubject .flatMapLatest { searchTerm in factsService.searchFacts(searchTerm: searchTerm) .trackActivity(loadingIndicator) @@ -85,11 +126,11 @@ final class FactsListViewModel { } let searchFactsError = searchFacts - .compactMap { $0.event.error } + .errors() .map { FactsListError.searchFacts($0) } self.facts = searchFacts - .compactMap { $0.event.element } + .elements() .map { $0.map { FactViewModel(fact: $0) } } .map { [FactsSectionModel(model: "", items: $0)] } diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/Views/EmptyListView.swift b/Chuck Norris Facts/App/Scenes/Facts/FactsList/Views/EmptyListView.swift similarity index 87% rename from Chuck Norris Facts/Scenes/Facts/FactsList/Views/EmptyListView.swift rename to Chuck Norris Facts/App/Scenes/Facts/FactsList/Views/EmptyListView.swift index fdcd5a4..bd72f77 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/Views/EmptyListView.swift +++ b/Chuck Norris Facts/App/Scenes/Facts/FactsList/Views/EmptyListView.swift @@ -24,7 +24,7 @@ final class EmptyListView: UIView { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false - label.font = UIFont.systemFont(ofSize: 16, weight: .semibold) + label.font = .preferredFont(forTextStyle: .headline) label.lineBreakMode = .byWordWrapping label.numberOfLines = 0 @@ -37,7 +37,7 @@ final class EmptyListView: UIView { button.translatesAutoresizingMaskIntoConstraints = false button.accessibilityLabel = "Search" button.setTitle("Search", for: .normal) - button.titleLabel?.font = .systemFont(ofSize: 16, weight: .regular) + button.titleLabel?.font = .preferredFont(forTextStyle: .body) button.accessibilityIdentifier = "searchButton" return button @@ -54,13 +54,15 @@ final class EmptyListView: UIView { } private func setupView() { + let animationSize: CGFloat = 200 + backgroundColor = .systemBackground addSubview(animation) animation.translatesAutoresizingMaskIntoConstraints = false - animation.widthAnchor.constraint(equalToConstant: 200).isActive = true - animation.heightAnchor.constraint(equalToConstant: 200).isActive = true + animation.widthAnchor.constraint(equalToConstant: animationSize).isActive = true + animation.heightAnchor.constraint(equalToConstant: animationSize).isActive = true animation.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true animation.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/Views/ErrorView.swift b/Chuck Norris Facts/App/Scenes/Facts/FactsList/Views/ErrorView.swift similarity index 83% rename from Chuck Norris Facts/Scenes/Facts/FactsList/Views/ErrorView.swift rename to Chuck Norris Facts/App/Scenes/Facts/FactsList/Views/ErrorView.swift index 1d51522..6c1592b 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/Views/ErrorView.swift +++ b/Chuck Norris Facts/App/Scenes/Facts/FactsList/Views/ErrorView.swift @@ -27,7 +27,7 @@ final class ErrorView: UIView { label.lineBreakMode = .byWordWrapping label.numberOfLines = 0 label.textAlignment = .center - label.font = .systemFont(ofSize: 16, weight: .semibold) + label.font = .preferredFont(forTextStyle: .body) return label }() @@ -38,7 +38,7 @@ final class ErrorView: UIView { button.translatesAutoresizingMaskIntoConstraints = false button.accessibilityLabel = "Retry" button.setTitle("Retry", for: .normal) - button.titleLabel?.font = .systemFont(ofSize: 16, weight: .regular) + button.titleLabel?.font = .preferredFont(forTextStyle: .headline) button.accessibilityIdentifier = "retryButton" return button @@ -57,17 +57,20 @@ final class ErrorView: UIView { } private func setupView() { + let animationSize: CGFloat = 200 + let padding: CGFloat = 16 + backgroundColor = .systemBackground addSubview(animation) - animation.widthAnchor.constraint(equalToConstant: 200).isActive = true - animation.heightAnchor.constraint(equalToConstant: 200).isActive = true + animation.widthAnchor.constraint(equalToConstant: animationSize).isActive = true + animation.heightAnchor.constraint(equalToConstant: animationSize).isActive = true animation.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true animation.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true addSubview(label) label.topAnchor.constraint(equalTo: animation.bottomAnchor).isActive = true - label.widthAnchor.constraint(equalTo: widthAnchor, constant: -16).isActive = true + label.widthAnchor.constraint(equalTo: widthAnchor, constant: -padding).isActive = true label.centerXAnchor.constraint(equalTo: animation.centerXAnchor).isActive = true addSubview(retryButton) diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/Views/LoadingView.swift b/Chuck Norris Facts/App/Scenes/Facts/FactsList/Views/LoadingView.swift similarity index 83% rename from Chuck Norris Facts/Scenes/Facts/FactsList/Views/LoadingView.swift rename to Chuck Norris Facts/App/Scenes/Facts/FactsList/Views/LoadingView.swift index 36e5b83..78afce2 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/Views/LoadingView.swift +++ b/Chuck Norris Facts/App/Scenes/Facts/FactsList/Views/LoadingView.swift @@ -31,12 +31,14 @@ final class LoadingView: UIView { } private func setupView() { + let animationSize: CGFloat = 48 + backgroundColor = .systemBackground addSubview(animation) animation.translatesAutoresizingMaskIntoConstraints = false - animation.widthAnchor.constraint(equalToConstant: 48).isActive = true - animation.heightAnchor.constraint(equalToConstant: 48).isActive = true + animation.widthAnchor.constraint(equalToConstant: animationSize).isActive = true + animation.heightAnchor.constraint(equalToConstant: animationSize).isActive = true animation.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true animation.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true } diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/PastSearch/PastSearchCell.swift b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/PastSearch/PastSearchCell.swift similarity index 100% rename from Chuck Norris Facts/Scenes/Facts/SearchFacts/PastSearch/PastSearchCell.swift rename to Chuck Norris Facts/App/Scenes/Facts/SearchFacts/PastSearch/PastSearchCell.swift diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/PastSearch/PastSearchViewModel.swift b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/PastSearch/PastSearchViewModel.swift similarity index 100% rename from Chuck Norris Facts/Scenes/Facts/SearchFacts/PastSearch/PastSearchViewModel.swift rename to Chuck Norris Facts/App/Scenes/Facts/SearchFacts/PastSearch/PastSearchViewModel.swift diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsCoordinator.swift b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/SearchFactsCoordinator.swift similarity index 73% rename from Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsCoordinator.swift rename to Chuck Norris Facts/App/Scenes/Facts/SearchFacts/SearchFactsCoordinator.swift index d977898..7decb8e 100644 --- a/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsCoordinator.swift +++ b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/SearchFactsCoordinator.swift @@ -29,12 +29,13 @@ class SearchFactsCoordinator: BaseCoordinator { let searchFactsViewModel = SearchFactsViewModel() searchFactsViewController.viewModel = searchFactsViewModel - let cancel = searchFactsViewModel.didCancel.map { _ in CoordinationResult.cancel } - let search = searchFactsViewModel.didSearchFacts.map { CoordinationResult.search($0) } + let cancelSearchFacts = searchFactsViewModel.outputs.didCancel.map { _ in CoordinationResult.cancel } + let selectSearchTerm = searchFactsViewModel.outputs.didSelectItem.map { CoordinationResult.search($0) } + let searchFacts = searchFactsViewModel.outputs.didSearchFacts.map { CoordinationResult.search($0) } rootViewController.present(navigationController, animated: true) - return Observable.merge(cancel, search) + return Observable.merge(cancelSearchFacts, selectSearchTerm, searchFacts) .take(1) .do(onNext: { [weak self] _ in self?.rootViewController.dismiss(animated: true) }) } diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsTableViewSection.swift b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/SearchFactsTableViewSection.swift similarity index 52% rename from Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsTableViewSection.swift rename to Chuck Norris Facts/App/Scenes/Facts/SearchFacts/SearchFactsTableViewSection.swift index 6db825d..a4e810f 100644 --- a/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsTableViewSection.swift +++ b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/SearchFactsTableViewSection.swift @@ -9,17 +9,17 @@ import RxDataSources enum SearchFactsTableViewItem { - case SuggestionsTableViewItem(suggestions: [SuggestionsSectionModel]) - case PastSearchTableViewItem(model: PastSearchViewModel) + case SuggestionsTableViewItem(suggestions: [FactCategoryViewModel]) + case PastSearchTableViewItem(pastSearch: PastSearchViewModel) } extension SearchFactsTableViewItem { - var quantity: Int { + var text: String { switch self { - case .SuggestionsTableViewItem(let suggestions): - return suggestions.first?.items.count ?? 0 - default: - return 0 + case .SuggestionsTableViewItem: + return "" + case .PastSearchTableViewItem(let pastSearch): + return pastSearch.text } } } @@ -50,6 +50,34 @@ extension SearchFactsTableViewSection: SectionModelType { } } + var isEmpty: Bool { + switch self { + case .SuggestionsSection(let items): + switch items.first { + case .SuggestionsTableViewItem(let suggestions): + return suggestions.isEmpty + default: + return true + } + case .PastSearchesSection(let items): + return items.isEmpty + } + } + + var count: Int { + switch self { + case .SuggestionsSection(let items): + switch items.first { + case .SuggestionsTableViewItem(let suggestions): + return suggestions.count + default: + return 0 + } + case .PastSearchesSection(let items): + return items.count + } + } + init(original: Self, items: [Self.Item]) { self = original } diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewController.swift b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/SearchFactsViewController.swift similarity index 80% rename from Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewController.swift rename to Chuck Norris Facts/App/Scenes/Facts/SearchFacts/SearchFactsViewController.swift index 2118935..57dd977 100644 --- a/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewController.swift +++ b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/SearchFactsViewController.swift @@ -22,19 +22,18 @@ final class SearchFactsViewController: UIViewController { switch dataSource[indexPath] { case .SuggestionsTableViewItem(let suggestions): let cell = tableView.dequeueReusableCell(cell: SuggestionsCell.self, indexPath: indexPath) + let viewModel = SuggestionsViewModel(suggestions: suggestions) cell.viewModel = viewModel - viewModel.didSelectSuggestion - .bind(to: self.viewModel.searchTerm) - .disposed(by: cell.disposeBag) - viewModel.didSelectSuggestion - .map { _ in () } - .bind(to: self.viewModel.searchAction) + + viewModel.outputs.didSelectSuggestion + .bind(to: self.viewModel.inputs.selectItem) .disposed(by: cell.disposeBag) + return cell - case .PastSearchTableViewItem(let model): + case .PastSearchTableViewItem(let pastSearch): let cell = tableView.dequeueReusableCell(cell: PastSearchCell.self, indexPath: indexPath) - cell.setup(model) + cell.setup(pastSearch) return cell } @@ -107,23 +106,23 @@ final class SearchFactsViewController: UIViewController { private func setupBindings() { rx.viewWillAppear - .bind(to: viewModel.viewWillAppear) + .bind(to: viewModel.inputs.viewWillAppear) .disposed(by: disposeBag) cancelButton.rx.tap - .bind(to: viewModel.cancel) + .bind(to: viewModel.inputs.cancel) .disposed(by: disposeBag) searchController.searchBar.rx.text .compactMap { $0 } - .bind(to: viewModel.searchTerm) + .bind(to: viewModel.inputs.searchTerm) .disposed(by: disposeBag) searchController.searchBar.rx.textDidEndEditing - .bind(to: viewModel.searchAction) + .bind(to: viewModel.inputs.searchAction) .disposed(by: disposeBag) - viewModel.items + viewModel.outputs.items .bind(to: tableView.rx.items(dataSource: itemsDataSource)) .disposed(by: disposeBag) @@ -132,22 +131,13 @@ final class SearchFactsViewController: UIViewController { .asObservable() pastSearchSelected - .compactMap { - switch $0 { - case .PastSearchTableViewItem(let model): - return model.text - default: - break - } - return "" - } - .filter { !$0.isEmpty } - .bind(to: viewModel.searchTerm) + .compactMap { $0.text } + .bind(to: viewModel.inputs.selectItem) .disposed(by: disposeBag) pastSearchSelected - .map { _ in () } - .bind(to: viewModel.searchAction) + .mapToVoid() + .bind(to: viewModel.inputs.searchAction) .disposed(by: disposeBag) } } diff --git a/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/SearchFactsViewModel.swift b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/SearchFactsViewModel.swift new file mode 100644 index 0000000..7ccc400 --- /dev/null +++ b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/SearchFactsViewModel.swift @@ -0,0 +1,118 @@ +// +// SearchFactsViewModel.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/13/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation +import RxDataSources +import RxSwift + +typealias PastSearchesSectionModel = AnimatableSectionModel + +protocol SearchFactsViewModelInputs { + // Call when search facts scene is cancelled + var cancel: AnyObserver { get } + + // Call when search term changes + var searchTerm: AnyObserver { get } + + // Call when a search is started (textDidEndEditing) + var searchAction: AnyObserver { get } + + // Call when view will appear to load categories and pastSearches + var viewWillAppear: AnyObserver { get } + + // Call when a item is selected to start a new search + var selectItem: AnyObserver { get } +} + +protocol SearchFactsViewModelOutputs { + // Emmits an event to SearchFacts coordinator dismiss SearchFacts Scene + var didCancel: Observable { get } + + // Emmits an string to start a new search when a term is sent by searchAction + var didSearchFacts: Observable { get } + + // Emmits an string to start a new search when a item is selected + var didSelectItem: Observable { get } + + // Emmits an array of SearchFacts TableView sections to bind on tableView + var items: Observable<[SearchFactsTableViewSection]> { get } +} + +final class SearchFactsViewModel: SearchFactsViewModelInputs, SearchFactsViewModelOutputs { + + var inputs: SearchFactsViewModelInputs { self } + + var outputs: SearchFactsViewModelOutputs { self } + + // MARK: - Inputs + + var cancel: AnyObserver + + var searchTerm: AnyObserver + + var searchAction: AnyObserver + + var viewWillAppear: AnyObserver + + var selectItem: AnyObserver + + // MARK: - Outputs + + var didCancel: Observable + + var didSearchFacts: Observable + + var didSelectItem: Observable + + var items: Observable<[SearchFactsTableViewSection]> + + init(factsService: FactsServiceType = FactsService()) { + let cancelSubject = PublishSubject() + self.cancel = cancelSubject.asObserver() + self.didCancel = cancelSubject.asObservable() + + let searchTermSubject = BehaviorSubject(value: "") + self.searchTerm = searchTermSubject.asObserver() + + let searchActionSubject = PublishSubject() + self.searchAction = searchActionSubject.asObserver() + + self.didSearchFacts = searchActionSubject + .withLatestFrom(searchTermSubject) + .filter { !$0.isEmpty } + + let selectItemSubject = BehaviorSubject(value: "") + self.selectItem = selectItemSubject.asObserver() + + self.didSelectItem = selectItemSubject + .filter { !$0.isEmpty } + + let viewWillAppearSubject = PublishSubject() + self.viewWillAppear = viewWillAppearSubject.asObserver() + + let categories = viewWillAppearSubject + .flatMapLatest { factsService.retrieveCategories() } + .map { Array($0.shuffled().prefix(8)) } + + let suggestions = categories + .map { $0.map { FactCategoryViewModel(category: $0) } } + .map { [SearchFactsTableViewItem.SuggestionsTableViewItem(suggestions: $0)] } + .map { suggestions -> SearchFactsTableViewSection in .SuggestionsSection(items: suggestions) } + + let pastSearches = viewWillAppearSubject + .flatMapLatest { factsService.retrievePastSearches() } + .map { $0.map { PastSearchViewModel(text: $0) } } + .map { $0.map { SearchFactsTableViewItem.PastSearchTableViewItem(pastSearch: $0) } } + .map { pastSearches -> SearchFactsTableViewSection in .PastSearchesSection(items: pastSearches)} + + self.items = Observable.combineLatest(suggestions, pastSearches) { suggestions, pastSearches in + [suggestions, pastSearches] + } + .map { $0.filter { !$0.isEmpty } } + } +} diff --git a/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryCell.swift b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryCell.swift new file mode 100644 index 0000000..517830c --- /dev/null +++ b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryCell.swift @@ -0,0 +1,39 @@ +// +// FactCategoryCell.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/20/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import UIKit + +class FactCategoryCell: UICollectionViewCell { + + private let categoryView: CategoryView = CategoryView() + + override init(frame: CGRect) { + super.init(frame: frame) + + setupView() + + isAccessibilityElement = true + accessibilityIdentifier = "factCategoryCell" + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setup(_ factCategory: FactCategoryViewModel) { + categoryView.label.text = factCategory.text.uppercased() + categoryView.label.font = .preferredFont(forTextStyle: .headline) + } + + func setupView() { + contentView.addSubview(categoryView) + categoryView.translatesAutoresizingMaskIntoConstraints = false + categoryView.widthAnchor.constraint(equalTo: contentView.widthAnchor).isActive = true + categoryView.heightAnchor.constraint(equalTo: contentView.heightAnchor).isActive = true + } +} diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryViewModel.swift b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryViewModel.swift similarity index 82% rename from Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryViewModel.swift rename to Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryViewModel.swift index 8d2b9a2..c4a6de3 100644 --- a/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryViewModel.swift +++ b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryViewModel.swift @@ -10,21 +10,23 @@ import Foundation import RxDataSources class FactCategoryViewModel { + let category: FactCategory let text: String init(category: FactCategory) { + self.category = category self.text = category.text } } extension FactCategoryViewModel: IdentifiableType { var identity: String { - text + category.text } } extension FactCategoryViewModel: Equatable { static func == (lhs: FactCategoryViewModel, rhs: FactCategoryViewModel) -> Bool { - return lhs.text == rhs.text + return lhs.category == rhs.category } } diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/SuggestionsCell.swift b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/SuggestionsCell.swift similarity index 82% rename from Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/SuggestionsCell.swift rename to Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/SuggestionsCell.swift index d5f6812..429f009 100644 --- a/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/SuggestionsCell.swift +++ b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/SuggestionsCell.swift @@ -44,12 +44,14 @@ class SuggestionsCell: UITableViewCell { } lazy var collectionView: DynamicHeightCollectionView = { - let layout = SuggestionsViewFlowLayout() - let collectionView = DynamicHeightCollectionView(frame: .zero, collectionViewLayout: layout) + let insets: CGFloat = 16 - layout.scrollDirection = .vertical - layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize - layout.sectionInset = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) + let suggestionsViewFlowLayout = SuggestionsViewFlowLayout() + let collectionView = DynamicHeightCollectionView(frame: .zero, collectionViewLayout: suggestionsViewFlowLayout) + + suggestionsViewFlowLayout.scrollDirection = .vertical + suggestionsViewFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize + suggestionsViewFlowLayout.sectionInset = UIEdgeInsets(top: insets, left: insets, bottom: insets, right: insets) collectionView.isScrollEnabled = false collectionView.translatesAutoresizingMaskIntoConstraints = false @@ -71,23 +73,18 @@ class SuggestionsCell: UITableViewCell { .setDelegate(self) .disposed(by: disposeBag) - viewModel.suggestions + viewModel.outputs.suggestions .observeOn(MainScheduler.instance) .bind(to: collectionView.rx.items(dataSource: suggestionsDataSource)) .disposed(by: disposeBag) - let categorySelected = collectionView.rx + let suggestionSelected = collectionView.rx .modelSelected(FactCategoryViewModel.self) .asObservable() - categorySelected + suggestionSelected .compactMap { $0.text } - .bind(to: viewModel.suggestion) - .disposed(by: disposeBag) - - categorySelected - .map { _ in () } - .bind(to: viewModel.selectAction) + .bind(to: viewModel.inputs.selectSuggestion) .disposed(by: disposeBag) } } diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/SuggestionsViewFlowLayout.swift b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/SuggestionsViewFlowLayout.swift similarity index 100% rename from Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/SuggestionsViewFlowLayout.swift rename to Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/SuggestionsViewFlowLayout.swift diff --git a/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/SuggestionsViewModel.swift b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/SuggestionsViewModel.swift new file mode 100644 index 0000000..b8875c3 --- /dev/null +++ b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/SuggestionsViewModel.swift @@ -0,0 +1,56 @@ +// +// SuggestionsViewModel.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 10/26/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import RxSwift +import RxDataSources + +typealias SuggestionsSectionModel = AnimatableSectionModel + +protocol SuggestionsViewModelInputs { + // Call when a suggestion is selected to start a new search + var selectSuggestion: AnyObserver { get } +} + +protocol SuggestionsViewModelOutputs { + // Emmits an array of suggestions to bind on tableView + var suggestions: Observable<[SuggestionsSectionModel]> { get } + + // Emmits an string of a selected suggestion + var didSelectSuggestion: Observable { get } +} + +struct SuggestionsViewModel: SuggestionsViewModelInputs, SuggestionsViewModelOutputs { + + var inputs: SuggestionsViewModelInputs { self } + + var outputs: SuggestionsViewModelOutputs { self } + + // MARK: - Inputs + + var selectSuggestion: AnyObserver + + // MARK: - Outputs + + var suggestions: Observable<[SuggestionsSectionModel]> + + var didSelectSuggestion: Observable + + init(suggestions: [FactCategoryViewModel]) { + let suggestionsSubject = BehaviorSubject<[SuggestionsSectionModel]>(value: []) + self.suggestions = suggestionsSubject.asObserver() + + suggestionsSubject.onNext([SuggestionsSectionModel(model: "", items: suggestions)]) + + let selectSuggestionSubject = BehaviorSubject(value: "") + self.selectSuggestion = selectSuggestionSubject.asObserver() + + self.didSelectSuggestion = selectSuggestionSubject + .filter { !$0.isEmpty } + .map { $0.capitalized } + } +} diff --git a/Chuck Norris Facts/Scenes/Facts/FactsList/Views/CategoryView.swift b/Chuck Norris Facts/App/Views/CategoryView.swift similarity index 78% rename from Chuck Norris Facts/Scenes/Facts/FactsList/Views/CategoryView.swift rename to Chuck Norris Facts/App/Views/CategoryView.swift index cd3593e..1f631b8 100644 --- a/Chuck Norris Facts/Scenes/Facts/FactsList/Views/CategoryView.swift +++ b/Chuck Norris Facts/App/Views/CategoryView.swift @@ -15,7 +15,7 @@ class CategoryView: UIView { label.translatesAutoresizingMaskIntoConstraints = false label.lineBreakMode = .byTruncatingTail - label.font = .systemFont(ofSize: 16, weight: .bold) + label.font = .preferredFont(forTextStyle: .headline) label.textColor = .white label.numberOfLines = 1 @@ -33,15 +33,18 @@ class CategoryView: UIView { } func setupView() { - layer.cornerRadius = 4 + let cornerRadius: CGFloat = 4 + let padding: CGFloat = 4 + + layer.cornerRadius = cornerRadius backgroundColor = .systemBlue addSubview(label) label.translatesAutoresizingMaskIntoConstraints = false - label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 4).isActive = true - label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -4).isActive = true - label.topAnchor.constraint(equalTo: topAnchor, constant: 4).isActive = true - label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4).isActive = true + label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: padding).isActive = true + label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -padding).isActive = true + label.topAnchor.constraint(equalTo: topAnchor, constant: padding).isActive = true + label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -padding).isActive = true } } diff --git a/Chuck Norris Facts/API/APIError.swift b/Chuck Norris Facts/Core/API/APIError.swift similarity index 100% rename from Chuck Norris Facts/API/APIError.swift rename to Chuck Norris Facts/Core/API/APIError.swift diff --git a/Chuck Norris Facts/API/APIProvider.swift b/Chuck Norris Facts/Core/API/APIProvider.swift similarity index 100% rename from Chuck Norris Facts/API/APIProvider.swift rename to Chuck Norris Facts/Core/API/APIProvider.swift diff --git a/Chuck Norris Facts/API/APIResponse.swift b/Chuck Norris Facts/Core/API/APIResponse.swift similarity index 100% rename from Chuck Norris Facts/API/APIResponse.swift rename to Chuck Norris Facts/Core/API/APIResponse.swift diff --git a/Chuck Norris Facts/API/APITarget.swift b/Chuck Norris Facts/Core/API/APITarget.swift similarity index 100% rename from Chuck Norris Facts/API/APITarget.swift rename to Chuck Norris Facts/Core/API/APITarget.swift diff --git a/Chuck Norris Facts/API/HTTP/HTTPMethod.swift b/Chuck Norris Facts/Core/API/HTTP/HTTPMethod.swift similarity index 100% rename from Chuck Norris Facts/API/HTTP/HTTPMethod.swift rename to Chuck Norris Facts/Core/API/HTTP/HTTPMethod.swift diff --git a/Chuck Norris Facts/API/HTTP/HTTPTask.swift b/Chuck Norris Facts/Core/API/HTTP/HTTPTask.swift similarity index 100% rename from Chuck Norris Facts/API/HTTP/HTTPTask.swift rename to Chuck Norris Facts/Core/API/HTTP/HTTPTask.swift diff --git a/Chuck Norris Facts/Data/Models/Fact.swift b/Chuck Norris Facts/Core/Data/Models/Fact.swift similarity index 84% rename from Chuck Norris Facts/Data/Models/Fact.swift rename to Chuck Norris Facts/Core/Data/Models/Fact.swift index 0d114b9..1c59215 100644 --- a/Chuck Norris Facts/Data/Models/Fact.swift +++ b/Chuck Norris Facts/Core/Data/Models/Fact.swift @@ -23,3 +23,9 @@ struct Fact: Decodable { self.categories = categories } } + +extension Fact: Equatable { + static func == (lhs: Fact, rhs: Fact) -> Bool { + lhs.id == rhs.id + } +} diff --git a/Chuck Norris Facts/Data/Models/FactCategory.swift b/Chuck Norris Facts/Core/Data/Models/FactCategory.swift similarity index 76% rename from Chuck Norris Facts/Data/Models/FactCategory.swift rename to Chuck Norris Facts/Core/Data/Models/FactCategory.swift index 98e516a..f9ed3e2 100644 --- a/Chuck Norris Facts/Data/Models/FactCategory.swift +++ b/Chuck Norris Facts/Core/Data/Models/FactCategory.swift @@ -19,3 +19,9 @@ struct FactCategory: Decodable { self.text = try decoder.singleValueContainer().decode(String.self) } } + +extension FactCategory: Equatable { + static func == (lhs: FactCategory, rhs: FactCategory) -> Bool { + lhs.text == rhs.text + } +} diff --git a/Chuck Norris Facts/Data/Networking/FactsAPI.swift b/Chuck Norris Facts/Core/Data/Networking/FactsAPI.swift similarity index 100% rename from Chuck Norris Facts/Data/Networking/FactsAPI.swift rename to Chuck Norris Facts/Core/Data/Networking/FactsAPI.swift diff --git a/Chuck Norris Facts/Data/Networking/Responses/SearchFactsResponse.swift b/Chuck Norris Facts/Core/Data/Networking/Responses/SearchFactsResponse.swift similarity index 100% rename from Chuck Norris Facts/Data/Networking/Responses/SearchFactsResponse.swift rename to Chuck Norris Facts/Core/Data/Networking/Responses/SearchFactsResponse.swift diff --git a/Chuck Norris Facts/Data/Services/FactsService.swift b/Chuck Norris Facts/Core/Data/Services/FactsService.swift similarity index 98% rename from Chuck Norris Facts/Data/Services/FactsService.swift rename to Chuck Norris Facts/Core/Data/Services/FactsService.swift index 5b9be21..8405535 100644 --- a/Chuck Norris Facts/Data/Services/FactsService.swift +++ b/Chuck Norris Facts/Core/Data/Services/FactsService.swift @@ -64,7 +64,7 @@ struct FactsService: FactsServiceType { .observeOn(self.scheduler ?? MainScheduler.asyncInstance) .map([FactCategory].self, using: JSON.decoder) .map { self.storage.storeCategories($0) } - .map { () } + .mapToVoid() } } diff --git a/Chuck Norris Facts/Data/Storage/Entities/FactCategoryEntity.swift b/Chuck Norris Facts/Core/Data/Storage/Entities/FactCategoryEntity.swift similarity index 100% rename from Chuck Norris Facts/Data/Storage/Entities/FactCategoryEntity.swift rename to Chuck Norris Facts/Core/Data/Storage/Entities/FactCategoryEntity.swift diff --git a/Chuck Norris Facts/Data/Storage/Entities/SearchEntity.swift b/Chuck Norris Facts/Core/Data/Storage/Entities/SearchEntity.swift similarity index 100% rename from Chuck Norris Facts/Data/Storage/Entities/SearchEntity.swift rename to Chuck Norris Facts/Core/Data/Storage/Entities/SearchEntity.swift diff --git a/Chuck Norris Facts/Data/Storage/FactsStorage.swift b/Chuck Norris Facts/Core/Data/Storage/FactsStorage.swift similarity index 100% rename from Chuck Norris Facts/Data/Storage/FactsStorage.swift rename to Chuck Norris Facts/Core/Data/Storage/FactsStorage.swift diff --git a/Chuck Norris Facts/Extensions/API+Rx.swift b/Chuck Norris Facts/Core/Extensions/API+Rx.swift similarity index 100% rename from Chuck Norris Facts/Extensions/API+Rx.swift rename to Chuck Norris Facts/Core/Extensions/API+Rx.swift diff --git a/Chuck Norris Facts/Extensions/Data+Stub.swift b/Chuck Norris Facts/Core/Extensions/Data+Stub.swift similarity index 100% rename from Chuck Norris Facts/Extensions/Data+Stub.swift rename to Chuck Norris Facts/Core/Extensions/Data+Stub.swift diff --git a/Chuck Norris Facts/Core/Extensions/RxSwift+Extensions.swift b/Chuck Norris Facts/Core/Extensions/RxSwift+Extensions.swift new file mode 100644 index 0000000..d015480 --- /dev/null +++ b/Chuck Norris Facts/Core/Extensions/RxSwift+Extensions.swift @@ -0,0 +1,26 @@ +// +// RxSwift+Extensions.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 11/1/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation +import RxSwift + +extension ObservableType { + func mapToVoid() -> Observable { + map { _ in () } + } +} + +extension ObservableType where Element: EventConvertible { + func elements() -> Observable { + compactMap { $0.event.element } + } + + func errors() -> Observable { + compactMap { $0.event.error } + } +} diff --git a/Chuck Norris Facts/Extensions/UICollectionView+Extensions.swift b/Chuck Norris Facts/Core/Extensions/UICollectionView+Extensions.swift similarity index 100% rename from Chuck Norris Facts/Extensions/UICollectionView+Extensions.swift rename to Chuck Norris Facts/Core/Extensions/UICollectionView+Extensions.swift diff --git a/Chuck Norris Facts/Extensions/UITableView+Extensions.swift b/Chuck Norris Facts/Core/Extensions/UITableView+Extensions.swift similarity index 100% rename from Chuck Norris Facts/Extensions/UITableView+Extensions.swift rename to Chuck Norris Facts/Core/Extensions/UITableView+Extensions.swift diff --git a/Chuck Norris Facts/Extensions/UIViewController+Rx.swift b/Chuck Norris Facts/Core/Extensions/UIViewController+Rx.swift similarity index 72% rename from Chuck Norris Facts/Extensions/UIViewController+Rx.swift rename to Chuck Norris Facts/Core/Extensions/UIViewController+Rx.swift index 44b2758..76a12ea 100644 --- a/Chuck Norris Facts/Extensions/UIViewController+Rx.swift +++ b/Chuck Norris Facts/Core/Extensions/UIViewController+Rx.swift @@ -11,10 +11,10 @@ import RxSwift extension Reactive where Base: UIViewController { var viewDidAppear: Observable { - sentMessage(#selector(Base.viewDidAppear(_:))).map { _ in () } + sentMessage(#selector(Base.viewDidAppear(_:))).mapToVoid() } var viewWillAppear: Observable { - sentMessage(#selector(Base.viewWillAppear(_:))).map { _ in () } + sentMessage(#selector(Base.viewWillAppear(_:))).mapToVoid() } } diff --git a/Chuck Norris Facts/Extensions/URLRequest+Encoded.swift b/Chuck Norris Facts/Core/Extensions/URLRequest+Encoded.swift similarity index 100% rename from Chuck Norris Facts/Extensions/URLRequest+Encoded.swift rename to Chuck Norris Facts/Core/Extensions/URLRequest+Encoded.swift diff --git a/Chuck Norris Facts/Library/ActivityIndicator.swift b/Chuck Norris Facts/Core/Library/ActivityIndicator.swift similarity index 100% rename from Chuck Norris Facts/Library/ActivityIndicator.swift rename to Chuck Norris Facts/Core/Library/ActivityIndicator.swift diff --git a/Chuck Norris Facts/Library/BaseCoordinator.swift b/Chuck Norris Facts/Core/Library/BaseCoordinator.swift similarity index 100% rename from Chuck Norris Facts/Library/BaseCoordinator.swift rename to Chuck Norris Facts/Core/Library/BaseCoordinator.swift diff --git a/Chuck Norris Facts/Library/DynamicHeightCollectionView.swift b/Chuck Norris Facts/Core/Library/DynamicHeightCollectionView.swift similarity index 100% rename from Chuck Norris Facts/Library/DynamicHeightCollectionView.swift rename to Chuck Norris Facts/Core/Library/DynamicHeightCollectionView.swift diff --git a/Chuck Norris Facts/Library/JSON.swift b/Chuck Norris Facts/Core/Library/JSON.swift similarity index 100% rename from Chuck Norris Facts/Library/JSON.swift rename to Chuck Norris Facts/Core/Library/JSON.swift diff --git a/Chuck Norris Facts/Library/LaunchArgument.swift b/Chuck Norris Facts/Core/Library/LaunchArgument.swift similarity index 100% rename from Chuck Norris Facts/Library/LaunchArgument.swift rename to Chuck Norris Facts/Core/Library/LaunchArgument.swift diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewModel.swift b/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewModel.swift deleted file mode 100644 index 60896bf..0000000 --- a/Chuck Norris Facts/Scenes/Facts/SearchFacts/SearchFactsViewModel.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// SearchFactsViewModel.swift -// Chuck Norris Facts -// -// Created by Djorkaeff Alexandre Vilela Pereira on 10/13/20. -// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. -// - -import Foundation -import RxDataSources -import RxSwift - -typealias SuggestionsSectionModel = AnimatableSectionModel - -typealias PastSearchesSectionModel = AnimatableSectionModel - -class SearchFactsViewModel { - - // MARK: - Inputs - - let cancel: AnyObserver - - let searchTerm: AnyObserver - - let searchAction: AnyObserver - - let viewWillAppear: AnyObserver - - // MARK: - Outputs - - let didCancel: Observable - - let didSearchFacts: Observable - - let items: Observable<[SearchFactsTableViewSection]> - - init(factsService: FactsServiceType = FactsService()) { - let cancelSubject = PublishSubject() - self.cancel = cancelSubject.asObserver() - self.didCancel = cancelSubject.asObservable() - - let searchTermSubject = BehaviorSubject(value: "") - self.searchTerm = searchTermSubject.asObserver() - - let searchActionSubject = PublishSubject() - self.searchAction = searchActionSubject.asObserver() - - self.didSearchFacts = searchActionSubject - .withLatestFrom(searchTermSubject) - .filter { !$0.isEmpty } - - let viewWillAppearSubject = PublishSubject() - self.viewWillAppear = viewWillAppearSubject.asObserver() - - let categories = viewWillAppearSubject - .flatMapLatest { factsService.retrieveCategories() } - .map { Array($0.shuffled().prefix(8)) } - .map { $0.map { FactCategoryViewModel(category: $0) } } - .map { [SuggestionsSectionModel(model: "", items: $0)] } - .map { suggestions -> [SearchFactsTableViewItem] in - if let firstSection = suggestions.first, firstSection.items.isEmpty { - return [] - } - return [SearchFactsTableViewItem.SuggestionsTableViewItem(suggestions: suggestions)] - } - - let pastSearches = viewWillAppearSubject - .flatMapLatest { factsService.retrievePastSearches() } - .map { $0.map { PastSearchViewModel(text: $0) } } - .map { $0.map { SearchFactsTableViewItem.PastSearchTableViewItem(model: $0) } } - - self.items = Observable.combineLatest(categories, pastSearches) - .map { categories, pastSearches -> [SearchFactsTableViewSection] in - [.SuggestionsSection(items: categories), .PastSearchesSection(items: pastSearches)] - } - .map { $0.filter { !$0.items.isEmpty } } - } -} diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryCell.swift b/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryCell.swift deleted file mode 100644 index 3d2c050..0000000 --- a/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryCell.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// FactCategoryCell.swift -// Chuck Norris Facts -// -// Created by Djorkaeff Alexandre Vilela Pereira on 10/20/20. -// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. -// - -import UIKit - -class FactCategoryCell: UICollectionViewCell { - - private lazy var bodyLabel: UILabel = { - let label = UILabel() - - label.translatesAutoresizingMaskIntoConstraints = false - label.lineBreakMode = .byWordWrapping - label.textColor = .white - - return label - }() - - override init(frame: CGRect) { - super.init(frame: frame) - - setupView() - - isAccessibilityElement = true - accessibilityIdentifier = "factCategoryCell" - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func setup(_ factCategory: FactCategoryViewModel) { - bodyLabel.text = factCategory.text - bodyLabel.font = .systemFont(ofSize: 16, weight: .bold) - } - - func setupView() { - layer.cornerRadius = 4 - - backgroundColor = .systemBlue - - contentView.addSubview(bodyLabel) - bodyLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 4).isActive = true - bodyLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -4).isActive = true - bodyLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 4).isActive = true - bodyLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4).isActive = true - } -} diff --git a/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/SuggestionsViewModel.swift b/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/SuggestionsViewModel.swift deleted file mode 100644 index 1de8910..0000000 --- a/Chuck Norris Facts/Scenes/Facts/SearchFacts/Suggestions/SuggestionsViewModel.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// SuggestionsViewModel.swift -// Chuck Norris Facts -// -// Created by Djorkaeff Alexandre Vilela Pereira on 10/26/20. -// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. -// - -import RxSwift - -struct SuggestionsViewModel { - - // MARK: - Inputs - - let suggestion: AnyObserver - - let selectAction: AnyObserver - - // MARK: - Outputs - - let suggestions: Observable<[SuggestionsSectionModel]> - - let didSelectSuggestion: Observable - - init(suggestions: [SuggestionsSectionModel]) { - let suggestionsSubject = BehaviorSubject<[SuggestionsSectionModel]>(value: []) - self.suggestions = suggestionsSubject.asObserver() - - suggestionsSubject.onNext(suggestions) - - let suggestionSubject = BehaviorSubject(value: "") - self.suggestion = suggestionSubject.asObserver() - - let selectActionSubject = PublishSubject() - self.selectAction = selectActionSubject.asObserver() - - self.didSelectSuggestion = selectActionSubject - .withLatestFrom(suggestionSubject) - .filter { !$0.isEmpty } - } -} diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/Fact/FactViewModelTests.swift b/Chuck Norris FactsTests/Scenes/Facts/FactsList/Fact/FactViewModelTests.swift index f4e2d8a..1875096 100644 --- a/Chuck Norris FactsTests/Scenes/Facts/FactsList/Fact/FactViewModelTests.swift +++ b/Chuck Norris FactsTests/Scenes/Facts/FactsList/Fact/FactViewModelTests.swift @@ -37,6 +37,6 @@ class FactViewModelTests: XCTestCase { let fact = try XCTUnwrap(factStub) let factViewModelTest = FactViewModel(fact: fact) - XCTAssertEqual(factViewModelTest.identity, fact.value) + XCTAssertEqual(factViewModelTest.identity, fact.id) } } diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift index 15de823..cfb2615 100644 --- a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift +++ b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift @@ -39,7 +39,7 @@ class FactsListViewControllerTests: XCTestCase { func test_FactsListViewController_WhenFactsIsEmpty_WhenSearchTermIsEmpty_ShouldShowEmptyList() { factsServiceMock.searchFactsReturnValue = .just([]) - factsListViewModel.viewDidAppear.onNext(()) + factsListViewModel.inputs.viewDidAppear.onNext(()) XCTAssertFalse(factsListViewController.emptyListView.isHidden) XCTAssertEqual(factsListViewController.emptyListView.label.text, L10n.EmptyView.empty) @@ -49,8 +49,8 @@ class FactsListViewControllerTests: XCTestCase { func test_FactsListViewController_WhenFactsIsEmpty_WhenSearchTermIsNotEmpty_ShouldShowEmptyList() { factsServiceMock.searchFactsReturnValue = .just([]) - factsListViewModel.setSearchTerm.onNext("games") - factsListViewModel.viewDidAppear.onNext(()) + factsListViewModel.inputs.setSearchTerm.onNext("games") + factsListViewModel.inputs.viewDidAppear.onNext(()) XCTAssertFalse(factsListViewController.emptyListView.isHidden) XCTAssertEqual(factsListViewController.emptyListView.label.text, L10n.EmptyView.emptySearch) @@ -62,35 +62,35 @@ class FactsListViewControllerTests: XCTestCase { let apiError = APIError.statusCode(response) factsServiceMock.searchFactsReturnValue = .error(apiError) - factsListViewModel.viewDidAppear.onNext(()) + factsListViewModel.inputs.setSearchTerm.onNext("") XCTAssertFalse(factsListViewController.errorView.isHidden) } - func test_FactCell_WhenContentIsShort_FontSizeShouldBe24() throws { + func test_FactCell_WhenContentIsShort_FontSizeShouldBeTitle1() throws { let factStub = try stub("short-fact", type: Fact.self) let fact = try XCTUnwrap(factStub) factsServiceMock.searchFactsReturnValue = .just([fact]) - factsListViewModel.viewDidAppear.onNext(()) + factsListViewModel.inputs.setSearchTerm.onNext("") let factCell = factsListFirstCell() - XCTAssertEqual(factCell?.bodyLabel.font.pointSize, 24) + XCTAssertEqual(factCell?.bodyLabel.font, .preferredFont(forTextStyle: .title1)) } - func test_FactCell_WhenContentIsLong_FontSizeShouldBe16() throws { + func test_FactCell_WhenContentIsLong_FontSizeShouldBeTitle3() throws { let factStub = try stub("long-fact", type: Fact.self) let fact = try XCTUnwrap(factStub) factsServiceMock.searchFactsReturnValue = .just([fact]) - factsListViewModel.viewDidAppear.onNext(()) + factsListViewModel.inputs.setSearchTerm.onNext("") let factCell = factsListFirstCell() - XCTAssertEqual(factCell?.bodyLabel.font.pointSize, 16) + XCTAssertEqual(factCell?.bodyLabel.font, .preferredFont(forTextStyle: .title3)) } func test_FactCell_WhenTapShareFact_ShouldShowShareActivity() throws { @@ -102,10 +102,10 @@ class FactsListViewControllerTests: XCTestCase { let testScheduler = TestScheduler(initialClock: 0) let shareFactObserver = testScheduler.createObserver(FactViewModel.self) - factsListViewModel.setSearchTerm.onNext("games") - factsListViewModel.viewDidAppear.onNext(()) + factsListViewModel.inputs.setSearchTerm.onNext("games") + factsListViewModel.inputs.viewDidAppear.onNext(()) - factsListViewModel.showShareFact + factsListViewModel.outputs.showShareFact .subscribe(shareFactObserver) .disposed(by: disposeBag) diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift index 4a86093..cc91fc2 100644 --- a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift +++ b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift @@ -39,11 +39,11 @@ class FactsListViewModelTests: XCTestCase { let factsObserver = testScheduler.createObserver([FactsSectionModel].self) - factsListViewModel.facts + factsListViewModel.outputs.facts .subscribe(factsObserver) .disposed(by: disposeBag) - factsListViewModel.viewDidAppear.onNext(()) + factsListViewModel.inputs.viewDidAppear.onNext(()) testScheduler.start() @@ -55,16 +55,16 @@ class FactsListViewModelTests: XCTestCase { let factStub = try stub("short-fact", type: Fact.self) let fact = try XCTUnwrap(factStub) - factsListViewModel.viewDidAppear.onNext(()) + factsListViewModel.inputs.viewDidAppear.onNext(()) let factObserver = testScheduler.createObserver(FactViewModel.self) - factsListViewModel.showShareFact + factsListViewModel.outputs.showShareFact .subscribe(factObserver) .disposed(by: disposeBag) let factViewModel = FactViewModel(fact: fact) - factsListViewModel.startShareFact.onNext(factViewModel) + factsListViewModel.inputs.startShareFact.onNext(factViewModel) let shareFact = factObserver.events.compactMap { $0.value.element }.first XCTAssertEqual(fact.value, shareFact?.text) @@ -77,11 +77,11 @@ class FactsListViewModelTests: XCTestCase { let errorObserver = testScheduler.createObserver(FactsListError.self) - factsListViewModel.errors + factsListViewModel.outputs.errors .subscribe(errorObserver) .disposed(by: disposeBag) - factsListViewModel.viewDidAppear.onNext(()) + factsListViewModel.inputs.viewDidAppear.onNext(()) testScheduler.start() @@ -96,11 +96,11 @@ class FactsListViewModelTests: XCTestCase { let errorObserver = testScheduler.createObserver(FactsListError.self) - factsListViewModel.errors + factsListViewModel.outputs.errors .subscribe(errorObserver) .disposed(by: disposeBag) - factsListViewModel.viewDidAppear.onNext(()) + factsListViewModel.inputs.viewDidAppear.onNext(()) testScheduler.start() @@ -115,11 +115,11 @@ class FactsListViewModelTests: XCTestCase { let errorObserver = testScheduler.createObserver(FactsListError.self) - factsListViewModel.errors + factsListViewModel.outputs.errors .subscribe(errorObserver) .disposed(by: disposeBag) - factsListViewModel.viewDidAppear.onNext(()) + factsListViewModel.inputs.viewDidAppear.onNext(()) testScheduler.start() diff --git a/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewControllerTests.swift b/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewControllerTests.swift index 2c97ab8..48e1a9d 100644 --- a/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewControllerTests.swift +++ b/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewControllerTests.swift @@ -43,7 +43,7 @@ class SearchFactsViewControllerTests: XCTestCase { let stubFactCategories = try stub("get-categories", type: [FactCategory].self) ?? [] factsServiceMock.retrieveCategoriesReturnValue = .just(stubFactCategories) - searchFactsViewModel.viewWillAppear.onNext(()) + searchFactsViewModel.inputs.viewWillAppear.onNext(()) let tableView = searchFactsViewController.tableView let searchFactsDataSource = tableView.dataSource @@ -57,7 +57,7 @@ class SearchFactsViewControllerTests: XCTestCase { func test_SearchFactsViewController_WhenViewWillAppear_ShouldLoadPastSearches() { factsServiceMock.retrievePastSearchesReturnValue = .just(["fashion", "games", "explicit"]) - searchFactsViewModel.viewWillAppear.onNext(()) + searchFactsViewModel.inputs.viewWillAppear.onNext(()) let tableView = searchFactsViewController.tableView let searchFactsDataSource = tableView.dataSource @@ -67,7 +67,7 @@ class SearchFactsViewControllerTests: XCTestCase { } func test_SearchFactsViewController_WhenViewWillAppearWithoutData_ShouldBeEmpty() { - searchFactsViewModel.viewWillAppear.onNext(()) + searchFactsViewModel.inputs.viewWillAppear.onNext(()) let tableView = searchFactsViewController.tableView let searchFactsDataSource = tableView.dataSource @@ -78,7 +78,7 @@ class SearchFactsViewControllerTests: XCTestCase { func test_Suggestions_WhenEmpty_ShouldBeHidden() { factsServiceMock.retrieveCategoriesReturnValue = .just([]) - searchFactsViewModel.viewWillAppear.onNext(()) + searchFactsViewModel.inputs.viewWillAppear.onNext(()) let tableView = searchFactsViewController.tableView let searchFactsDataSource = tableView.dataSource @@ -89,7 +89,7 @@ class SearchFactsViewControllerTests: XCTestCase { func test_PastSearches_WhenEmpty_ShouldBeHidden() { factsServiceMock.retrievePastSearchesReturnValue = .just([]) - searchFactsViewModel.viewWillAppear.onNext(()) + searchFactsViewModel.inputs.viewWillAppear.onNext(()) let tableView = searchFactsViewController.tableView let searchFactsDataSource = tableView.dataSource diff --git a/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewModelTests.swift b/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewModelTests.swift index 05e970b..3abb60e 100644 --- a/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewModelTests.swift +++ b/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/SearchFactsViewModelTests.swift @@ -37,12 +37,12 @@ class SearchFactsViewModelTests: XCTestCase { func test_SeachFactsViewModel_WhenSearchFacts_ShouldSetSearchTerm() { let searchFactsObserver = testScheduler.createObserver(String.self) - searchFactsViewModel.didSearchFacts + searchFactsViewModel.outputs.didSearchFacts .subscribe(searchFactsObserver) .disposed(by: disposeBag) - searchFactsViewModel.searchTerm.onNext("games") - searchFactsViewModel.searchAction.onNext(()) + searchFactsViewModel.inputs.searchTerm.onNext("games") + searchFactsViewModel.inputs.searchAction.onNext(()) testScheduler.start() @@ -53,7 +53,7 @@ class SearchFactsViewModelTests: XCTestCase { func test_SearchFactsViewModel_WhenCancelSearch_ShouldCancelSearchScene() { let cancelObserver = testScheduler.createObserver(Void.self) - searchFactsViewModel.didCancel + searchFactsViewModel.outputs.didCancel .subscribe(cancelObserver) .disposed(by: disposeBag) @@ -71,16 +71,16 @@ class SearchFactsViewModelTests: XCTestCase { let testCategories = try stub("get-categories", type: [FactCategory].self) ?? [] factsServiceMock.retrieveCategoriesReturnValue = .just(testCategories) - searchFactsViewModel.items + searchFactsViewModel.outputs.items .subscribe(searchFactsItemsObserver) .disposed(by: disposeBag) - searchFactsViewModel.viewWillAppear.onNext(()) + searchFactsViewModel.inputs.viewWillAppear.onNext(()) testScheduler.start() let searchFactsViewModelEvents = searchFactsItemsObserver.events.compactMap { $0.value.element }.first - XCTAssertEqual(searchFactsViewModelEvents?.first?.items.first?.quantity, 8) + XCTAssertEqual(searchFactsViewModelEvents?.first?.count, 8) } func test_SearchFactsViewModel_WhenViewWillAppear_ShouldLoadPastSearches() { @@ -89,11 +89,11 @@ class SearchFactsViewModelTests: XCTestCase { let pastSearches = ["fashion", "games", "food"] factsServiceMock.retrievePastSearchesReturnValue = .just(pastSearches) - searchFactsViewModel.items + searchFactsViewModel.outputs.items .subscribe(searchFactsItemsObserver) .disposed(by: disposeBag) - searchFactsViewModel.viewWillAppear.onNext(()) + searchFactsViewModel.inputs.viewWillAppear.onNext(()) testScheduler.start() diff --git a/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryViewModelTests.swift b/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryViewModelTests.swift index 07be07a..4d7d930 100644 --- a/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryViewModelTests.swift +++ b/Chuck Norris FactsTests/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryViewModelTests.swift @@ -15,6 +15,14 @@ import RxTest class FactCategoryViewModelTests: XCTestCase { + func test_FactCategoryViewModel_WhenFormat_ShouldHaveText() throws { + let factCategoryStub = try stub("fact-category", type: FactCategory.self) + let factCategory = try XCTUnwrap(factCategoryStub) + + let factCategoryViewModel = FactCategoryViewModel(category: factCategory) + XCTAssertEqual(factCategoryViewModel.text, factCategory.text) + } + func test_FactCategoryViewModel_WhenCompare_ShouldBeEquatable() throws { let factCategoryStub = try stub("fact-category", type: FactCategory.self) let factCategory = try XCTUnwrap(factCategoryStub) From b8b2abbcb51614aaf02ec7169ee08716936afd54 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Mon, 2 Nov 2020 14:26:18 -0300 Subject: [PATCH 18/18] Refactor code (#39) * Check searchFactsViewModel to bind SuggestionsCell * Improve Category View * Use NSLayoutConstraint * Make classes final * Refactor APIError * FactsListError ViewModel * Fix unit tests * ErrorView -> ErrorAlert * Remove reference to errorView --- Chuck Norris Facts.xcodeproj/project.pbxproj | 22 ++-- Chuck Norris Facts/App/AppDelegate.swift | 4 +- .../{ => Error}/FactsListError.swift | 22 +++- .../Error/FactsListErrorViewModel.swift | 31 ++++++ .../Facts/FactsList/Fact/FactCell.swift | 50 +++++---- .../FactsList/FactsListCoordinator.swift | 31 ++++++ .../FactsList/FactsListViewController.swift | 64 ++++------- .../Facts/FactsList/FactsListViewModel.swift | 15 ++- .../Facts/FactsList/Views/EmptyListView.swift | 32 +++--- .../Facts/FactsList/Views/ErrorView.swift | 84 -------------- .../Facts/FactsList/Views/LoadingView.swift | 12 +- .../PastSearch/PastSearchCell.swift | 2 +- .../SearchFacts/SearchFactsCoordinator.swift | 2 +- .../SearchFactsViewController.swift | 16 ++- .../FactCategory/FactCategoryCell.swift | 15 ++- .../FactCategory/FactCategoryViewModel.swift | 2 +- .../Suggestions/SuggestionsCell.swift | 12 +- .../App/Views/CategoryView.swift | 4 +- Chuck Norris Facts/Core/API/APIError.swift | 104 ++++++------------ Chuck Norris Facts/Core/API/APIProvider.swift | 54 ++++----- Chuck Norris Facts/Core/API/APIResponse.swift | 13 +++ .../Core/Data/Services/FactsService.swift | 2 + .../Core/Extensions/API+Rx.swift | 10 +- .../Core/Library/ActivityIndicator.swift | 12 +- .../Library/DynamicHeightCollectionView.swift | 2 +- Chuck Norris Facts/Core/Library/JSON.swift | 6 +- .../Resources/Animations/error.json | 1 - .../Resources/Generated/Strings.swift | 23 ++-- .../Resources/Localizable.strings | 10 +- .../FactsListViewControllerTests.swift | 10 -- .../FactsList/FactsListViewModelTests.swift | 30 +++-- .../Scenes/FactsListScene.swift | 2 - .../Tests/FactsListUITests.swift | 14 ++- 33 files changed, 341 insertions(+), 372 deletions(-) rename Chuck Norris Facts/App/Scenes/Facts/FactsList/{ => Error}/FactsListError.swift (62%) create mode 100644 Chuck Norris Facts/App/Scenes/Facts/FactsList/Error/FactsListErrorViewModel.swift delete mode 100644 Chuck Norris Facts/App/Scenes/Facts/FactsList/Views/ErrorView.swift delete mode 100644 Chuck Norris Facts/Resources/Animations/error.json diff --git a/Chuck Norris Facts.xcodeproj/project.pbxproj b/Chuck Norris Facts.xcodeproj/project.pbxproj index 4428b46..e64b70e 100644 --- a/Chuck Norris Facts.xcodeproj/project.pbxproj +++ b/Chuck Norris Facts.xcodeproj/project.pbxproj @@ -8,11 +8,9 @@ /* Begin PBXBuildFile section */ 1E049E13254F5A5300226E0B /* RxSwift+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E049E12254F5A5300226E0B /* RxSwift+Extensions.swift */; }; - 1E0E4BA22549F7E30030BC49 /* error.json in Resources */ = {isa = PBXBuildFile; fileRef = 1E0E4BA12549F7E30030BC49 /* error.json */; }; 1E135FAA254B4E66009D18AF /* LaunchArgument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E135FA9254B4E66009D18AF /* LaunchArgument.swift */; }; 1E135FAD254B4E9F009D18AF /* XCUIApplication+LaunchArgument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E135FAC254B4E9F009D18AF /* XCUIApplication+LaunchArgument.swift */; }; 1E135FAF254B52E0009D18AF /* facts.json in Resources */ = {isa = PBXBuildFile; fileRef = 1E135FAE254B52E0009D18AF /* facts.json */; }; - 1E15408F2549FA6200675DC4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E15408E2549FA6200675DC4 /* ErrorView.swift */; }; 1E23683E253FB07200BE17F3 /* FactCategoryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E23683D253FB07200BE17F3 /* FactCategoryCell.swift */; }; 1E236840253FB13A00BE17F3 /* FactCategoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E23683F253FB13A00BE17F3 /* FactCategoryViewModel.swift */; }; 1E3075C2254C9D0B0082A194 /* APITarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E3075C1254C9D0B0082A194 /* APITarget.swift */; }; @@ -33,6 +31,7 @@ 1E655788254CB13B00950706 /* APIResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E655787254CB13B00950706 /* APIResponse.swift */; }; 1E65578C254CB20D00950706 /* API+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E65578B254CB20D00950706 /* API+Rx.swift */; }; 1E65578E254CB22800950706 /* URLRequest+Encoded.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E65578D254CB22800950706 /* URLRequest+Encoded.swift */; }; + 1E6D568F25505D5700D27284 /* FactsListErrorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E6D568E25505D5700D27284 /* FactsListErrorViewModel.swift */; }; 1E7A6528254DA2B1006E493B /* HTTPTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7A6527254DA2B1006E493B /* HTTPTask.swift */; }; 1E7F15BE253324CF0006887B /* FactsListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7F15BD253324CF0006887B /* FactsListViewModelTests.swift */; }; 1E7F15C0253324FD0006887B /* FactsListViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7F15BF253324FD0006887B /* FactsListViewControllerTests.swift */; }; @@ -113,11 +112,9 @@ /* Begin PBXFileReference section */ 02186229E02F0A408D2EE0C2 /* Pods_Chuck_Norris_Facts.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Chuck_Norris_Facts.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1E049E12254F5A5300226E0B /* RxSwift+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RxSwift+Extensions.swift"; sourceTree = ""; }; - 1E0E4BA12549F7E30030BC49 /* error.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = error.json; sourceTree = ""; }; 1E135FA9254B4E66009D18AF /* LaunchArgument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchArgument.swift; sourceTree = ""; }; 1E135FAC254B4E9F009D18AF /* XCUIApplication+LaunchArgument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCUIApplication+LaunchArgument.swift"; sourceTree = ""; }; 1E135FAE254B52E0009D18AF /* facts.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = facts.json; sourceTree = ""; }; - 1E15408E2549FA6200675DC4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; 1E23683D253FB07200BE17F3 /* FactCategoryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactCategoryCell.swift; sourceTree = ""; }; 1E23683F253FB13A00BE17F3 /* FactCategoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactCategoryViewModel.swift; sourceTree = ""; }; 1E3075C1254C9D0B0082A194 /* APITarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APITarget.swift; sourceTree = ""; }; @@ -138,6 +135,7 @@ 1E655787254CB13B00950706 /* APIResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIResponse.swift; sourceTree = ""; }; 1E65578B254CB20D00950706 /* API+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "API+Rx.swift"; sourceTree = ""; }; 1E65578D254CB22800950706 /* URLRequest+Encoded.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+Encoded.swift"; sourceTree = ""; }; + 1E6D568E25505D5700D27284 /* FactsListErrorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListErrorViewModel.swift; sourceTree = ""; }; 1E7A6527254DA2B1006E493B /* HTTPTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPTask.swift; sourceTree = ""; }; 1E7F15BD253324CF0006887B /* FactsListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListViewModelTests.swift; sourceTree = ""; }; 1E7F15BF253324FD0006887B /* FactsListViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListViewControllerTests.swift; sourceTree = ""; }; @@ -280,7 +278,6 @@ children = ( 1E32758E2532A2C0007E838A /* EmptyListView.swift */, 1ED06C942548AAD300139151 /* LoadingView.swift */, - 1E15408E2549FA6200675DC4 /* ErrorView.swift */, ); path = Views; sourceTree = ""; @@ -288,7 +285,6 @@ 1E3275902532A2C4007E838A /* Animations */ = { isa = PBXGroup; children = ( - 1E0E4BA12549F7E30030BC49 /* error.json */, 1EACEC98253649BD0006B36D /* loading.json */, 1E3275912532A2CD007E838A /* empty-box.json */, ); @@ -343,6 +339,15 @@ path = Services; sourceTree = ""; }; + 1E6D568D25505D3F00D27284 /* Error */ = { + isa = PBXGroup; + children = ( + 1EEDC6A4254A408B00D75F3E /* FactsListError.swift */, + 1E6D568E25505D5700D27284 /* FactsListErrorViewModel.swift */, + ); + path = Error; + sourceTree = ""; + }; 1E7F15BA253324760006887B /* Scenes */ = { isa = PBXGroup; children = ( @@ -681,12 +686,12 @@ 1EFE288C25321337008806B9 /* FactsList */ = { isa = PBXGroup; children = ( + 1E6D568D25505D3F00D27284 /* Error */, 1E32758D2532A2A3007E838A /* Views */, 1EFE289325321CB4008806B9 /* Fact */, 1EFE288D2532135B008806B9 /* FactsListViewController.swift */, 1EFE288F2532137C008806B9 /* FactsListCoordinator.swift */, 1EFE2891253214DB008806B9 /* FactsListViewModel.swift */, - 1EEDC6A4254A408B00D75F3E /* FactsListError.swift */, ); path = FactsList; sourceTree = ""; @@ -837,7 +842,6 @@ files = ( 1E135FAF254B52E0009D18AF /* facts.json in Resources */, 1EFE287E25321071008806B9 /* search-facts.json in Resources */, - 1E0E4BA22549F7E30030BC49 /* error.json in Resources */, 1EE0714925314AF600F6BF6D /* LaunchScreen.storyboard in Resources */, 1E5617282540FAF200BF26A0 /* get-categories.json in Resources */, 1EE0714625314AF600F6BF6D /* Assets.xcassets in Resources */, @@ -1059,7 +1063,6 @@ 1EE0713F25314AF500F6BF6D /* SceneDelegate.swift in Sources */, 1E8A0FF42547768500565A86 /* DynamicHeightCollectionView.swift in Sources */, 1EFE288E2532135B008806B9 /* FactsListViewController.swift in Sources */, - 1E15408F2549FA6200675DC4 /* ErrorView.swift in Sources */, 1EEDC6A1254A331500D75F3E /* UICollectionView+Extensions.swift in Sources */, 1E655788254CB13B00950706 /* APIResponse.swift in Sources */, 1EF066E32545CE1D00ECF611 /* PastSearchViewModel.swift in Sources */, @@ -1081,6 +1084,7 @@ 1E135FAA254B4E66009D18AF /* LaunchArgument.swift in Sources */, 1EFE2867253206D3008806B9 /* BaseCoordinator.swift in Sources */, 1E92113B253F90BF00DB340B /* FactCategory.swift in Sources */, + 1E6D568F25505D5700D27284 /* FactsListErrorViewModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Chuck Norris Facts/App/AppDelegate.swift b/Chuck Norris Facts/App/AppDelegate.swift index 62f35c0..293b0b3 100644 --- a/Chuck Norris Facts/App/AppDelegate.swift +++ b/Chuck Norris Facts/App/AppDelegate.swift @@ -37,7 +37,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { if LaunchArgument.check(.mockStorage) { let entities = [ SearchEntity(searchTerm: "games"), - SearchEntity(searchTerm: "fashion") + SearchEntity(searchTerm: "fashion"), + FactCategoryEntity(category: FactCategory(text: "games")), + FactCategoryEntity(category: FactCategory(text: "fashion")) ] let realm = try? Realm() diff --git a/Chuck Norris Facts/App/Scenes/Facts/FactsList/FactsListError.swift b/Chuck Norris Facts/App/Scenes/Facts/FactsList/Error/FactsListError.swift similarity index 62% rename from Chuck Norris Facts/App/Scenes/Facts/FactsList/FactsListError.swift rename to Chuck Norris Facts/App/Scenes/Facts/FactsList/Error/FactsListError.swift index 6e326e1..5fff962 100644 --- a/Chuck Norris Facts/App/Scenes/Facts/FactsList/FactsListError.swift +++ b/Chuck Norris Facts/App/Scenes/Facts/FactsList/Error/FactsListError.swift @@ -9,10 +9,15 @@ import Foundation enum FactsListError { - + // Error related to syncCategories request case syncCategories(Error) + // Error related to searchFacts request case searchFacts(Error) +} + +extension FactsListError { + // APIError related to the error. var error: APIError { switch self { case .syncCategories(let error): @@ -22,12 +27,23 @@ enum FactsListError { } } + // A code to check where the error come. var code: Int { switch self { case .syncCategories: - return -100 + return 0 + case .searchFacts: + return 1 + } + } + + // A message that will be shown to user. + var message: String { + switch self { + case .syncCategories: + return L10n.Errors.cantSyncCategories case .searchFacts: - return -101 + return L10n.Errors.cantSearchFacts } } } diff --git a/Chuck Norris Facts/App/Scenes/Facts/FactsList/Error/FactsListErrorViewModel.swift b/Chuck Norris Facts/App/Scenes/Facts/FactsList/Error/FactsListErrorViewModel.swift new file mode 100644 index 0000000..92f943a --- /dev/null +++ b/Chuck Norris Facts/App/Scenes/Facts/FactsList/Error/FactsListErrorViewModel.swift @@ -0,0 +1,31 @@ +// +// FactsListErrorViewModel.swift +// Chuck Norris Facts +// +// Created by Djorkaeff Alexandre Vilela Pereira on 11/2/20. +// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. +// + +import Foundation + +struct FactsListErrorViewModel { + + let error: APIError + let title: String + let message: String + var shouldRetry: Bool = false + + init(factsListError: FactsListError) { + self.error = factsListError.error + + self.title = factsListError.message + self.message = error.message + + switch factsListError { + case .syncCategories: + self.shouldRetry = error.code != APIError.noConnection.code + default: + break + } + } +} diff --git a/Chuck Norris Facts/App/Scenes/Facts/FactsList/Fact/FactCell.swift b/Chuck Norris Facts/App/Scenes/Facts/FactsList/Fact/FactCell.swift index af645fb..9e4fe3f 100644 --- a/Chuck Norris Facts/App/Scenes/Facts/FactsList/Fact/FactCell.swift +++ b/Chuck Norris Facts/App/Scenes/Facts/FactsList/Fact/FactCell.swift @@ -9,12 +9,16 @@ import UIKit import RxSwift -class FactCell: UITableViewCell { - - private let categoryView = CategoryView() +final class FactCell: UITableViewCell { var disposeBag = DisposeBag() + private lazy var categoryView: CategoryView = { + let categoryView = CategoryView() + categoryView.translatesAutoresizingMaskIntoConstraints = false + return categoryView + }() + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupView() @@ -73,26 +77,32 @@ class FactCell: UITableViewCell { contentView.clipsToBounds = false contentView.addSubview(shadowView) - shadowView.addSubview(bodyLabel) - shadowView.addSubview(shareButton) - shadowView.addSubview(categoryView) - - shadowView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: padding).isActive = true - shadowView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -padding).isActive = true - shadowView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding / 2).isActive = true - shadowView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -padding / 2).isActive = true + NSLayoutConstraint.activate([ + shadowView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: padding), + shadowView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -padding), + shadowView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding / 2), + shadowView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -padding / 2) + ]) - bodyLabel.topAnchor.constraint(equalTo: shadowView.topAnchor, constant: padding).isActive = true - bodyLabel.leadingAnchor.constraint(equalTo: shadowView.leadingAnchor, constant: padding).isActive = true - bodyLabel.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor, constant: -padding).isActive = true + shadowView.addSubview(bodyLabel) + NSLayoutConstraint.activate([ + bodyLabel.topAnchor.constraint(equalTo: shadowView.topAnchor, constant: padding), + bodyLabel.leadingAnchor.constraint(equalTo: shadowView.leadingAnchor, constant: padding), + bodyLabel.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor, constant: -padding) + ]) - shareButton.topAnchor.constraint(equalTo: bodyLabel.bottomAnchor, constant: padding).isActive = true - shareButton.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor, constant: -padding).isActive = true - shareButton.bottomAnchor.constraint(equalTo: shadowView.bottomAnchor, constant: -padding).isActive = true + shadowView.addSubview(shareButton) + NSLayoutConstraint.activate([ + shareButton.topAnchor.constraint(equalTo: bodyLabel.bottomAnchor, constant: padding), + shareButton.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor, constant: -padding), + shareButton.bottomAnchor.constraint(equalTo: shadowView.bottomAnchor, constant: -padding) + ]) - categoryView.translatesAutoresizingMaskIntoConstraints = false - categoryView.centerYAnchor.constraint(equalTo: shareButton.centerYAnchor).isActive = true - categoryView.leftAnchor.constraint(equalTo: shadowView.leftAnchor, constant: padding).isActive = true + shadowView.addSubview(categoryView) + NSLayoutConstraint.activate([ + categoryView.centerYAnchor.constraint(equalTo: shareButton.centerYAnchor), + categoryView.leftAnchor.constraint(equalTo: shadowView.leftAnchor, constant: padding) + ]) } func setup(_ fact: FactViewModel) { diff --git a/Chuck Norris Facts/App/Scenes/Facts/FactsList/FactsListCoordinator.swift b/Chuck Norris Facts/App/Scenes/Facts/FactsList/FactsListCoordinator.swift index 2a7b855..1249619 100644 --- a/Chuck Norris Facts/App/Scenes/Facts/FactsList/FactsListCoordinator.swift +++ b/Chuck Norris Facts/App/Scenes/Facts/FactsList/FactsListCoordinator.swift @@ -38,6 +38,15 @@ final class FactsListCoordinator: BaseCoordinator { .bind(to: factsListViewModel.inputs.setSearchTerm) .disposed(by: disposeBag) + factsListViewModel.outputs.factsListError + .flatMap { [weak self] error in + self?.showFactsListError(error: error, in: navigationController) ?? .empty() + } + .filter { $0.shouldRetry } + .mapToVoid() + .bind(to: factsListViewModel.inputs.retryAction) + .disposed(by: disposeBag) + window.rootViewController = navigationController window.makeKeyAndVisible() @@ -65,4 +74,26 @@ final class FactsListCoordinator: BaseCoordinator { } } } + + private func showFactsListError( + error: FactsListErrorViewModel, + in navigationController: UINavigationController + ) -> Observable { + Observable.create { observer in + let alert = UIAlertController(title: error.title, message: error.message, preferredStyle: .alert) + + let action = UIAlertAction(title: L10n.Common.ok, style: .default) { _ in + observer.onNext(error) + observer.onCompleted() + } + + alert.addAction(action) + + navigationController.present(alert, animated: true, completion: nil) + + return Disposables.create { + alert.dismiss(animated: true, completion: nil) + } + } + } } diff --git a/Chuck Norris Facts/App/Scenes/Facts/FactsList/FactsListViewController.swift b/Chuck Norris Facts/App/Scenes/Facts/FactsList/FactsListViewController.swift index 965942a..c71ff14 100644 --- a/Chuck Norris Facts/App/Scenes/Facts/FactsList/FactsListViewController.swift +++ b/Chuck Norris Facts/App/Scenes/Facts/FactsList/FactsListViewController.swift @@ -12,14 +12,13 @@ import RxCocoa import RxDataSources import Lottie -class FactsListViewController: UIViewController { +final class FactsListViewController: UIViewController { var viewModel: FactsListViewModel! private let disposeBag = DisposeBag() let tableView = UITableView() - let errorView = ErrorView() let loadingView = LoadingView() let emptyListView = EmptyListView() let searchButton = UIBarButtonItem(barButtonSystemItem: .search, target: nil, action: nil) @@ -46,7 +45,6 @@ class FactsListViewController: UIViewController { setupView() setupBindings() setupTableView() - setupErrorView() setupEmptyListView() setupLoadingView() setupNavigationBar() @@ -63,10 +61,12 @@ class FactsListViewController: UIViewController { tableView.register(FactCell.self) tableView.translatesAutoresizingMaskIntoConstraints = false - tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true - tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true - tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true - tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor) + ]) tableView.accessibilityIdentifier = "factsTableView" } @@ -75,31 +75,24 @@ class FactsListViewController: UIViewController { view.addSubview(loadingView) loadingView.translatesAutoresizingMaskIntoConstraints = false - loadingView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true - loadingView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true - loadingView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true - loadingView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true - } - - private func setupErrorView() { - view.addSubview(errorView) - - errorView.isHidden = true - errorView.translatesAutoresizingMaskIntoConstraints = false - errorView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true - errorView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true - errorView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true - errorView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + NSLayoutConstraint.activate([ + loadingView.topAnchor.constraint(equalTo: view.topAnchor), + loadingView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + loadingView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + loadingView.trailingAnchor.constraint(equalTo: view.trailingAnchor) + ]) } private func setupEmptyListView() { view.addSubview(emptyListView) emptyListView.translatesAutoresizingMaskIntoConstraints = false - emptyListView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true - emptyListView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true - emptyListView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true - emptyListView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true + NSLayoutConstraint.activate([ + emptyListView.topAnchor.constraint(equalTo: view.topAnchor), + emptyListView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + emptyListView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + emptyListView.trailingAnchor.constraint(equalTo: view.trailingAnchor) + ]) } private func setupNavigationBar() { @@ -149,16 +142,6 @@ class FactsListViewController: UIViewController { emptyListView.searchButton.rx.tap .bind(to: viewModel.inputs.startSearchFacts) .disposed(by: disposeBag) - - errorView.retryButton.rx.tap - .bind(to: viewModel.inputs.retryAction) - .disposed(by: disposeBag) - - viewModel.outputs.errors - .bind(onNext: { [weak self] error in - self?.showErrorView(error) - }) - .disposed(by: disposeBag) } private func showEmptyView(_ listEmpty: Bool, _ searchEmpty: Bool) { @@ -188,13 +171,4 @@ class FactsListViewController: UIViewController { loadingView.stop() } } - - private func showErrorView(_ factsListError: FactsListError) { - emptyListView.isHidden = true - - let localizedErrorDescription = factsListError.error.underlyingError?.localizedDescription - errorView.label.text = localizedErrorDescription ?? L10n.FactListError.serviceUnavailable - errorView.isHidden = false - errorView.play() - } } diff --git a/Chuck Norris Facts/App/Scenes/Facts/FactsList/FactsListViewModel.swift b/Chuck Norris Facts/App/Scenes/Facts/FactsList/FactsListViewModel.swift index 0fa2478..24402cd 100644 --- a/Chuck Norris Facts/App/Scenes/Facts/FactsList/FactsListViewModel.swift +++ b/Chuck Norris Facts/App/Scenes/Facts/FactsList/FactsListViewModel.swift @@ -45,8 +45,8 @@ protocol FactsListViewModelOutputs { // Emmits an ActivityIndicator to check if there is a facts search happening var isLoading: ActivityIndicator { get } - // Emmits an FactsListError to be shown - var errors: Observable { get } + // Emmits an FactsListErrorViewModel to be shown + var factsListError: Observable { get } } final class FactsListViewModel: FactsListViewModelInputs, FactsListViewModelOutputs { @@ -79,7 +79,7 @@ final class FactsListViewModel: FactsListViewModelInputs, FactsListViewModelOutp var isLoading: ActivityIndicator - var errors: Observable + var factsListError: Observable init(factsService: FactsServiceType = FactsService()) { let loadingIndicator = ActivityIndicator() @@ -125,16 +125,15 @@ final class FactsListViewModel: FactsListViewModelInputs, FactsListViewModelOutp .materialize() } - let searchFactsError = searchFacts - .errors() + let searchFactsError = searchFacts.errors() .map { FactsListError.searchFacts($0) } - self.facts = searchFacts - .elements() + self.facts = searchFacts.elements() .map { $0.map { FactViewModel(fact: $0) } } .map { [FactsSectionModel(model: "", items: $0)] } - self.errors = Observable.merge(syncCategoriesError, searchFactsError) + self.factsListError = Observable.merge(syncCategoriesError, searchFactsError) .do(onNext: currentErrorSubject.onNext) + .map { FactsListErrorViewModel(factsListError: $0) } } } diff --git a/Chuck Norris Facts/App/Scenes/Facts/FactsList/Views/EmptyListView.swift b/Chuck Norris Facts/App/Scenes/Facts/FactsList/Views/EmptyListView.swift index bd72f77..0ea9a71 100644 --- a/Chuck Norris Facts/App/Scenes/Facts/FactsList/Views/EmptyListView.swift +++ b/Chuck Norris Facts/App/Scenes/Facts/FactsList/Views/EmptyListView.swift @@ -14,6 +14,7 @@ final class EmptyListView: UIView { private lazy var animation: AnimationView = { let animation = AnimationView() + animation.translatesAutoresizingMaskIntoConstraints = false animation.animation = Animation.named("empty-box") animation.loopMode = .loop @@ -23,6 +24,7 @@ final class EmptyListView: UIView { lazy var label: UILabel = { let label = UILabel() + label.accessibilityIdentifier = "emptyListLabelView" label.translatesAutoresizingMaskIntoConstraints = false label.font = .preferredFont(forTextStyle: .headline) label.lineBreakMode = .byWordWrapping @@ -36,7 +38,7 @@ final class EmptyListView: UIView { button.translatesAutoresizingMaskIntoConstraints = false button.accessibilityLabel = "Search" - button.setTitle("Search", for: .normal) + button.setTitle(L10n.EmptyView.search, for: .normal) button.titleLabel?.font = .preferredFont(forTextStyle: .body) button.accessibilityIdentifier = "searchButton" @@ -59,26 +61,26 @@ final class EmptyListView: UIView { backgroundColor = .systemBackground addSubview(animation) - - animation.translatesAutoresizingMaskIntoConstraints = false - animation.widthAnchor.constraint(equalToConstant: animationSize).isActive = true - animation.heightAnchor.constraint(equalToConstant: animationSize).isActive = true - animation.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true - animation.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true + NSLayoutConstraint.activate([ + animation.widthAnchor.constraint(equalToConstant: animationSize), + animation.heightAnchor.constraint(equalToConstant: animationSize), + animation.centerXAnchor.constraint(equalTo: centerXAnchor), + animation.centerYAnchor.constraint(equalTo: centerYAnchor) + ]) addSubview(label) - - label.translatesAutoresizingMaskIntoConstraints = false - label.topAnchor.constraint(equalTo: animation.bottomAnchor).isActive = true - label.centerXAnchor.constraint(equalTo: animation.centerXAnchor).isActive = true + NSLayoutConstraint.activate([ + label.topAnchor.constraint(equalTo: animation.bottomAnchor), + label.centerXAnchor.constraint(equalTo: animation.centerXAnchor) + ]) addSubview(searchButton) - searchButton.translatesAutoresizingMaskIntoConstraints = false - searchButton.topAnchor.constraint(equalTo: label.bottomAnchor).isActive = true - searchButton.centerXAnchor.constraint(equalTo: label.centerXAnchor).isActive = true + NSLayoutConstraint.activate([ + searchButton.topAnchor.constraint(equalTo: label.bottomAnchor), + searchButton.centerXAnchor.constraint(equalTo: label.centerXAnchor) + ]) accessibilityIdentifier = "emptyListView" - label.accessibilityIdentifier = "emptyListLabelView" } func play() { diff --git a/Chuck Norris Facts/App/Scenes/Facts/FactsList/Views/ErrorView.swift b/Chuck Norris Facts/App/Scenes/Facts/FactsList/Views/ErrorView.swift deleted file mode 100644 index 6c1592b..0000000 --- a/Chuck Norris Facts/App/Scenes/Facts/FactsList/Views/ErrorView.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// ErrorView.swift -// Chuck Norris Facts -// -// Created by Djorkaeff Alexandre Vilela Pereira on 10/28/20. -// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved. -// - -import UIKit -import Lottie - -final class ErrorView: UIView { - - private lazy var animation: AnimationView = { - let loading = AnimationView() - - loading.translatesAutoresizingMaskIntoConstraints = false - loading.animation = Animation.named("error") - - return loading - }() - - lazy var label: UILabel = { - let label = UILabel() - - label.translatesAutoresizingMaskIntoConstraints = false - label.lineBreakMode = .byWordWrapping - label.numberOfLines = 0 - label.textAlignment = .center - label.font = .preferredFont(forTextStyle: .body) - - return label - }() - - lazy var retryButton: UIButton = { - let button = UIButton(type: .system) - - button.translatesAutoresizingMaskIntoConstraints = false - button.accessibilityLabel = "Retry" - button.setTitle("Retry", for: .normal) - button.titleLabel?.font = .preferredFont(forTextStyle: .headline) - button.accessibilityIdentifier = "retryButton" - - return button - }() - - override init(frame: CGRect) { - super.init(frame: frame) - - setupView() - - accessibilityIdentifier = "errorView" - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupView() { - let animationSize: CGFloat = 200 - let padding: CGFloat = 16 - - backgroundColor = .systemBackground - - addSubview(animation) - animation.widthAnchor.constraint(equalToConstant: animationSize).isActive = true - animation.heightAnchor.constraint(equalToConstant: animationSize).isActive = true - animation.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true - animation.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true - - addSubview(label) - label.topAnchor.constraint(equalTo: animation.bottomAnchor).isActive = true - label.widthAnchor.constraint(equalTo: widthAnchor, constant: -padding).isActive = true - label.centerXAnchor.constraint(equalTo: animation.centerXAnchor).isActive = true - - addSubview(retryButton) - retryButton.topAnchor.constraint(equalTo: label.bottomAnchor).isActive = true - retryButton.centerXAnchor.constraint(equalTo: label.centerXAnchor).isActive = true - } - - func play() { - animation.play() - } -} diff --git a/Chuck Norris Facts/App/Scenes/Facts/FactsList/Views/LoadingView.swift b/Chuck Norris Facts/App/Scenes/Facts/FactsList/Views/LoadingView.swift index 78afce2..0dab464 100644 --- a/Chuck Norris Facts/App/Scenes/Facts/FactsList/Views/LoadingView.swift +++ b/Chuck Norris Facts/App/Scenes/Facts/FactsList/Views/LoadingView.swift @@ -14,6 +14,7 @@ final class LoadingView: UIView { private lazy var animation: AnimationView = { let loading = AnimationView() + loading.translatesAutoresizingMaskIntoConstraints = false loading.animation = Animation.named("loading") loading.loopMode = .loop @@ -36,11 +37,12 @@ final class LoadingView: UIView { backgroundColor = .systemBackground addSubview(animation) - animation.translatesAutoresizingMaskIntoConstraints = false - animation.widthAnchor.constraint(equalToConstant: animationSize).isActive = true - animation.heightAnchor.constraint(equalToConstant: animationSize).isActive = true - animation.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true - animation.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true + NSLayoutConstraint.activate([ + animation.widthAnchor.constraint(equalToConstant: animationSize), + animation.heightAnchor.constraint(equalToConstant: animationSize), + animation.centerXAnchor.constraint(equalTo: centerXAnchor), + animation.centerYAnchor.constraint(equalTo: centerYAnchor) + ]) } func play() { diff --git a/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/PastSearch/PastSearchCell.swift b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/PastSearch/PastSearchCell.swift index 636b8db..8abb4ba 100644 --- a/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/PastSearch/PastSearchCell.swift +++ b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/PastSearch/PastSearchCell.swift @@ -8,7 +8,7 @@ import UIKit -class PastSearchCell: UITableViewCell { +final class PastSearchCell: UITableViewCell { static let identifier = "PastSearchCell" diff --git a/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/SearchFactsCoordinator.swift b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/SearchFactsCoordinator.swift index 7decb8e..8008375 100644 --- a/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/SearchFactsCoordinator.swift +++ b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/SearchFactsCoordinator.swift @@ -14,7 +14,7 @@ enum SearchFactsCoordinationResult { case search(String) } -class SearchFactsCoordinator: BaseCoordinator { +final class SearchFactsCoordinator: BaseCoordinator { private let rootViewController: UIViewController diff --git a/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/SearchFactsViewController.swift b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/SearchFactsViewController.swift index 57dd977..ed4c827 100644 --- a/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/SearchFactsViewController.swift +++ b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/SearchFactsViewController.swift @@ -17,17 +17,19 @@ final class SearchFactsViewController: UIViewController { let disposeBag = DisposeBag() private lazy var itemsDataSource = RxTableViewSectionedReloadDataSource( - configureCell: { dataSource, tableView, indexPath, _ -> UITableViewCell in + configureCell: { [weak self] dataSource, tableView, indexPath, _ -> UITableViewCell in switch dataSource[indexPath] { case .SuggestionsTableViewItem(let suggestions): + guard let searchFactsViewModel = self?.viewModel else { return UITableViewCell() } + let cell = tableView.dequeueReusableCell(cell: SuggestionsCell.self, indexPath: indexPath) let viewModel = SuggestionsViewModel(suggestions: suggestions) cell.viewModel = viewModel viewModel.outputs.didSelectSuggestion - .bind(to: self.viewModel.inputs.selectItem) + .bind(to: searchFactsViewModel.inputs.selectItem) .disposed(by: cell.disposeBag) return cell @@ -88,10 +90,12 @@ final class SearchFactsViewController: UIViewController { tableView.backgroundColor = .systemBackground tableView.rowHeight = UITableView.automaticDimension - tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true - tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true - tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true - tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) tableView.register(SuggestionsCell.self) tableView.register(PastSearchCell.self) diff --git a/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryCell.swift b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryCell.swift index 517830c..eec5857 100644 --- a/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryCell.swift +++ b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryCell.swift @@ -8,9 +8,13 @@ import UIKit -class FactCategoryCell: UICollectionViewCell { +final class FactCategoryCell: UICollectionViewCell { - private let categoryView: CategoryView = CategoryView() + private lazy var categoryView: CategoryView = { + let categoryView = CategoryView() + categoryView.translatesAutoresizingMaskIntoConstraints = false + return categoryView + }() override init(frame: CGRect) { super.init(frame: frame) @@ -32,8 +36,9 @@ class FactCategoryCell: UICollectionViewCell { func setupView() { contentView.addSubview(categoryView) - categoryView.translatesAutoresizingMaskIntoConstraints = false - categoryView.widthAnchor.constraint(equalTo: contentView.widthAnchor).isActive = true - categoryView.heightAnchor.constraint(equalTo: contentView.heightAnchor).isActive = true + NSLayoutConstraint.activate([ + categoryView.widthAnchor.constraint(equalTo: contentView.widthAnchor), + categoryView.heightAnchor.constraint(equalTo: contentView.heightAnchor) + ]) } } diff --git a/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryViewModel.swift b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryViewModel.swift index c4a6de3..3c15e44 100644 --- a/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryViewModel.swift +++ b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/FactCategory/FactCategoryViewModel.swift @@ -9,7 +9,7 @@ import Foundation import RxDataSources -class FactCategoryViewModel { +final class FactCategoryViewModel { let category: FactCategory let text: String diff --git a/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/SuggestionsCell.swift b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/SuggestionsCell.swift index 429f009..4267534 100644 --- a/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/SuggestionsCell.swift +++ b/Chuck Norris Facts/App/Scenes/Facts/SearchFacts/Suggestions/SuggestionsCell.swift @@ -11,7 +11,7 @@ import RxSwift import RxCocoa import RxDataSources -class SuggestionsCell: UITableViewCell { +final class SuggestionsCell: UITableViewCell { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) @@ -61,11 +61,13 @@ class SuggestionsCell: UITableViewCell { }() private func setupView() { - contentView.addSubview(collectionView) - collectionView.backgroundColor = .systemBackground - collectionView.widthAnchor.constraint(equalTo: contentView.widthAnchor).isActive = true - collectionView.heightAnchor.constraint(equalTo: contentView.heightAnchor).isActive = true + + contentView.addSubview(collectionView) + NSLayoutConstraint.activate([ + collectionView.widthAnchor.constraint(equalTo: contentView.widthAnchor), + collectionView.heightAnchor.constraint(equalTo: contentView.heightAnchor) + ]) } private func setupBindings() { diff --git a/Chuck Norris Facts/App/Views/CategoryView.swift b/Chuck Norris Facts/App/Views/CategoryView.swift index 1f631b8..cb35179 100644 --- a/Chuck Norris Facts/App/Views/CategoryView.swift +++ b/Chuck Norris Facts/App/Views/CategoryView.swift @@ -8,7 +8,7 @@ import UIKit -class CategoryView: UIView { +final class CategoryView: UIView { lazy var label: UILabel = { let label = UILabel() @@ -40,8 +40,6 @@ class CategoryView: UIView { backgroundColor = .systemBlue addSubview(label) - - label.translatesAutoresizingMaskIntoConstraints = false label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: padding).isActive = true label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -padding).isActive = true label.topAnchor.constraint(equalTo: topAnchor, constant: padding).isActive = true diff --git a/Chuck Norris Facts/Core/API/APIError.swift b/Chuck Norris Facts/Core/API/APIError.swift index 4224a6e..8d9758e 100644 --- a/Chuck Norris Facts/Core/API/APIError.swift +++ b/Chuck Norris Facts/Core/API/APIError.swift @@ -8,97 +8,61 @@ import Foundation -/// A type representing possible errors API can throw. -enum APIError: Swift.Error { - - // Indicates a response failed to map to a Decodable object. - case objectMapping(Swift.Error, APIResponse) +protocol APIErrorType: LocalizedError { + var code: Int { get } + var message: String { get } +} - // Indicated data was not received. - case dataMapping(Swift.Error?) +// A type representing possible errors API can throw. +enum APIError: Swift.Error { - // Indicates a response failed with an invalid HTTP status code. - case statusCode(APIResponse) + // Indicates that an Unknown error happened. + case unknown(Swift.Error?) - // Indicates a response failed due to an underlying `Error`. - case underlying(Swift.Error, APIResponse?) + // Indicates data was not received. + case mapping(Swift.Error?) - // Indicates that an `Endpoint` failed to map to a `URLRequest`. - case requestMapping(String) + // Indicates that user doesn't have a network connection. + case noConnection - // Indicates that an `Endpoint` failed to encode the parameters for the `URLRequest`. - case parameterEncoding(Swift.Error) + // Indicates a response failed with an invalid HTTP status code. + case statusCode(Int) - // Indicates that an Unknown error happened - case unknown(Error?) + // Indicates that the network response was not convertible to HTTPURLResponse. + case connectionError } -extension APIError { +extension APIError: APIErrorType { // Code for each error type. var code: Int { switch self { - case .objectMapping: + case .unknown: + return 0 + case .mapping: return 1 - case .dataMapping: + case .noConnection: return 2 case .statusCode: return 3 - case .underlying: + case .connectionError: return 4 - case .requestMapping: - return 5 - case .parameterEncoding: - return 6 - case .unknown: - return 7 } } - // Depending on error type, returns a `Response` object. - var response: APIResponse? { + // A description about the error. + var message: String { switch self { - case .objectMapping: return nil - case .requestMapping: return nil - case .parameterEncoding: return nil - case .statusCode: return nil - case .underlying: return nil - case .dataMapping: return nil - case .unknown: return nil - } - } - - // Depending on error type, returns an underlying `Error`. - var underlyingError: Swift.Error? { - switch self { - case .objectMapping(let error, _): return error - case .statusCode: return nil - case .underlying(let error, _): return error - case .requestMapping: return nil - case .parameterEncoding(let error): return error - case .dataMapping: return nil - case .unknown: return nil - } - } -} - -extension APIError: LocalizedError { - public var errorDescription: String? { - switch self { - case .dataMapping: - return "Failed to read data from request." - case .objectMapping: - return "Failed to map data to a Decodable object." - case .statusCode: - return "Status code didn't fall within the given range." - case .underlying(let error, _): - return error.localizedDescription - case .requestMapping: - return "Failed to map Endpoint to a URLRequest." - case .parameterEncoding(let error): - return "Failed to encode parameters for URLRequest. \(error.localizedDescription)" - case .unknown: - return "Something unexpected happened." + case .unknown(let error): + return error?.localizedDescription ?? "Something unexpected happened." + case .mapping(let error): + return error?.localizedDescription ?? "Error while trying to map response." + case .noConnection: + return "Internet Connection appears to be offline." + case .statusCode(let code): + return "Chuck Norris API returned \(code) statusCode." + case .connectionError: + return "Something unexpected happened with your connection." } } } diff --git a/Chuck Norris Facts/Core/API/APIProvider.swift b/Chuck Norris Facts/Core/API/APIProvider.swift index 9d61380..2cb477c 100644 --- a/Chuck Norris Facts/Core/API/APIProvider.swift +++ b/Chuck Norris Facts/Core/API/APIProvider.swift @@ -44,53 +44,37 @@ class APIProvider: APIProviderType { let request = requestClosure(target) + // Check if request has some sampleData if let sampleData = target.sampleData { completion(.success(APIResponse(statusCode: 200, data: sampleData))) return nil } let task = urlSession.dataTask(with: request) { (data, response, error) in - let response = response as? HTTPURLResponse + // Check if error is not connected to internet + if let error = error as NSError?, error.code == NSURLErrorNotConnectedToInternet { + completion(.failure(.noConnection)) + return + } + + // Check if there is an error + if let error = error { + completion(.failure(.unknown(error))) + return + } - let result = self.convertResponseToResult( - response, - request: request, - data: data, - error: error - ) + // Check if response is a HTTPURLResponse + guard let response = response as? HTTPURLResponse else { + completion(.failure(.connectionError)) + return + } - completion(result) + // Complete with an APIResponse + completion(.success(APIResponse(statusCode: response.statusCode, data: data))) } task.resume() return task } - - // A function responsible for converting the result of a `URLRequest` to a Result. - private func convertResponseToResult( - _ response: HTTPURLResponse?, - request: URLRequest?, - data: Data?, - error: Swift.Error? - ) -> Result { - switch (response, data, error) { - case let (.some(response), data, .none): - let response = APIResponse(statusCode: response.statusCode, data: data ?? Data()) - return .success(response) - case let (.some(response), _, .some(error)): - let response = APIResponse(statusCode: response.statusCode, data: data ?? Data()) - let error = APIError.underlying(error, response) - return .failure(error) - case let (_, _, .some(error)): - let error = APIError.underlying(error, nil) - return .failure(error) - default: - let error = APIError.underlying( - NSError(domain: NSURLErrorDomain, code: NSURLErrorUnknown, userInfo: nil), - nil - ) - return .failure(error) - } - } } diff --git a/Chuck Norris Facts/Core/API/APIResponse.swift b/Chuck Norris Facts/Core/API/APIResponse.swift index 91d4bab..1f5cd8a 100644 --- a/Chuck Norris Facts/Core/API/APIResponse.swift +++ b/Chuck Norris Facts/Core/API/APIResponse.swift @@ -12,3 +12,16 @@ struct APIResponse { let statusCode: Int let data: Data? } + +extension APIResponse { + func filter(statusCodes: R) throws -> APIResponse where R.Bound == Int { + guard statusCodes.contains(statusCode) else { + throw APIError.statusCode(statusCode) + } + return self + } + + func filterSuccessfulStatusCodes() throws -> APIResponse { + return try filter(statusCodes: 200...299) + } +} diff --git a/Chuck Norris Facts/Core/Data/Services/FactsService.swift b/Chuck Norris Facts/Core/Data/Services/FactsService.swift index 8405535..b5e0372 100644 --- a/Chuck Norris Facts/Core/Data/Services/FactsService.swift +++ b/Chuck Norris Facts/Core/Data/Services/FactsService.swift @@ -45,6 +45,7 @@ struct FactsService: FactsServiceType { return provider.rx .request(.searchFacts(searchTerm: searchTerm)) .asObservable() + .filterSuccessfulStatusCodes() .observeOn(self.scheduler ?? MainScheduler.asyncInstance) .map(SearchFactsResponse.self, using: JSON.decoder) .map { $0.facts } @@ -61,6 +62,7 @@ struct FactsService: FactsServiceType { return self.provider.rx .request(.getCategories) .asObservable() + .filterSuccessfulStatusCodes() .observeOn(self.scheduler ?? MainScheduler.asyncInstance) .map([FactCategory].self, using: JSON.decoder) .map { self.storage.storeCategories($0) } diff --git a/Chuck Norris Facts/Core/Extensions/API+Rx.swift b/Chuck Norris Facts/Core/Extensions/API+Rx.swift index 422c251..c58d06d 100644 --- a/Chuck Norris Facts/Core/Extensions/API+Rx.swift +++ b/Chuck Norris Facts/Core/Extensions/API+Rx.swift @@ -32,17 +32,23 @@ extension Reactive where Base: APIProviderType { } extension ObservableType where Element == APIResponse { + // Maps received data into a Decodable object. If the conversion fails, throw an APIError. func map(_ type: D.Type, using decoder: JSONDecoder = JSON.decoder) -> Observable { flatMap { response -> Observable in do { guard let data = response.data else { - throw APIError.dataMapping(nil) + throw APIError.mapping(nil) } return Observable.just(try decoder.decode(D.self, from: data)) } catch let error { - throw APIError.objectMapping(error, response) + throw APIError.mapping(error) } } } + + // Filters out responses where `statusCode` falls within the range 200 - 299. + func filterSuccessfulStatusCodes() -> Observable { + return flatMap { Observable.just(try $0.filterSuccessfulStatusCodes()) } + } } diff --git a/Chuck Norris Facts/Core/Library/ActivityIndicator.swift b/Chuck Norris Facts/Core/Library/ActivityIndicator.swift index 9cc47dc..7bfed22 100644 --- a/Chuck Norris Facts/Core/Library/ActivityIndicator.swift +++ b/Chuck Norris Facts/Core/Library/ActivityIndicator.swift @@ -31,15 +31,15 @@ Enables monitoring of sequence computation. If there is at least one sequence computation in progress, `true` will be sent. When all activities complete `false` will be sent. */ -public class ActivityIndicator: SharedSequenceConvertibleType { - public typealias Element = Bool - public typealias SharingStrategy = DriverSharingStrategy +class ActivityIndicator: SharedSequenceConvertibleType { + typealias Element = Bool + typealias SharingStrategy = DriverSharingStrategy private let _lock = NSRecursiveLock() private let _relay = BehaviorRelay(value: 0) private let _loading: SharedSequence - public init() { + init() { _loading = _relay.asDriver() .map { $0 > 0 } .distinctUntilChanged() @@ -68,13 +68,13 @@ public class ActivityIndicator: SharedSequenceConvertibleType { _lock.unlock() } - public func asSharedSequence() -> SharedSequence { + func asSharedSequence() -> SharedSequence { _loading } } extension ObservableConvertibleType { - public func trackActivity(_ activityIndicator: ActivityIndicator) -> Observable { + func trackActivity(_ activityIndicator: ActivityIndicator) -> Observable { activityIndicator.trackActivityOfObservable(self) } } diff --git a/Chuck Norris Facts/Core/Library/DynamicHeightCollectionView.swift b/Chuck Norris Facts/Core/Library/DynamicHeightCollectionView.swift index 619129c..48a3371 100644 --- a/Chuck Norris Facts/Core/Library/DynamicHeightCollectionView.swift +++ b/Chuck Norris Facts/Core/Library/DynamicHeightCollectionView.swift @@ -8,7 +8,7 @@ import UIKit -class DynamicHeightCollectionView: UICollectionView { +final class DynamicHeightCollectionView: UICollectionView { override func layoutSubviews() { super.layoutSubviews() diff --git a/Chuck Norris Facts/Core/Library/JSON.swift b/Chuck Norris Facts/Core/Library/JSON.swift index f2e390a..0c0783f 100644 --- a/Chuck Norris Facts/Core/Library/JSON.swift +++ b/Chuck Norris Facts/Core/Library/JSON.swift @@ -8,14 +8,14 @@ import Foundation -public struct JSON { - public static var decoder: JSONDecoder { +struct JSON { + static var decoder: JSONDecoder { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase return decoder } - public static var encoder: JSONEncoder { + static var encoder: JSONEncoder { JSONEncoder() } } diff --git a/Chuck Norris Facts/Resources/Animations/error.json b/Chuck Norris Facts/Resources/Animations/error.json deleted file mode 100644 index 83d5793..0000000 --- a/Chuck Norris Facts/Resources/Animations/error.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.0.1","fr":60,"ip":0,"op":67.98,"w":120,"h":140,"ddd":0,"assets":[],"layers":[{"ind":2,"nm":"Layer 2","ks":{"p":{"a":0,"k":[59.997,60.37]},"a":{"a":0,"k":[8.95,-21.138,0]},"s":{"a":1,"k":[{"t":14,"s":[0,0,100],"i":{"x":[0.07],"y":[1]},"o":{"x":[0.86],"y":[0]},"e":[92,92,100]},{"t":63,"s":[92,92,100]}]},"r":{"a":0,"k":0},"o":{"a":1,"k":[{"t":14,"s":[0],"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"e":[100]},{"t":63,"s":[100]}]}},"ao":0,"ip":0,"op":68,"st":0,"bm":0,"sr":1,"ty":4,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"i":[[0,0],[-0.059,2.139],[0,0],[0,0.147],[2.197,0],[0,-2.285],[0,-0.205],[0,0],[-2.08,0]],"o":[[2.08,0],[0,0],[0,-0.205],[0,-2.285],[-2.197,0],[0,0.147],[0,0],[0.058,2.139],[0,0]],"v":[[8.936,-13.535],[12.217,-16.963],[12.598,-38.613],[12.627,-39.17],[8.965,-42.861],[5.273,-39.17],[5.303,-38.613],[5.684,-16.963],[8.936,-13.535]],"c":true},"hd":false}},{"ty":"sh","d":1,"ks":{"a":0,"k":{"i":[[0,0],[0,2.314],[2.402,0],[0,-2.314],[-2.373,0]],"o":[[2.402,0],[0,-2.315],[-2.374,0],[0,2.314],[0,0]],"v":[[8.936,0.586],[13.213,-3.574],[8.936,-7.705],[4.687,-3.574],[8.936,0.586]],"c":true},"hd":false}},{"ty":"fl","c":{"a":0,"k":[0.8,0.8196078431372549,0.8509803921568627,1]},"hd":false,"o":{"a":0,"k":100},"r":1},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"Object","hd":false}]},{"ind":1,"nm":"Layer 1","ks":{"p":{"a":0,"k":[60,60.18]},"a":{"a":0,"k":[51,51,0]},"s":{"a":0,"k":[104,104,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100}},"ao":0,"ip":0,"op":68,"st":0,"bm":0,"sr":1,"ty":4,"shapes":[{"ty":"gr","it":[{"ty":"sh","d":1,"ks":{"a":0,"k":{"i":[[0,0],[28.167,0],[0,28.167],[-28.166,0],[0,-28.166]],"o":[[0,28.167],[-28.166,0],[0,-28.166],[29,0],[0,0]],"v":[[51,0],[0,51],[-51,0],[0,-51],[51,0]],"c":true},"hd":false}},{"ty":"st","c":{"a":0,"k":[0.8,0.8196078431372549,0.8509803921568627,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":5},"lc":1,"lj":1,"ml":4},{"ty":"tr","p":{"a":0,"k":[51,51]},"a":{"a":0,"k":[0,0]},"s":{"a":1,"k":[{"t":0,"s":[0,0],"i":{"x":[0.07],"y":[1]},"o":{"x":[0.86],"y":[0]},"e":[105,105]},{"t":49,"s":[105,105],"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"e":[92,92]},{"t":68,"s":[92,92]}]},"r":{"a":0,"k":0},"o":{"a":1,"k":[{"t":0,"s":[0],"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"e":[100]},{"t":49,"s":[100]}]},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0}}],"nm":"Object","hd":false}]}],"markers":[]} \ No newline at end of file diff --git a/Chuck Norris Facts/Resources/Generated/Strings.swift b/Chuck Norris Facts/Resources/Generated/Strings.swift index 101e3ad..c898466 100644 --- a/Chuck Norris Facts/Resources/Generated/Strings.swift +++ b/Chuck Norris Facts/Resources/Generated/Strings.swift @@ -11,11 +11,27 @@ import Foundation // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces internal enum L10n { + internal enum Common { + /// Ok + internal static let ok = L10n.tr("Localizable", "Common.ok") + /// Oops + internal static let oops = L10n.tr("Localizable", "Common.oops") + } + internal enum EmptyView { /// Looks like there are no Facts internal static let empty = L10n.tr("Localizable", "EmptyView.empty") /// There are no facts to your search internal static let emptySearch = L10n.tr("Localizable", "EmptyView.emptySearch") + /// Search + internal static let search = L10n.tr("Localizable", "EmptyView.search") + } + + internal enum Errors { + /// Can't search facts + internal static let cantSearchFacts = L10n.tr("Localizable", "Errors.cantSearchFacts") + /// Can't sync categories + internal static let cantSyncCategories = L10n.tr("Localizable", "Errors.cantSyncCategories") } internal enum FactCategory { @@ -23,13 +39,6 @@ internal enum L10n { internal static let uncategorized = L10n.tr("Localizable", "FactCategory.uncategorized") } - internal enum FactListError { - /// Internet Connection appears to be offline - internal static let noConnection = L10n.tr("Localizable", "FactListError.noConnection") - /// Looks like the Chuck Norris Service is unavailable - internal static let serviceUnavailable = L10n.tr("Localizable", "FactListError.serviceUnavailable") - } - internal enum FactsList { /// Chuck Norris Facts internal static let title = L10n.tr("Localizable", "FactsList.title") diff --git a/Chuck Norris Facts/Resources/Localizable.strings b/Chuck Norris Facts/Resources/Localizable.strings index ff20991..96fbb5c 100644 --- a/Chuck Norris Facts/Resources/Localizable.strings +++ b/Chuck Norris Facts/Resources/Localizable.strings @@ -12,10 +12,14 @@ "SearchFacts.sections.suggestions" = "Suggestions"; "SearchFacts.sections.pastSearches" = "Past Searches"; +"EmptyView.search" = "Search"; "EmptyView.empty" = "Looks like there are no Facts"; "EmptyView.emptySearch" = "There are no facts to your search"; -"FactCategory.uncategorized" = "UNCATEGORIZED"; +"Common.oops" = "Oops"; +"Common.ok" = "Ok"; + +"Errors.cantSyncCategories" = "Can't sync categories"; +"Errors.cantSearchFacts" = "Can't search facts"; -"FactListError.noConnection" = "Internet Connection appears to be offline"; -"FactListError.serviceUnavailable" = "Looks like the Chuck Norris Service is unavailable"; +"FactCategory.uncategorized" = "UNCATEGORIZED"; diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift index cfb2615..0a91b42 100644 --- a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift +++ b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewControllerTests.swift @@ -57,16 +57,6 @@ class FactsListViewControllerTests: XCTestCase { XCTAssertTrue(factsListViewController.emptyListView.searchButton.isHidden) } - func test_FactsListViewController_WhenThereIsAnError_ShouldShowErrorView() { - let response = APIResponse(statusCode: 500, data: nil) - let apiError = APIError.statusCode(response) - factsServiceMock.searchFactsReturnValue = .error(apiError) - - factsListViewModel.inputs.setSearchTerm.onNext("") - - XCTAssertFalse(factsListViewController.errorView.isHidden) - } - func test_FactCell_WhenContentIsShort_FontSizeShouldBeTitle1() throws { let factStub = try stub("short-fact", type: Fact.self) let fact = try XCTUnwrap(factStub) diff --git a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift index cc91fc2..5187d85 100644 --- a/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift +++ b/Chuck Norris FactsTests/Scenes/Facts/FactsList/FactsListViewModelTests.swift @@ -75,9 +75,9 @@ class FactsListViewModelTests: XCTestCase { let categories = try XCTUnwrap(stubCategories) factsServiceMock.retrieveCategoriesReturnValue = .just(categories) - let errorObserver = testScheduler.createObserver(FactsListError.self) + let errorObserver = testScheduler.createObserver(FactsListErrorViewModel.self) - factsListViewModel.outputs.errors + factsListViewModel.outputs.factsListError .subscribe(errorObserver) .disposed(by: disposeBag) @@ -89,14 +89,13 @@ class FactsListViewModelTests: XCTestCase { XCTAssertNil(error) } - func test_FactsListViewModel_WhenSearchFactsWithError_ShouldEmmitFactListError() throws { - let response = APIResponse(statusCode: 500, data: nil) - let apiError = APIError.statusCode(response) + func test_FactsListViewModel_WhenSearchFactsWithError_ShouldEmmitFactsListError() throws { + let apiError = APIError.statusCode(500) factsServiceMock.searchFactsReturnValue = .error(apiError) - let errorObserver = testScheduler.createObserver(FactsListError.self) + let errorObserver = testScheduler.createObserver(FactsListErrorViewModel.self) - factsListViewModel.outputs.errors + factsListViewModel.outputs.factsListError .subscribe(errorObserver) .disposed(by: disposeBag) @@ -104,18 +103,17 @@ class FactsListViewModelTests: XCTestCase { testScheduler.start() - let error = errorObserver.events.compactMap { $0.value.element }.first - XCTAssertEqual(error?.code, FactsListError.searchFacts(apiError).code) + let factsListError = errorObserver.events.compactMap { $0.value.element }.first + XCTAssertEqual(factsListError?.error.code, apiError.code) } - func test_FactsListViewModel_WhenSyncCategoriesWithError_ShouldEmmitFactListError() throws { - let response = APIResponse(statusCode: 500, data: nil) - let apiError = APIError.statusCode(response) + func test_FactsListViewModel_WhenSyncCategoriesWithError_ShouldEmmitFactsListError() throws { + let apiError = APIError.statusCode(500) factsServiceMock.syncCategoriesReturnValue = .error(apiError) - let errorObserver = testScheduler.createObserver(FactsListError.self) + let errorObserver = testScheduler.createObserver(FactsListErrorViewModel.self) - factsListViewModel.outputs.errors + factsListViewModel.outputs.factsListError .subscribe(errorObserver) .disposed(by: disposeBag) @@ -123,7 +121,7 @@ class FactsListViewModelTests: XCTestCase { testScheduler.start() - let error = errorObserver.events.compactMap { $0.value.element }.first - XCTAssertEqual(error?.code, FactsListError.syncCategories(apiError).code) + let factsListError = errorObserver.events.compactMap { $0.value.element }.first + XCTAssertEqual(factsListError?.error.code, apiError.code) } } diff --git a/Chuck Norris FactsUITests/Scenes/FactsListScene.swift b/Chuck Norris FactsUITests/Scenes/FactsListScene.swift index ba3452d..8a4ed3d 100644 --- a/Chuck Norris FactsUITests/Scenes/FactsListScene.swift +++ b/Chuck Norris FactsUITests/Scenes/FactsListScene.swift @@ -15,7 +15,6 @@ struct FactsListScene { let emptyListView: XCUIElement let emptyListLabelView: XCUIElement let searchButton: XCUIElement - let errorView: XCUIElement let retryButton: XCUIElement init() { @@ -25,7 +24,6 @@ struct FactsListScene { emptyListView = app.otherElements["emptyListView"] emptyListLabelView = app.staticTexts["emptyListLabelView"] searchButton = app.navigationBars.buttons["searchButton"] - errorView = app.otherElements["errorView"] retryButton = app.buttons["retryButton"] } diff --git a/Chuck Norris FactsUITests/Tests/FactsListUITests.swift b/Chuck Norris FactsUITests/Tests/FactsListUITests.swift index 9700977..f5b27ff 100644 --- a/Chuck Norris FactsUITests/Tests/FactsListUITests.swift +++ b/Chuck Norris FactsUITests/Tests/FactsListUITests.swift @@ -77,8 +77,8 @@ final class FactsListUITests: XCTestCase { XCTAssertTrue(searchFactsView.exists) } - func test_FactsList_WhenSearchFails_ShouldShowErrorView() { - app.setLaunchArguments([.uiTest, .mockHttpError]) + func test_FactsList_WhenSearchFails_ShouldShowErrorAlert() { + app.setLaunchArguments([.uiTest, .mockStorage, .mockHttpError]) app.launch() let factsListScene = FactsListScene() @@ -95,7 +95,13 @@ final class FactsListUITests: XCTestCase { app.keyboards.buttons["Search"].tap() - XCTAssertTrue(factsListScene.errorView.exists) - XCTAssertTrue(factsListScene.retryButton.exists) + XCTAssertTrue(app.alerts.firstMatch.waitForExistence(timeout: 1)) + } + + func test_FactsList_WhenSyncCategories_ShouldShowErrorAlert() { + app.setLaunchArguments([.uiTest, .resetData, .mockHttpError]) + app.launch() + + XCTAssertTrue(app.alerts.firstMatch.waitForExistence(timeout: 1)) } }

eodEp%hodo7XGLwv*bnUCnD-;vPo>Em5{MxMJi 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Ξ2k*HRW`oiMwLqxFNW@}b8mOhb3e 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 P@FNXT5HA#a{QUm`$NM~E literal 0 HcmV?d00001 diff --git a/README.md b/README.md new file mode 100644 index 0000000..b3ae18e --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# Chuck Norris Facts +Funny facts about Chuck Norris provided by https://api.chucknorris.io. +![Actions Status](https://github.com/djorkaeffalexandre/chuck-norris-facts/workflows/Run%20tests/badge.svg) + +## Screenshots +