From 8252d54a8921690f655edca194e0f9822775e65c Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sun, 13 Sep 2020 00:58:32 +0430 Subject: [PATCH 001/108] -added Podfile and initial installation. --- DutchNews.xcodeproj/project.pbxproj | 119 ++++++++++++++++++ .../contents.xcworkspacedata | 10 ++ Podfile | 19 +++ Podfile.lock | 3 + 4 files changed, 151 insertions(+) create mode 100644 DutchNews.xcworkspace/contents.xcworkspacedata create mode 100644 Podfile create mode 100644 Podfile.lock diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index e88d1ac..06721cb 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 21E07E636FF6F2984928E95F /* Pods_DutchNews.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6D6B9A18971CCB93B1E1B562 /* Pods_DutchNews.framework */; }; + B66DD8163B0CF2FFF3CA6143 /* Pods_DutchNews_DutchNewsUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1159F72E8FEAEF1D70FDC6E8 /* Pods_DutchNews_DutchNewsUITests.framework */; }; + D9C19360BE6ACB6A2FE7DA86 /* Pods_DutchNewsTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4DDFCE580208FED7E50E9D2 /* Pods_DutchNewsTests.framework */; }; F89B021E250D446000B41293 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89B021D250D446000B41293 /* AppDelegate.swift */; }; F89B0220250D446200B41293 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F89B021F250D446200B41293 /* Assets.xcassets */; }; F89B0223250D446200B41293 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F89B0221250D446200B41293 /* LaunchScreen.storyboard */; }; @@ -32,6 +35,15 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 1159F72E8FEAEF1D70FDC6E8 /* Pods_DutchNews_DutchNewsUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DutchNews_DutchNewsUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4F28822A80945682344AAC12 /* Pods-DutchNews-DutchNewsUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DutchNews-DutchNewsUITests.debug.xcconfig"; path = "Target Support Files/Pods-DutchNews-DutchNewsUITests/Pods-DutchNews-DutchNewsUITests.debug.xcconfig"; sourceTree = ""; }; + 5BB041D0AA6B599D4F5E7D81 /* Pods-DutchNewsTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DutchNewsTests.debug.xcconfig"; path = "Target Support Files/Pods-DutchNewsTests/Pods-DutchNewsTests.debug.xcconfig"; sourceTree = ""; }; + 60D3AE4F11A8491BCCC459D7 /* Pods-DutchNews-DutchNewsUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DutchNews-DutchNewsUITests.release.xcconfig"; path = "Target Support Files/Pods-DutchNews-DutchNewsUITests/Pods-DutchNews-DutchNewsUITests.release.xcconfig"; sourceTree = ""; }; + 6D6B9A18971CCB93B1E1B562 /* Pods_DutchNews.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DutchNews.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9F5E3588EBC310FDA84D4BC9 /* Pods-DutchNews.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DutchNews.release.xcconfig"; path = "Target Support Files/Pods-DutchNews/Pods-DutchNews.release.xcconfig"; sourceTree = ""; }; + C8E2BC6DABFC66D3F898B924 /* Pods-DutchNewsTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DutchNewsTests.release.xcconfig"; path = "Target Support Files/Pods-DutchNewsTests/Pods-DutchNewsTests.release.xcconfig"; sourceTree = ""; }; + CE0BB7F85E1175162F017DD0 /* Pods-DutchNews.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DutchNews.debug.xcconfig"; path = "Target Support Files/Pods-DutchNews/Pods-DutchNews.debug.xcconfig"; sourceTree = ""; }; + E4DDFCE580208FED7E50E9D2 /* Pods_DutchNewsTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DutchNewsTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F89B021A250D446000B41293 /* DutchNews.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DutchNews.app; sourceTree = BUILT_PRODUCTS_DIR; }; F89B021D250D446000B41293 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F89B021F250D446200B41293 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -50,6 +62,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 21E07E636FF6F2984928E95F /* Pods_DutchNews.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -57,6 +70,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D9C19360BE6ACB6A2FE7DA86 /* Pods_DutchNewsTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -64,12 +78,23 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + B66DD8163B0CF2FFF3CA6143 /* Pods_DutchNews_DutchNewsUITests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 4B3F257B8F205BA3F43A73E1 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 6D6B9A18971CCB93B1E1B562 /* Pods_DutchNews.framework */, + 1159F72E8FEAEF1D70FDC6E8 /* Pods_DutchNews_DutchNewsUITests.framework */, + E4DDFCE580208FED7E50E9D2 /* Pods_DutchNewsTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; F89B0211250D446000B41293 = { isa = PBXGroup; children = ( @@ -77,6 +102,8 @@ F89B022C250D446200B41293 /* DutchNewsTests */, F89B0237250D446200B41293 /* DutchNewsUITests */, F89B021B250D446000B41293 /* Products */, + FA6C9BAEFCB0D31A29283E4B /* Pods */, + 4B3F257B8F205BA3F43A73E1 /* Frameworks */, ); sourceTree = ""; }; @@ -119,6 +146,20 @@ path = DutchNewsUITests; sourceTree = ""; }; + FA6C9BAEFCB0D31A29283E4B /* Pods */ = { + isa = PBXGroup; + children = ( + CE0BB7F85E1175162F017DD0 /* Pods-DutchNews.debug.xcconfig */, + 9F5E3588EBC310FDA84D4BC9 /* Pods-DutchNews.release.xcconfig */, + 4F28822A80945682344AAC12 /* Pods-DutchNews-DutchNewsUITests.debug.xcconfig */, + 60D3AE4F11A8491BCCC459D7 /* Pods-DutchNews-DutchNewsUITests.release.xcconfig */, + 5BB041D0AA6B599D4F5E7D81 /* Pods-DutchNewsTests.debug.xcconfig */, + C8E2BC6DABFC66D3F898B924 /* Pods-DutchNewsTests.release.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -126,6 +167,7 @@ isa = PBXNativeTarget; buildConfigurationList = F89B023D250D446200B41293 /* Build configuration list for PBXNativeTarget "DutchNews" */; buildPhases = ( + ED77C5D597B0DE2AE4BC3C93 /* [CP] Check Pods Manifest.lock */, F89B0216250D446000B41293 /* Sources */, F89B0217250D446000B41293 /* Frameworks */, F89B0218250D446000B41293 /* Resources */, @@ -143,6 +185,7 @@ isa = PBXNativeTarget; buildConfigurationList = F89B0240250D446200B41293 /* Build configuration list for PBXNativeTarget "DutchNewsTests" */; buildPhases = ( + EF9A2F60853CCD39B6A80215 /* [CP] Check Pods Manifest.lock */, F89B0225250D446200B41293 /* Sources */, F89B0226250D446200B41293 /* Frameworks */, F89B0227250D446200B41293 /* Resources */, @@ -161,6 +204,7 @@ isa = PBXNativeTarget; buildConfigurationList = F89B0243250D446200B41293 /* Build configuration list for PBXNativeTarget "DutchNewsUITests" */; buildPhases = ( + B8172C4B044C0C51B6EEBC05 /* [CP] Check Pods Manifest.lock */, F89B0230250D446200B41293 /* Sources */, F89B0231250D446200B41293 /* Frameworks */, F89B0232250D446200B41293 /* Resources */, @@ -244,6 +288,75 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + B8172C4B044C0C51B6EEBC05 /* [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-DutchNews-DutchNewsUITests-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; + }; + ED77C5D597B0DE2AE4BC3C93 /* [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-DutchNews-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; + }; + EF9A2F60853CCD39B6A80215 /* [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-DutchNewsTests-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 */ F89B0216250D446000B41293 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -412,6 +525,7 @@ }; F89B023E250D446200B41293 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = CE0BB7F85E1175162F017DD0 /* Pods-DutchNews.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; @@ -431,6 +545,7 @@ }; F89B023F250D446200B41293 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 9F5E3588EBC310FDA84D4BC9 /* Pods-DutchNews.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; @@ -450,6 +565,7 @@ }; F89B0241250D446200B41293 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 5BB041D0AA6B599D4F5E7D81 /* Pods-DutchNewsTests.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -472,6 +588,7 @@ }; F89B0242250D446200B41293 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = C8E2BC6DABFC66D3F898B924 /* Pods-DutchNewsTests.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -494,6 +611,7 @@ }; F89B0244250D446200B41293 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 4F28822A80945682344AAC12 /* Pods-DutchNews-DutchNewsUITests.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; @@ -514,6 +632,7 @@ }; F89B0245250D446200B41293 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 60D3AE4F11A8491BCCC459D7 /* Pods-DutchNews-DutchNewsUITests.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; diff --git a/DutchNews.xcworkspace/contents.xcworkspacedata b/DutchNews.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..010f476 --- /dev/null +++ b/DutchNews.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/Podfile b/Podfile new file mode 100644 index 0000000..fc92965 --- /dev/null +++ b/Podfile @@ -0,0 +1,19 @@ +# Uncomment the next line to define a global platform for your project +platform :ios, '11.0' + +target 'DutchNews' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + # Pods for DutchNews + + target 'DutchNewsTests' do + inherit! :search_paths + # Pods for testing + end + + target 'DutchNewsUITests' do + # Pods for testing + end + +end diff --git a/Podfile.lock b/Podfile.lock new file mode 100644 index 0000000..1f8e4f1 --- /dev/null +++ b/Podfile.lock @@ -0,0 +1,3 @@ +PODFILE CHECKSUM: 177eee4cacb51d95bacf7cffe7929b672dda1dfe + +COCOAPODS: 1.9.3 From 4ca23ddd3c0b68ecbde8e5de93f0a24d0d5a3a79 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sun, 13 Sep 2020 01:17:06 +0430 Subject: [PATCH 002/108] -installed all necessory libraries. --- DutchNews.xcodeproj/project.pbxproj | 54 ++++++++++++ .../xcschemes/xcschememanagement.plist | 14 --- Podfile | 14 +++ Podfile.lock | 85 ++++++++++++++++++- 4 files changed, 152 insertions(+), 15 deletions(-) delete mode 100644 DutchNews.xcodeproj/xcuserdata/farshad.xcuserdatad/xcschemes/xcschememanagement.plist diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index 06721cb..31197c6 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -171,6 +171,7 @@ F89B0216250D446000B41293 /* Sources */, F89B0217250D446000B41293 /* Frameworks */, F89B0218250D446000B41293 /* Resources */, + 2A72020948C2D75438B5DA75 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -189,6 +190,7 @@ F89B0225250D446200B41293 /* Sources */, F89B0226250D446200B41293 /* Frameworks */, F89B0227250D446200B41293 /* Resources */, + B48216F6A905881BBE665BFD /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -208,6 +210,7 @@ F89B0230250D446200B41293 /* Sources */, F89B0231250D446200B41293 /* Frameworks */, F89B0232250D446200B41293 /* Resources */, + 1FD7F41EF9A054827A1D8B78 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -289,6 +292,57 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 1FD7F41EF9A054827A1D8B78 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-DutchNews-DutchNewsUITests/Pods-DutchNews-DutchNewsUITests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-DutchNews-DutchNewsUITests/Pods-DutchNews-DutchNewsUITests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-DutchNews-DutchNewsUITests/Pods-DutchNews-DutchNewsUITests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 2A72020948C2D75438B5DA75 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-DutchNews/Pods-DutchNews-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-DutchNews/Pods-DutchNews-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-DutchNews/Pods-DutchNews-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + B48216F6A905881BBE665BFD /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-DutchNewsTests/Pods-DutchNewsTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-DutchNewsTests/Pods-DutchNewsTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-DutchNewsTests/Pods-DutchNewsTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; B8172C4B044C0C51B6EEBC05 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/DutchNews.xcodeproj/xcuserdata/farshad.xcuserdatad/xcschemes/xcschememanagement.plist b/DutchNews.xcodeproj/xcuserdata/farshad.xcuserdatad/xcschemes/xcschememanagement.plist deleted file mode 100644 index 11f2ed6..0000000 --- a/DutchNews.xcodeproj/xcuserdata/farshad.xcuserdatad/xcschemes/xcschememanagement.plist +++ /dev/null @@ -1,14 +0,0 @@ - - - - - SchemeUserState - - DutchNews.xcscheme_^#shared#^_ - - orderHint - 0 - - - - diff --git a/Podfile b/Podfile index fc92965..9f28bd8 100644 --- a/Podfile +++ b/Podfile @@ -4,11 +4,25 @@ platform :ios, '11.0' target 'DutchNews' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! + + pod 'Alamofire' + pod 'RxSwift' + pod 'RxCocoa' + pod 'RxDataSources' + pod 'RxAlamofire' + pod 'PureLayout' + pod 'MXParallaxHeader' + pod 'Pageboy' + pod 'JEKScrollableSectionCollectionViewLayout', :git => 'https://github.com/farshadmb/JEKScrollableSectionCollectionViewLayout.git' # Pods for DutchNews target 'DutchNewsTests' do inherit! :search_paths + pod 'RxTest' + pod 'RxBlocking' + pod 'Nimble' + # Pods for testing end diff --git a/Podfile.lock b/Podfile.lock index 1f8e4f1..342e352 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,3 +1,86 @@ -PODFILE CHECKSUM: 177eee4cacb51d95bacf7cffe7929b672dda1dfe +PODS: + - Alamofire (5.2.1) + - Differentiator (4.0.1) + - JEKScrollableSectionCollectionViewLayout (1.3.0) + - MXParallaxHeader (1.1.0) + - Nimble (8.1.2) + - Pageboy (3.6.1) + - PureLayout (3.1.6) + - RxAlamofire (5.5.0): + - RxAlamofire/Core (= 5.5.0) + - RxAlamofire/Core (5.5.0): + - Alamofire (~> 5.1) + - RxSwift (~> 5.1) + - 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) + +DEPENDENCIES: + - Alamofire + - JEKScrollableSectionCollectionViewLayout (from `https://github.com/farshadmb/JEKScrollableSectionCollectionViewLayout.git`) + - MXParallaxHeader + - Nimble + - Pageboy + - PureLayout + - RxAlamofire + - RxBlocking + - RxCocoa + - RxDataSources + - RxSwift + - RxTest + +SPEC REPOS: + trunk: + - Alamofire + - Differentiator + - MXParallaxHeader + - Nimble + - Pageboy + - PureLayout + - RxAlamofire + - RxBlocking + - RxCocoa + - RxDataSources + - RxRelay + - RxSwift + - RxTest + +EXTERNAL SOURCES: + JEKScrollableSectionCollectionViewLayout: + :git: https://github.com/farshadmb/JEKScrollableSectionCollectionViewLayout.git + +CHECKOUT OPTIONS: + JEKScrollableSectionCollectionViewLayout: + :commit: df6250fd4b6e3334a5598d74ae182b8d64774aa8 + :git: https://github.com/farshadmb/JEKScrollableSectionCollectionViewLayout.git + +SPEC CHECKSUMS: + Alamofire: e911732990610fe89af59ac0077f923d72dc3dfd + Differentiator: 886080237d9f87f322641dedbc5be257061b0602 + JEKScrollableSectionCollectionViewLayout: 80def4834e535880029917c374324ef8b089c448 + MXParallaxHeader: de3c867e10ba46e8f6e20c8ee1f2a910372b3b94 + Nimble: 3864815b4703c7ebffba875973c70e854489fbae + Pageboy: 29a2d474ad99404b4d77f325e0ab6d705930a4fb + PureLayout: bd3c4ec3a3819ad387c99ebb72c6b129c3ed4d2d + RxAlamofire: 22287c710761466d0123504c566a8381520d4d63 + RxBlocking: 5f700a78cad61ce253ebd37c9a39b5ccc76477b4 + RxCocoa: 32065309a38d29b5b0db858819b5bf9ef038b601 + RxDataSources: efee07fa4de48477eca0a4611e6d11e2da9c1114 + RxRelay: d77f7d771495f43c556cbc43eebd1bb54d01e8e9 + RxSwift: 81470a2074fa8780320ea5fe4102807cb7118178 + RxTest: 711632d5644dffbeb62c936a521b5b008a1e1faa + +PODFILE CHECKSUM: ca81cbf52ef031ebf77e41d07beee4a837bad53f COCOAPODS: 1.9.3 From 66d6ca6646317fbe70fa9cc54e800d44d9c93797 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sun, 13 Sep 2020 01:45:03 +0430 Subject: [PATCH 003/108] - restructured files and resources. --- DutchNews.xcodeproj/project.pbxproj | 99 ++++++++++++++++++- DutchNews/AppDelegate.swift | 5 +- DutchNews/Info.plist | 2 + .../AppIcon.appiconset/Contents.json | 0 .../Assets.xcassets/Contents.json | 0 .../Base.lproj/LaunchScreen.storyboard | 12 ++- .../Storyboards/Base.lproj/Main.storyboard | 7 ++ 7 files changed, 112 insertions(+), 13 deletions(-) rename DutchNews/{ => Resources}/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename DutchNews/{ => Resources}/Assets.xcassets/Contents.json (100%) rename DutchNews/{ => Resources/Storyboards}/Base.lproj/LaunchScreen.storyboard (67%) create mode 100644 DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index 31197c6..210c5ce 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 51; objects = { /* Begin PBXBuildFile section */ @@ -15,6 +15,7 @@ F89B0223250D446200B41293 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F89B0221250D446200B41293 /* LaunchScreen.storyboard */; }; F89B022E250D446200B41293 /* DutchNewsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89B022D250D446200B41293 /* DutchNewsTests.swift */; }; F89B0239250D446200B41293 /* DutchNewsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89B0238250D446200B41293 /* DutchNewsUITests.swift */; }; + F8F14C74250D719800C24FF5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F8F14C72250D719800C24FF5 /* Main.storyboard */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -55,6 +56,7 @@ F89B0234250D446200B41293 /* DutchNewsUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DutchNewsUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F89B0238250D446200B41293 /* DutchNewsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DutchNewsUITests.swift; sourceTree = ""; }; F89B023A250D446200B41293 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F8F14C73250D719800C24FF5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -120,9 +122,9 @@ F89B021C250D446000B41293 /* DutchNews */ = { isa = PBXGroup; children = ( + F8F14C68250D70AA00C24FF5 /* Classes */, + F8F14C70250D712400C24FF5 /* Resources */, F89B021D250D446000B41293 /* AppDelegate.swift */, - F89B021F250D446200B41293 /* Assets.xcassets */, - F89B0221250D446200B41293 /* LaunchScreen.storyboard */, F89B0224250D446200B41293 /* Info.plist */, ); path = DutchNews; @@ -146,6 +148,87 @@ path = DutchNewsUITests; sourceTree = ""; }; + F8F14C68250D70AA00C24FF5 /* Classes */ = { + isa = PBXGroup; + children = ( + F8F14C69250D70B400C24FF5 /* Data Layers */, + F8F14C6F250D710100C24FF5 /* Extensions */, + F8F14C6D250D70DC00C24FF5 /* Utilites */, + F8F14C6C250D70CF00C24FF5 /* ViewControllers */, + F8F14C6B250D70C700C24FF5 /* ViewModels */, + F8F14C6A250D70BD00C24FF5 /* Views */, + ); + path = Classes; + sourceTree = ""; + }; + F8F14C69250D70B400C24FF5 /* Data Layers */ = { + isa = PBXGroup; + children = ( + F8F14C6E250D70F900C24FF5 /* Networking */, + ); + path = "Data Layers"; + sourceTree = ""; + }; + F8F14C6A250D70BD00C24FF5 /* Views */ = { + isa = PBXGroup; + children = ( + ); + path = Views; + sourceTree = ""; + }; + F8F14C6B250D70C700C24FF5 /* ViewModels */ = { + isa = PBXGroup; + children = ( + ); + path = ViewModels; + sourceTree = ""; + }; + F8F14C6C250D70CF00C24FF5 /* ViewControllers */ = { + isa = PBXGroup; + children = ( + ); + path = ViewControllers; + sourceTree = ""; + }; + F8F14C6D250D70DC00C24FF5 /* Utilites */ = { + isa = PBXGroup; + children = ( + ); + path = Utilites; + sourceTree = ""; + }; + F8F14C6E250D70F900C24FF5 /* Networking */ = { + isa = PBXGroup; + children = ( + ); + path = Networking; + sourceTree = ""; + }; + F8F14C6F250D710100C24FF5 /* Extensions */ = { + isa = PBXGroup; + children = ( + ); + path = Extensions; + sourceTree = ""; + }; + F8F14C70250D712400C24FF5 /* Resources */ = { + isa = PBXGroup; + children = ( + F8F14C71250D716A00C24FF5 /* Storyboards */, + F89B021F250D446200B41293 /* Assets.xcassets */, + ); + path = Resources; + sourceTree = ""; + }; + F8F14C71250D716A00C24FF5 /* Storyboards */ = { + isa = PBXGroup; + children = ( + F89B0221250D446200B41293 /* LaunchScreen.storyboard */, + F8F14C72250D719800C24FF5 /* Main.storyboard */, + ); + path = Storyboards; + sourceTree = ""; + }; FA6C9BAEFCB0D31A29283E4B /* Pods */ = { isa = PBXGroup; children = ( @@ -156,7 +239,6 @@ 5BB041D0AA6B599D4F5E7D81 /* Pods-DutchNewsTests.debug.xcconfig */, C8E2BC6DABFC66D3F898B924 /* Pods-DutchNewsTests.release.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -270,6 +352,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + F8F14C74250D719800C24FF5 /* Main.storyboard in Resources */, F89B0220250D446200B41293 /* Assets.xcassets in Resources */, F89B0223250D446200B41293 /* LaunchScreen.storyboard in Resources */, ); @@ -460,6 +543,14 @@ name = LaunchScreen.storyboard; sourceTree = ""; }; + F8F14C72250D719800C24FF5 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + F8F14C73250D719800C24FF5 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ diff --git a/DutchNews/AppDelegate.swift b/DutchNews/AppDelegate.swift index 4640717..f18d12b 100644 --- a/DutchNews/AppDelegate.swift +++ b/DutchNews/AppDelegate.swift @@ -16,10 +16,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. - window = UIWindow() - window?.backgroundColor = UIColor.lightGray - window?.rootViewController = UIViewController() - window?.makeKeyAndVisible() + return true } diff --git a/DutchNews/Info.plist b/DutchNews/Info.plist index 5a63475..ea513af 100644 --- a/DutchNews/Info.plist +++ b/DutchNews/Info.plist @@ -20,6 +20,8 @@ 1 LSRequiresIPhoneOS + NSMainNibFile + Main UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities diff --git a/DutchNews/Assets.xcassets/AppIcon.appiconset/Contents.json b/DutchNews/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from DutchNews/Assets.xcassets/AppIcon.appiconset/Contents.json rename to DutchNews/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/DutchNews/Assets.xcassets/Contents.json b/DutchNews/Resources/Assets.xcassets/Contents.json similarity index 100% rename from DutchNews/Assets.xcassets/Contents.json rename to DutchNews/Resources/Assets.xcassets/Contents.json diff --git a/DutchNews/Base.lproj/LaunchScreen.storyboard b/DutchNews/Resources/Storyboards/Base.lproj/LaunchScreen.storyboard similarity index 67% rename from DutchNews/Base.lproj/LaunchScreen.storyboard rename to DutchNews/Resources/Storyboards/Base.lproj/LaunchScreen.storyboard index 865e932..5ca9c85 100644 --- a/DutchNews/Base.lproj/LaunchScreen.storyboard +++ b/DutchNews/Resources/Storyboards/Base.lproj/LaunchScreen.storyboard @@ -1,7 +1,9 @@ - - + + + - + + @@ -11,9 +13,9 @@ - + - + diff --git a/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard b/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f9a048e --- /dev/null +++ b/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard @@ -0,0 +1,7 @@ + + + + + + + From d18122cc335b9730d14309041f785ce9b3640079 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Thu, 17 Sep 2020 18:04:10 +0430 Subject: [PATCH 004/108] -added and installed fastlane for testing and deployment. --- Gemfile | 3 + Gemfile.lock | 178 ++++++++++++++++++++++++++++++++++++++++++++++ fastlane/Appfile | 6 ++ fastlane/Fastfile | 23 ++++++ 4 files changed, 210 insertions(+) create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 fastlane/Appfile create mode 100644 fastlane/Fastfile diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..7a118b4 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "fastlane" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..cb7461b --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,178 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.2) + addressable (2.7.0) + public_suffix (>= 2.0.2, < 5.0) + atomos (0.1.3) + aws-eventstream (1.1.0) + aws-partitions (1.371.0) + aws-sdk-core (3.107.0) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.239.0) + aws-sigv4 (~> 1.1) + jmespath (~> 1.0) + aws-sdk-kms (1.38.0) + aws-sdk-core (~> 3, >= 3.99.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.81.0) + aws-sdk-core (~> 3, >= 3.104.3) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.1) + aws-sigv4 (1.2.2) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.3) + claide (1.0.3) + colored (1.2) + colored2 (3.1.2) + commander-fastlane (4.4.6) + highline (~> 1.7.2) + 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) + 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.160.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) + 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.28.0) + 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.13.1) + 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) + jmespath (1.4.0) + json (2.3.0) + jwt (2.2.2) + memoist (0.16.2) + mini_magick (4.10.1) + mini_mime (1.0.2) + multi_json (1.15.0) + multipart-post (2.0.0) + nanaimo (0.3.0) + naturally (2.2.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) + 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) + tty-cursor (0.7.1) + tty-screen (0.8.1) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + 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.18.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 + fastlane + +BUNDLED WITH + 2.1.4 diff --git a/fastlane/Appfile b/fastlane/Appfile new file mode 100644 index 0000000..1803063 --- /dev/null +++ b/fastlane/Appfile @@ -0,0 +1,6 @@ +# app_identifier("[[APP_IDENTIFIER]]") # The bundle identifier of your app +# apple_id("[[APPLE_ID]]") # Your Apple email address + + +# For more information about the Appfile, see: +# https://docs.fastlane.tools/advanced/#appfile diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 0000000..0f39ea6 --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,23 @@ +# This file contains the fastlane.tools configuration +# You can find the documentation at https://docs.fastlane.tools +# +# For a list of all available actions, check out +# +# https://docs.fastlane.tools/actions +# +# For a list of all available plugins, check out +# +# https://docs.fastlane.tools/plugins/available-plugins +# + +# Uncomment the line if you want fastlane to automatically update itself +# update_fastlane + +default_platform(:ios) + +platform :ios do + desc "Description of what the lane does" + lane :custom_lane do + # add actions here: https://docs.fastlane.tools/actions + end +end From b1876cedf45955f0ee5c34cd3396cd1dccb3af72 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Thu, 17 Sep 2020 21:36:06 +0430 Subject: [PATCH 005/108] - added run_ci_tests method for testing purposes. --- Gemfile | 2 ++ Gemfile.lock | 69 ++++++++++++++++++++++++++++++++++++++++++++++ fastlane/Fastfile | 63 ++++++++++++++++++++++++++++++++++++++++-- fastlane/README.md | 29 +++++++++++++++++++ 4 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 fastlane/README.md diff --git a/Gemfile b/Gemfile index 7a118b4..f9733d6 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,5 @@ source "https://rubygems.org" gem "fastlane" +gem "xcode-install" +gem "cocoapods" \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index cb7461b..3843a38 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,8 +2,16 @@ 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.371.0) @@ -23,10 +31,48 @@ GEM aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.3) 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) @@ -35,6 +81,9 @@ GEM 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) @@ -80,6 +129,9 @@ GEM 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) @@ -113,16 +165,22 @@ GEM 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.0) 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) @@ -133,6 +191,7 @@ GEM 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) @@ -147,16 +206,24 @@ GEM 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) + xcode-install (2.6.6) + claide (>= 0.9.1, < 1.1.0) + fastlane (>= 2.1.0, < 3.0.0) xcodeproj (1.18.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) @@ -172,7 +239,9 @@ PLATFORMS ruby DEPENDENCIES + cocoapods fastlane + xcode-install BUNDLED WITH 2.1.4 diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 0f39ea6..a97dc94 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -16,8 +16,65 @@ default_platform(:ios) platform :ios do - desc "Description of what the lane does" - lane :custom_lane do - # add actions here: https://docs.fastlane.tools/actions + + # Variables # + scheme = "DutchNews" + workspace = "#{scheme}.xcworkspace" + projectspace = "#{scheme}.xcodeproj" + + version = "" + + before_all do |lane| + + UI.message "prepare for builds" + xcversion(version: "~> 11.2") + version = get_version_number(xcodeproj:projectspace,target:scheme) + + + if lane != :debugTestVersion + #for cocoapods install dependecy + cocoapods() + end + + if lane != :debugTestVersion + clear_derived_data() #clear all derived_data + enable_automatic_code_signing() #autosiging + end + + UI.message "prepared for build" + + end + + after_all do |lane| + notification(title:"Success",subtitle:"#{scheme} #{version}",message:lane.to_s,sound:"Default") + UI.message "Success Build #{scheme} #{version} in lane #{lane.to_s} " end + + error do |lane, exception| + # Send error notification + notification(title:"Failure on #{scheme} #{version}",subtitle:lane.to_s,message:exception.to_s,sound:"Default") + raise "Failure on #{scheme} #{version} \n in lane #{lane.to_s} \n exception = #{exception.to_s}" + end + + desc "Run App Unit tests on given devices name" + lane :run_ci_tests do |options| + begin + scan(workspace: workspace, + #scheme: scheme, # Project scheme name + clean: true, # clean project folder before test execution + devices: options[:devices], # Devices for testing + result_bundle: "TestResults") # To generate test reports + rescue => ex + UI.error(ex) + generate_report() + end + end + + desc "generate report after running tests" + def generate_report + puts "Generating Test Report ..." + sh "xchtmlreport -r fastlane/test_output/#{scheme}-#{version}.test_result" + puts "Test Report Successfully generated" + end + end diff --git a/fastlane/README.md b/fastlane/README.md new file mode 100644 index 0000000..b4ebc14 --- /dev/null +++ b/fastlane/README.md @@ -0,0 +1,29 @@ +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 run_ci_tests +``` +fastlane ios run_ci_tests +``` +Run App Unit tests on given devices name + +---- + +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 e780360e0ccd3d9c94b331323fccc47be1c283ac Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Thu, 17 Sep 2020 21:41:23 +0430 Subject: [PATCH 006/108] - first build for testing --- DutchNews/AppDelegate.swift | 4 +- DutchNews/Info.plist | 4 +- .../Storyboards/Base.lproj/Main.storyboard | 74 ++++++++++++++++++- Podfile | 3 +- Podfile.lock | 6 +- 5 files changed, 81 insertions(+), 10 deletions(-) diff --git a/DutchNews/AppDelegate.swift b/DutchNews/AppDelegate.swift index f18d12b..75d4c99 100644 --- a/DutchNews/AppDelegate.swift +++ b/DutchNews/AppDelegate.swift @@ -11,12 +11,12 @@ import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? + @IBOutlet var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. - + UICollectionViewDiffableDataSource return true } diff --git a/DutchNews/Info.plist b/DutchNews/Info.plist index ea513af..af8868b 100644 --- a/DutchNews/Info.plist +++ b/DutchNews/Info.plist @@ -20,10 +20,10 @@ 1 LSRequiresIPhoneOS - NSMainNibFile - Main UILaunchStoryboardName LaunchScreen + UIMainStoryboardFile + Main UIRequiredDeviceCapabilities armv7 diff --git a/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard b/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard index f9a048e..a512696 100644 --- a/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard +++ b/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard @@ -1,7 +1,73 @@ - - + + + - + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Podfile b/Podfile index 9f28bd8..97e2d9b 100644 --- a/Podfile +++ b/Podfile @@ -14,7 +14,8 @@ target 'DutchNews' do pod 'MXParallaxHeader' pod 'Pageboy' pod 'JEKScrollableSectionCollectionViewLayout', :git => 'https://github.com/farshadmb/JEKScrollableSectionCollectionViewLayout.git' - + pod 'MagazineLayout' + # Pods for DutchNews target 'DutchNewsTests' do diff --git a/Podfile.lock b/Podfile.lock index 342e352..9e7a025 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -2,6 +2,7 @@ PODS: - Alamofire (5.2.1) - Differentiator (4.0.1) - JEKScrollableSectionCollectionViewLayout (1.3.0) + - MagazineLayout (1.6.2) - MXParallaxHeader (1.1.0) - Nimble (8.1.2) - Pageboy (3.6.1) @@ -29,6 +30,7 @@ PODS: DEPENDENCIES: - Alamofire - JEKScrollableSectionCollectionViewLayout (from `https://github.com/farshadmb/JEKScrollableSectionCollectionViewLayout.git`) + - MagazineLayout - MXParallaxHeader - Nimble - Pageboy @@ -44,6 +46,7 @@ SPEC REPOS: trunk: - Alamofire - Differentiator + - MagazineLayout - MXParallaxHeader - Nimble - Pageboy @@ -69,6 +72,7 @@ SPEC CHECKSUMS: Alamofire: e911732990610fe89af59ac0077f923d72dc3dfd Differentiator: 886080237d9f87f322641dedbc5be257061b0602 JEKScrollableSectionCollectionViewLayout: 80def4834e535880029917c374324ef8b089c448 + MagazineLayout: 8e995730bc2b1ff8f11f44cb7d7926ab9640892f MXParallaxHeader: de3c867e10ba46e8f6e20c8ee1f2a910372b3b94 Nimble: 3864815b4703c7ebffba875973c70e854489fbae Pageboy: 29a2d474ad99404b4d77f325e0ab6d705930a4fb @@ -81,6 +85,6 @@ SPEC CHECKSUMS: RxSwift: 81470a2074fa8780320ea5fe4102807cb7118178 RxTest: 711632d5644dffbeb62c936a521b5b008a1e1faa -PODFILE CHECKSUM: ca81cbf52ef031ebf77e41d07beee4a837bad53f +PODFILE CHECKSUM: da3abe129fd0db3a54bf168fb3e15fa15dfef33c COCOAPODS: 1.9.3 From c24c21c536c8a6f7f7753234c532cc72791a8bf0 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Thu, 17 Sep 2020 22:15:52 +0430 Subject: [PATCH 007/108] - Fixed UITest Dependencies buidling error. --- DutchNews.xcodeproj/project.pbxproj | 36 ++++++++----------------- DutchNews/AppDelegate.swift | 2 +- DutchNewsUITests/DutchNewsUITests.swift | 3 ++- Podfile | 18 +++++++++---- Podfile.lock | 2 +- fastlane/Fastfile | 2 +- 6 files changed, 29 insertions(+), 34 deletions(-) diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index 210c5ce..94cad93 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -8,7 +8,7 @@ /* Begin PBXBuildFile section */ 21E07E636FF6F2984928E95F /* Pods_DutchNews.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6D6B9A18971CCB93B1E1B562 /* Pods_DutchNews.framework */; }; - B66DD8163B0CF2FFF3CA6143 /* Pods_DutchNews_DutchNewsUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1159F72E8FEAEF1D70FDC6E8 /* Pods_DutchNews_DutchNewsUITests.framework */; }; + 8B7AA46F365111AC7BEAABB4 /* Pods_DutchNewsUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06FCAD588464F494AF84398E /* Pods_DutchNewsUITests.framework */; }; D9C19360BE6ACB6A2FE7DA86 /* Pods_DutchNewsTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4DDFCE580208FED7E50E9D2 /* Pods_DutchNewsTests.framework */; }; F89B021E250D446000B41293 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89B021D250D446000B41293 /* AppDelegate.swift */; }; F89B0220250D446200B41293 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F89B021F250D446200B41293 /* Assets.xcassets */; }; @@ -36,14 +36,16 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 1159F72E8FEAEF1D70FDC6E8 /* Pods_DutchNews_DutchNewsUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DutchNews_DutchNewsUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 06FCAD588464F494AF84398E /* Pods_DutchNewsUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DutchNewsUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4F28822A80945682344AAC12 /* Pods-DutchNews-DutchNewsUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DutchNews-DutchNewsUITests.debug.xcconfig"; path = "Target Support Files/Pods-DutchNews-DutchNewsUITests/Pods-DutchNews-DutchNewsUITests.debug.xcconfig"; sourceTree = ""; }; + 58F7ABB66E4031DDAE7CBDC7 /* Pods-DutchNewsUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DutchNewsUITests.debug.xcconfig"; path = "Target Support Files/Pods-DutchNewsUITests/Pods-DutchNewsUITests.debug.xcconfig"; sourceTree = ""; }; 5BB041D0AA6B599D4F5E7D81 /* Pods-DutchNewsTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DutchNewsTests.debug.xcconfig"; path = "Target Support Files/Pods-DutchNewsTests/Pods-DutchNewsTests.debug.xcconfig"; sourceTree = ""; }; 60D3AE4F11A8491BCCC459D7 /* Pods-DutchNews-DutchNewsUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DutchNews-DutchNewsUITests.release.xcconfig"; path = "Target Support Files/Pods-DutchNews-DutchNewsUITests/Pods-DutchNews-DutchNewsUITests.release.xcconfig"; sourceTree = ""; }; 6D6B9A18971CCB93B1E1B562 /* Pods_DutchNews.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DutchNews.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9F5E3588EBC310FDA84D4BC9 /* Pods-DutchNews.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DutchNews.release.xcconfig"; path = "Target Support Files/Pods-DutchNews/Pods-DutchNews.release.xcconfig"; sourceTree = ""; }; C8E2BC6DABFC66D3F898B924 /* Pods-DutchNewsTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DutchNewsTests.release.xcconfig"; path = "Target Support Files/Pods-DutchNewsTests/Pods-DutchNewsTests.release.xcconfig"; sourceTree = ""; }; CE0BB7F85E1175162F017DD0 /* Pods-DutchNews.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DutchNews.debug.xcconfig"; path = "Target Support Files/Pods-DutchNews/Pods-DutchNews.debug.xcconfig"; sourceTree = ""; }; + DD3A1F2B03FE42DB1EAECC2E /* Pods-DutchNewsUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DutchNewsUITests.release.xcconfig"; path = "Target Support Files/Pods-DutchNewsUITests/Pods-DutchNewsUITests.release.xcconfig"; sourceTree = ""; }; E4DDFCE580208FED7E50E9D2 /* Pods_DutchNewsTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DutchNewsTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F89B021A250D446000B41293 /* DutchNews.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DutchNews.app; sourceTree = BUILT_PRODUCTS_DIR; }; F89B021D250D446000B41293 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -80,7 +82,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - B66DD8163B0CF2FFF3CA6143 /* Pods_DutchNews_DutchNewsUITests.framework in Frameworks */, + 8B7AA46F365111AC7BEAABB4 /* Pods_DutchNewsUITests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -91,8 +93,8 @@ isa = PBXGroup; children = ( 6D6B9A18971CCB93B1E1B562 /* Pods_DutchNews.framework */, - 1159F72E8FEAEF1D70FDC6E8 /* Pods_DutchNews_DutchNewsUITests.framework */, E4DDFCE580208FED7E50E9D2 /* Pods_DutchNewsTests.framework */, + 06FCAD588464F494AF84398E /* Pods_DutchNewsUITests.framework */, ); name = Frameworks; sourceTree = ""; @@ -238,6 +240,8 @@ 60D3AE4F11A8491BCCC459D7 /* Pods-DutchNews-DutchNewsUITests.release.xcconfig */, 5BB041D0AA6B599D4F5E7D81 /* Pods-DutchNewsTests.debug.xcconfig */, C8E2BC6DABFC66D3F898B924 /* Pods-DutchNewsTests.release.xcconfig */, + 58F7ABB66E4031DDAE7CBDC7 /* Pods-DutchNewsUITests.debug.xcconfig */, + DD3A1F2B03FE42DB1EAECC2E /* Pods-DutchNewsUITests.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -292,7 +296,6 @@ F89B0230250D446200B41293 /* Sources */, F89B0231250D446200B41293 /* Frameworks */, F89B0232250D446200B41293 /* Resources */, - 1FD7F41EF9A054827A1D8B78 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -375,23 +378,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 1FD7F41EF9A054827A1D8B78 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-DutchNews-DutchNewsUITests/Pods-DutchNews-DutchNewsUITests-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-DutchNews-DutchNewsUITests/Pods-DutchNews-DutchNewsUITests-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-DutchNews-DutchNewsUITests/Pods-DutchNews-DutchNewsUITests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 2A72020948C2D75438B5DA75 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -441,7 +427,7 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-DutchNews-DutchNewsUITests-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-DutchNewsUITests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -756,7 +742,7 @@ }; F89B0244250D446200B41293 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 4F28822A80945682344AAC12 /* Pods-DutchNews-DutchNewsUITests.debug.xcconfig */; + baseConfigurationReference = 58F7ABB66E4031DDAE7CBDC7 /* Pods-DutchNewsUITests.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; @@ -777,7 +763,7 @@ }; F89B0245250D446200B41293 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 60D3AE4F11A8491BCCC459D7 /* Pods-DutchNews-DutchNewsUITests.release.xcconfig */; + baseConfigurationReference = DD3A1F2B03FE42DB1EAECC2E /* Pods-DutchNewsUITests.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; diff --git a/DutchNews/AppDelegate.swift b/DutchNews/AppDelegate.swift index 75d4c99..f57900a 100644 --- a/DutchNews/AppDelegate.swift +++ b/DutchNews/AppDelegate.swift @@ -16,7 +16,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. - UICollectionViewDiffableDataSource + return true } diff --git a/DutchNewsUITests/DutchNewsUITests.swift b/DutchNewsUITests/DutchNewsUITests.swift index b8b7246..15886c9 100644 --- a/DutchNewsUITests/DutchNewsUITests.swift +++ b/DutchNewsUITests/DutchNewsUITests.swift @@ -7,6 +7,7 @@ // import XCTest +@testable import DutchNews class DutchNewsUITests: XCTestCase { @@ -33,7 +34,7 @@ class DutchNewsUITests: XCTestCase { } func testLaunchPerformance() { - if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { + if #available(iOS 11.0, *) { // This measures how long it takes to launch your application. measure(metrics: [XCTOSSignpostMetric.applicationLaunch]) { XCUIApplication().launch() diff --git a/Podfile b/Podfile index 97e2d9b..f484f07 100644 --- a/Podfile +++ b/Podfile @@ -1,9 +1,16 @@ # Uncomment the next line to define a global platform for your project platform :ios, '11.0' +# ignore all warnings from all dependencies +inhibit_all_warnings! + +# supportet swift versions +supports_swift_versions '>= 4.0' + +# Comment the next line if you don't want to use dynamic frameworks +use_frameworks! + target 'DutchNews' do - # Comment the next line if you don't want to use dynamic frameworks - use_frameworks! pod 'Alamofire' pod 'RxSwift' @@ -27,8 +34,9 @@ target 'DutchNews' do # Pods for testing end - target 'DutchNewsUITests' do - # Pods for testing - end +end +target 'DutchNewsUITests' do + inherit! :search_paths + # Pods for testing end diff --git a/Podfile.lock b/Podfile.lock index 9e7a025..4c31b9e 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -85,6 +85,6 @@ SPEC CHECKSUMS: RxSwift: 81470a2074fa8780320ea5fe4102807cb7118178 RxTest: 711632d5644dffbeb62c936a521b5b008a1e1faa -PODFILE CHECKSUM: da3abe129fd0db3a54bf168fb3e15fa15dfef33c +PODFILE CHECKSUM: 756e5fa320a01e8d0e238bf0390bee87e2747652 COCOAPODS: 1.9.3 diff --git a/fastlane/Fastfile b/fastlane/Fastfile index a97dc94..a71df33 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -60,7 +60,7 @@ platform :ios do lane :run_ci_tests do |options| begin scan(workspace: workspace, - #scheme: scheme, # Project scheme name + scheme: scheme, # Project scheme name clean: true, # clean project folder before test execution devices: options[:devices], # Devices for testing result_bundle: "TestResults") # To generate test reports From 9453a8e214f4064f0137a3c32390ddcbb010b570 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Thu, 17 Sep 2020 22:33:15 +0430 Subject: [PATCH 008/108] - added swift lint configuration file and some pod libraries. --- .swiftlint.yml | 96 +++++++ Podfile | 10 +- Podfile.lock | 677 ++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 781 insertions(+), 2 deletions(-) create mode 100644 .swiftlint.yml diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..01318f2 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,96 @@ +disabled_rules: + - line_length + - valid_docs + - type_body_length + - operator_whitespace + - statement_position + - trailing_whitespace + - nesting + - cyclomatic_complexity + - function_parameter_count + - vertical_parameter_alignment + - comma + - control_statement + +opt_in_rules: + # - function_body_length + - colon + - type_name + - closure_spacing + - redundant_nil_coalescing + # - attributes + - operator_usage_whitespace + - closure_end_indentation + - first_where + - object_literal + - number_separator + - fatal_error_message + # - missing_docs + +excluded: # paths to ignore during linting. Takes precedence over `included`. + - Carthage + - Pods + - build + +force_cast: warning +force_try: warning +force_unwrapping: error + +number_separator: + minimum_length: 5 + +function_body_length: + max_length: + warning: 75 + error : 150 + min_length: + warning: 0 + +variable_name: + max_length: + warning: 40 + error: 50 + min_length: + error: 3 + excluded: + - row + - key + - id + - url + - uri + - URI + - URL + - in + - to + - vc + - vw + - red + - rgb + - GlobalAPIKey + - up + - x + - y + +custom_rules: + empty_first_line: # from https://github.com/brandenr/swiftlintconfig + name: "Empty First Line" + regex: '(^[ a-zA-Z ]*(?:protocol|extension|class|struct) (?!(?:var|let))[ a-zA-Z:]*\{\n *\S+)' + message: "There should be an empty line after a declaration" + severity: warning + empty_line_after_guard: # from https://github.com/brandenr/swiftlintconfig + name: "Empty Line After Guard" + regex: '(^ *guard[ a-zA-Z0-9=?.\(\),> 'https://github.com/farshadmb/JEKScrollableSectionCollectionViewLayout.git' pod 'MagazineLayout' - # Pods for DutchNews + pod 'RealmSwift' + pod 'MaterialComponents' + pod 'SwiftLint' + pod 'CryptoSwift', '1.1.2' + + #Logger Framework + pod 'CocoaLumberjack/Swift' target 'DutchNewsTests' do inherit! :search_paths diff --git a/Podfile.lock b/Podfile.lock index 4c31b9e..4961109 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,12 +1,661 @@ PODS: - Alamofire (5.2.1) + - CocoaLumberjack/Core (3.6.1) + - CocoaLumberjack/Swift (3.6.1): + - CocoaLumberjack/Core + - CryptoSwift (1.1.2) - Differentiator (4.0.1) - JEKScrollableSectionCollectionViewLayout (1.3.0) - MagazineLayout (1.6.2) + - MaterialComponents (109.8.0): + - MaterialComponents/ActionSheet (= 109.8.0) + - "MaterialComponents/ActionSheet+Theming (= 109.8.0)" + - MaterialComponents/ActivityIndicator (= 109.8.0) + - MaterialComponents/AnimationTiming (= 109.8.0) + - MaterialComponents/AppBar (= 109.8.0) + - "MaterialComponents/AppBar+ColorThemer (= 109.8.0)" + - "MaterialComponents/AppBar+Theming (= 109.8.0)" + - "MaterialComponents/AppBar+TypographyThemer (= 109.8.0)" + - MaterialComponents/Availability (= 109.8.0) + - MaterialComponents/Banner (= 109.8.0) + - "MaterialComponents/Banner+Theming (= 109.8.0)" + - MaterialComponents/BottomAppBar (= 109.8.0) + - MaterialComponents/BottomNavigation (= 109.8.0) + - "MaterialComponents/BottomNavigation+Theming (= 109.8.0)" + - MaterialComponents/BottomSheet (= 109.8.0) + - "MaterialComponents/BottomSheet+ShapeThemer (= 109.8.0)" + - MaterialComponents/ButtonBar (= 109.8.0) + - MaterialComponents/Buttons (= 109.8.0) + - "MaterialComponents/Buttons+ButtonThemer (= 109.8.0)" + - "MaterialComponents/Buttons+ColorThemer (= 109.8.0)" + - "MaterialComponents/Buttons+ShapeThemer (= 109.8.0)" + - "MaterialComponents/Buttons+Theming (= 109.8.0)" + - "MaterialComponents/Buttons+TitleColorAccessibilityMutator (= 109.8.0)" + - "MaterialComponents/Buttons+TypographyThemer (= 109.8.0)" + - MaterialComponents/Cards (= 109.8.0) + - "MaterialComponents/Cards+Theming (= 109.8.0)" + - MaterialComponents/Chips (= 109.8.0) + - "MaterialComponents/Chips+Theming (= 109.8.0)" + - MaterialComponents/CollectionCells (= 109.8.0) + - MaterialComponents/CollectionLayoutAttributes (= 109.8.0) + - MaterialComponents/Collections (= 109.8.0) + - MaterialComponents/Dialogs (= 109.8.0) + - "MaterialComponents/Dialogs+ColorThemer (= 109.8.0)" + - "MaterialComponents/Dialogs+Theming (= 109.8.0)" + - "MaterialComponents/Dialogs+TypographyThemer (= 109.8.0)" + - MaterialComponents/Elevation (= 109.8.0) + - MaterialComponents/FeatureHighlight (= 109.8.0) + - "MaterialComponents/FeatureHighlight+ColorThemer (= 109.8.0)" + - "MaterialComponents/FeatureHighlight+FeatureHighlightAccessibilityMutator (= 109.8.0)" + - MaterialComponents/FlexibleHeader (= 109.8.0) + - "MaterialComponents/FlexibleHeader+CanAlwaysExpandToMaximumHeight (= 109.8.0)" + - "MaterialComponents/FlexibleHeader+ShiftBehavior (= 109.8.0)" + - "MaterialComponents/FlexibleHeader+ShiftBehaviorEnabledWithStatusBar (= 109.8.0)" + - MaterialComponents/HeaderStackView (= 109.8.0) + - "MaterialComponents/HeaderStackView+ColorThemer (= 109.8.0)" + - MaterialComponents/Ink (= 109.8.0) + - MaterialComponents/LibraryInfo (= 109.8.0) + - MaterialComponents/List (= 109.8.0) + - "MaterialComponents/List+Theming (= 109.8.0)" + - MaterialComponents/NavigationBar (= 109.8.0) + - "MaterialComponents/NavigationBar+ColorThemer (= 109.8.0)" + - "MaterialComponents/NavigationBar+TypographyThemer (= 109.8.0)" + - MaterialComponents/NavigationDrawer (= 109.8.0) + - "MaterialComponents/NavigationDrawer+ColorThemer (= 109.8.0)" + - "MaterialComponents/NavigationDrawer+Theming (= 109.8.0)" + - MaterialComponents/OverlayWindow (= 109.8.0) + - MaterialComponents/PageControl (= 109.8.0) + - MaterialComponents/Palettes (= 109.8.0) + - MaterialComponents/private (= 109.8.0) + - MaterialComponents/ProgressView (= 109.8.0) + - "MaterialComponents/ProgressView+Theming (= 109.8.0)" + - MaterialComponents/Ripple (= 109.8.0) + - MaterialComponents/schemes (= 109.8.0) + - MaterialComponents/ShadowElevations (= 109.8.0) + - MaterialComponents/ShadowLayer (= 109.8.0) + - MaterialComponents/ShapeLibrary (= 109.8.0) + - MaterialComponents/Shapes (= 109.8.0) + - MaterialComponents/Slider (= 109.8.0) + - "MaterialComponents/Slider+ColorThemer (= 109.8.0)" + - MaterialComponents/Snackbar (= 109.8.0) + - "MaterialComponents/Snackbar+FontThemer (= 109.8.0)" + - "MaterialComponents/Snackbar+TypographyThemer (= 109.8.0)" + - MaterialComponents/Tabs (= 109.8.0) + - "MaterialComponents/Tabs+Theming (= 109.8.0)" + - "MaterialComponents/Tabs+TypographyThemer (= 109.8.0)" + - "MaterialComponents/TextControls+BaseTextAreas (= 109.8.0)" + - "MaterialComponents/TextControls+BaseTextFields (= 109.8.0)" + - "MaterialComponents/TextControls+Enums (= 109.8.0)" + - "MaterialComponents/TextControls+FilledTextAreas (= 109.8.0)" + - "MaterialComponents/TextControls+FilledTextAreasTheming (= 109.8.0)" + - "MaterialComponents/TextControls+FilledTextFields (= 109.8.0)" + - "MaterialComponents/TextControls+FilledTextFieldsTheming (= 109.8.0)" + - "MaterialComponents/TextControls+OutlinedTextAreas (= 109.8.0)" + - "MaterialComponents/TextControls+OutlinedTextAreasTheming (= 109.8.0)" + - "MaterialComponents/TextControls+OutlinedTextFields (= 109.8.0)" + - "MaterialComponents/TextControls+OutlinedTextFieldsTheming (= 109.8.0)" + - "MaterialComponents/TextControls+UnderlinedTextFields (= 109.8.0)" + - "MaterialComponents/TextControls+UnderlinedTextFieldsTheming (= 109.8.0)" + - MaterialComponents/TextFields (= 109.8.0) + - "MaterialComponents/TextFields+ColorThemer (= 109.8.0)" + - "MaterialComponents/TextFields+Theming (= 109.8.0)" + - MaterialComponents/Themes (= 109.8.0) + - MaterialComponents/Typography (= 109.8.0) + - MaterialComponents/ActionSheet (109.8.0): + - MaterialComponents/Availability + - MaterialComponents/BottomSheet + - MaterialComponents/Elevation + - MaterialComponents/Ink + - MaterialComponents/private/Math + - MaterialComponents/Ripple + - MaterialComponents/ShadowElevations + - MaterialComponents/Typography + - "MaterialComponents/ActionSheet+Theming (109.8.0)": + - MaterialComponents/ActionSheet + - MaterialComponents/Availability + - MaterialComponents/Elevation + - MaterialComponents/private/Color + - MaterialComponents/schemes/Container + - MaterialComponents/ShadowElevations + - MaterialComponents/ActivityIndicator (109.8.0): + - MaterialComponents/Palettes + - MaterialComponents/private/Application + - MDFInternationalization + - MotionAnimator (~> 2.0) + - MaterialComponents/AnimationTiming (109.8.0) + - MaterialComponents/AppBar (109.8.0): + - MaterialComponents/FlexibleHeader + - MaterialComponents/HeaderStackView + - MaterialComponents/NavigationBar + - MaterialComponents/private/Application + - MaterialComponents/private/Icons/ic_arrow_back + - MaterialComponents/private/UIMetrics + - MaterialComponents/ShadowElevations + - MaterialComponents/ShadowLayer + - MaterialComponents/Typography + - MDFInternationalization + - "MaterialComponents/AppBar+ColorThemer (109.8.0)": + - MaterialComponents/AppBar + - "MaterialComponents/NavigationBar+ColorThemer" + - MaterialComponents/Themes + - "MaterialComponents/AppBar+Theming (109.8.0)": + - MaterialComponents/AppBar + - MaterialComponents/schemes/Container + - "MaterialComponents/AppBar+TypographyThemer (109.8.0)": + - MaterialComponents/AppBar + - "MaterialComponents/NavigationBar+TypographyThemer" + - MaterialComponents/Availability (109.8.0) + - MaterialComponents/Banner (109.8.0): + - MaterialComponents/Availability + - MaterialComponents/Buttons + - MaterialComponents/Elevation + - MaterialComponents/Typography + - "MaterialComponents/Banner+Theming (109.8.0)": + - MaterialComponents/Banner + - MaterialComponents/Buttons + - "MaterialComponents/Buttons+Theming" + - MaterialComponents/Elevation + - MaterialComponents/schemes/Container + - MaterialComponents/Typography + - MaterialComponents/BottomAppBar (109.8.0): + - MaterialComponents/Buttons + - MaterialComponents/NavigationBar + - MaterialComponents/private/Math + - MDFInternationalization + - MaterialComponents/BottomNavigation (109.8.0): + - MaterialComponents/Availability + - MaterialComponents/Elevation + - MaterialComponents/Ink + - MaterialComponents/Palettes + - MaterialComponents/private/Math + - MaterialComponents/Ripple + - MaterialComponents/ShadowElevations + - MaterialComponents/ShadowLayer + - MaterialComponents/Typography + - MDFInternationalization + - "MaterialComponents/BottomNavigation+Theming (109.8.0)": + - MaterialComponents/BottomNavigation + - MaterialComponents/schemes/Color + - MaterialComponents/schemes/Container + - MaterialComponents/schemes/Typography + - MaterialComponents/ShadowElevations + - MaterialComponents/BottomSheet (109.8.0): + - MaterialComponents/Elevation + - MaterialComponents/private/KeyboardWatcher + - MaterialComponents/private/Math + - MaterialComponents/ShadowElevations + - MaterialComponents/ShadowLayer + - MaterialComponents/ShapeLibrary + - MaterialComponents/Shapes + - "MaterialComponents/BottomSheet+ShapeThemer (109.8.0)": + - MaterialComponents/BottomSheet + - MaterialComponents/schemes/Shape + - MaterialComponents/ButtonBar (109.8.0): + - MaterialComponents/Buttons + - MaterialComponents/private/Application + - MDFInternationalization + - MaterialComponents/Buttons (109.8.0): + - MaterialComponents/Elevation + - MaterialComponents/Ink + - MaterialComponents/private/Math + - MaterialComponents/Ripple + - MaterialComponents/ShadowElevations + - MaterialComponents/ShadowLayer + - MaterialComponents/ShapeLibrary + - MaterialComponents/Shapes + - MaterialComponents/Typography + - MDFInternationalization + - MDFTextAccessibility + - "MaterialComponents/Buttons+ButtonThemer (109.8.0)": + - MaterialComponents/Buttons + - "MaterialComponents/Buttons+ColorThemer" + - "MaterialComponents/Buttons+ShapeThemer" + - "MaterialComponents/Buttons+TypographyThemer" + - MaterialComponents/Palettes + - "MaterialComponents/Buttons+ColorThemer (109.8.0)": + - MaterialComponents/Buttons + - MaterialComponents/schemes/Color + - "MaterialComponents/Buttons+ShapeThemer (109.8.0)": + - MaterialComponents/Buttons + - MaterialComponents/schemes/Shape + - "MaterialComponents/Buttons+Theming (109.8.0)": + - MaterialComponents/Buttons + - "MaterialComponents/Buttons+ColorThemer" + - "MaterialComponents/Buttons+ShapeThemer" + - "MaterialComponents/Buttons+TypographyThemer" + - MaterialComponents/schemes/Container + - MaterialComponents/ShadowElevations + - "MaterialComponents/Buttons+TitleColorAccessibilityMutator (109.8.0)": + - MaterialComponents/Buttons + - MDFTextAccessibility + - "MaterialComponents/Buttons+TypographyThemer (109.8.0)": + - MaterialComponents/Buttons + - MaterialComponents/schemes/Typography + - MaterialComponents/Cards (109.8.0): + - MaterialComponents/Elevation + - MaterialComponents/Ink + - MaterialComponents/private/Icons/ic_check_circle + - MaterialComponents/private/Math + - MaterialComponents/Ripple + - MaterialComponents/ShadowLayer + - MaterialComponents/Shapes + - "MaterialComponents/Cards+Theming (109.8.0)": + - MaterialComponents/Cards + - MaterialComponents/schemes/Container + - MaterialComponents/Chips (109.8.0): + - MaterialComponents/Elevation + - MaterialComponents/Ink + - MaterialComponents/private/Math + - MaterialComponents/Ripple + - MaterialComponents/ShadowElevations + - MaterialComponents/ShadowLayer + - MaterialComponents/ShapeLibrary + - MaterialComponents/Shapes + - MaterialComponents/TextFields + - MaterialComponents/Typography + - MDFInternationalization + - "MaterialComponents/Chips+Theming (109.8.0)": + - MaterialComponents/Chips + - MaterialComponents/schemes/Container + - MaterialComponents/Typography + - MaterialComponents/CollectionCells (109.8.0): + - MaterialComponents/CollectionLayoutAttributes + - MaterialComponents/Ink + - MaterialComponents/Palettes + - MaterialComponents/private/Icons/ic_check + - MaterialComponents/private/Icons/ic_check_circle + - MaterialComponents/private/Icons/ic_chevron_right + - MaterialComponents/private/Icons/ic_info + - MaterialComponents/private/Icons/ic_radio_button_unchecked + - MaterialComponents/private/Icons/ic_reorder + - MaterialComponents/private/Math + - MaterialComponents/Typography + - MDFInternationalization + - MaterialComponents/CollectionLayoutAttributes (109.8.0) + - MaterialComponents/Collections (109.8.0): + - MaterialComponents/Availability + - MaterialComponents/CollectionCells + - MaterialComponents/CollectionLayoutAttributes + - MaterialComponents/Ink + - MaterialComponents/Palettes + - MaterialComponents/ShadowElevations + - MaterialComponents/ShadowLayer + - MaterialComponents/Typography + - MaterialComponents/Dialogs (109.8.0): + - MaterialComponents/Buttons + - MaterialComponents/Elevation + - MaterialComponents/private/KeyboardWatcher + - MaterialComponents/private/Math + - MaterialComponents/ShadowElevations + - MaterialComponents/ShadowLayer + - MaterialComponents/Typography + - MDFInternationalization + - "MaterialComponents/Dialogs+ColorThemer (109.8.0)": + - "MaterialComponents/Buttons+ColorThemer" + - MaterialComponents/Dialogs + - MaterialComponents/Themes + - "MaterialComponents/Dialogs+Theming (109.8.0)": + - "MaterialComponents/Buttons+Theming" + - MaterialComponents/Dialogs + - "MaterialComponents/Dialogs+ColorThemer" + - "MaterialComponents/Dialogs+TypographyThemer" + - MaterialComponents/schemes/Container + - MaterialComponents/ShadowElevations + - "MaterialComponents/Dialogs+TypographyThemer (109.8.0)": + - "MaterialComponents/Buttons+TypographyThemer" + - MaterialComponents/Dialogs + - MaterialComponents/schemes/Typography + - MaterialComponents/Elevation (109.8.0): + - MaterialComponents/Availability + - MaterialComponents/private/Color + - MaterialComponents/private/Math + - MaterialComponents/FeatureHighlight (109.8.0): + - MaterialComponents/Availability + - MaterialComponents/private/Math + - MaterialComponents/Typography + - MDFTextAccessibility + - "MaterialComponents/FeatureHighlight+ColorThemer (109.8.0)": + - MaterialComponents/FeatureHighlight + - MaterialComponents/Themes + - "MaterialComponents/FeatureHighlight+FeatureHighlightAccessibilityMutator (109.8.0)": + - MaterialComponents/FeatureHighlight + - MDFTextAccessibility + - MaterialComponents/FlexibleHeader (109.8.0): + - MaterialComponents/Availability + - MaterialComponents/Elevation + - "MaterialComponents/FlexibleHeader+ShiftBehavior" + - "MaterialComponents/FlexibleHeader+ShiftBehaviorEnabledWithStatusBar" + - MaterialComponents/private/Application + - MaterialComponents/private/Math + - MaterialComponents/private/UIMetrics + - MaterialComponents/ShadowElevations + - MaterialComponents/ShadowLayer + - MDFTextAccessibility + - "MaterialComponents/FlexibleHeader+CanAlwaysExpandToMaximumHeight (109.8.0)": + - MaterialComponents/FlexibleHeader + - "MaterialComponents/FlexibleHeader+ShiftBehavior (109.8.0)" + - "MaterialComponents/FlexibleHeader+ShiftBehaviorEnabledWithStatusBar (109.8.0)": + - "MaterialComponents/FlexibleHeader+ShiftBehavior" + - MaterialComponents/HeaderStackView (109.8.0) + - "MaterialComponents/HeaderStackView+ColorThemer (109.8.0)": + - MaterialComponents/HeaderStackView + - MaterialComponents/Themes + - MaterialComponents/Ink (109.8.0): + - MaterialComponents/Availability + - MaterialComponents/private/Color + - MaterialComponents/private/Math + - MaterialComponents/LibraryInfo (109.8.0) + - MaterialComponents/List (109.8.0): + - MaterialComponents/Elevation + - MaterialComponents/Ink + - MaterialComponents/private/Math + - MaterialComponents/Ripple + - MaterialComponents/ShadowElevations + - MaterialComponents/ShadowLayer + - MaterialComponents/Typography + - MDFInternationalization + - "MaterialComponents/List+Theming (109.8.0)": + - MaterialComponents/List + - MaterialComponents/schemes/Container + - MaterialComponents/NavigationBar (109.8.0): + - MaterialComponents/ButtonBar + - MaterialComponents/Elevation + - MaterialComponents/private/Math + - MaterialComponents/Typography + - MDFInternationalization + - MDFTextAccessibility + - "MaterialComponents/NavigationBar+ColorThemer (109.8.0)": + - MaterialComponents/NavigationBar + - MaterialComponents/schemes/Color + - "MaterialComponents/NavigationBar+TypographyThemer (109.8.0)": + - MaterialComponents/NavigationBar + - MaterialComponents/schemes/Typography + - MaterialComponents/NavigationDrawer (109.8.0): + - MaterialComponents/Elevation + - MaterialComponents/Palettes + - MaterialComponents/private/Math + - MaterialComponents/private/UIMetrics + - MaterialComponents/ShadowLayer + - "MaterialComponents/NavigationDrawer+ColorThemer (109.8.0)": + - MaterialComponents/NavigationDrawer + - MaterialComponents/schemes/Color + - "MaterialComponents/NavigationDrawer+Theming (109.8.0)": + - MaterialComponents/NavigationDrawer + - MaterialComponents/schemes/Container + - MaterialComponents/OverlayWindow (109.8.0): + - MaterialComponents/Availability + - MaterialComponents/private/Application + - MaterialComponents/PageControl (109.8.0): + - MDFInternationalization + - MaterialComponents/Palettes (109.8.0) + - MaterialComponents/private (109.8.0): + - MaterialComponents/private/Application (= 109.8.0) + - MaterialComponents/private/Color (= 109.8.0) + - MaterialComponents/private/Icons (= 109.8.0) + - MaterialComponents/private/KeyboardWatcher (= 109.8.0) + - MaterialComponents/private/Math (= 109.8.0) + - MaterialComponents/private/Overlay (= 109.8.0) + - "MaterialComponents/private/TextControlsPrivate+BaseStyle (= 109.8.0)" + - "MaterialComponents/private/TextControlsPrivate+FilledStyle (= 109.8.0)" + - "MaterialComponents/private/TextControlsPrivate+OutlinedStyle (= 109.8.0)" + - "MaterialComponents/private/TextControlsPrivate+Shared (= 109.8.0)" + - "MaterialComponents/private/TextControlsPrivate+TextFields (= 109.8.0)" + - "MaterialComponents/private/TextControlsPrivate+UnderlinedStyle (= 109.8.0)" + - MaterialComponents/private/ThumbTrack (= 109.8.0) + - MaterialComponents/private/UIMetrics (= 109.8.0) + - MaterialComponents/private/Application (109.8.0) + - MaterialComponents/private/Color (109.8.0): + - MaterialComponents/Availability + - MaterialComponents/private/Icons (109.8.0): + - MaterialComponents/private/Icons/Base (= 109.8.0) + - MaterialComponents/private/Icons/ic_arrow_back (= 109.8.0) + - MaterialComponents/private/Icons/ic_check (= 109.8.0) + - MaterialComponents/private/Icons/ic_check_circle (= 109.8.0) + - MaterialComponents/private/Icons/ic_chevron_right (= 109.8.0) + - MaterialComponents/private/Icons/ic_color_lens (= 109.8.0) + - MaterialComponents/private/Icons/ic_help_outline (= 109.8.0) + - MaterialComponents/private/Icons/ic_info (= 109.8.0) + - MaterialComponents/private/Icons/ic_more_horiz (= 109.8.0) + - MaterialComponents/private/Icons/ic_radio_button_unchecked (= 109.8.0) + - MaterialComponents/private/Icons/ic_reorder (= 109.8.0) + - MaterialComponents/private/Icons/ic_settings (= 109.8.0) + - MaterialComponents/private/Icons/Base (109.8.0) + - MaterialComponents/private/Icons/ic_arrow_back (109.8.0): + - MaterialComponents/private/Icons/Base + - MaterialComponents/private/Icons/ic_check (109.8.0): + - MaterialComponents/private/Icons/Base + - MaterialComponents/private/Icons/ic_check_circle (109.8.0): + - MaterialComponents/private/Icons/Base + - MaterialComponents/private/Icons/ic_chevron_right (109.8.0): + - MaterialComponents/private/Icons/Base + - MaterialComponents/private/Icons/ic_color_lens (109.8.0): + - MaterialComponents/private/Icons/Base + - MaterialComponents/private/Icons/ic_help_outline (109.8.0): + - MaterialComponents/private/Icons/Base + - MaterialComponents/private/Icons/ic_info (109.8.0): + - MaterialComponents/private/Icons/Base + - MaterialComponents/private/Icons/ic_more_horiz (109.8.0): + - MaterialComponents/private/Icons/Base + - MaterialComponents/private/Icons/ic_radio_button_unchecked (109.8.0): + - MaterialComponents/private/Icons/Base + - MaterialComponents/private/Icons/ic_reorder (109.8.0): + - MaterialComponents/private/Icons/Base + - MaterialComponents/private/Icons/ic_settings (109.8.0): + - MaterialComponents/private/Icons/Base + - MaterialComponents/private/KeyboardWatcher (109.8.0): + - MaterialComponents/private/Application + - MaterialComponents/private/Math (109.8.0) + - MaterialComponents/private/Overlay (109.8.0) + - "MaterialComponents/private/TextControlsPrivate+BaseStyle (109.8.0)": + - MaterialComponents/AnimationTiming + - MaterialComponents/private/Math + - "MaterialComponents/private/TextControlsPrivate+Shared" + - "MaterialComponents/private/TextControlsPrivate+FilledStyle (109.8.0)": + - MaterialComponents/AnimationTiming + - MaterialComponents/Availability + - MaterialComponents/private/Math + - "MaterialComponents/private/TextControlsPrivate+Shared" + - "MaterialComponents/private/TextControlsPrivate+UnderlinedStyle" + - "MaterialComponents/private/TextControlsPrivate+OutlinedStyle (109.8.0)": + - MaterialComponents/AnimationTiming + - MaterialComponents/Availability + - MaterialComponents/private/Math + - "MaterialComponents/private/TextControlsPrivate+Shared" + - "MaterialComponents/private/TextControlsPrivate+Shared (109.8.0)": + - MaterialComponents/AnimationTiming + - MaterialComponents/private/Math + - "MaterialComponents/TextControls+Enums" + - "MaterialComponents/private/TextControlsPrivate+TextFields (109.8.0)": + - MaterialComponents/private/Math + - "MaterialComponents/private/TextControlsPrivate+Shared" + - "MaterialComponents/private/TextControlsPrivate+UnderlinedStyle (109.8.0)": + - MaterialComponents/AnimationTiming + - MaterialComponents/Availability + - MaterialComponents/private/Math + - "MaterialComponents/private/TextControlsPrivate+Shared" + - MaterialComponents/private/ThumbTrack (109.8.0): + - MaterialComponents/Availability + - MaterialComponents/Ink + - MaterialComponents/private/Math + - MaterialComponents/Ripple + - MaterialComponents/ShadowElevations + - MaterialComponents/ShadowLayer + - MaterialComponents/Typography + - MDFInternationalization + - MaterialComponents/private/UIMetrics (109.8.0): + - MaterialComponents/private/Application + - MaterialComponents/ProgressView (109.8.0): + - MaterialComponents/Palettes + - MaterialComponents/private/Math + - MDFInternationalization + - "MaterialComponents/ProgressView+Theming (109.8.0)": + - MaterialComponents/ProgressView + - MaterialComponents/schemes/Container + - MaterialComponents/Ripple (109.8.0): + - MaterialComponents/AnimationTiming + - MaterialComponents/Availability + - MaterialComponents/private/Color + - MaterialComponents/private/Math + - MaterialComponents/schemes (109.8.0): + - MaterialComponents/schemes/Color (= 109.8.0) + - MaterialComponents/schemes/Container (= 109.8.0) + - MaterialComponents/schemes/Shape (= 109.8.0) + - MaterialComponents/schemes/Typography (= 109.8.0) + - "MaterialComponents/schemes/Typography+BasicFontScheme (= 109.8.0)" + - "MaterialComponents/schemes/Typography+Scheming (= 109.8.0)" + - MaterialComponents/schemes/Color (109.8.0): + - MaterialComponents/Availability + - MaterialComponents/private/Color + - MaterialComponents/schemes/Container (109.8.0): + - MaterialComponents/schemes/Color + - MaterialComponents/schemes/Shape + - MaterialComponents/schemes/Typography + - MaterialComponents/schemes/Shape (109.8.0): + - MaterialComponents/ShapeLibrary + - MaterialComponents/Shapes + - MaterialComponents/schemes/Typography (109.8.0): + - "MaterialComponents/schemes/Typography+BasicFontScheme" + - "MaterialComponents/schemes/Typography+Scheming" + - MaterialComponents/Typography + - "MaterialComponents/schemes/Typography+BasicFontScheme (109.8.0)" + - "MaterialComponents/schemes/Typography+Scheming (109.8.0)" + - MaterialComponents/ShadowElevations (109.8.0) + - MaterialComponents/ShadowLayer (109.8.0): + - MaterialComponents/ShadowElevations + - MaterialComponents/ShapeLibrary (109.8.0): + - MaterialComponents/private/Math + - MaterialComponents/Shapes + - MaterialComponents/Shapes (109.8.0): + - MaterialComponents/private/Color + - MaterialComponents/private/Math + - MaterialComponents/ShadowLayer + - MaterialComponents/Slider (109.8.0): + - MaterialComponents/Elevation + - MaterialComponents/Palettes + - MaterialComponents/private/Math + - MaterialComponents/private/ThumbTrack + - MaterialComponents/ShadowElevations + - "MaterialComponents/Slider+ColorThemer (109.8.0)": + - MaterialComponents/Palettes + - MaterialComponents/schemes/Color + - MaterialComponents/Slider + - MaterialComponents/Snackbar (109.8.0): + - MaterialComponents/AnimationTiming + - MaterialComponents/Availability + - MaterialComponents/Buttons + - MaterialComponents/Elevation + - MaterialComponents/OverlayWindow + - MaterialComponents/private/Application + - MaterialComponents/private/KeyboardWatcher + - MaterialComponents/private/Math + - MaterialComponents/private/Overlay + - MaterialComponents/ShadowElevations + - MaterialComponents/ShadowLayer + - MaterialComponents/Typography + - "MaterialComponents/Snackbar+FontThemer (109.8.0)": + - MaterialComponents/Snackbar + - MaterialComponents/Themes + - "MaterialComponents/Snackbar+TypographyThemer (109.8.0)": + - MaterialComponents/schemes/Typography + - MaterialComponents/Snackbar + - MaterialComponents/Tabs (109.8.0): + - MaterialComponents/AnimationTiming + - MaterialComponents/Elevation + - MaterialComponents/Ink + - MaterialComponents/Palettes + - MaterialComponents/private/Math + - MaterialComponents/Ripple + - MaterialComponents/ShadowElevations + - MaterialComponents/ShadowLayer + - MaterialComponents/Typography + - MDFInternationalization + - "MaterialComponents/Tabs+Theming (109.8.0)": + - MaterialComponents/schemes/Container + - MaterialComponents/Tabs + - "MaterialComponents/Tabs+TypographyThemer" + - "MaterialComponents/Tabs+TypographyThemer (109.8.0)": + - MaterialComponents/schemes/Typography + - MaterialComponents/Tabs + - "MaterialComponents/TextControls+BaseTextAreas (109.8.0)": + - "MaterialComponents/private/TextControlsPrivate+BaseStyle" + - "MaterialComponents/private/TextControlsPrivate+Shared" + - MDFInternationalization + - "MaterialComponents/TextControls+BaseTextFields (109.8.0)": + - "MaterialComponents/private/TextControlsPrivate+BaseStyle" + - "MaterialComponents/private/TextControlsPrivate+Shared" + - "MaterialComponents/private/TextControlsPrivate+TextFields" + - MDFInternationalization + - "MaterialComponents/TextControls+Enums (109.8.0)" + - "MaterialComponents/TextControls+FilledTextAreas (109.8.0)": + - MaterialComponents/Availability + - "MaterialComponents/private/TextControlsPrivate+FilledStyle" + - "MaterialComponents/TextControls+BaseTextAreas" + - "MaterialComponents/TextControls+FilledTextAreasTheming (109.8.0)": + - MaterialComponents/schemes/Container + - "MaterialComponents/TextControls+FilledTextAreas" + - "MaterialComponents/TextControls+FilledTextFields (109.8.0)": + - MaterialComponents/Availability + - "MaterialComponents/private/TextControlsPrivate+FilledStyle" + - "MaterialComponents/TextControls+BaseTextFields" + - "MaterialComponents/TextControls+FilledTextFieldsTheming (109.8.0)": + - MaterialComponents/schemes/Container + - "MaterialComponents/TextControls+FilledTextFields" + - "MaterialComponents/TextControls+OutlinedTextAreas (109.8.0)": + - MaterialComponents/Availability + - "MaterialComponents/private/TextControlsPrivate+OutlinedStyle" + - "MaterialComponents/TextControls+BaseTextAreas" + - "MaterialComponents/TextControls+OutlinedTextAreasTheming (109.8.0)": + - MaterialComponents/schemes/Container + - "MaterialComponents/TextControls+OutlinedTextAreas" + - "MaterialComponents/TextControls+OutlinedTextFields (109.8.0)": + - MaterialComponents/Availability + - "MaterialComponents/private/TextControlsPrivate+OutlinedStyle" + - "MaterialComponents/TextControls+BaseTextFields" + - "MaterialComponents/TextControls+OutlinedTextFieldsTheming (109.8.0)": + - MaterialComponents/schemes/Container + - "MaterialComponents/TextControls+OutlinedTextFields" + - "MaterialComponents/TextControls+UnderlinedTextFields (109.8.0)": + - MaterialComponents/Availability + - "MaterialComponents/private/TextControlsPrivate+UnderlinedStyle" + - "MaterialComponents/TextControls+BaseTextFields" + - "MaterialComponents/TextControls+UnderlinedTextFieldsTheming (109.8.0)": + - MaterialComponents/schemes/Container + - "MaterialComponents/TextControls+UnderlinedTextFields" + - MaterialComponents/TextFields (109.8.0): + - MaterialComponents/AnimationTiming + - MaterialComponents/Buttons + - MaterialComponents/Elevation + - MaterialComponents/Palettes + - MaterialComponents/private/Math + - MaterialComponents/Typography + - MDFInternationalization + - "MaterialComponents/TextFields+ColorThemer (109.8.0)": + - MaterialComponents/TextFields + - MaterialComponents/Themes + - "MaterialComponents/TextFields+Theming (109.8.0)": + - MaterialComponents/schemes/Container + - MaterialComponents/TextFields + - "MaterialComponents/TextFields+ColorThemer" + - MaterialComponents/Themes (109.8.0): + - MaterialComponents/schemes/Color + - MaterialComponents/schemes/Typography + - MaterialComponents/Typography (109.8.0): + - MaterialComponents/private/Application + - MaterialComponents/private/Math + - MDFInternationalization (2.0.0) + - MDFTextAccessibility (2.0.0) + - MotionAnimator (2.8.1): + - MotionInterchange (~> 1.6) + - MotionInterchange (1.6.0) - MXParallaxHeader (1.1.0) - Nimble (8.1.2) - Pageboy (3.6.1) - PureLayout (3.1.6) + - Realm (5.1.0): + - Realm/Headers (= 5.1.0) + - Realm/Headers (5.1.0) + - RealmSwift (5.1.0): + - Realm (= 5.1.0) - RxAlamofire (5.5.0): - RxAlamofire/Core (= 5.5.0) - RxAlamofire/Core (5.5.0): @@ -26,31 +675,46 @@ PODS: - RxSwift (5.1.1) - RxTest (5.1.1): - RxSwift (~> 5) + - SwiftLint (0.39.2) DEPENDENCIES: - Alamofire + - CocoaLumberjack/Swift + - CryptoSwift (= 1.1.2) - JEKScrollableSectionCollectionViewLayout (from `https://github.com/farshadmb/JEKScrollableSectionCollectionViewLayout.git`) - MagazineLayout + - MaterialComponents - MXParallaxHeader - Nimble - Pageboy - PureLayout + - RealmSwift - RxAlamofire - RxBlocking - RxCocoa - RxDataSources - RxSwift - RxTest + - SwiftLint SPEC REPOS: trunk: - Alamofire + - CocoaLumberjack + - CryptoSwift - Differentiator - MagazineLayout + - MaterialComponents + - MDFInternationalization + - MDFTextAccessibility + - MotionAnimator + - MotionInterchange - MXParallaxHeader - Nimble - Pageboy - PureLayout + - Realm + - RealmSwift - RxAlamofire - RxBlocking - RxCocoa @@ -58,6 +722,7 @@ SPEC REPOS: - RxRelay - RxSwift - RxTest + - SwiftLint EXTERNAL SOURCES: JEKScrollableSectionCollectionViewLayout: @@ -70,13 +735,22 @@ CHECKOUT OPTIONS: SPEC CHECKSUMS: Alamofire: e911732990610fe89af59ac0077f923d72dc3dfd + CocoaLumberjack: b17ae15142558d08bbacf69775fa10c4abbebcc9 + CryptoSwift: 31dacd1f13427439ddae5b5cbaae4c8dbc43047e Differentiator: 886080237d9f87f322641dedbc5be257061b0602 JEKScrollableSectionCollectionViewLayout: 80def4834e535880029917c374324ef8b089c448 MagazineLayout: 8e995730bc2b1ff8f11f44cb7d7926ab9640892f + MaterialComponents: 00df0652f52cd6968b02d531bd2e6956b0f907b8 + MDFInternationalization: 010097556d6b09d2c4ea38e0820ea6d37be6a314 + MDFTextAccessibility: 85c09a1bd9c321f494348e632a25063bcda35a53 + MotionAnimator: ee16aa30567c5bae0fb2750c132915829cfaaf8a + MotionInterchange: ead0e3ae1f3a5fb539e289debbc7ae036160a10d MXParallaxHeader: de3c867e10ba46e8f6e20c8ee1f2a910372b3b94 Nimble: 3864815b4703c7ebffba875973c70e854489fbae Pageboy: 29a2d474ad99404b4d77f325e0ab6d705930a4fb PureLayout: bd3c4ec3a3819ad387c99ebb72c6b129c3ed4d2d + Realm: bdea546851e37b4cf0a2400cef115c7c26fda488 + RealmSwift: 70945aa168db93b215c460e9c8ef680261aa28af RxAlamofire: 22287c710761466d0123504c566a8381520d4d63 RxBlocking: 5f700a78cad61ce253ebd37c9a39b5ccc76477b4 RxCocoa: 32065309a38d29b5b0db858819b5bf9ef038b601 @@ -84,7 +758,8 @@ SPEC CHECKSUMS: RxRelay: d77f7d771495f43c556cbc43eebd1bb54d01e8e9 RxSwift: 81470a2074fa8780320ea5fe4102807cb7118178 RxTest: 711632d5644dffbeb62c936a521b5b008a1e1faa + SwiftLint: 22ccbbe3b8008684be5955693bab135e0ed6a447 -PODFILE CHECKSUM: 756e5fa320a01e8d0e238bf0390bee87e2747652 +PODFILE CHECKSUM: 2c16ebee96276ba1c25ed4d13e31ea42a8fe65ed COCOAPODS: 1.9.3 From f64e871e06f57bc04e77a25fb6bfe08c9760e15a Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Thu, 17 Sep 2020 23:13:11 +0430 Subject: [PATCH 009/108] - fastlane scan configuration was done. --- DutchNews.xcodeproj/project.pbxproj | 3 +++ Podfile | 16 ++++++++-------- Podfile.lock | 2 +- fastlane/Fastfile | 7 +++++-- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index 94cad93..5751f39 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -319,13 +319,16 @@ TargetAttributes = { F89B0219250D446000B41293 = { CreatedOnToolsVersion = 11.2.1; + ProvisioningStyle = Automatic; }; F89B0228250D446200B41293 = { CreatedOnToolsVersion = 11.2.1; + ProvisioningStyle = Automatic; TestTargetID = F89B0219250D446000B41293; }; F89B0233250D446200B41293 = { CreatedOnToolsVersion = 11.2.1; + ProvisioningStyle = Automatic; TestTargetID = F89B0219250D446000B41293; }; }; diff --git a/Podfile b/Podfile index 1069952..838a40b 100644 --- a/Podfile +++ b/Podfile @@ -33,15 +33,15 @@ target 'DutchNews' do #Logger Framework pod 'CocoaLumberjack/Swift' - target 'DutchNewsTests' do - inherit! :search_paths - pod 'RxTest' - pod 'RxBlocking' - pod 'Nimble' - - # Pods for testing - end +end +target 'DutchNewsTests' do + inherit! :search_paths + pod 'RxTest' + pod 'RxBlocking' + pod 'Nimble' + + # Pods for testing end target 'DutchNewsUITests' do diff --git a/Podfile.lock b/Podfile.lock index 4961109..f083212 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -760,6 +760,6 @@ SPEC CHECKSUMS: RxTest: 711632d5644dffbeb62c936a521b5b008a1e1faa SwiftLint: 22ccbbe3b8008684be5955693bab135e0ed6a447 -PODFILE CHECKSUM: 2c16ebee96276ba1c25ed4d13e31ea42a8fe65ed +PODFILE CHECKSUM: dad14a82ee68b249d6aab11f63d18d16df1b5076 COCOAPODS: 1.9.3 diff --git a/fastlane/Fastfile b/fastlane/Fastfile index a71df33..50a5767 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -13,6 +13,8 @@ # Uncomment the line if you want fastlane to automatically update itself # update_fastlane +require 'json' + default_platform(:ios) platform :ios do @@ -36,7 +38,7 @@ platform :ios do cocoapods() end - if lane != :debugTestVersion + if lane != :run_ci_tests clear_derived_data() #clear all derived_data enable_automatic_code_signing() #autosiging end @@ -59,10 +61,11 @@ platform :ios do desc "Run App Unit tests on given devices name" lane :run_ci_tests do |options| begin + devices = eval options[:devices] scan(workspace: workspace, scheme: scheme, # Project scheme name clean: true, # clean project folder before test execution - devices: options[:devices], # Devices for testing + devices: devices, # Devices for testing result_bundle: "TestResults") # To generate test reports rescue => ex UI.error(ex) From 605693b69d100117e8c7db6be5005e38ed8746cc Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Thu, 17 Sep 2020 23:14:26 +0430 Subject: [PATCH 010/108] Create unit-test.yml --- .github/workflows/unit-test.yml | 46 +++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/unit-test.yml diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml new file mode 100644 index 0000000..95610bb --- /dev/null +++ b/.github/workflows/unit-test.yml @@ -0,0 +1,46 @@ +# This is a basic workflow to help you get started with Actions + +name: CI Testing + +# Controls when the action will run. Triggers the workflow on push or pull request +# events but only for the master branch +on: + push: + branches: + - develop + - feature/* + - master + - bugfix/* + - hotfix/* + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + test: + name: Test codebase before merge request + runs-on: macOS-lastest + strategy: + matrix: + iphones: ['platform=iOS Simulator,OS=11.2,name=iPhone 8','platform=iOS Simulator,OS=12.2,name=iPhone X','platform=iOS Simulator,OS=13.1,name=iPhone SE'] + ipads: ['platform=iOS Simulator,OS=11.2,name=iPad Air','platform=iOS Simulator,OS=12.2,name=iPad Air 2','platform=iOS Simulator,OS=13.1,name=iPad Pro (10.5-inch)','iPad Pro (12.9-inch)'] + xcode: ['/Applications/Xcode_11.4.app/Contents/Developer'] + steps: + - name: Checkout Branch + uses: actions/checkout@v1 + - name: Install Dependencies + run: bundle install +# env: +# BUNDLE_GITHUB__COM: x-access-token:${{ secrets.GITHUB_PERSONAL_ACCESS_TOKEN }} + - name: Build and test on iPhones + run: bundle exec fastlane run_ci_tests destination:"${destination}" + env: + destination: ${{ matrix.iphones }} + - name: Build and test on iPads + run: bundle exec fastlane run_ci_tests destination:"${destination}" + env: + destination: ${{ matrix.ipads }} + - name: Archive Failed Tests artifacts + if: failure() + uses: actions/upload-artifact@v1 + with: + name: FailureDiff + path: YouAppTests/FailureDiffs From 6942b5f35f6b20893e4f17202090ec0d2f1c3c1b Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Thu, 17 Sep 2020 23:18:35 +0430 Subject: [PATCH 011/108] - fixed syntax error in unit-test.yml --- .github/workflows/unit-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 95610bb..f61a6c8 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -17,7 +17,7 @@ on: jobs: test: name: Test codebase before merge request - runs-on: macOS-lastest + runs-on: macOS-latest strategy: matrix: iphones: ['platform=iOS Simulator,OS=11.2,name=iPhone 8','platform=iOS Simulator,OS=12.2,name=iPhone X','platform=iOS Simulator,OS=13.1,name=iPhone SE'] From 559906bcbf78746bceb6476569f992be2fd517b8 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Thu, 17 Sep 2020 23:25:57 +0430 Subject: [PATCH 012/108] - fixed device list in unit-test.yml --- .github/workflows/unit-test.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index f61a6c8..aa9029a 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -20,8 +20,6 @@ jobs: runs-on: macOS-latest strategy: matrix: - iphones: ['platform=iOS Simulator,OS=11.2,name=iPhone 8','platform=iOS Simulator,OS=12.2,name=iPhone X','platform=iOS Simulator,OS=13.1,name=iPhone SE'] - ipads: ['platform=iOS Simulator,OS=11.2,name=iPad Air','platform=iOS Simulator,OS=12.2,name=iPad Air 2','platform=iOS Simulator,OS=13.1,name=iPad Pro (10.5-inch)','iPad Pro (12.9-inch)'] xcode: ['/Applications/Xcode_11.4.app/Contents/Developer'] steps: - name: Checkout Branch @@ -31,11 +29,11 @@ jobs: # env: # BUNDLE_GITHUB__COM: x-access-token:${{ secrets.GITHUB_PERSONAL_ACCESS_TOKEN }} - name: Build and test on iPhones - run: bundle exec fastlane run_ci_tests destination:"${destination}" + run: bundle exec fastlane run_ci_tests destination:"['platform=iOS Simulator,OS=11.2,name=iPhone 8','platform=iOS Simulator,OS=12.2,name=iPhone X','platform=iOS Simulator,OS=13.1,name=iPhone SE']" env: destination: ${{ matrix.iphones }} - name: Build and test on iPads - run: bundle exec fastlane run_ci_tests destination:"${destination}" + run: bundle exec fastlane run_ci_tests destination:"['platform=iOS Simulator,OS=11.2,name=iPad Air','platform=iOS Simulator,OS=12.2,name=iPad Air 2','platform=iOS Simulator,OS=13.1,name=iPad Pro (10.5-inch)','iPad Pro (12.9-inch)']" env: destination: ${{ matrix.ipads }} - name: Archive Failed Tests artifacts From 9f9237a8cf616c50e5687dd1a97ac45633fe6e86 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Thu, 17 Sep 2020 23:47:36 +0430 Subject: [PATCH 013/108] - adjusted destination device list in unit-test.yml --- .github/workflows/unit-test.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index aa9029a..e65728f 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -16,9 +16,11 @@ on: # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: test: - name: Test codebase before merge request + name: Run Unit Test runs-on: macOS-latest strategy: + iphones: ['platform=iOS Simulator,OS=11.2,name=iPhone 8','platform=iOS Simulator,OS=12.2,name=iPhone X','platform=iOS Simulator,OS=13.1,name=iPhone SE'] + ipads: ['platform=iOS Simulator,OS=11.2,name=iPad Air','platform=iOS Simulator,OS=12.2,name=iPad Air 2','platform=iOS Simulator,OS=13.1,name=iPad Pro (10.5-inch)','iPad Pro (12.9-inch)'] matrix: xcode: ['/Applications/Xcode_11.4.app/Contents/Developer'] steps: @@ -29,13 +31,13 @@ jobs: # env: # BUNDLE_GITHUB__COM: x-access-token:${{ secrets.GITHUB_PERSONAL_ACCESS_TOKEN }} - name: Build and test on iPhones - run: bundle exec fastlane run_ci_tests destination:"['platform=iOS Simulator,OS=11.2,name=iPhone 8','platform=iOS Simulator,OS=12.2,name=iPhone X','platform=iOS Simulator,OS=13.1,name=iPhone SE']" + run: bundle exec fastlane run_ci_tests destination: env: - destination: ${{ matrix.iphones }} + destination: ${{ iphones }} - name: Build and test on iPads - run: bundle exec fastlane run_ci_tests destination:"['platform=iOS Simulator,OS=11.2,name=iPad Air','platform=iOS Simulator,OS=12.2,name=iPad Air 2','platform=iOS Simulator,OS=13.1,name=iPad Pro (10.5-inch)','iPad Pro (12.9-inch)']" + run: bundle exec fastlane run_ci_tests destination:"${destination}" env: - destination: ${{ matrix.ipads }} + destination: ${{ ipads }} - name: Archive Failed Tests artifacts if: failure() uses: actions/upload-artifact@v1 From af10423e5a76b9c810237bbaa2a10983ab4588cb Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Thu, 17 Sep 2020 23:50:17 +0430 Subject: [PATCH 014/108] - adjusted destination device list in unit-test.yml --- .github/workflows/unit-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index e65728f..5906c94 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -31,7 +31,7 @@ jobs: # env: # BUNDLE_GITHUB__COM: x-access-token:${{ secrets.GITHUB_PERSONAL_ACCESS_TOKEN }} - name: Build and test on iPhones - run: bundle exec fastlane run_ci_tests destination: + run: bundle exec fastlane run_ci_tests destination:"${destination}" env: destination: ${{ iphones }} - name: Build and test on iPads From 57dafc6243049d25aa2d711578485b0dee03c5d6 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Thu, 17 Sep 2020 23:53:51 +0430 Subject: [PATCH 015/108] - dealing with destination device list in unit-test.yml --- .github/workflows/unit-test.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 5906c94..a377f49 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -19,8 +19,6 @@ jobs: name: Run Unit Test runs-on: macOS-latest strategy: - iphones: ['platform=iOS Simulator,OS=11.2,name=iPhone 8','platform=iOS Simulator,OS=12.2,name=iPhone X','platform=iOS Simulator,OS=13.1,name=iPhone SE'] - ipads: ['platform=iOS Simulator,OS=11.2,name=iPad Air','platform=iOS Simulator,OS=12.2,name=iPad Air 2','platform=iOS Simulator,OS=13.1,name=iPad Pro (10.5-inch)','iPad Pro (12.9-inch)'] matrix: xcode: ['/Applications/Xcode_11.4.app/Contents/Developer'] steps: @@ -33,11 +31,11 @@ jobs: - name: Build and test on iPhones run: bundle exec fastlane run_ci_tests destination:"${destination}" env: - destination: ${{ iphones }} + destination: ${{ "['platform=iOS Simulator,OS=11.2,name=iPhone 8','platform=iOS Simulator,OS=12.2,name=iPhone X','platform=iOS Simulator,OS=13.1,name=iPhone SE']" }} - name: Build and test on iPads run: bundle exec fastlane run_ci_tests destination:"${destination}" env: - destination: ${{ ipads }} + destination: ${{ "['platform=iOS Simulator,OS=11.2,name=iPad Air','platform=iOS Simulator,OS=12.2,name=iPad Air 2','platform=iOS Simulator,OS=13.1,name=iPad Pro (10.5-inch)','iPad Pro (12.9-inch)']" }} - name: Archive Failed Tests artifacts if: failure() uses: actions/upload-artifact@v1 From c49415bf36520ae7ca0788b53b5d58ef27813e78 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Thu, 17 Sep 2020 23:55:35 +0430 Subject: [PATCH 016/108] - dealing with destination device list in unit-test.yml --- .github/workflows/unit-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index a377f49..f263926 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -31,11 +31,11 @@ jobs: - name: Build and test on iPhones run: bundle exec fastlane run_ci_tests destination:"${destination}" env: - destination: ${{ "['platform=iOS Simulator,OS=11.2,name=iPhone 8','platform=iOS Simulator,OS=12.2,name=iPhone X','platform=iOS Simulator,OS=13.1,name=iPhone SE']" }} + destination: ${{ '["platform=iOS Simulator,OS=11.2,name=iPhone 8","platform=iOS Simulator,OS=12.2,name=iPhone X","platform=iOS Simulator,OS=13.1,name=iPhone SE"]' }} - name: Build and test on iPads run: bundle exec fastlane run_ci_tests destination:"${destination}" env: - destination: ${{ "['platform=iOS Simulator,OS=11.2,name=iPad Air','platform=iOS Simulator,OS=12.2,name=iPad Air 2','platform=iOS Simulator,OS=13.1,name=iPad Pro (10.5-inch)','iPad Pro (12.9-inch)']" }} + destination: ${{ '["platform=iOS Simulator,OS=11.2,name=iPad Air","platform=iOS Simulator,OS=12.2,name=iPad Air 2","platform=iOS Simulator,OS=13.1,name=iPad Pro (10.5-inch)","iPad Pro (12.9-inch)"]' }} - name: Archive Failed Tests artifacts if: failure() uses: actions/upload-artifact@v1 From 37cf18b42066418cb14c7516bac55a5e75e5bc9f Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Thu, 17 Sep 2020 23:57:40 +0430 Subject: [PATCH 017/108] - added verbose option to testing section. --- .github/workflows/unit-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index f263926..399e9f4 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -29,11 +29,11 @@ jobs: # env: # BUNDLE_GITHUB__COM: x-access-token:${{ secrets.GITHUB_PERSONAL_ACCESS_TOKEN }} - name: Build and test on iPhones - run: bundle exec fastlane run_ci_tests destination:"${destination}" + run: bundle exec fastlane run_ci_tests destination:"${destination} --verbose" env: destination: ${{ '["platform=iOS Simulator,OS=11.2,name=iPhone 8","platform=iOS Simulator,OS=12.2,name=iPhone X","platform=iOS Simulator,OS=13.1,name=iPhone SE"]' }} - name: Build and test on iPads - run: bundle exec fastlane run_ci_tests destination:"${destination}" + run: bundle exec fastlane run_ci_tests destination:"${destination} --verbose" env: destination: ${{ '["platform=iOS Simulator,OS=11.2,name=iPad Air","platform=iOS Simulator,OS=12.2,name=iPad Air 2","platform=iOS Simulator,OS=13.1,name=iPad Pro (10.5-inch)","iPad Pro (12.9-inch)"]' }} - name: Archive Failed Tests artifacts From 1c89f3270542accc0f493186ca88b5b7532c8c4c Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 00:06:14 +0430 Subject: [PATCH 018/108] - dealing with destination device list in unit-test.yml --- fastlane/Fastfile | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 50a5767..6d6d6ee 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -61,9 +61,12 @@ platform :ios do desc "Run App Unit tests on given devices name" lane :run_ci_tests do |options| begin + UI.message "The devices raw : #{options[:devices]}" devices = eval options[:devices] - scan(workspace: workspace, - scheme: scheme, # Project scheme name + UI.message "The devices list : #{devices}" + + scan(workspace: workspace, #workspace name + # scheme: scheme, # Project scheme name clean: true, # clean project folder before test execution devices: devices, # Devices for testing result_bundle: "TestResults") # To generate test reports From f5db5c4bf5c6f5d29549cabfd6bd9cbc9fe91347 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 00:09:14 +0430 Subject: [PATCH 019/108] - fixed verbose option to testing section. --- .github/workflows/unit-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 399e9f4..276c256 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -29,11 +29,11 @@ jobs: # env: # BUNDLE_GITHUB__COM: x-access-token:${{ secrets.GITHUB_PERSONAL_ACCESS_TOKEN }} - name: Build and test on iPhones - run: bundle exec fastlane run_ci_tests destination:"${destination} --verbose" + run: bundle exec fastlane run_ci_tests destination:"${destination}" --verbose env: destination: ${{ '["platform=iOS Simulator,OS=11.2,name=iPhone 8","platform=iOS Simulator,OS=12.2,name=iPhone X","platform=iOS Simulator,OS=13.1,name=iPhone SE"]' }} - name: Build and test on iPads - run: bundle exec fastlane run_ci_tests destination:"${destination} --verbose" + run: bundle exec fastlane run_ci_tests destination:"${destination}" --verbose env: destination: ${{ '["platform=iOS Simulator,OS=11.2,name=iPad Air","platform=iOS Simulator,OS=12.2,name=iPad Air 2","platform=iOS Simulator,OS=13.1,name=iPad Pro (10.5-inch)","iPad Pro (12.9-inch)"]' }} - name: Archive Failed Tests artifacts From edf472aa7f6ac6f9027470c2b3fcb805186263a5 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 00:16:26 +0430 Subject: [PATCH 020/108] - fixed devices option name to testing section. --- .github/workflows/unit-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 276c256..dac5cb2 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -29,11 +29,11 @@ jobs: # env: # BUNDLE_GITHUB__COM: x-access-token:${{ secrets.GITHUB_PERSONAL_ACCESS_TOKEN }} - name: Build and test on iPhones - run: bundle exec fastlane run_ci_tests destination:"${destination}" --verbose + run: bundle exec fastlane run_ci_tests devices:"${destination}" --verbose env: destination: ${{ '["platform=iOS Simulator,OS=11.2,name=iPhone 8","platform=iOS Simulator,OS=12.2,name=iPhone X","platform=iOS Simulator,OS=13.1,name=iPhone SE"]' }} - name: Build and test on iPads - run: bundle exec fastlane run_ci_tests destination:"${destination}" --verbose + run: bundle exec fastlane run_ci_tests devices:"${destination}" --verbose env: destination: ${{ '["platform=iOS Simulator,OS=11.2,name=iPad Air","platform=iOS Simulator,OS=12.2,name=iPad Air 2","platform=iOS Simulator,OS=13.1,name=iPad Pro (10.5-inch)","iPad Pro (12.9-inch)"]' }} - name: Archive Failed Tests artifacts From b7b249193dc44220c30940e7060fbe3191b36709 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 00:47:56 +0430 Subject: [PATCH 021/108] - added prepare ios simulators step. --- .github/workflows/unit-test.yml | 36 +++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index dac5cb2..13735f7 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -28,14 +28,46 @@ jobs: run: bundle install # env: # BUNDLE_GITHUB__COM: x-access-token:${{ secrets.GITHUB_PERSONAL_ACCESS_TOKEN }} + - name: Prepare iOS Simulators + run: | + sudo mkdir -p /Library/Developer/CoreSimulator/Profiles/Runtimes + sudo ln -s /Applications/Xcode_9.4.1.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 11.4.simruntime + sudo ln -s /Applications/Xcode_10.3.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 12.4.simruntime + sudo ln -s /Applications/Xcode_11.6.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 13.6.simruntime + xcrun simctl list runtimes + xcrun simctl create custom-test-device "iPhone 8" "com.apple.CoreSimulator.SimRuntime.iOS-11-4" + xcrun simctl create custom-test-device "iPhone SE" "com.apple.CoreSimulator.SimRuntime.iOS-12-4" + xcrun simctl create custom-test-device "iPhone X" "com.apple.CoreSimulator.SimRuntime.iOS-13-6" + xcrun simctl create custom-test-device "iPad Air" "com.apple.CoreSimulator.SimRuntime.iOS-11-4" + xcrun simctl create custom-test-device "iPad Air 2" "com.apple.CoreSimulator.SimRuntime.iOS-12-4" + xcrun simctl create custom-test-device "iPad Pro (10.5-inch)" "com.apple.CoreSimulator.SimRuntime.iOS-13-6" + xcrun simctl list devices 11.4 + xcrun simctl list devices 12.4 + xcrun simctl list devices 13.6 + + - name: Prepare iOS 12 simulator + run: | + sudo mkdir -p /Library/Developer/CoreSimulator/Profiles/Runtimes + + xcrun simctl list runtimes + + xcrun simctl list devices 12.4 + - name: Prepare iOS 13 simulator + run: | + sudo mkdir -p /Library/Developer/CoreSimulator/Profiles/Runtimes + + xcrun simctl list runtimes + xcrun simctl create custom-test-device "iPad Pro (9.7-inch)" "com.apple.CoreSimulator.SimRuntime.iOS-12-4" + xcrun simctl list devices 12.4 + - name: Build and test on iPhones run: bundle exec fastlane run_ci_tests devices:"${destination}" --verbose env: - destination: ${{ '["platform=iOS Simulator,OS=11.2,name=iPhone 8","platform=iOS Simulator,OS=12.2,name=iPhone X","platform=iOS Simulator,OS=13.1,name=iPhone SE"]' }} + destination: ${{ '["platform=iOS Simulator,OS=11.4,name=iPhone 8","platform=iOS Simulator,OS=12.4,name=iPhone SE","platform=iOS Simulator,OS=13.6,name=iPhone X","iPhone 11"]' }} - name: Build and test on iPads run: bundle exec fastlane run_ci_tests devices:"${destination}" --verbose env: - destination: ${{ '["platform=iOS Simulator,OS=11.2,name=iPad Air","platform=iOS Simulator,OS=12.2,name=iPad Air 2","platform=iOS Simulator,OS=13.1,name=iPad Pro (10.5-inch)","iPad Pro (12.9-inch)"]' }} + destination: ${{ '["platform=iOS Simulator,OS=11.4,name=iPad Air","platform=iOS Simulator,OS=12.4,name=iPad Air 2","platform=iOS Simulator,OS=13.6,name=iPad Pro (10.5-inch)","iPad Pro (12.9-inch)"]' }} - name: Archive Failed Tests artifacts if: failure() uses: actions/upload-artifact@v1 From f214fe79fa649cb9706d9af87a075335558160f9 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 00:49:06 +0430 Subject: [PATCH 022/108] - added prepare ios simulators step. --- .github/workflows/unit-test.yml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 13735f7..38498eb 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -44,22 +44,6 @@ jobs: xcrun simctl list devices 11.4 xcrun simctl list devices 12.4 xcrun simctl list devices 13.6 - - - name: Prepare iOS 12 simulator - run: | - sudo mkdir -p /Library/Developer/CoreSimulator/Profiles/Runtimes - - xcrun simctl list runtimes - - xcrun simctl list devices 12.4 - - name: Prepare iOS 13 simulator - run: | - sudo mkdir -p /Library/Developer/CoreSimulator/Profiles/Runtimes - - xcrun simctl list runtimes - xcrun simctl create custom-test-device "iPad Pro (9.7-inch)" "com.apple.CoreSimulator.SimRuntime.iOS-12-4" - xcrun simctl list devices 12.4 - - name: Build and test on iPhones run: bundle exec fastlane run_ci_tests devices:"${destination}" --verbose env: From 47c41d6ea5f9834b04b5f3eb0e08e75dddcfe4d7 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 01:09:10 +0430 Subject: [PATCH 023/108] - added some tweak on Preparing iOS Simulator. --- .github/workflows/unit-test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 38498eb..7a0912a 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -31,6 +31,7 @@ jobs: - name: Prepare iOS Simulators run: | sudo mkdir -p /Library/Developer/CoreSimulator/Profiles/Runtimes + ls /Applications | grep 'Xc' sudo ln -s /Applications/Xcode_9.4.1.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 11.4.simruntime sudo ln -s /Applications/Xcode_10.3.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 12.4.simruntime sudo ln -s /Applications/Xcode_11.6.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 13.6.simruntime @@ -57,4 +58,4 @@ jobs: uses: actions/upload-artifact@v1 with: name: FailureDiff - path: YouAppTests/FailureDiffs + path: fastlane/test_output From cb51aae615a0d3153ed55809f6eea6ca34d3f2b9 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 01:25:41 +0430 Subject: [PATCH 024/108] - Fixed Github action missing iOS 11 simulator runtimes. --- .github/workflows/unit-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 7a0912a..1c6e7a5 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -31,7 +31,7 @@ jobs: - name: Prepare iOS Simulators run: | sudo mkdir -p /Library/Developer/CoreSimulator/Profiles/Runtimes - ls /Applications | grep 'Xc' + bundle exec xcversion simulators --install="11.4" sudo ln -s /Applications/Xcode_9.4.1.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 11.4.simruntime sudo ln -s /Applications/Xcode_10.3.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 12.4.simruntime sudo ln -s /Applications/Xcode_11.6.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 13.6.simruntime From 0b99b7f9a97172a250f5a58ea1c9088aab08b288 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 01:32:18 +0430 Subject: [PATCH 025/108] - Fixed Github action missing iOS 11 simulator runtimes. --- .github/workflows/unit-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 1c6e7a5..b2fe640 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -31,7 +31,7 @@ jobs: - name: Prepare iOS Simulators run: | sudo mkdir -p /Library/Developer/CoreSimulator/Profiles/Runtimes - bundle exec xcversion simulators --install="11.4" + bundle exec xcversion simulators --install="11.4" --no-progress sudo ln -s /Applications/Xcode_9.4.1.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 11.4.simruntime sudo ln -s /Applications/Xcode_10.3.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 12.4.simruntime sudo ln -s /Applications/Xcode_11.6.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 13.6.simruntime From 3ee949ac14cdfb1b321e1d5352e48496cece7c8d Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 01:36:50 +0430 Subject: [PATCH 026/108] - Fixed bundle action missing iOS 11 simulator runtimes. --- .github/workflows/unit-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index b2fe640..2a3eec6 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -31,7 +31,7 @@ jobs: - name: Prepare iOS Simulators run: | sudo mkdir -p /Library/Developer/CoreSimulator/Profiles/Runtimes - bundle exec xcversion simulators --install="11.4" --no-progress + bundle exec xcversion simulators --install="iOS 11.4 Simulator" --verbose sudo ln -s /Applications/Xcode_9.4.1.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 11.4.simruntime sudo ln -s /Applications/Xcode_10.3.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 12.4.simruntime sudo ln -s /Applications/Xcode_11.6.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 13.6.simruntime From 3c4d0909a7341e224ad4f4dab7a29c77add9b46d Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 01:43:58 +0430 Subject: [PATCH 027/108] - tweaked on ios simulator installation. --- .github/workflows/unit-test.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 2a3eec6..52956d0 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -30,11 +30,9 @@ jobs: # BUNDLE_GITHUB__COM: x-access-token:${{ secrets.GITHUB_PERSONAL_ACCESS_TOKEN }} - name: Prepare iOS Simulators run: | - sudo mkdir -p /Library/Developer/CoreSimulator/Profiles/Runtimes - bundle exec xcversion simulators --install="iOS 11.4 Simulator" --verbose - sudo ln -s /Applications/Xcode_9.4.1.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 11.4.simruntime - sudo ln -s /Applications/Xcode_10.3.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 12.4.simruntime - sudo ln -s /Applications/Xcode_11.6.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 13.6.simruntime + bundle exec xcversion simulators --install="iOS 11.4 Simulator" --verbose --no-progress + bundle exec xcversion simulators --install="iOS 12.4 Simulator" --verbose --no-progress + bundle exec xcversion simulators --install="iOS 13.6 Simulator" --verbose --no-progress xcrun simctl list runtimes xcrun simctl create custom-test-device "iPhone 8" "com.apple.CoreSimulator.SimRuntime.iOS-11-4" xcrun simctl create custom-test-device "iPhone SE" "com.apple.CoreSimulator.SimRuntime.iOS-12-4" From 53cc30dcde18cefe7d550cbb182aaca4c67ec579 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 01:50:12 +0430 Subject: [PATCH 028/108] - Fixed permission requirement for installing iOS Simulators. --- .github/workflows/unit-test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 52956d0..cebec43 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -30,9 +30,9 @@ jobs: # BUNDLE_GITHUB__COM: x-access-token:${{ secrets.GITHUB_PERSONAL_ACCESS_TOKEN }} - name: Prepare iOS Simulators run: | - bundle exec xcversion simulators --install="iOS 11.4 Simulator" --verbose --no-progress - bundle exec xcversion simulators --install="iOS 12.4 Simulator" --verbose --no-progress - bundle exec xcversion simulators --install="iOS 13.6 Simulator" --verbose --no-progress + sudo bundle exec xcversion simulators --install="iOS 11.4 Simulator" --verbose --no-progress + sudo bundle exec xcversion simulators --install="iOS 12.4 Simulator" --verbose --no-progress + sudo bundle exec xcversion simulators --install="iOS 13.6 Simulator" --verbose --no-progress xcrun simctl list runtimes xcrun simctl create custom-test-device "iPhone 8" "com.apple.CoreSimulator.SimRuntime.iOS-11-4" xcrun simctl create custom-test-device "iPhone SE" "com.apple.CoreSimulator.SimRuntime.iOS-12-4" From 7c1fbb18d9ba26647be0208ff6e3964011093838 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 15:37:17 +0430 Subject: [PATCH 029/108] - seperated iOS Simulator Installation and Creating. --- .github/workflows/unit-test.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index cebec43..c135127 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -28,18 +28,21 @@ jobs: run: bundle install # env: # BUNDLE_GITHUB__COM: x-access-token:${{ secrets.GITHUB_PERSONAL_ACCESS_TOKEN }} - - name: Prepare iOS Simulators + - name: Installing iOS Simulators run: | sudo bundle exec xcversion simulators --install="iOS 11.4 Simulator" --verbose --no-progress sudo bundle exec xcversion simulators --install="iOS 12.4 Simulator" --verbose --no-progress sudo bundle exec xcversion simulators --install="iOS 13.6 Simulator" --verbose --no-progress xcrun simctl list runtimes - xcrun simctl create custom-test-device "iPhone 8" "com.apple.CoreSimulator.SimRuntime.iOS-11-4" - xcrun simctl create custom-test-device "iPhone SE" "com.apple.CoreSimulator.SimRuntime.iOS-12-4" - xcrun simctl create custom-test-device "iPhone X" "com.apple.CoreSimulator.SimRuntime.iOS-13-6" - xcrun simctl create custom-test-device "iPad Air" "com.apple.CoreSimulator.SimRuntime.iOS-11-4" - xcrun simctl create custom-test-device "iPad Air 2" "com.apple.CoreSimulator.SimRuntime.iOS-12-4" - xcrun simctl create custom-test-device "iPad Pro (10.5-inch)" "com.apple.CoreSimulator.SimRuntime.iOS-13-6" + xcrun simctl list devicetypes + - name: Creating iOS Simulators + run: | + xcrun simctl create 'iPhone 8' com.apple.CoreSimulator.SimDeviceType.iPhone-8 com.apple.CoreSimulator.SimRuntime.iOS-11-4 + xcrun simctl create "iPhone SE" com.apple.CoreSimulator.SimDeviceType.iPhone-SE com.apple.CoreSimulator.SimRuntime.iOS-12-4 + xcrun simctl create "iPhone X" com.apple.CoreSimulator.SimDeviceType.iPhone-X "com.apple.CoreSimulator.SimRuntime.iOS-13-6" + xcrun simctl create "iPad Air" com.apple.CoreSimulator.SimDeviceType.iPad-Air "com.apple.CoreSimulator.SimRuntime.iOS-11-4" + xcrun simctl create "iPad Air 2" com.apple.CoreSimulator.SimDeviceType.iPad-Air-2 "com.apple.CoreSimulator.SimRuntime.iOS-12-4" + xcrun simctl create "iPad Pro (10.5-inch)" com.apple.CoreSimulator.SimDeviceType.iPad-Pro--10-5-inch- "com.apple.CoreSimulator.SimRuntime.iOS-13-6" xcrun simctl list devices 11.4 xcrun simctl list devices 12.4 xcrun simctl list devices 13.6 From c18bc9726979079a12f4ad86179ba203b71537be Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 16:11:36 +0430 Subject: [PATCH 030/108] - added instruments -s to github action. --- .github/workflows/unit-test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index c135127..3d842c6 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -37,7 +37,7 @@ jobs: xcrun simctl list devicetypes - name: Creating iOS Simulators run: | - xcrun simctl create 'iPhone 8' com.apple.CoreSimulator.SimDeviceType.iPhone-8 com.apple.CoreSimulator.SimRuntime.iOS-11-4 + xcrun simctl create create-custom'iPhone 8' com.apple.CoreSimulator.SimDeviceType.iPhone-8 com.apple.CoreSimulator.SimRuntime.iOS-11-4 xcrun simctl create "iPhone SE" com.apple.CoreSimulator.SimDeviceType.iPhone-SE com.apple.CoreSimulator.SimRuntime.iOS-12-4 xcrun simctl create "iPhone X" com.apple.CoreSimulator.SimDeviceType.iPhone-X "com.apple.CoreSimulator.SimRuntime.iOS-13-6" xcrun simctl create "iPad Air" com.apple.CoreSimulator.SimDeviceType.iPad-Air "com.apple.CoreSimulator.SimRuntime.iOS-11-4" @@ -46,6 +46,7 @@ jobs: xcrun simctl list devices 11.4 xcrun simctl list devices 12.4 xcrun simctl list devices 13.6 + instruments -s - name: Build and test on iPhones run: bundle exec fastlane run_ci_tests devices:"${destination}" --verbose env: From 60bd757e161619bc324df9b04d1ecb54a2da8882 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 16:27:17 +0430 Subject: [PATCH 031/108] - removed unknown statement --- .github/workflows/unit-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 3d842c6..d2e9959 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -37,7 +37,7 @@ jobs: xcrun simctl list devicetypes - name: Creating iOS Simulators run: | - xcrun simctl create create-custom'iPhone 8' com.apple.CoreSimulator.SimDeviceType.iPhone-8 com.apple.CoreSimulator.SimRuntime.iOS-11-4 + xcrun simctl create iPhone 8' com.apple.CoreSimulator.SimDeviceType.iPhone-8 com.apple.CoreSimulator.SimRuntime.iOS-11-4 xcrun simctl create "iPhone SE" com.apple.CoreSimulator.SimDeviceType.iPhone-SE com.apple.CoreSimulator.SimRuntime.iOS-12-4 xcrun simctl create "iPhone X" com.apple.CoreSimulator.SimDeviceType.iPhone-X "com.apple.CoreSimulator.SimRuntime.iOS-13-6" xcrun simctl create "iPad Air" com.apple.CoreSimulator.SimDeviceType.iPad-Air "com.apple.CoreSimulator.SimRuntime.iOS-11-4" From 98de41bec26ba72f96b70f702315b12cd25b8322 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 16:40:16 +0430 Subject: [PATCH 032/108] devices option in scan turned into destination --- .github/workflows/unit-test.yml | 6 +++--- fastlane/Fastfile | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index d2e9959..9a7c57d 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -30,9 +30,9 @@ jobs: # BUNDLE_GITHUB__COM: x-access-token:${{ secrets.GITHUB_PERSONAL_ACCESS_TOKEN }} - name: Installing iOS Simulators run: | - sudo bundle exec xcversion simulators --install="iOS 11.4 Simulator" --verbose --no-progress - sudo bundle exec xcversion simulators --install="iOS 12.4 Simulator" --verbose --no-progress - sudo bundle exec xcversion simulators --install="iOS 13.6 Simulator" --verbose --no-progress + bundle exec xcversion simulators --install="iOS 11.4 Simulator" --verbose --no-progress + bundle exec xcversion simulators --install="iOS 12.4 Simulator" --verbose --no-progress + bundle exec xcversion simulators --install="iOS 13.6 Simulator" --verbose --no-progress xcrun simctl list runtimes xcrun simctl list devicetypes - name: Creating iOS Simulators diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 6d6d6ee..11ca532 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -68,7 +68,7 @@ platform :ios do scan(workspace: workspace, #workspace name # scheme: scheme, # Project scheme name clean: true, # clean project folder before test execution - devices: devices, # Devices for testing + destination: devices, # Devices for testing result_bundle: "TestResults") # To generate test reports rescue => ex UI.error(ex) From 153a4621eaf28a52a5e22e7b24d30681fcd1336d Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 16:56:49 +0430 Subject: [PATCH 033/108] Update unit-test.yml Check iOS Simulator Cores before Installing to save time. --- .github/workflows/unit-test.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 9a7c57d..3c7bc34 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -28,7 +28,18 @@ jobs: run: bundle install # env: # BUNDLE_GITHUB__COM: x-access-token:${{ secrets.GITHUB_PERSONAL_ACCESS_TOKEN }} + - name: Check iOS Simulators existence + id: check_simulator + uses: andstor/file-existence-action@v1 + with: + files: "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 11.4.simruntime, /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 12.4.simruntime, /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 13.6.simruntime" + - name: iOS Simulator Cores exists + if: steps.check_files.outputs.files_exists == 'true' + # Only runs if all of the conditions true + run: echo All Simulator Core Exists! - name: Installing iOS Simulators + if: steps.check_files.outputs.files_exists == 'false' + # Only runs if iOS Simulator didn't exists run: | bundle exec xcversion simulators --install="iOS 11.4 Simulator" --verbose --no-progress bundle exec xcversion simulators --install="iOS 12.4 Simulator" --verbose --no-progress From f95df835e6f42ed6eabcd0d3aeae6135eb85aa99 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 17:00:35 +0430 Subject: [PATCH 034/108] Update unit-test.yml Fixed if condition in 2 steps. --- .github/workflows/unit-test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 3c7bc34..b346728 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -34,11 +34,11 @@ jobs: with: files: "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 11.4.simruntime, /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 12.4.simruntime, /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 13.6.simruntime" - name: iOS Simulator Cores exists - if: steps.check_files.outputs.files_exists == 'true' + if: steps.check_simulator.outputs.files_exists == 'true' # Only runs if all of the conditions true run: echo All Simulator Core Exists! - name: Installing iOS Simulators - if: steps.check_files.outputs.files_exists == 'false' + if: steps.check_simulator.outputs.files_exists == 'false' # Only runs if iOS Simulator didn't exists run: | bundle exec xcversion simulators --install="iOS 11.4 Simulator" --verbose --no-progress @@ -48,7 +48,7 @@ jobs: xcrun simctl list devicetypes - name: Creating iOS Simulators run: | - xcrun simctl create iPhone 8' com.apple.CoreSimulator.SimDeviceType.iPhone-8 com.apple.CoreSimulator.SimRuntime.iOS-11-4 + xcrun simctl create "iPhone 8" com.apple.CoreSimulator.SimDeviceType.iPhone-8 com.apple.CoreSimulator.SimRuntime.iOS-11-4 xcrun simctl create "iPhone SE" com.apple.CoreSimulator.SimDeviceType.iPhone-SE com.apple.CoreSimulator.SimRuntime.iOS-12-4 xcrun simctl create "iPhone X" com.apple.CoreSimulator.SimDeviceType.iPhone-X "com.apple.CoreSimulator.SimRuntime.iOS-13-6" xcrun simctl create "iPad Air" com.apple.CoreSimulator.SimDeviceType.iPad-Air "com.apple.CoreSimulator.SimRuntime.iOS-11-4" From 5fd86c6494e467f35f7d1778e17f51e4794dc91b Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 17:48:48 +0430 Subject: [PATCH 035/108] reverted devices options --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 11ca532..6d6d6ee 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -68,7 +68,7 @@ platform :ios do scan(workspace: workspace, #workspace name # scheme: scheme, # Project scheme name clean: true, # clean project folder before test execution - destination: devices, # Devices for testing + devices: devices, # Devices for testing result_bundle: "TestResults") # To generate test reports rescue => ex UI.error(ex) From d8c84a6742e6e81e73926f58884d348514f4d15b Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 17:54:37 +0430 Subject: [PATCH 036/108] Update unit-test.yml --- .github/workflows/unit-test.yml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index b346728..9b858ff 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -7,12 +7,16 @@ name: CI Testing on: push: branches: - - develop - feature/* - - master - bugfix/* - hotfix/* - + pull_request: + branches: + - develop + - feature/* + - master + - bugfix/* + - hotfix/* # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: test: @@ -30,9 +34,9 @@ jobs: # BUNDLE_GITHUB__COM: x-access-token:${{ secrets.GITHUB_PERSONAL_ACCESS_TOKEN }} - name: Check iOS Simulators existence id: check_simulator - uses: andstor/file-existence-action@v1 + uses: actions/ with: - files: "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 11.4.simruntime, /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 12.4.simruntime, /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 13.6.simruntime" + files: "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 11.4.simruntime/Contents/Info.plist, /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 12.4.simruntime/Contents/Info.plist, /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 13.6.simruntime/Contents/Info.plist" - name: iOS Simulator Cores exists if: steps.check_simulator.outputs.files_exists == 'true' # Only runs if all of the conditions true @@ -61,11 +65,11 @@ jobs: - name: Build and test on iPhones run: bundle exec fastlane run_ci_tests devices:"${destination}" --verbose env: - destination: ${{ '["platform=iOS Simulator,OS=11.4,name=iPhone 8","platform=iOS Simulator,OS=12.4,name=iPhone SE","platform=iOS Simulator,OS=13.6,name=iPhone X","iPhone 11"]' }} + destination: ${{ '["iPhone 8 (11.4)","iPhone SE (12.4)","iPhone X (13.6)","iPhone 11"]' }} - name: Build and test on iPads run: bundle exec fastlane run_ci_tests devices:"${destination}" --verbose env: - destination: ${{ '["platform=iOS Simulator,OS=11.4,name=iPad Air","platform=iOS Simulator,OS=12.4,name=iPad Air 2","platform=iOS Simulator,OS=13.6,name=iPad Pro (10.5-inch)","iPad Pro (12.9-inch)"]' }} + destination: ${{ '["iPad Air (11.4)","iPad Air 2 (12.4)","iPad Pro (10.5-inch) (13.6)","iPad Pro (12.9-inch)"]' }} - name: Archive Failed Tests artifacts if: failure() uses: actions/upload-artifact@v1 From 0b89520e1b2b90732dfadda7167add26110cca18 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 17:56:05 +0430 Subject: [PATCH 037/108] Update unit-test.yml --- .github/workflows/unit-test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 9b858ff..c6006ca 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -17,6 +17,7 @@ on: - master - bugfix/* - hotfix/* + - # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: test: @@ -34,7 +35,7 @@ jobs: # BUNDLE_GITHUB__COM: x-access-token:${{ secrets.GITHUB_PERSONAL_ACCESS_TOKEN }} - name: Check iOS Simulators existence id: check_simulator - uses: actions/ + uses: andstor/file-existence-action@v1 with: files: "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 11.4.simruntime/Contents/Info.plist, /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 12.4.simruntime/Contents/Info.plist, /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 13.6.simruntime/Contents/Info.plist" - name: iOS Simulator Cores exists From 551078cb045b8fad18e502c41fb464b511c3bf91 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 18:08:12 +0430 Subject: [PATCH 038/108] -tweaked Simulator Installation to save time. --- .github/workflows/unit-test.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index c6006ca..d35047e 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -46,9 +46,11 @@ jobs: if: steps.check_simulator.outputs.files_exists == 'false' # Only runs if iOS Simulator didn't exists run: | + ls -s /Library/Developer/CoreSimulator/Profiles/Runtimes/ + sudo mkdir -p /Library/Developer/CoreSimulator/Profiles/Runtimes bundle exec xcversion simulators --install="iOS 11.4 Simulator" --verbose --no-progress - bundle exec xcversion simulators --install="iOS 12.4 Simulator" --verbose --no-progress - bundle exec xcversion simulators --install="iOS 13.6 Simulator" --verbose --no-progress + sudo ln -s /Applications/Xcode_10.3.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 12.4.simruntime + sudo ln -s /Applications/Xcode_11.6.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 13.6.simruntime xcrun simctl list runtimes xcrun simctl list devicetypes - name: Creating iOS Simulators From 7b7157ffb889867abf2af489e23a256ff18079fe Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 18:12:06 +0430 Subject: [PATCH 039/108] -Fixed error in Installing iOS Simulators step. --- .github/workflows/unit-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index d35047e..3a1ece1 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -46,11 +46,11 @@ jobs: if: steps.check_simulator.outputs.files_exists == 'false' # Only runs if iOS Simulator didn't exists run: | - ls -s /Library/Developer/CoreSimulator/Profiles/Runtimes/ + hostname sudo mkdir -p /Library/Developer/CoreSimulator/Profiles/Runtimes - bundle exec xcversion simulators --install="iOS 11.4 Simulator" --verbose --no-progress sudo ln -s /Applications/Xcode_10.3.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 12.4.simruntime sudo ln -s /Applications/Xcode_11.6.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 13.6.simruntime + bundle exec xcversion simulators --install="iOS 11.4 Simulator" --verbose --no-progress xcrun simctl list runtimes xcrun simctl list devicetypes - name: Creating iOS Simulators From d8555eb58f33dc893eaebeb01e391de3bf7394be Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 18:20:59 +0430 Subject: [PATCH 040/108] --- .github/workflows/unit-test.yml | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 3a1ece1..c6ccab8 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -33,24 +33,13 @@ jobs: run: bundle install # env: # BUNDLE_GITHUB__COM: x-access-token:${{ secrets.GITHUB_PERSONAL_ACCESS_TOKEN }} - - name: Check iOS Simulators existence - id: check_simulator - uses: andstor/file-existence-action@v1 - with: - files: "/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 11.4.simruntime/Contents/Info.plist, /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 12.4.simruntime/Contents/Info.plist, /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 13.6.simruntime/Contents/Info.plist" - - name: iOS Simulator Cores exists - if: steps.check_simulator.outputs.files_exists == 'true' - # Only runs if all of the conditions true - run: echo All Simulator Core Exists! - name: Installing iOS Simulators - if: steps.check_simulator.outputs.files_exists == 'false' - # Only runs if iOS Simulator didn't exists run: | hostname sudo mkdir -p /Library/Developer/CoreSimulator/Profiles/Runtimes sudo ln -s /Applications/Xcode_10.3.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 12.4.simruntime - sudo ln -s /Applications/Xcode_11.6.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 13.6.simruntime bundle exec xcversion simulators --install="iOS 11.4 Simulator" --verbose --no-progress + bundle exec xcversion simulators --install="iOS 13.6 Simulator" --verbose --no-progress xcrun simctl list runtimes xcrun simctl list devicetypes - name: Creating iOS Simulators From f95f06b2c13172a284812b52f3938c5a2615f3ad Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 18:55:08 +0430 Subject: [PATCH 041/108] -Added another github action configuration file. --- .github/workflows/unit-test-1.yml | 58 +++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 .github/workflows/unit-test-1.yml diff --git a/.github/workflows/unit-test-1.yml b/.github/workflows/unit-test-1.yml new file mode 100644 index 0000000..50a975c --- /dev/null +++ b/.github/workflows/unit-test-1.yml @@ -0,0 +1,58 @@ +# This is a basic workflow to help you get started with Actions + +name: CI Testing 2 + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + test: + name: Run Unit Test + runs-on: macOS-latest + strategy: + matrix: + xcode: ['/Applications/Xcode_11.4.app/Contents/Developer'] + steps: + - name: Checkout Branch + uses: actions/checkout@v1 + - name: Install Dependencies + run: bundle install +# env: +# BUNDLE_GITHUB__COM: x-access-token:${{ secrets.GITHUB_PERSONAL_ACCESS_TOKEN }} + - name: Installing iOS Simulators + run: | + hostname + sudo mkdir -p /Library/Developer/CoreSimulator/Profiles/Runtimes + sudo ln -s /Applications/Xcode_10.3.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 12.4.simruntime + bundle exec xcversion simulators --install="iOS 11.4 Simulator" --verbose --no-progress + bundle exec xcversion simulators --install="iOS 13.6 Simulator" --verbose --no-progress + xcrun simctl list runtimes + xcrun simctl list devicetypes + - name: Creating iOS Simulators + run: | + xcrun simctl create "iPhone 8" com.apple.CoreSimulator.SimDeviceType.iPhone-8 com.apple.CoreSimulator.SimRuntime.iOS-11-4 + xcrun simctl create "iPhone SE" com.apple.CoreSimulator.SimDeviceType.iPhone-SE com.apple.CoreSimulator.SimRuntime.iOS-12-4 + xcrun simctl create "iPhone X" com.apple.CoreSimulator.SimDeviceType.iPhone-X "com.apple.CoreSimulator.SimRuntime.iOS-13-6" + xcrun simctl create "iPad Air" com.apple.CoreSimulator.SimDeviceType.iPad-Air "com.apple.CoreSimulator.SimRuntime.iOS-11-4" + xcrun simctl create "iPad Air 2" com.apple.CoreSimulator.SimDeviceType.iPad-Air-2 "com.apple.CoreSimulator.SimRuntime.iOS-12-4" + xcrun simctl create "iPad Pro (10.5-inch)" com.apple.CoreSimulator.SimDeviceType.iPad-Pro--10-5-inch- "com.apple.CoreSimulator.SimRuntime.iOS-13-6" + xcrun simctl list devices 11.4 + xcrun simctl list devices 12.4 + xcrun simctl list devices 13.6 + instruments -s + - name: Build and test on iPhones + run: | + echo ${devices} + bundle exec fastlane run_ci_tests devices:${devices} --verbose + env: + devices: ${{ '["iPhone 8 (11.4)","iPhone SE (12.4)","iPhone X (13.6)","iPhone 11"]' }} + - name: Build and test on iPads + run: | + echo ${devices} + bundle exec fastlane run_ci_tests devices:${devices} --verbose + env: + devices: ${{ '["iPad Air (11.4)","iPad Air 2 (12.4)","iPad Pro (10.5-inch) (13.6)","iPad Pro (12.9-inch)"]' }} + - name: Archive Failed Tests artifacts + if: failure() + uses: actions/upload-artifact@v1 + with: + name: FailureDiff + path: fastlane/test_output From 5931b42c80665f828ce6782d9805fefd1e06494e Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 21:05:47 +0430 Subject: [PATCH 042/108] -set min ios version to 11.0 for test targets. --- DutchNews.xcodeproj/project.pbxproj | 10 ++++++++-- DutchNewsUITests/DutchNewsUITests.swift | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index 5751f39..64abbcb 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -662,6 +662,7 @@ baseConfigurationReference = CE0BB7F85E1175162F017DD0 /* Pods-DutchNews.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 638B4QA28J; INFOPLIST_FILE = DutchNews/Info.plist; @@ -672,6 +673,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.ifarshad.DutchNews; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -682,6 +684,7 @@ baseConfigurationReference = 9F5E3588EBC310FDA84D4BC9 /* Pods-DutchNews.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 638B4QA28J; INFOPLIST_FILE = DutchNews/Info.plist; @@ -692,6 +695,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.ifarshad.DutchNews; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -706,7 +710,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 638B4QA28J; INFOPLIST_FILE = DutchNewsTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.2; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -729,7 +733,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 638B4QA28J; INFOPLIST_FILE = DutchNewsTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.2; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -751,6 +755,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 638B4QA28J; INFOPLIST_FILE = DutchNewsUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -772,6 +777,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 638B4QA28J; INFOPLIST_FILE = DutchNewsUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/DutchNewsUITests/DutchNewsUITests.swift b/DutchNewsUITests/DutchNewsUITests.swift index 15886c9..1b7f2ed 100644 --- a/DutchNewsUITests/DutchNewsUITests.swift +++ b/DutchNewsUITests/DutchNewsUITests.swift @@ -34,7 +34,7 @@ class DutchNewsUITests: XCTestCase { } func testLaunchPerformance() { - if #available(iOS 11.0, *) { + if #available(iOS 13.0, *) { // This measures how long it takes to launch your application. measure(metrics: [XCTOSSignpostMetric.applicationLaunch]) { XCUIApplication().launch() From dbbed5a39aaa762643b660daa2f0b08a6f80f2ff Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 22:08:42 +0430 Subject: [PATCH 043/108] -Testing Infrastructure is done. --- Podfile | 4 +++- Podfile.lock | 2 +- fastlane/Fastfile | 42 +++++++++++++++++++++++++----------------- fastlane/README.md | 2 ++ 4 files changed, 31 insertions(+), 19 deletions(-) diff --git a/Podfile b/Podfile index 838a40b..02c8051 100644 --- a/Podfile +++ b/Podfile @@ -12,6 +12,7 @@ use_frameworks! target 'DutchNews' do + inhibit_all_warnings! # Pods for DutchNews pod 'Alamofire' @@ -32,7 +33,7 @@ target 'DutchNews' do #Logger Framework pod 'CocoaLumberjack/Swift' - + end target 'DutchNewsTests' do @@ -48,3 +49,4 @@ target 'DutchNewsUITests' do inherit! :search_paths # Pods for testing end + diff --git a/Podfile.lock b/Podfile.lock index f083212..2a0607b 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -760,6 +760,6 @@ SPEC CHECKSUMS: RxTest: 711632d5644dffbeb62c936a521b5b008a1e1faa SwiftLint: 22ccbbe3b8008684be5955693bab135e0ed6a447 -PODFILE CHECKSUM: dad14a82ee68b249d6aab11f63d18d16df1b5076 +PODFILE CHECKSUM: 9c4baee10c033d3323f4ed65a57d7bdff5880966 COCOAPODS: 1.9.3 diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 6d6d6ee..3409579 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -39,9 +39,9 @@ platform :ios do end if lane != :run_ci_tests - clear_derived_data() #clear all derived_data - enable_automatic_code_signing() #autosiging - end + clear_derived_data() #clear all derived_data + enable_automatic_code_signing() #autosiging + end UI.message "prepared for build" @@ -58,6 +58,13 @@ platform :ios do raise "Failure on #{scheme} #{version} \n in lane #{lane.to_s} \n exception = #{exception.to_s}" end + desc "generate report after running tests" + def generate_report + puts "Generating Test Report ..." + # sh "xchtmlreport -r fastlane/test_output/#{scheme}-#{version}.test_result" + puts "Test Report Successfully generated" + end + desc "Run App Unit tests on given devices name" lane :run_ci_tests do |options| begin @@ -65,22 +72,23 @@ platform :ios do devices = eval options[:devices] UI.message "The devices list : #{devices}" - scan(workspace: workspace, #workspace name - # scheme: scheme, # Project scheme name - clean: true, # clean project folder before test execution + clean = false + + if options[:clean] + clean = options[:clean] + end + + scan(workspace: "#{workspace}", #workspace name + # xcargs:"CODE_SIGNING_REQUIRED=NO", + scheme: scheme, # Project scheme name + clean: clean, # clean project folder before test execution devices: devices, # Devices for testing - result_bundle: "TestResults") # To generate test reports - rescue => ex - UI.error(ex) - generate_report() - end - end + result_bundle: false) # To generate test reports - desc "generate report after running tests" - def generate_report - puts "Generating Test Report ..." - sh "xchtmlreport -r fastlane/test_output/#{scheme}-#{version}.test_result" - puts "Test Report Successfully generated" + rescue => ex + UI.error "Error Occured #{ex}" + generate_report + end end end diff --git a/fastlane/README.md b/fastlane/README.md index b4ebc14..8edd373 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -20,6 +20,8 @@ or alternatively using `brew install fastlane` ``` fastlane ios run_ci_tests ``` +generate report after running tests + Run App Unit tests on given devices name ---- From 760958e9be7a8244d6a98bc08d0bb88964c1890d Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 22:37:45 +0430 Subject: [PATCH 044/108] -removed extra step and modified destination device. (cherry picked from commit 70158687f948fe66f167ec3016c10de405a3d78f) --- .github/workflows/unit-test.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index c6ccab8..5185081 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -54,14 +54,10 @@ jobs: xcrun simctl list devices 12.4 xcrun simctl list devices 13.6 instruments -s - - name: Build and test on iPhones + - name: Build and test on Devices run: bundle exec fastlane run_ci_tests devices:"${destination}" --verbose env: - destination: ${{ '["iPhone 8 (11.4)","iPhone SE (12.4)","iPhone X (13.6)","iPhone 11"]' }} - - name: Build and test on iPads - run: bundle exec fastlane run_ci_tests devices:"${destination}" --verbose - env: - destination: ${{ '["iPad Air (11.4)","iPad Air 2 (12.4)","iPad Pro (10.5-inch) (13.6)","iPad Pro (12.9-inch)"]' }} + destination: ${{ '["iPhone 8 (11.4)","iPhone SE (12.4)","iPhone X (13.6)","iPhone 11","iPad Air (11.4)","iPad Air 2 (12.4)","iPad Pro (10.5-inch) (13.6)","iPad Pro (12.9-inch)"]' }} - name: Archive Failed Tests artifacts if: failure() uses: actions/upload-artifact@v1 From 7b319561f447a0b084a36e12df0348eda97af49a Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 18 Sep 2020 22:51:06 +0430 Subject: [PATCH 045/108] -removed extra unit-test-1.yml. --- .github/workflows/unit-test-1.yml | 58 ------------------------------- 1 file changed, 58 deletions(-) delete mode 100644 .github/workflows/unit-test-1.yml diff --git a/.github/workflows/unit-test-1.yml b/.github/workflows/unit-test-1.yml deleted file mode 100644 index 50a975c..0000000 --- a/.github/workflows/unit-test-1.yml +++ /dev/null @@ -1,58 +0,0 @@ -# This is a basic workflow to help you get started with Actions - -name: CI Testing 2 - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - test: - name: Run Unit Test - runs-on: macOS-latest - strategy: - matrix: - xcode: ['/Applications/Xcode_11.4.app/Contents/Developer'] - steps: - - name: Checkout Branch - uses: actions/checkout@v1 - - name: Install Dependencies - run: bundle install -# env: -# BUNDLE_GITHUB__COM: x-access-token:${{ secrets.GITHUB_PERSONAL_ACCESS_TOKEN }} - - name: Installing iOS Simulators - run: | - hostname - sudo mkdir -p /Library/Developer/CoreSimulator/Profiles/Runtimes - sudo ln -s /Applications/Xcode_10.3.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 12.4.simruntime - bundle exec xcversion simulators --install="iOS 11.4 Simulator" --verbose --no-progress - bundle exec xcversion simulators --install="iOS 13.6 Simulator" --verbose --no-progress - xcrun simctl list runtimes - xcrun simctl list devicetypes - - name: Creating iOS Simulators - run: | - xcrun simctl create "iPhone 8" com.apple.CoreSimulator.SimDeviceType.iPhone-8 com.apple.CoreSimulator.SimRuntime.iOS-11-4 - xcrun simctl create "iPhone SE" com.apple.CoreSimulator.SimDeviceType.iPhone-SE com.apple.CoreSimulator.SimRuntime.iOS-12-4 - xcrun simctl create "iPhone X" com.apple.CoreSimulator.SimDeviceType.iPhone-X "com.apple.CoreSimulator.SimRuntime.iOS-13-6" - xcrun simctl create "iPad Air" com.apple.CoreSimulator.SimDeviceType.iPad-Air "com.apple.CoreSimulator.SimRuntime.iOS-11-4" - xcrun simctl create "iPad Air 2" com.apple.CoreSimulator.SimDeviceType.iPad-Air-2 "com.apple.CoreSimulator.SimRuntime.iOS-12-4" - xcrun simctl create "iPad Pro (10.5-inch)" com.apple.CoreSimulator.SimDeviceType.iPad-Pro--10-5-inch- "com.apple.CoreSimulator.SimRuntime.iOS-13-6" - xcrun simctl list devices 11.4 - xcrun simctl list devices 12.4 - xcrun simctl list devices 13.6 - instruments -s - - name: Build and test on iPhones - run: | - echo ${devices} - bundle exec fastlane run_ci_tests devices:${devices} --verbose - env: - devices: ${{ '["iPhone 8 (11.4)","iPhone SE (12.4)","iPhone X (13.6)","iPhone 11"]' }} - - name: Build and test on iPads - run: | - echo ${devices} - bundle exec fastlane run_ci_tests devices:${devices} --verbose - env: - devices: ${{ '["iPad Air (11.4)","iPad Air 2 (12.4)","iPad Pro (10.5-inch) (13.6)","iPad Pro (12.9-inch)"]' }} - - name: Archive Failed Tests artifacts - if: failure() - uses: actions/upload-artifact@v1 - with: - name: FailureDiff - path: fastlane/test_output From 708dd54858d2b6dab4b6ea616439d9af0db217a6 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sat, 19 Sep 2020 05:41:12 +0430 Subject: [PATCH 046/108] - added Networking Service Abstract. --- DutchNews.xcodeproj/project.pbxproj | 4 + .../Networking/NetworkService.swift | 74 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 DutchNews/Classes/Data Layers/Networking/NetworkService.swift diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index 64abbcb..64a6646 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ F89B0223250D446200B41293 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F89B0221250D446200B41293 /* LaunchScreen.storyboard */; }; F89B022E250D446200B41293 /* DutchNewsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89B022D250D446200B41293 /* DutchNewsTests.swift */; }; F89B0239250D446200B41293 /* DutchNewsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89B0238250D446200B41293 /* DutchNewsUITests.swift */; }; + F8DE79E32515904400A6C2D5 /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DE79E22515904400A6C2D5 /* NetworkService.swift */; }; F8F14C74250D719800C24FF5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F8F14C72250D719800C24FF5 /* Main.storyboard */; }; /* End PBXBuildFile section */ @@ -58,6 +59,7 @@ F89B0234250D446200B41293 /* DutchNewsUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DutchNewsUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F89B0238250D446200B41293 /* DutchNewsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DutchNewsUITests.swift; sourceTree = ""; }; F89B023A250D446200B41293 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F8DE79E22515904400A6C2D5 /* NetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = ""; }; F8F14C73250D719800C24FF5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; /* End PBXFileReference section */ @@ -202,6 +204,7 @@ F8F14C6E250D70F900C24FF5 /* Networking */ = { isa = PBXGroup; children = ( + F8DE79E22515904400A6C2D5 /* NetworkService.swift */, ); path = Networking; sourceTree = ""; @@ -488,6 +491,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F8DE79E32515904400A6C2D5 /* NetworkService.swift in Sources */, F89B021E250D446000B41293 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/DutchNews/Classes/Data Layers/Networking/NetworkService.swift b/DutchNews/Classes/Data Layers/Networking/NetworkService.swift new file mode 100644 index 0000000..01bfb9c --- /dev/null +++ b/DutchNews/Classes/Data Layers/Networking/NetworkService.swift @@ -0,0 +1,74 @@ +// +// NetworkService.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/19/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import RxSwift +import Alamofire + +/// `NetworkService` Abstract +protocol NetworkService { + + /// <#Description#> + typealias NetworkHeadersType = [String : String] + + /// <#Description#> + typealias NetworkParametersType = Parameters + + /// <#Description#> + typealias ResponseResult = Swift.Result + + typealias Parameters = [String: Any] + + typealias EndPoint = URLConvertible + + + /// <#Description#> + /// - Parameters: + /// - endpoint: <#endpoint description#> + /// - parameters: <#parameters description#> + /// - method: <#method description#> + /// - headers: <#headers description#> + func executeRequest(endpoint: EndPoint, + parameters: Parameters, + method: HTTPMethod, + headers: NetworkHeadersType) -> Observable> + + + + /// <#Description#> + /// - Parameters: + /// - endpoint: <#endpoint description#> + /// - parameter: <#parameter description#> + /// - headers: <#headers description#> + func executeRequest(endpoint: EndPoint, + parameter: P, headers: NetworkHeadersType) -> Observable> + +} + +protocol NetworkServiceInterceptable: NetworkService { + + /// <#Description#> + /// - Parameter interceptor: <#interceptor description#> + func addingRequest(interceptor: RequestInterceptor) +} + +extension NetworkService { + + + func executeRequest(endpoint: EndPoint, + parameters: Parameters, + method: HTTPMethod, headers: NetworkHeadersType) -> Observable> { + return .empty() + } + + func executeRequest(endpoint:EndPoint, + parameter: P, + headers: NetworkHeadersType) -> Observable> { + return .empty() + } +} From 4ba96d1d53db1560a05d6a610779c64ad7b66379 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sat, 19 Sep 2020 06:08:29 +0430 Subject: [PATCH 047/108] - Added APIClientService class for general purpose. --- DutchNews.xcodeproj/project.pbxproj | 4 + .../Networking/APIClientService.swift | 106 ++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 DutchNews/Classes/Data Layers/Networking/APIClientService.swift diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index 64a6646..318e7ae 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ F89B022E250D446200B41293 /* DutchNewsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89B022D250D446200B41293 /* DutchNewsTests.swift */; }; F89B0239250D446200B41293 /* DutchNewsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89B0238250D446200B41293 /* DutchNewsUITests.swift */; }; F8DE79E32515904400A6C2D5 /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DE79E22515904400A6C2D5 /* NetworkService.swift */; }; + F8DE79E5251594D700A6C2D5 /* APIClientService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DE79E4251594D700A6C2D5 /* APIClientService.swift */; }; F8F14C74250D719800C24FF5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F8F14C72250D719800C24FF5 /* Main.storyboard */; }; /* End PBXBuildFile section */ @@ -60,6 +61,7 @@ F89B0238250D446200B41293 /* DutchNewsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DutchNewsUITests.swift; sourceTree = ""; }; F89B023A250D446200B41293 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F8DE79E22515904400A6C2D5 /* NetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = ""; }; + F8DE79E4251594D700A6C2D5 /* APIClientService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClientService.swift; sourceTree = ""; }; F8F14C73250D719800C24FF5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; /* End PBXFileReference section */ @@ -205,6 +207,7 @@ isa = PBXGroup; children = ( F8DE79E22515904400A6C2D5 /* NetworkService.swift */, + F8DE79E4251594D700A6C2D5 /* APIClientService.swift */, ); path = Networking; sourceTree = ""; @@ -492,6 +495,7 @@ buildActionMask = 2147483647; files = ( F8DE79E32515904400A6C2D5 /* NetworkService.swift in Sources */, + F8DE79E5251594D700A6C2D5 /* APIClientService.swift in Sources */, F89B021E250D446000B41293 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/DutchNews/Classes/Data Layers/Networking/APIClientService.swift b/DutchNews/Classes/Data Layers/Networking/APIClientService.swift new file mode 100644 index 0000000..40e63b7 --- /dev/null +++ b/DutchNews/Classes/Data Layers/Networking/APIClientService.swift @@ -0,0 +1,106 @@ +// +// APIClientService.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/19/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import RxSwift +import RxAlamofire +import Alamofire + +private let queueName = "com.ifarshad.DutchNews.networking.response" + +fileprivate extension DispatchQueue { + + + /// Default queue for handling response + static let networkResponseQueue = DispatchQueue(label: queueName, + qos: .background, + attributes: .concurrent, + autoreleaseFrequency: .workItem) +} + +final class APIClientService: NetworkServiceInterceptable { + + typealias SessionManager = Session + + /// <#Description#> + let baseURL: URL + + /// <#Description#> + private(set) var session: SessionManager + + /// <#Description#> + let workQueue: DispatchQueue + + private var interceptor: RequestInterceptor? + + let decoder: DataDecoder + + + /// <#Description#> + /// - Parameters: + /// - baseURL: <#baseURL description#> + /// - session: <#session description#> + /// - queue: <#queue description#> + /// - decoder: <#decoder description#> + init(baseURL: URL, + session: SessionManager = .default, + queue: DispatchQueue = .networkResponseQueue, + decoder: @autoclosure () -> DataDecoder) { + self.baseURL = baseURL + self.session = session + self.workQueue = queue + self.decoder = decoder() + + } + + func addingRequest(interceptor: RequestInterceptor) { + let locker = NSLock() + locker.lock() + defer { + locker.unlock() + } + + self.interceptor = interceptor + } + + //////////////////////////////////////////////////////////////// + //MARK:- + //MARK:Private Methods + //MARK:- + //////////////////////////////////////////////////////////////// + + + + /// <#Description#> + /// - Parameter endpoint: <#endpoint description#> + private func attachBaseURL(into endpoint: URLConvertible) throws -> URLConvertible { + + let endPoint = try endpoint.asURL().absoluteString + + guard let joinedURL = URL(string: endPoint, relativeTo: baseURL) else { + throw AFError.invalidURL(url: "\(baseURL.absoluteString)/\(endPoint)") + } + + return joinedURL + } + + + /// <#Description#> + /// - Parameters: + /// - dataRequest: <#dataRequest description#> + /// - decoder: <#decoder description#> + private func map (dataRequest: DataRequest, decoder: DataDecoder) -> Observable> { + print(dataRequest) + return dataRequest.rx.decodable(decoder: decoder) + .map { value in + return Result { value } + }.catchError { (error) -> Observable> in + .just(.failure(error)) + } + } + +} From 4857b0c53d0cb0a176da5a5ff4ad6d9f866af9f7 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sat, 19 Sep 2020 18:23:19 +0430 Subject: [PATCH 048/108] - Tweaked NetworkService Abstract --- DutchNews/AppDelegate.swift | 4 -- .../Networking/NetworkService.swift | 70 ++++++++++++++++--- 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/DutchNews/AppDelegate.swift b/DutchNews/AppDelegate.swift index f57900a..4ffa404 100644 --- a/DutchNews/AppDelegate.swift +++ b/DutchNews/AppDelegate.swift @@ -13,10 +13,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { @IBOutlet var window: UIWindow? - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. - return true } @@ -38,6 +36,4 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. } - } - diff --git a/DutchNews/Classes/Data Layers/Networking/NetworkService.swift b/DutchNews/Classes/Data Layers/Networking/NetworkService.swift index 01bfb9c..5abaed4 100644 --- a/DutchNews/Classes/Data Layers/Networking/NetworkService.swift +++ b/DutchNews/Classes/Data Layers/Networking/NetworkService.swift @@ -14,7 +14,7 @@ import Alamofire protocol NetworkService { /// <#Description#> - typealias NetworkHeadersType = [String : String] + typealias NetworkHeadersType = [String: String] /// <#Description#> typealias NetworkParametersType = Parameters @@ -22,10 +22,11 @@ protocol NetworkService { /// <#Description#> typealias ResponseResult = Swift.Result + typealias ResponseCompletion = (ResponseResult) -> Void + typealias Parameters = [String: Any] typealias EndPoint = URLConvertible - /// <#Description#> /// - Parameters: @@ -33,23 +34,55 @@ protocol NetworkService { /// - parameters: <#parameters description#> /// - method: <#method description#> /// - headers: <#headers description#> + /// - completion: <#completion description#> func executeRequest(endpoint: EndPoint, parameters: Parameters, method: HTTPMethod, - headers: NetworkHeadersType) -> Observable> + headers: NetworkHeadersType, + completion: @escaping ResponseCompletion) -> DataRequest? + /// <#Description#> + /// - Parameters: + /// - endpoint: <#endpoint description#> + /// - method: <#method description#> + /// - parameter: <#parameter description#> + /// - headers: <#headers description#> + /// - completion: <#completion description#> + func executeRequest(endpoint: EndPoint, + method: HTTPMethod, + parameter: P, headers: NetworkHeadersType, + completion: @escaping ResponseCompletion) -> DataRequest? + //////////////////////////////////////////////////////////////// + // MARK: - + // MARK: RxSwift Methods + // MARK: - + //////////////////////////////////////////////////////////////// + + /// <#Description#> + /// - Parameters: + /// - endpoint: <#endpoint description#> + /// - parameters: <#parameters description#> + /// - method: <#method description#> + /// - headers: <#headers description#> + func executeRequest(endpoint: EndPoint, + parameters: Parameters, + method: HTTPMethod, + headers: NetworkHeadersType) -> Observable> /// <#Description#> /// - Parameters: /// - endpoint: <#endpoint description#> + /// - method: <#method description#> /// - parameter: <#parameter description#> /// - headers: <#headers description#> func executeRequest(endpoint: EndPoint, - parameter: P, headers: NetworkHeadersType) -> Observable> + method: HTTPMethod, + parameter: P, headers: NetworkHeadersType) -> Observable> } +/// <#Description#> protocol NetworkServiceInterceptable: NetworkService { /// <#Description#> @@ -58,17 +91,32 @@ protocol NetworkServiceInterceptable: NetworkService { } extension NetworkService { + + func executeRequest(endpoint: EndPoint, + parameters: Parameters, + method: HTTPMethod, + headers: NetworkHeadersType, + completion: @escaping ResponseCompletion) -> DataRequest? { + return nil + } + + func executeRequest(endpoint: EndPoint, + method: HTTPMethod, + parameter: P, headers: NetworkHeadersType, + completion: @escaping ResponseCompletion ) -> DataRequest? { + return nil + } - func executeRequest(endpoint: EndPoint, - parameters: Parameters, - method: HTTPMethod, headers: NetworkHeadersType) -> Observable> { + parameters: Parameters, + method: HTTPMethod, + headers: NetworkHeadersType) -> Observable> { return .empty() } - - func executeRequest(endpoint:EndPoint, - parameter: P, - headers: NetworkHeadersType) -> Observable> { + + func executeRequest(endpoint: EndPoint, + method: HTTPMethod, + parameter: P, headers: NetworkHeadersType) -> Observable> { return .empty() } } From 01ef0a6e01e7b66c7426f6f58ff8945cec72eb73 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sat, 19 Sep 2020 20:12:14 +0430 Subject: [PATCH 049/108] - Implemented APIClient abstract methods. --- .../Networking/APIClientService.swift | 161 ++++++++++++++++-- 1 file changed, 148 insertions(+), 13 deletions(-) diff --git a/DutchNews/Classes/Data Layers/Networking/APIClientService.swift b/DutchNews/Classes/Data Layers/Networking/APIClientService.swift index 40e63b7..e1bdcd7 100644 --- a/DutchNews/Classes/Data Layers/Networking/APIClientService.swift +++ b/DutchNews/Classes/Data Layers/Networking/APIClientService.swift @@ -14,7 +14,6 @@ private let queueName = "com.ifarshad.DutchNews.networking.response" fileprivate extension DispatchQueue { - /// Default queue for handling response static let networkResponseQueue = DispatchQueue(label: queueName, qos: .background, @@ -22,8 +21,9 @@ fileprivate extension DispatchQueue { autoreleaseFrequency: .workItem) } +/// <#Description#> final class APIClientService: NetworkServiceInterceptable { - + typealias SessionManager = Session /// <#Description#> @@ -39,7 +39,6 @@ final class APIClientService: NetworkServiceInterceptable { let decoder: DataDecoder - /// <#Description#> /// - Parameters: /// - baseURL: <#baseURL description#> @@ -49,12 +48,11 @@ final class APIClientService: NetworkServiceInterceptable { init(baseURL: URL, session: SessionManager = .default, queue: DispatchQueue = .networkResponseQueue, - decoder: @autoclosure () -> DataDecoder) { + decoder: DataDecoder = JSONDecoder()) { self.baseURL = baseURL self.session = session self.workQueue = queue - self.decoder = decoder() - + self.decoder = decoder } func addingRequest(interceptor: RequestInterceptor) { @@ -68,12 +66,10 @@ final class APIClientService: NetworkServiceInterceptable { } //////////////////////////////////////////////////////////////// - //MARK:- - //MARK:Private Methods - //MARK:- + // MARK: - + // MARK: Private Methods + // MARK: - //////////////////////////////////////////////////////////////// - - /// <#Description#> /// - Parameter endpoint: <#endpoint description#> @@ -88,7 +84,6 @@ final class APIClientService: NetworkServiceInterceptable { return joinedURL } - /// <#Description#> /// - Parameters: /// - dataRequest: <#dataRequest description#> @@ -98,8 +93,148 @@ final class APIClientService: NetworkServiceInterceptable { return dataRequest.rx.decodable(decoder: decoder) .map { value in return Result { value } - }.catchError { (error) -> Observable> in + }.catchError { (error) -> Observable> in .just(.failure(error)) + } + } + + //////////////////////////////////////////////////////////////// + // MARK: - + // MARK: Abstract Implementation + // MARK: - + //////////////////////////////////////////////////////////////// + + /// <#Description#> + /// - Parameters: + /// - endpoint: <#endpoint description#> + /// - parameters: <#parameters description#> + /// - method: <#method description#> + /// - headers: <#headers description#> + /// - completion: <#completion description#> + func executeRequest(endpoint: EndPoint, + parameters: Parameters, + method: HTTPMethod, + headers: NetworkHeadersType, + completion: @escaping ResponseCompletion) -> DataRequest? { + do { + + let url = try attachBaseURL(into: endpoint) + var headers = HTTPHeaders(headers) + let dataTask = session.request(url, + method: method, + parameters: parameters, + encoding: URLEncoding.default, + headers: headers, + interceptor: interceptor) + .validate() + .responseDecodable(queue: workQueue, decoder: decoder) { (response: DataResponse ) in + let result = response.result.flatMapError { (error) -> Result in + return .failure(error) + } + AuthenticationInterceptor + completion(result) + } + + return dataTask + }catch let error { + completion(.failure(error)) + return nil + } + } + + /// <#Description#> + /// - Parameters: + /// - endpoint: <#endpoint description#> + /// - method: <#method description#> + /// - parameter: <#parameter description#> + /// - headers: <#headers description#> + /// - completion: <#completion description#> + func executeRequest(endpoint: EndPoint, + method: HTTPMethod, + parameter: P, headers: NetworkHeadersType, + completion: @escaping ResponseCompletion) -> DataRequest? { + do { + + let url = try attachBaseURL(into: endpoint) + let dataTask = session.request(url, + method: method, + parameters: parameter, + encoder: JSONParameterEncoder.prettyPrinted, + headers: HTTPHeaders(headers), + interceptor: interceptor) + .validate() + .responseDecodable(queue: workQueue, decoder: decoder) { (response: DataResponse ) in + let result = response.result.flatMapError { (error) -> Result in + return .failure(error) + } + + completion(result) + } + + return dataTask + }catch let error { + completion(.failure(error)) + return nil + } + } + + //////////////////////////////////////////////////////////////// + // MARK: - + // MARK: RxSwift Methods + // MARK: - + //////////////////////////////////////////////////////////////// + + /// <#Description#> + /// - Parameters: + /// - endpoint: <#endpoint description#> + /// - parameters: <#parameters description#> + /// - method: <#method description#> + /// - headers: <#headers description#> + func executeRequest(endpoint: EndPoint, + parameters: Parameters, + method: HTTPMethod, + headers: NetworkHeadersType) -> Observable> { + do { + + let url = try attachBaseURL(into: endpoint) + let dataTask = session.request(url, + method: method, + parameters: parameters, + encoding: URLEncoding.default, + headers: HTTPHeaders(headers), + interceptor: interceptor) + .validate() + + return map(dataRequest: dataTask, decoder: decoder) + + }catch let error { + return .just(.failure(error)) + } + } + + /// <#Description#> + /// - Parameters: + /// - endpoint: <#endpoint description#> + /// - method: <#method description#> + /// - parameter: <#parameter description#> + /// - headers: <#headers description#> + func executeRequest(endpoint: EndPoint, + method: HTTPMethod, + parameter: P, headers: NetworkHeadersType) -> Observable> { + do { + + let url = try attachBaseURL(into: endpoint) + let dataTask = session.request(url, + method: method, + parameters: parameter, + encoder: JSONParameterEncoder.prettyPrinted, + headers: HTTPHeaders(headers), + interceptor: interceptor) + .validate() + return map(dataRequest: dataTask, decoder: decoder) + + }catch let error { + return .just(.failure(error)) } } From 6fa9f3788259f9cf863e29bc806362a2959860b9 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sat, 19 Sep 2020 20:13:13 +0430 Subject: [PATCH 050/108] - Added Helper Class for Network Mocking . - Added and Implemented APIClientServiceTests. --- DutchNews.xcodeproj/project.pbxproj | 28 +++ .../NetworkTests/APIClientServiceTests.swift | 189 ++++++++++++++++++ .../NetworkTests/Helper/NetworkMocking.swift | 61 ++++++ .../Helper/NetworkMockingDataFactory.swift | 43 ++++ Podfile | 4 +- Podfile.lock | 6 +- 6 files changed, 329 insertions(+), 2 deletions(-) create mode 100644 DutchNewsTests/NetworkTests/APIClientServiceTests.swift create mode 100644 DutchNewsTests/NetworkTests/Helper/NetworkMocking.swift create mode 100644 DutchNewsTests/NetworkTests/Helper/NetworkMockingDataFactory.swift diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index 318e7ae..b3b159f 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -10,6 +10,9 @@ 21E07E636FF6F2984928E95F /* Pods_DutchNews.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6D6B9A18971CCB93B1E1B562 /* Pods_DutchNews.framework */; }; 8B7AA46F365111AC7BEAABB4 /* Pods_DutchNewsUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06FCAD588464F494AF84398E /* Pods_DutchNewsUITests.framework */; }; D9C19360BE6ACB6A2FE7DA86 /* Pods_DutchNewsTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4DDFCE580208FED7E50E9D2 /* Pods_DutchNewsTests.framework */; }; + F82C8EFD2516304D002B27B3 /* APIClientServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8EFC2516304D002B27B3 /* APIClientServiceTests.swift */; }; + F82C8EFF25163073002B27B3 /* NetworkMocking.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8EFE25163073002B27B3 /* NetworkMocking.swift */; }; + F82C8F0125163898002B27B3 /* NetworkMockingDataFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8F0025163898002B27B3 /* NetworkMockingDataFactory.swift */; }; F89B021E250D446000B41293 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89B021D250D446000B41293 /* AppDelegate.swift */; }; F89B0220250D446200B41293 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F89B021F250D446200B41293 /* Assets.xcassets */; }; F89B0223250D446200B41293 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F89B0221250D446200B41293 /* LaunchScreen.storyboard */; }; @@ -49,6 +52,9 @@ CE0BB7F85E1175162F017DD0 /* Pods-DutchNews.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DutchNews.debug.xcconfig"; path = "Target Support Files/Pods-DutchNews/Pods-DutchNews.debug.xcconfig"; sourceTree = ""; }; DD3A1F2B03FE42DB1EAECC2E /* Pods-DutchNewsUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DutchNewsUITests.release.xcconfig"; path = "Target Support Files/Pods-DutchNewsUITests/Pods-DutchNewsUITests.release.xcconfig"; sourceTree = ""; }; E4DDFCE580208FED7E50E9D2 /* Pods_DutchNewsTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DutchNewsTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F82C8EFC2516304D002B27B3 /* APIClientServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClientServiceTests.swift; sourceTree = ""; }; + F82C8EFE25163073002B27B3 /* NetworkMocking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMocking.swift; sourceTree = ""; }; + F82C8F0025163898002B27B3 /* NetworkMockingDataFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMockingDataFactory.swift; sourceTree = ""; }; F89B021A250D446000B41293 /* DutchNews.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DutchNews.app; sourceTree = BUILT_PRODUCTS_DIR; }; F89B021D250D446000B41293 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F89B021F250D446200B41293 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -103,6 +109,24 @@ name = Frameworks; sourceTree = ""; }; + F82C8EFB2516051D002B27B3 /* NetworkTests */ = { + isa = PBXGroup; + children = ( + F82C8F0225163931002B27B3 /* Helper */, + F82C8EFC2516304D002B27B3 /* APIClientServiceTests.swift */, + ); + path = NetworkTests; + sourceTree = ""; + }; + F82C8F0225163931002B27B3 /* Helper */ = { + isa = PBXGroup; + children = ( + F82C8EFE25163073002B27B3 /* NetworkMocking.swift */, + F82C8F0025163898002B27B3 /* NetworkMockingDataFactory.swift */, + ); + path = Helper; + sourceTree = ""; + }; F89B0211250D446000B41293 = { isa = PBXGroup; children = ( @@ -139,6 +163,7 @@ F89B022C250D446200B41293 /* DutchNewsTests */ = { isa = PBXGroup; children = ( + F82C8EFB2516051D002B27B3 /* NetworkTests */, F89B022D250D446200B41293 /* DutchNewsTests.swift */, F89B022F250D446200B41293 /* Info.plist */, ); @@ -505,6 +530,9 @@ buildActionMask = 2147483647; files = ( F89B022E250D446200B41293 /* DutchNewsTests.swift in Sources */, + F82C8F0125163898002B27B3 /* NetworkMockingDataFactory.swift in Sources */, + F82C8EFF25163073002B27B3 /* NetworkMocking.swift in Sources */, + F82C8EFD2516304D002B27B3 /* APIClientServiceTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/DutchNewsTests/NetworkTests/APIClientServiceTests.swift b/DutchNewsTests/NetworkTests/APIClientServiceTests.swift new file mode 100644 index 0000000..d613a18 --- /dev/null +++ b/DutchNewsTests/NetworkTests/APIClientServiceTests.swift @@ -0,0 +1,189 @@ +// +// APIClientServiceTests.swift +// DutchNewsTests +// +// Created by Farshad Mousalou on 9/19/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import XCTest +import Foundation +import UIKit +import Mocker +import Alamofire +@testable import DutchNews + +class APIClientServiceTests: XCTestCase { + + typealias DataDecoder = Alamofire.DataDecoder + + var networkService: NetworkService! + + override func setUp() { + let configuration = URLSessionConfiguration.af.default + configuration.protocolClasses = [MockingURLProtocol.self] + let sessionManager = Alamofire.Session(configuration: configuration) + + networkService = APIClientService(baseURL: URL(string: "https://domain.com/")!, + session: sessionManager, decoder: JSONDecoder()) + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + networkService = nil + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testSimpleRequestResponse() { + let expectedData = NetworkMockingDataFactory.createSimpleJSONData() + + let expectations = self.expectation(description: "SimpleRequestResponse") + + do { + let mock = try NetworkMockBuilder(URL: "https://domain.com/api/user") + .set(method: .get) + .set(statusCode: 200) + .set(data: [.get: expectedData]) + .set(contentType: .json) + .build() + + Mocker.register(mock) + + _ = networkService.executeRequest(endpoint: "api/user", + parameters: [:], + method: .get, + headers: [:], completion: { (result: Result) in + switch result { + case .success(let value): + print(value) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + expectations.fulfill() + }) + + } catch let error { + XCTFail(error.localizedDescription) + } + + self.waitForExpectations(timeout: 10.0) { (error) in + XCTAssertNil(error, "error occured \(error!)") + } + } + + func testSimpleResponseContentTypeErrorHandling() { + + let expectedData = NetworkMockingDataFactory.createSimpleJSONData() + let expectations = self.expectation(description: "SimpleResponseContentType") + + do { + let mock = try NetworkMockBuilder(URL: "https://domain.com/api/user") + .set(method: .get) + .set(contentType: .html) + .set(statusCode: 200) + .set(data: [.get: expectedData]) + .build() + + Mocker.register(mock) + + _ = networkService.executeRequest(endpoint: "/api/user", + parameters: [:], + method: .get, + headers: ["Accept": "application/json"], + completion: { ( result : Result) in + print("result => ", result) + switch result { + case .success(let _): + XCTFail("SimpleResponse should have a value response") + case .failure(let error): + print("Error Occured ",error.localizedDescription) + } + expectations.fulfill() + }) + + } catch let error { + XCTFail(error.localizedDescription) + } + + self.waitForExpectations(timeout: 10.0) { (error) in + XCTAssertNil(error, "error occured \(error!)") + } + } + + func testSimpleResponseStatusCodeError() { + + let expectations = self.expectation(description: "SimpleResponseStatusCode") + + do { + let mock = try NetworkMockBuilder(URL: "https://domain.com/api/user") + .set(method: .get) + .set(statusCode: 400) + .set(data: [.get: "".data(using: .utf8)!]) + .set(contentType: .json) + .build() + + Mocker.register(mock) + + _ = networkService.executeRequest(endpoint: "api/user", + parameters: [:], + method: .get, + headers: [:], completion: { (result: Result) in + switch result { + case .success(let value): + print(value) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + expectations.fulfill() + }) + + } catch let error { + XCTFail(error.localizedDescription) + } + + self.waitForExpectations(timeout: 10.0) { (error) in + XCTAssertNil(error, "error occured \(error!)") + } + } + + func testRequestWithEmptyResponse() { + + let expectations = self.expectation(description: "EmptyRequestResponse") + + do { + let mock = try NetworkMockBuilder(URL: "https://domain.com/api/user") + .set(method: .get) + .set(statusCode: 204) + .set(data: [.head: "".data(using: .utf8)!]) + .set(contentType: .json) + .build() + + Mocker.register(mock) + + _ = networkService.executeRequest(endpoint: "api/user", + parameters: [:], + method: .head, + headers: [:], completion: { (result: Result) in + switch result { + case .success(let _): + XCTFail("The response should had a value.") + case .failure(let error): + print("error descripition :",error,error.localizedDescription) + } + + expectations.fulfill() + }) + + } catch let error { + XCTFail(error.localizedDescription) + } + + self.waitForExpectations(timeout: 10.0) { (error) in + XCTAssertNil(error, "error occured \(error!)") + } + } + + +} diff --git a/DutchNewsTests/NetworkTests/Helper/NetworkMocking.swift b/DutchNewsTests/NetworkTests/Helper/NetworkMocking.swift new file mode 100644 index 0000000..a86a62a --- /dev/null +++ b/DutchNewsTests/NetworkTests/Helper/NetworkMocking.swift @@ -0,0 +1,61 @@ +// +// NetworkMocking.swift +// DutchNewsTests +// +// Created by Farshad Mousalou on 9/19/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import UIKit +import Mocker +import Alamofire +@testable import DutchNews + +class NetworkMockBuilder { + + private var url: URLConvertible + private var method: Mock.HTTPMethod = .get + private var contentType: Mock.ContentType = .html + private var statusCode: Int = 200 + private var data: [Mock.HTTPMethod: Data] = [:] + private var headers: [String: String] = [:] + + init(URL: URLConvertible) { + self.url = URL + } + + func set(method: Mock.HTTPMethod) -> NetworkMockBuilder { + self.method = method + return self as NetworkMockBuilder + } + + func set(contentType: Mock.ContentType) -> NetworkMockBuilder { + self.contentType = contentType + return self + } + + func set(statusCode: Int) -> NetworkMockBuilder { + self.statusCode = statusCode + return self + } + + func set(headers: [String: String]) -> NetworkMockBuilder { + self.headers = headers + return self + } + + func set(data: [Mock.HTTPMethod: Data]) -> NetworkMockBuilder { + self.data = data + return self + } + + func build() throws -> Mock { + let mock = Mock(url: try url.asURL(), + contentType: contentType, + statusCode: statusCode, data: data, additionalHeaders: headers) + + return mock + } + +} diff --git a/DutchNewsTests/NetworkTests/Helper/NetworkMockingDataFactory.swift b/DutchNewsTests/NetworkTests/Helper/NetworkMockingDataFactory.swift new file mode 100644 index 0000000..c44738a --- /dev/null +++ b/DutchNewsTests/NetworkTests/Helper/NetworkMockingDataFactory.swift @@ -0,0 +1,43 @@ +// +// NetworkMockingDataFactory.swift +// DutchNewsTests +// +// Created by Farshad Mousalou on 9/19/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import UIKit +import Mocker +import Alamofire +@testable import DutchNews + +struct NetworkMockingDataFactory { + + static func createSimpleJSONData() -> Data { + let person = StubPerson(name: "John", age: 20, email: "john@gmail.com") + let data = try! JSONEncoder().encode(person) + return data + } + + static func createSimpleArrayJSONData() -> Data { + let persons = [StubPerson(name: "John", age: 20, email: "john@gmail.com"), StubPerson(name: "Smith", age: 40, email: nil)] + let data = try! JSONEncoder().encode(persons) + return data + } + + static func createRealJSONData() -> Data { + fatalError("") + } + + static func createImageData() -> Data { + fatalError("") + } + +} + +struct StubPerson: Codable { + let name: String + let age: Int + let email: String? +} diff --git a/Podfile b/Podfile index 02c8051..33646f5 100644 --- a/Podfile +++ b/Podfile @@ -37,10 +37,12 @@ target 'DutchNews' do end target 'DutchNewsTests' do - inherit! :search_paths + inherit! :complete + pod 'RxAlamofire' pod 'RxTest' pod 'RxBlocking' pod 'Nimble' + pod 'Mocker', '~> 1.0.0' # Pods for testing end diff --git a/Podfile.lock b/Podfile.lock index 2a0607b..e41bdbe 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -644,6 +644,7 @@ PODS: - MaterialComponents/private/Math - MDFInternationalization (2.0.0) - MDFTextAccessibility (2.0.0) + - Mocker (1.0.0) - MotionAnimator (2.8.1): - MotionInterchange (~> 1.6) - MotionInterchange (1.6.0) @@ -684,6 +685,7 @@ DEPENDENCIES: - JEKScrollableSectionCollectionViewLayout (from `https://github.com/farshadmb/JEKScrollableSectionCollectionViewLayout.git`) - MagazineLayout - MaterialComponents + - Mocker (~> 1.0.0) - MXParallaxHeader - Nimble - Pageboy @@ -707,6 +709,7 @@ SPEC REPOS: - MaterialComponents - MDFInternationalization - MDFTextAccessibility + - Mocker - MotionAnimator - MotionInterchange - MXParallaxHeader @@ -743,6 +746,7 @@ SPEC CHECKSUMS: MaterialComponents: 00df0652f52cd6968b02d531bd2e6956b0f907b8 MDFInternationalization: 010097556d6b09d2c4ea38e0820ea6d37be6a314 MDFTextAccessibility: 85c09a1bd9c321f494348e632a25063bcda35a53 + Mocker: 58560cc516f6240e92492dad66f295e7cdd7cdb2 MotionAnimator: ee16aa30567c5bae0fb2750c132915829cfaaf8a MotionInterchange: ead0e3ae1f3a5fb539e289debbc7ae036160a10d MXParallaxHeader: de3c867e10ba46e8f6e20c8ee1f2a910372b3b94 @@ -760,6 +764,6 @@ SPEC CHECKSUMS: RxTest: 711632d5644dffbeb62c936a521b5b008a1e1faa SwiftLint: 22ccbbe3b8008684be5955693bab135e0ed6a447 -PODFILE CHECKSUM: 9c4baee10c033d3323f4ed65a57d7bdff5880966 +PODFILE CHECKSUM: ab5e0d8b8f4de3440f514d8d3a2877ecd5be440f COCOAPODS: 1.9.3 From 64641eeeb81c28a5f84d86ce7a12d5d7adcecbc6 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sat, 19 Sep 2020 20:41:56 +0430 Subject: [PATCH 051/108] -removed extra word in APIClientService. --- DutchNews/Classes/Data Layers/Networking/APIClientService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DutchNews/Classes/Data Layers/Networking/APIClientService.swift b/DutchNews/Classes/Data Layers/Networking/APIClientService.swift index e1bdcd7..82f0f3a 100644 --- a/DutchNews/Classes/Data Layers/Networking/APIClientService.swift +++ b/DutchNews/Classes/Data Layers/Networking/APIClientService.swift @@ -131,7 +131,7 @@ final class APIClientService: NetworkServiceInterceptable { let result = response.result.flatMapError { (error) -> Result in return .failure(error) } - AuthenticationInterceptor + completion(result) } From 65b8f82886a21eb6b566029f92324a7cac60d701 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sat, 19 Sep 2020 20:49:37 +0430 Subject: [PATCH 052/108] -added tests report command in fastlane. --- Gemfile | 4 +++- Gemfile.lock | 2 ++ fastlane/Fastfile | 11 +++-------- fastlane/Pluginfile | 5 +++++ 4 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 fastlane/Pluginfile diff --git a/Gemfile b/Gemfile index f9733d6..c7de801 100644 --- a/Gemfile +++ b/Gemfile @@ -2,4 +2,6 @@ source "https://rubygems.org" gem "fastlane" gem "xcode-install" -gem "cocoapods" \ No newline at end of file +gem "cocoapods" +plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') +eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/Gemfile.lock b/Gemfile.lock index 3843a38..09bf10f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -129,6 +129,7 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) + fastlane-plugin-xchtmlreport (0.1.1) ffi (1.13.1) fourflusher (2.3.1) fuzzy_match (2.0.4) @@ -241,6 +242,7 @@ PLATFORMS DEPENDENCIES cocoapods fastlane + fastlane-plugin-xchtmlreport xcode-install BUNDLED WITH diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 3409579..943592f 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -67,7 +67,7 @@ platform :ios do desc "Run App Unit tests on given devices name" lane :run_ci_tests do |options| - begin + UI.message "The devices raw : #{options[:devices]}" devices = eval options[:devices] UI.message "The devices list : #{devices}" @@ -79,16 +79,11 @@ platform :ios do end scan(workspace: "#{workspace}", #workspace name - # xcargs:"CODE_SIGNING_REQUIRED=NO", scheme: scheme, # Project scheme name clean: clean, # clean project folder before test execution devices: devices, # Devices for testing - result_bundle: false) # To generate test reports - - rescue => ex - UI.error "Error Occured #{ex}" - generate_report - end + result_bundle: true) # To generate test reports + xchtmlreport() end end diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile new file mode 100644 index 0000000..9cbfa72 --- /dev/null +++ b/fastlane/Pluginfile @@ -0,0 +1,5 @@ +# Autogenerated by fastlane +# +# Ensure this file is checked in to source control! + +gem 'fastlane-plugin-xchtmlreport' From 63b994af30a9a44a7de8b5c74816db9fc6ffb249 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sat, 19 Sep 2020 22:29:41 +0430 Subject: [PATCH 053/108] - removed UITest Bundle for project. (cherry picked from commit f5648685fd1ea17c564e10328449de5480276cf0) --- DutchNews.xcodeproj/project.pbxproj | 151 ------------------------ DutchNewsUITests/DutchNewsUITests.swift | 44 ------- DutchNewsUITests/Info.plist | 22 ---- Podfile | 5 - 4 files changed, 222 deletions(-) delete mode 100644 DutchNewsUITests/DutchNewsUITests.swift delete mode 100644 DutchNewsUITests/Info.plist diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index b3b159f..411497e 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ 21E07E636FF6F2984928E95F /* Pods_DutchNews.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6D6B9A18971CCB93B1E1B562 /* Pods_DutchNews.framework */; }; - 8B7AA46F365111AC7BEAABB4 /* Pods_DutchNewsUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06FCAD588464F494AF84398E /* Pods_DutchNewsUITests.framework */; }; D9C19360BE6ACB6A2FE7DA86 /* Pods_DutchNewsTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4DDFCE580208FED7E50E9D2 /* Pods_DutchNewsTests.framework */; }; F82C8EFD2516304D002B27B3 /* APIClientServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8EFC2516304D002B27B3 /* APIClientServiceTests.swift */; }; F82C8EFF25163073002B27B3 /* NetworkMocking.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8EFE25163073002B27B3 /* NetworkMocking.swift */; }; @@ -17,7 +16,6 @@ F89B0220250D446200B41293 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F89B021F250D446200B41293 /* Assets.xcassets */; }; F89B0223250D446200B41293 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F89B0221250D446200B41293 /* LaunchScreen.storyboard */; }; F89B022E250D446200B41293 /* DutchNewsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89B022D250D446200B41293 /* DutchNewsTests.swift */; }; - F89B0239250D446200B41293 /* DutchNewsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89B0238250D446200B41293 /* DutchNewsUITests.swift */; }; F8DE79E32515904400A6C2D5 /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DE79E22515904400A6C2D5 /* NetworkService.swift */; }; F8DE79E5251594D700A6C2D5 /* APIClientService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DE79E4251594D700A6C2D5 /* APIClientService.swift */; }; F8F14C74250D719800C24FF5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F8F14C72250D719800C24FF5 /* Main.storyboard */; }; @@ -31,13 +29,6 @@ remoteGlobalIDString = F89B0219250D446000B41293; remoteInfo = DutchNews; }; - F89B0235250D446200B41293 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = F89B0212250D446000B41293 /* Project object */; - proxyType = 1; - remoteGlobalIDString = F89B0219250D446000B41293; - remoteInfo = DutchNews; - }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ @@ -63,9 +54,6 @@ F89B0229250D446200B41293 /* DutchNewsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DutchNewsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F89B022D250D446200B41293 /* DutchNewsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DutchNewsTests.swift; sourceTree = ""; }; F89B022F250D446200B41293 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - F89B0234250D446200B41293 /* DutchNewsUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DutchNewsUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - F89B0238250D446200B41293 /* DutchNewsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DutchNewsUITests.swift; sourceTree = ""; }; - F89B023A250D446200B41293 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F8DE79E22515904400A6C2D5 /* NetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = ""; }; F8DE79E4251594D700A6C2D5 /* APIClientService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClientService.swift; sourceTree = ""; }; F8F14C73250D719800C24FF5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -88,14 +76,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - F89B0231250D446200B41293 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 8B7AA46F365111AC7BEAABB4 /* Pods_DutchNewsUITests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -132,7 +112,6 @@ children = ( F89B021C250D446000B41293 /* DutchNews */, F89B022C250D446200B41293 /* DutchNewsTests */, - F89B0237250D446200B41293 /* DutchNewsUITests */, F89B021B250D446000B41293 /* Products */, FA6C9BAEFCB0D31A29283E4B /* Pods */, 4B3F257B8F205BA3F43A73E1 /* Frameworks */, @@ -144,7 +123,6 @@ children = ( F89B021A250D446000B41293 /* DutchNews.app */, F89B0229250D446200B41293 /* DutchNewsTests.xctest */, - F89B0234250D446200B41293 /* DutchNewsUITests.xctest */, ); name = Products; sourceTree = ""; @@ -170,15 +148,6 @@ path = DutchNewsTests; sourceTree = ""; }; - F89B0237250D446200B41293 /* DutchNewsUITests */ = { - isa = PBXGroup; - children = ( - F89B0238250D446200B41293 /* DutchNewsUITests.swift */, - F89B023A250D446200B41293 /* Info.plist */, - ); - path = DutchNewsUITests; - sourceTree = ""; - }; F8F14C68250D70AA00C24FF5 /* Classes */ = { isa = PBXGroup; children = ( @@ -319,25 +288,6 @@ productReference = F89B0229250D446200B41293 /* DutchNewsTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - F89B0233250D446200B41293 /* DutchNewsUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = F89B0243250D446200B41293 /* Build configuration list for PBXNativeTarget "DutchNewsUITests" */; - buildPhases = ( - B8172C4B044C0C51B6EEBC05 /* [CP] Check Pods Manifest.lock */, - F89B0230250D446200B41293 /* Sources */, - F89B0231250D446200B41293 /* Frameworks */, - F89B0232250D446200B41293 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - F89B0236250D446200B41293 /* PBXTargetDependency */, - ); - name = DutchNewsUITests; - productName = DutchNewsUITests; - productReference = F89B0234250D446200B41293 /* DutchNewsUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -357,11 +307,6 @@ ProvisioningStyle = Automatic; TestTargetID = F89B0219250D446000B41293; }; - F89B0233250D446200B41293 = { - CreatedOnToolsVersion = 11.2.1; - ProvisioningStyle = Automatic; - TestTargetID = F89B0219250D446000B41293; - }; }; }; buildConfigurationList = F89B0215250D446000B41293 /* Build configuration list for PBXProject "DutchNews" */; @@ -379,7 +324,6 @@ targets = ( F89B0219250D446000B41293 /* DutchNews */, F89B0228250D446200B41293 /* DutchNewsTests */, - F89B0233250D446200B41293 /* DutchNewsUITests */, ); }; /* End PBXProject section */ @@ -402,13 +346,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - F89B0232250D446200B41293 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -446,28 +383,6 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-DutchNewsTests/Pods-DutchNewsTests-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - B8172C4B044C0C51B6EEBC05 /* [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-DutchNewsUITests-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; - }; ED77C5D597B0DE2AE4BC3C93 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -536,14 +451,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - F89B0230250D446200B41293 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F89B0239250D446200B41293 /* DutchNewsUITests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -552,11 +459,6 @@ target = F89B0219250D446000B41293 /* DutchNews */; targetProxy = F89B022A250D446200B41293 /* PBXContainerItemProxy */; }; - F89B0236250D446200B41293 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = F89B0219250D446000B41293 /* DutchNews */; - targetProxy = F89B0235250D446200B41293 /* PBXContainerItemProxy */; - }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -783,50 +685,6 @@ }; name = Release; }; - F89B0244250D446200B41293 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 58F7ABB66E4031DDAE7CBDC7 /* Pods-DutchNewsUITests.debug.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 638B4QA28J; - INFOPLIST_FILE = DutchNewsUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.ifarshad.DutchNewsUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = DutchNews; - }; - name = Debug; - }; - F89B0245250D446200B41293 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = DD3A1F2B03FE42DB1EAECC2E /* Pods-DutchNewsUITests.release.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 638B4QA28J; - INFOPLIST_FILE = DutchNewsUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.ifarshad.DutchNewsUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = DutchNews; - }; - name = Release; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -857,15 +715,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - F89B0243250D446200B41293 /* Build configuration list for PBXNativeTarget "DutchNewsUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F89B0244250D446200B41293 /* Debug */, - F89B0245250D446200B41293 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ }; rootObject = F89B0212250D446000B41293 /* Project object */; diff --git a/DutchNewsUITests/DutchNewsUITests.swift b/DutchNewsUITests/DutchNewsUITests.swift deleted file mode 100644 index 1b7f2ed..0000000 --- a/DutchNewsUITests/DutchNewsUITests.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// DutchNewsUITests.swift -// DutchNewsUITests -// -// Created by Farshad Mousalou on 9/12/20. -// Copyright © 2020 Farshad Mousalou. All rights reserved. -// - -import XCTest -@testable import DutchNews - -class DutchNewsUITests: XCTestCase { - - override func setUp() { - // 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 tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() { - // 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() { - if #available(iOS 13.0, *) { - // This measures how long it takes to launch your application. - measure(metrics: [XCTOSSignpostMetric.applicationLaunch]) { - XCUIApplication().launch() - } - } - } -} diff --git a/DutchNewsUITests/Info.plist b/DutchNewsUITests/Info.plist deleted file mode 100644 index 64d65ca..0000000 --- a/DutchNewsUITests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/Podfile b/Podfile index 33646f5..f83db76 100644 --- a/Podfile +++ b/Podfile @@ -47,8 +47,3 @@ target 'DutchNewsTests' do # Pods for testing end -target 'DutchNewsUITests' do - inherit! :search_paths - # Pods for testing -end - From f947bc10f2d4598a61c0510d9856e8fff6bb8cc3 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sat, 19 Sep 2020 23:09:10 +0430 Subject: [PATCH 054/108] - Added and Implemented Authenticator. --- DutchNews.xcodeproj/project.pbxproj | 12 ++++++++ .../Authenticator/Authenticator.swift | 28 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 DutchNews/Classes/Data Layers/Authenticator/Authenticator.swift diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index 411497e..f648ff2 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ F82C8EFD2516304D002B27B3 /* APIClientServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8EFC2516304D002B27B3 /* APIClientServiceTests.swift */; }; F82C8EFF25163073002B27B3 /* NetworkMocking.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8EFE25163073002B27B3 /* NetworkMocking.swift */; }; F82C8F0125163898002B27B3 /* NetworkMockingDataFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8F0025163898002B27B3 /* NetworkMockingDataFactory.swift */; }; + F865F74C251686D2001FD067 /* Authenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F74B251686D2001FD067 /* Authenticator.swift */; }; F89B021E250D446000B41293 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89B021D250D446000B41293 /* AppDelegate.swift */; }; F89B0220250D446200B41293 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F89B021F250D446200B41293 /* Assets.xcassets */; }; F89B0223250D446200B41293 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F89B0221250D446200B41293 /* LaunchScreen.storyboard */; }; @@ -46,6 +47,7 @@ F82C8EFC2516304D002B27B3 /* APIClientServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClientServiceTests.swift; sourceTree = ""; }; F82C8EFE25163073002B27B3 /* NetworkMocking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMocking.swift; sourceTree = ""; }; F82C8F0025163898002B27B3 /* NetworkMockingDataFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMockingDataFactory.swift; sourceTree = ""; }; + F865F74B251686D2001FD067 /* Authenticator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Authenticator.swift; sourceTree = ""; }; F89B021A250D446000B41293 /* DutchNews.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DutchNews.app; sourceTree = BUILT_PRODUCTS_DIR; }; F89B021D250D446000B41293 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F89B021F250D446200B41293 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -107,6 +109,14 @@ path = Helper; sourceTree = ""; }; + F865F74A251686C1001FD067 /* Authenticator */ = { + isa = PBXGroup; + children = ( + F865F74B251686D2001FD067 /* Authenticator.swift */, + ); + path = Authenticator; + sourceTree = ""; + }; F89B0211250D446000B41293 = { isa = PBXGroup; children = ( @@ -164,6 +174,7 @@ F8F14C69250D70B400C24FF5 /* Data Layers */ = { isa = PBXGroup; children = ( + F865F74A251686C1001FD067 /* Authenticator */, F8F14C6E250D70F900C24FF5 /* Networking */, ); path = "Data Layers"; @@ -435,6 +446,7 @@ buildActionMask = 2147483647; files = ( F8DE79E32515904400A6C2D5 /* NetworkService.swift in Sources */, + F865F74C251686D2001FD067 /* Authenticator.swift in Sources */, F8DE79E5251594D700A6C2D5 /* APIClientService.swift in Sources */, F89B021E250D446000B41293 /* AppDelegate.swift in Sources */, ); diff --git a/DutchNews/Classes/Data Layers/Authenticator/Authenticator.swift b/DutchNews/Classes/Data Layers/Authenticator/Authenticator.swift new file mode 100644 index 0000000..79dd386 --- /dev/null +++ b/DutchNews/Classes/Data Layers/Authenticator/Authenticator.swift @@ -0,0 +1,28 @@ +// +// Authenticator.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/19/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import Alamofire + +struct APIAuthenticator: RequestInterceptor { + + let token : String + + init(token: String) { + self.token = token + } + + func adapt(_ urlRequest: URLRequest, + for session: Session, + completion: @escaping (Result) -> Void) { + var request = urlRequest + request.headers.add(HTTPHeader.authorization(bearerToken: self.token)) + completion(Result { request }) + } + +} From 64e3e40de214940dbe2e6e01d1c0579b096b5426 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sat, 19 Sep 2020 23:43:13 +0430 Subject: [PATCH 055/108] - Added AppConfig for app configuration constants. - Added APIAuthenticatorTests. --- DutchNews.xcodeproj/project.pbxproj | 8 ++ DutchNews/AppConfig.swift | 13 +++ .../Authenticator/Authenticator.swift | 2 +- .../NetworkTests/APIAuthenticatorTests.swift | 109 ++++++++++++++++++ .../NetworkTests/APIClientServiceTests.swift | 7 +- Podfile.lock | 2 +- 6 files changed, 135 insertions(+), 6 deletions(-) create mode 100644 DutchNews/AppConfig.swift create mode 100644 DutchNewsTests/NetworkTests/APIAuthenticatorTests.swift diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index f648ff2..c6e01d5 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -13,6 +13,8 @@ F82C8EFF25163073002B27B3 /* NetworkMocking.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8EFE25163073002B27B3 /* NetworkMocking.swift */; }; F82C8F0125163898002B27B3 /* NetworkMockingDataFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8F0025163898002B27B3 /* NetworkMockingDataFactory.swift */; }; F865F74C251686D2001FD067 /* Authenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F74B251686D2001FD067 /* Authenticator.swift */; }; + F865F74E251687E4001FD067 /* APIAuthenticatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F74D251687E4001FD067 /* APIAuthenticatorTests.swift */; }; + F865F75025168F05001FD067 /* AppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F74F25168F05001FD067 /* AppConfig.swift */; }; F89B021E250D446000B41293 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89B021D250D446000B41293 /* AppDelegate.swift */; }; F89B0220250D446200B41293 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F89B021F250D446200B41293 /* Assets.xcassets */; }; F89B0223250D446200B41293 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F89B0221250D446200B41293 /* LaunchScreen.storyboard */; }; @@ -48,6 +50,8 @@ F82C8EFE25163073002B27B3 /* NetworkMocking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMocking.swift; sourceTree = ""; }; F82C8F0025163898002B27B3 /* NetworkMockingDataFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMockingDataFactory.swift; sourceTree = ""; }; F865F74B251686D2001FD067 /* Authenticator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Authenticator.swift; sourceTree = ""; }; + F865F74D251687E4001FD067 /* APIAuthenticatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIAuthenticatorTests.swift; sourceTree = ""; }; + F865F74F25168F05001FD067 /* AppConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfig.swift; sourceTree = ""; }; F89B021A250D446000B41293 /* DutchNews.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DutchNews.app; sourceTree = BUILT_PRODUCTS_DIR; }; F89B021D250D446000B41293 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F89B021F250D446200B41293 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -96,6 +100,7 @@ children = ( F82C8F0225163931002B27B3 /* Helper */, F82C8EFC2516304D002B27B3 /* APIClientServiceTests.swift */, + F865F74D251687E4001FD067 /* APIAuthenticatorTests.swift */, ); path = NetworkTests; sourceTree = ""; @@ -144,6 +149,7 @@ F8F14C70250D712400C24FF5 /* Resources */, F89B021D250D446000B41293 /* AppDelegate.swift */, F89B0224250D446200B41293 /* Info.plist */, + F865F74F25168F05001FD067 /* AppConfig.swift */, ); path = DutchNews; sourceTree = ""; @@ -446,6 +452,7 @@ buildActionMask = 2147483647; files = ( F8DE79E32515904400A6C2D5 /* NetworkService.swift in Sources */, + F865F75025168F05001FD067 /* AppConfig.swift in Sources */, F865F74C251686D2001FD067 /* Authenticator.swift in Sources */, F8DE79E5251594D700A6C2D5 /* APIClientService.swift in Sources */, F89B021E250D446000B41293 /* AppDelegate.swift in Sources */, @@ -459,6 +466,7 @@ F89B022E250D446200B41293 /* DutchNewsTests.swift in Sources */, F82C8F0125163898002B27B3 /* NetworkMockingDataFactory.swift in Sources */, F82C8EFF25163073002B27B3 /* NetworkMocking.swift in Sources */, + F865F74E251687E4001FD067 /* APIAuthenticatorTests.swift in Sources */, F82C8EFD2516304D002B27B3 /* APIClientServiceTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/DutchNews/AppConfig.swift b/DutchNews/AppConfig.swift new file mode 100644 index 0000000..bc7f8ef --- /dev/null +++ b/DutchNews/AppConfig.swift @@ -0,0 +1,13 @@ +// +// AppConfig.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/19/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation + +struct AppConfig { + static let APIKey = "56450901b0134dcbb5627035b12fca99" +} diff --git a/DutchNews/Classes/Data Layers/Authenticator/Authenticator.swift b/DutchNews/Classes/Data Layers/Authenticator/Authenticator.swift index 79dd386..3c775b3 100644 --- a/DutchNews/Classes/Data Layers/Authenticator/Authenticator.swift +++ b/DutchNews/Classes/Data Layers/Authenticator/Authenticator.swift @@ -11,7 +11,7 @@ import Alamofire struct APIAuthenticator: RequestInterceptor { - let token : String + let token: String init(token: String) { self.token = token diff --git a/DutchNewsTests/NetworkTests/APIAuthenticatorTests.swift b/DutchNewsTests/NetworkTests/APIAuthenticatorTests.swift new file mode 100644 index 0000000..47e60e9 --- /dev/null +++ b/DutchNewsTests/NetworkTests/APIAuthenticatorTests.swift @@ -0,0 +1,109 @@ +// +// APIAuthenticatorTests.swift +// DutchNewsTests +// +// Created by Farshad Mousalou on 9/19/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import XCTest +import Foundation +import UIKit +import Mocker +import Alamofire +@testable import DutchNews + +class APIAuthenticatorTests: XCTestCase { + + var networkService: NetworkServiceInterceptable! + + override func setUp() { + + networkService = APIClientService(baseURL: URL(string: "https://newsapi.org/")!) + + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + networkService = nil + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testUnauthenticatedRequest() { + + let expectations = self.expectation(description: "UnaunthenticatedRequest") + + _ = networkService.executeRequest(endpoint: "v2/top-headlines", + parameters: ["country": "nl"], + method: .get, + headers: [:], completion: { (result: Result) in + switch result { + case .success(let value): + print(value) + XCTFail("Server must return unauthorized error with status code 401") + case .failure(let error): + print("Response Error => ",error, "localized Info: ",error.localizedDescription) + } + + expectations.fulfill() + }) + + self.waitForExpectations(timeout: 30.0) { (error) in + XCTAssertNil(error, "error occured \(error!)") + } + } + + func testEmptyAPIKeyAuthenticationRequest() { + + let expectations = self.expectation(description: "EmptyAPIKeyAuthentication") + let authenticator: RequestInterceptor = APIAuthenticator(token: "") + + networkService.addingRequest(interceptor: authenticator) + + _ = networkService.executeRequest(endpoint: "v2/top-headlines", + parameters: ["country": "nl"], + method: .get, + headers: [:], completion: { (result: Result) in + switch result { + + case .success(let value): + print(value) + XCTFail("Server must return unauthorized error with status code 401") + case .failure(let error): + print("Response Error => ",error, "localized Info: ",error.localizedDescription) + } + + expectations.fulfill() + }) + + self.waitForExpectations(timeout: 30.0) { (error) in + XCTAssertNil(error, "error occured \(error!)") + } + } + + func testAuthenticationSuccessfullRequest() { + + let expectations = self.expectation(description: "AuthenticationSuccessfull") + let authenticator: RequestInterceptor = APIAuthenticator(token: AppConfig.APIKey) + + networkService.addingRequest(interceptor: authenticator) + + _ = networkService.executeRequest(endpoint: "v2/top-headlines", + parameters: ["country": "nl"], + method: .get, + headers: [:], completion: { (result: Result) in + switch result { + case .success(let value): + print(value) + case .failure(let error): + XCTFail("Response Error => \(error), localized Info: \(error.localizedDescription)") + } + + expectations.fulfill() + }) + + self.waitForExpectations(timeout: 30.0) { (error) in + XCTAssertNil(error, "error occured \(error!)") + } + } +} diff --git a/DutchNewsTests/NetworkTests/APIClientServiceTests.swift b/DutchNewsTests/NetworkTests/APIClientServiceTests.swift index d613a18..147a319 100644 --- a/DutchNewsTests/NetworkTests/APIClientServiceTests.swift +++ b/DutchNewsTests/NetworkTests/APIClientServiceTests.swift @@ -91,10 +91,10 @@ class APIClientServiceTests: XCTestCase { parameters: [:], method: .get, headers: ["Accept": "application/json"], - completion: { ( result : Result) in + completion: { ( result: Result) in print("result => ", result) switch result { - case .success(let _): + case .success(_): XCTFail("SimpleResponse should have a value response") case .failure(let error): print("Error Occured ",error.localizedDescription) @@ -167,7 +167,7 @@ class APIClientServiceTests: XCTestCase { method: .head, headers: [:], completion: { (result: Result) in switch result { - case .success(let _): + case .success(_): XCTFail("The response should had a value.") case .failure(let error): print("error descripition :",error,error.localizedDescription) @@ -185,5 +185,4 @@ class APIClientServiceTests: XCTestCase { } } - } diff --git a/Podfile.lock b/Podfile.lock index e41bdbe..fc6bf4d 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -764,6 +764,6 @@ SPEC CHECKSUMS: RxTest: 711632d5644dffbeb62c936a521b5b008a1e1faa SwiftLint: 22ccbbe3b8008684be5955693bab135e0ed6a447 -PODFILE CHECKSUM: ab5e0d8b8f4de3440f514d8d3a2877ecd5be440f +PODFILE CHECKSUM: 642d3a8ac7db7cbfe1ddc2c86cd18cf4177143c5 COCOAPODS: 1.9.3 From c35fc57faf5b3c15e0b3d8820d8f6fb12a86ef34 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sun, 20 Sep 2020 00:06:19 +0430 Subject: [PATCH 056/108] -Fixed result expectation in APIClientServiceTests.swift --- DutchNewsTests/NetworkTests/APIClientServiceTests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/DutchNewsTests/NetworkTests/APIClientServiceTests.swift b/DutchNewsTests/NetworkTests/APIClientServiceTests.swift index 147a319..ff77e2f 100644 --- a/DutchNewsTests/NetworkTests/APIClientServiceTests.swift +++ b/DutchNewsTests/NetworkTests/APIClientServiceTests.swift @@ -95,7 +95,7 @@ class APIClientServiceTests: XCTestCase { print("result => ", result) switch result { case .success(_): - XCTFail("SimpleResponse should have a value response") + XCTFail("SimpleResponse should not have a value response") case .failure(let error): print("Error Occured ",error.localizedDescription) } @@ -130,10 +130,10 @@ class APIClientServiceTests: XCTestCase { method: .get, headers: [:], completion: { (result: Result) in switch result { - case .success(let value): - print(value) + case .success(_): + XCTFail("SimpleResponse should not have a value response") case .failure(let error): - XCTFail(error.localizedDescription) + print("Error Occured ",error.localizedDescription) } expectations.fulfill() From 618dc6bff3c2271614e41ca384061fe0add0f98d Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sun, 20 Sep 2020 00:36:51 +0430 Subject: [PATCH 057/108] - Added WebRepository Abstract. - Added SearchNewsRepository and implemented WebRepository. - Added NewsHeadlinesRepository and implemented WebRepository. --- DutchNews.xcodeproj/project.pbxproj | 20 +++++++++++++++++ .../NewsHeadlinesRepository.swift | 22 +++++++++++++++++++ .../Repositories/SearchNewsRepository.swift | 22 +++++++++++++++++++ .../Repositories/WebRepository.swift | 21 ++++++++++++++++++ 4 files changed, 85 insertions(+) create mode 100644 DutchNews/Classes/Data Layers/Repositories/NewsHeadlinesRepository.swift create mode 100644 DutchNews/Classes/Data Layers/Repositories/SearchNewsRepository.swift create mode 100644 DutchNews/Classes/Data Layers/Repositories/WebRepository.swift diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index c6e01d5..4e7131f 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -15,6 +15,9 @@ F865F74C251686D2001FD067 /* Authenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F74B251686D2001FD067 /* Authenticator.swift */; }; F865F74E251687E4001FD067 /* APIAuthenticatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F74D251687E4001FD067 /* APIAuthenticatorTests.swift */; }; F865F75025168F05001FD067 /* AppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F74F25168F05001FD067 /* AppConfig.swift */; }; + F865F753251699AF001FD067 /* WebRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F752251699AF001FD067 /* WebRepository.swift */; }; + F865F75525169A2A001FD067 /* NewsHeadlinesRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F75425169A2A001FD067 /* NewsHeadlinesRepository.swift */; }; + F865F75725169ACF001FD067 /* SearchNewsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F75625169ACF001FD067 /* SearchNewsRepository.swift */; }; F89B021E250D446000B41293 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89B021D250D446000B41293 /* AppDelegate.swift */; }; F89B0220250D446200B41293 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F89B021F250D446200B41293 /* Assets.xcassets */; }; F89B0223250D446200B41293 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F89B0221250D446200B41293 /* LaunchScreen.storyboard */; }; @@ -52,6 +55,9 @@ F865F74B251686D2001FD067 /* Authenticator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Authenticator.swift; sourceTree = ""; }; F865F74D251687E4001FD067 /* APIAuthenticatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIAuthenticatorTests.swift; sourceTree = ""; }; F865F74F25168F05001FD067 /* AppConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfig.swift; sourceTree = ""; }; + F865F752251699AF001FD067 /* WebRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRepository.swift; sourceTree = ""; }; + F865F75425169A2A001FD067 /* NewsHeadlinesRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsHeadlinesRepository.swift; sourceTree = ""; }; + F865F75625169ACF001FD067 /* SearchNewsRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchNewsRepository.swift; sourceTree = ""; }; F89B021A250D446000B41293 /* DutchNews.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DutchNews.app; sourceTree = BUILT_PRODUCTS_DIR; }; F89B021D250D446000B41293 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F89B021F250D446200B41293 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -122,6 +128,16 @@ path = Authenticator; sourceTree = ""; }; + F865F7512516998A001FD067 /* Repositories */ = { + isa = PBXGroup; + children = ( + F865F752251699AF001FD067 /* WebRepository.swift */, + F865F75425169A2A001FD067 /* NewsHeadlinesRepository.swift */, + F865F75625169ACF001FD067 /* SearchNewsRepository.swift */, + ); + path = Repositories; + sourceTree = ""; + }; F89B0211250D446000B41293 = { isa = PBXGroup; children = ( @@ -180,6 +196,7 @@ F8F14C69250D70B400C24FF5 /* Data Layers */ = { isa = PBXGroup; children = ( + F865F7512516998A001FD067 /* Repositories */, F865F74A251686C1001FD067 /* Authenticator */, F8F14C6E250D70F900C24FF5 /* Networking */, ); @@ -451,9 +468,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F865F75725169ACF001FD067 /* SearchNewsRepository.swift in Sources */, F8DE79E32515904400A6C2D5 /* NetworkService.swift in Sources */, F865F75025168F05001FD067 /* AppConfig.swift in Sources */, + F865F753251699AF001FD067 /* WebRepository.swift in Sources */, F865F74C251686D2001FD067 /* Authenticator.swift in Sources */, + F865F75525169A2A001FD067 /* NewsHeadlinesRepository.swift in Sources */, F8DE79E5251594D700A6C2D5 /* APIClientService.swift in Sources */, F89B021E250D446000B41293 /* AppDelegate.swift in Sources */, ); diff --git a/DutchNews/Classes/Data Layers/Repositories/NewsHeadlinesRepository.swift b/DutchNews/Classes/Data Layers/Repositories/NewsHeadlinesRepository.swift new file mode 100644 index 0000000..2c5d041 --- /dev/null +++ b/DutchNews/Classes/Data Layers/Repositories/NewsHeadlinesRepository.swift @@ -0,0 +1,22 @@ +// +// NewsHeadlinesRepository.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import Alamofire + +/// The `NewsHeadlinesRepository` Class +final class NewsHeadlinesRepository: WebRepository { + + let networkService: NetworkServiceInterceptable + + init(networkService: NetworkServiceInterceptable, authenticator: RequestInterceptor) { + self.networkService = networkService + self.networkService.addingRequest(interceptor: authenticator) + } + +} diff --git a/DutchNews/Classes/Data Layers/Repositories/SearchNewsRepository.swift b/DutchNews/Classes/Data Layers/Repositories/SearchNewsRepository.swift new file mode 100644 index 0000000..4f75259 --- /dev/null +++ b/DutchNews/Classes/Data Layers/Repositories/SearchNewsRepository.swift @@ -0,0 +1,22 @@ +// +// SearchNewsRepository.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import Alamofire + +/// SearchNewsRepository +final class SearchNewsRepository: WebRepository { + + let networkService: NetworkServiceInterceptable + + init(networkService: NetworkServiceInterceptable, authenticator: RequestInterceptor) { + self.networkService = networkService + self.networkService.addingRequest(interceptor: authenticator) + } + +} diff --git a/DutchNews/Classes/Data Layers/Repositories/WebRepository.swift b/DutchNews/Classes/Data Layers/Repositories/WebRepository.swift new file mode 100644 index 0000000..b1acaa8 --- /dev/null +++ b/DutchNews/Classes/Data Layers/Repositories/WebRepository.swift @@ -0,0 +1,21 @@ +// +// WebRepository.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import Alamofire + +/// The `WebRepository` Abstract +protocol WebRepository { + + /// <#Description#> + /// - Parameters: + /// - networkService: <#networkService description#> + /// - authenticator: <#authenticator description#> + init(networkService: NetworkServiceInterceptable, authenticator: RequestInterceptor) + +} From ac12bcffa5aca8e128d97b71681b1e180c1249e5 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sun, 20 Sep 2020 02:07:12 +0430 Subject: [PATCH 058/108] - Added APIServerResponse. - Added APIServerResponseError. - Added APIServerResponseStatus enum. - Fixed var waring for header variable in APIClientService. --- DutchNews.xcodeproj/project.pbxproj | 24 ++++++ .../Networking/APIClientService.swift | 2 +- .../Response/APIServerResponse.swift | 77 +++++++++++++++++++ .../Response/APIServerResponseError.swift | 52 +++++++++++++ .../Response/APIServerResponseStatus.swift | 16 ++++ 5 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 DutchNews/Classes/Data Layers/Networking/Response/APIServerResponse.swift create mode 100644 DutchNews/Classes/Data Layers/Networking/Response/APIServerResponseError.swift create mode 100644 DutchNews/Classes/Data Layers/Networking/Response/APIServerResponseStatus.swift diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index 4e7131f..d623f71 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -18,6 +18,10 @@ F865F753251699AF001FD067 /* WebRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F752251699AF001FD067 /* WebRepository.swift */; }; F865F75525169A2A001FD067 /* NewsHeadlinesRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F75425169A2A001FD067 /* NewsHeadlinesRepository.swift */; }; F865F75725169ACF001FD067 /* SearchNewsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F75625169ACF001FD067 /* SearchNewsRepository.swift */; }; + F865F7602516A643001FD067 /* APIServerResponseStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F75F2516A643001FD067 /* APIServerResponseStatus.swift */; }; + F865F7622516A6C1001FD067 /* APIServerResponseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F7612516A6C1001FD067 /* APIServerResponseError.swift */; }; + F865F7642516A9D5001FD067 /* APIServerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F7632516A9D5001FD067 /* APIServerResponse.swift */; }; + F865F7662516AB66001FD067 /* APIServerResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F7652516AB66001FD067 /* APIServerResponseTests.swift */; }; F89B021E250D446000B41293 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89B021D250D446000B41293 /* AppDelegate.swift */; }; F89B0220250D446200B41293 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F89B021F250D446200B41293 /* Assets.xcassets */; }; F89B0223250D446200B41293 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F89B0221250D446200B41293 /* LaunchScreen.storyboard */; }; @@ -58,6 +62,10 @@ F865F752251699AF001FD067 /* WebRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRepository.swift; sourceTree = ""; }; F865F75425169A2A001FD067 /* NewsHeadlinesRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsHeadlinesRepository.swift; sourceTree = ""; }; F865F75625169ACF001FD067 /* SearchNewsRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchNewsRepository.swift; sourceTree = ""; }; + F865F75F2516A643001FD067 /* APIServerResponseStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIServerResponseStatus.swift; sourceTree = ""; }; + F865F7612516A6C1001FD067 /* APIServerResponseError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIServerResponseError.swift; sourceTree = ""; }; + F865F7632516A9D5001FD067 /* APIServerResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIServerResponse.swift; sourceTree = ""; }; + F865F7652516AB66001FD067 /* APIServerResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIServerResponseTests.swift; sourceTree = ""; }; F89B021A250D446000B41293 /* DutchNews.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DutchNews.app; sourceTree = BUILT_PRODUCTS_DIR; }; F89B021D250D446000B41293 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F89B021F250D446200B41293 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -107,6 +115,7 @@ F82C8F0225163931002B27B3 /* Helper */, F82C8EFC2516304D002B27B3 /* APIClientServiceTests.swift */, F865F74D251687E4001FD067 /* APIAuthenticatorTests.swift */, + F865F7652516AB66001FD067 /* APIServerResponseTests.swift */, ); path = NetworkTests; sourceTree = ""; @@ -138,6 +147,16 @@ path = Repositories; sourceTree = ""; }; + F865F75C2516A4E6001FD067 /* Response */ = { + isa = PBXGroup; + children = ( + F865F75F2516A643001FD067 /* APIServerResponseStatus.swift */, + F865F7612516A6C1001FD067 /* APIServerResponseError.swift */, + F865F7632516A9D5001FD067 /* APIServerResponse.swift */, + ); + path = Response; + sourceTree = ""; + }; F89B0211250D446000B41293 = { isa = PBXGroup; children = ( @@ -234,6 +253,7 @@ F8F14C6E250D70F900C24FF5 /* Networking */ = { isa = PBXGroup; children = ( + F865F75C2516A4E6001FD067 /* Response */, F8DE79E22515904400A6C2D5 /* NetworkService.swift */, F8DE79E4251594D700A6C2D5 /* APIClientService.swift */, ); @@ -469,8 +489,11 @@ buildActionMask = 2147483647; files = ( F865F75725169ACF001FD067 /* SearchNewsRepository.swift in Sources */, + F865F7622516A6C1001FD067 /* APIServerResponseError.swift in Sources */, + F865F7602516A643001FD067 /* APIServerResponseStatus.swift in Sources */, F8DE79E32515904400A6C2D5 /* NetworkService.swift in Sources */, F865F75025168F05001FD067 /* AppConfig.swift in Sources */, + F865F7642516A9D5001FD067 /* APIServerResponse.swift in Sources */, F865F753251699AF001FD067 /* WebRepository.swift in Sources */, F865F74C251686D2001FD067 /* Authenticator.swift in Sources */, F865F75525169A2A001FD067 /* NewsHeadlinesRepository.swift in Sources */, @@ -483,6 +506,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F865F7662516AB66001FD067 /* APIServerResponseTests.swift in Sources */, F89B022E250D446200B41293 /* DutchNewsTests.swift in Sources */, F82C8F0125163898002B27B3 /* NetworkMockingDataFactory.swift in Sources */, F82C8EFF25163073002B27B3 /* NetworkMocking.swift in Sources */, diff --git a/DutchNews/Classes/Data Layers/Networking/APIClientService.swift b/DutchNews/Classes/Data Layers/Networking/APIClientService.swift index 82f0f3a..f80f828 100644 --- a/DutchNews/Classes/Data Layers/Networking/APIClientService.swift +++ b/DutchNews/Classes/Data Layers/Networking/APIClientService.swift @@ -119,7 +119,7 @@ final class APIClientService: NetworkServiceInterceptable { do { let url = try attachBaseURL(into: endpoint) - var headers = HTTPHeaders(headers) + let headers = HTTPHeaders(headers) let dataTask = session.request(url, method: method, parameters: parameters, diff --git a/DutchNews/Classes/Data Layers/Networking/Response/APIServerResponse.swift b/DutchNews/Classes/Data Layers/Networking/Response/APIServerResponse.swift new file mode 100644 index 0000000..a49481c --- /dev/null +++ b/DutchNews/Classes/Data Layers/Networking/Response/APIServerResponse.swift @@ -0,0 +1,77 @@ +// +// APIServerResponse.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation + +struct APIServerResponse where T: Decodable { + + var status: APIServerResponseStatus = .success + var message: String? + var articles: T? + + enum CodingKeys: String, CodingKey { + case status = "status" + case code = "code" + case message = "message" + case data = "articles" + } + + init(status: APIServerResponseStatus) { + self.status = status + } + +} + +extension APIServerResponse: Decodable { + + public init(from decoder: Decoder) throws { + + let values = try decoder.container(keyedBy: CodingKeys.self) + self.status = try values.decode(APIServerResponseStatus.self, forKey: .status) + + do { + self.message = try values.decodeIfPresent(String.self, forKey: .message) + }catch { + self.message = nil + } + + guard status == .success else { + + if let errorType = try? values.decodeIfPresent(String.self, forKey: .code) { + + if let message = try? values.decodeIfPresent(String.self, forKey: .message) { + throw APIServerResponseError.message("\(errorType)", message) + }else { + throw APIServerResponseError.code("Code: \(errorType)") + } + + }else { + throw APIServerResponseError.unknown + } + + self.articles = nil + return + } + + do { + self.articles = try values.decodeIfPresent(T.self, forKey: .data) + }catch { + self.articles = nil + throw APIServerResponseError.code(error.localizedDescription) + } + + } + +} + +extension APIServerResponse: CustomDebugStringConvertible { + + var debugDescription: String { + return "[Server-Response] status = \(status) message= \(message ?? "no message") error = empty data = \(articles)" + } +} diff --git a/DutchNews/Classes/Data Layers/Networking/Response/APIServerResponseError.swift b/DutchNews/Classes/Data Layers/Networking/Response/APIServerResponseError.swift new file mode 100644 index 0000000..25cdcd3 --- /dev/null +++ b/DutchNews/Classes/Data Layers/Networking/Response/APIServerResponseError.swift @@ -0,0 +1,52 @@ +// +// APIServerResponseError.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation + +enum APIServerResponseError: Error { + case code(String) + case message(String, String) + case unknown +} + +extension APIServerResponseError: LocalizedError { + + var errorDescription: String? { + return self.errorDes + } + + fileprivate var errorDes: String { + return self.message + } +} + +extension APIServerResponseError { + + var type: String { + switch self { + case .code(let type), + .message(let type, _): + return type + case .unknown : + return "UNKNOWN_ERROR" + } + } + + var message: String { + + switch self { + case .code(let value): + return value + case .message(_ , let msg): + return msg + case .unknown : + return "Unknown Error Occured. Please Contact Support" + } + } + +} diff --git a/DutchNews/Classes/Data Layers/Networking/Response/APIServerResponseStatus.swift b/DutchNews/Classes/Data Layers/Networking/Response/APIServerResponseStatus.swift new file mode 100644 index 0000000..1bdce42 --- /dev/null +++ b/DutchNews/Classes/Data Layers/Networking/Response/APIServerResponseStatus.swift @@ -0,0 +1,16 @@ +// +// APIServerResponseStatus.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation + +enum APIServerResponseStatus: String, Codable { + + case success = "ok" + case failure = "error" + +} From 009e42affde8b7308183cc5f306d552add6ab06d Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sun, 20 Sep 2020 02:07:35 +0430 Subject: [PATCH 059/108] - Added and Run APIServerResponse tests. --- .../NetworkTests/APIServerResponseTests.swift | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 DutchNewsTests/NetworkTests/APIServerResponseTests.swift diff --git a/DutchNewsTests/NetworkTests/APIServerResponseTests.swift b/DutchNewsTests/NetworkTests/APIServerResponseTests.swift new file mode 100644 index 0000000..111174b --- /dev/null +++ b/DutchNewsTests/NetworkTests/APIServerResponseTests.swift @@ -0,0 +1,97 @@ +// +// APIServerResponseTests.swift +// DutchNewsTests +// +// Created by Farshad Mousalou on 9/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import XCTest +import Foundation + +@testable import DutchNews + +class APIServerResponseTests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testSuccessAPIResponse() { + let data = """ + { + "status": "ok", + "totalResults": 34, + "articles": [] + } + """.data(using: .utf8)! + do { + let response = try JSONDecoder().decode(APIServerResponse<[String]>.self, from: data) + XCTAssert(response.status == .success , "APIServerResponse data was not able to decode.") + XCTAssertNotNil(response.articles, "APIServerResponse data was not able to decode.") + + print("Decoded Objc ", response) + }catch let error { + XCTFail("Error Occurred info: \(error) \(error.localizedDescription)") + } + } + + func testFailureAPIResponse() { + let data = """ + { + "status": "error", + "code": "apiKeyInvalid", + "message": "Your API key is invalid or incorrect. Check your key, or go to https://newsapi.org to create a free API key." + } + """.data(using: .utf8)! + + do { + let response = try JSONDecoder().decode(APIServerResponse<[String]>.self, from: data) + XCTAssertNil(response, "Responsed was decoded successfully. detail: \(response)") + }catch let error { + print("expected result -> ",error,error.localizedDescription) + } + + } + + func testSuccessDecoding() { + let data = """ + { + "status": "ok", + "totalResults": 34, + "articles": [] + } + """.data(using: .utf8)! + do { + let response = try JSONDecoder().decode(APIServerResponse<[String]>.self, from: data) + XCTAssert(response.status == .success , "APIServerResponse data was not able to decode.") + XCTAssertNotNil(response, "APIServerResponse data was not able to decode.") + + print("Decoded Objc ", response) + }catch let error { + XCTFail("Error Occurred info: \(error) \(error.localizedDescription)") + } + } + + func testFailureDecoding() { + let data = """ + { + "status": "fail", + "message": "Your API key is invalid or incorrect. Check your key, or go to https://newsapi.org to create a free API key." + } + """.data(using: .utf8)! + + do { + let response = try JSONDecoder().decode(APIServerResponse<[String]>.self, from: data) + XCTAssertNil(response, "Responsed was decoded successfully. detail: \(response)") + }catch let error { + print("expected result -> ",error,error.localizedDescription) + } + } + + +} From 3ace2bfd640e7fcc71ef92b7951fa9564d2f5a37 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sun, 20 Sep 2020 03:56:17 +0430 Subject: [PATCH 060/108] - Added NetworkValidResponse Abstract. - Added Default Implementation of NetworkValidResponse - Modified NetworkService Abstract - Modified APIClientService. - Fixed UnitTest Errors. --- DutchNews.xcodeproj/project.pbxproj | 8 +++ .../Networking/APIClientService.swift | 64 ++++++++++++------- .../Networking/NetworkService.swift | 16 +++-- .../Networking/NetworkValidResponse.swift | 22 +++++++ .../Response/DefaultAPIValidResponse.swift | 14 ++++ .../NetworkTests/APIAuthenticatorTests.swift | 12 +++- .../NetworkTests/APIClientServiceTests.swift | 13 +++- 7 files changed, 116 insertions(+), 33 deletions(-) create mode 100644 DutchNews/Classes/Data Layers/Networking/NetworkValidResponse.swift create mode 100644 DutchNews/Classes/Data Layers/Networking/Response/DefaultAPIValidResponse.swift diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index d623f71..bd0e87e 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -22,6 +22,8 @@ F865F7622516A6C1001FD067 /* APIServerResponseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F7612516A6C1001FD067 /* APIServerResponseError.swift */; }; F865F7642516A9D5001FD067 /* APIServerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F7632516A9D5001FD067 /* APIServerResponse.swift */; }; F865F7662516AB66001FD067 /* APIServerResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F7652516AB66001FD067 /* APIServerResponseTests.swift */; }; + F865F76B2516C08C001FD067 /* NetworkValidResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F76A2516C08C001FD067 /* NetworkValidResponse.swift */; }; + F865F76D2516C826001FD067 /* DefaultAPIValidResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F76C2516C826001FD067 /* DefaultAPIValidResponse.swift */; }; F89B021E250D446000B41293 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89B021D250D446000B41293 /* AppDelegate.swift */; }; F89B0220250D446200B41293 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F89B021F250D446200B41293 /* Assets.xcassets */; }; F89B0223250D446200B41293 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F89B0221250D446200B41293 /* LaunchScreen.storyboard */; }; @@ -66,6 +68,8 @@ F865F7612516A6C1001FD067 /* APIServerResponseError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIServerResponseError.swift; sourceTree = ""; }; F865F7632516A9D5001FD067 /* APIServerResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIServerResponse.swift; sourceTree = ""; }; F865F7652516AB66001FD067 /* APIServerResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIServerResponseTests.swift; sourceTree = ""; }; + F865F76A2516C08C001FD067 /* NetworkValidResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkValidResponse.swift; sourceTree = ""; }; + F865F76C2516C826001FD067 /* DefaultAPIValidResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultAPIValidResponse.swift; sourceTree = ""; }; F89B021A250D446000B41293 /* DutchNews.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DutchNews.app; sourceTree = BUILT_PRODUCTS_DIR; }; F89B021D250D446000B41293 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F89B021F250D446200B41293 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -153,6 +157,7 @@ F865F75F2516A643001FD067 /* APIServerResponseStatus.swift */, F865F7612516A6C1001FD067 /* APIServerResponseError.swift */, F865F7632516A9D5001FD067 /* APIServerResponse.swift */, + F865F76C2516C826001FD067 /* DefaultAPIValidResponse.swift */, ); path = Response; sourceTree = ""; @@ -254,6 +259,7 @@ isa = PBXGroup; children = ( F865F75C2516A4E6001FD067 /* Response */, + F865F76A2516C08C001FD067 /* NetworkValidResponse.swift */, F8DE79E22515904400A6C2D5 /* NetworkService.swift */, F8DE79E4251594D700A6C2D5 /* APIClientService.swift */, ); @@ -492,12 +498,14 @@ F865F7622516A6C1001FD067 /* APIServerResponseError.swift in Sources */, F865F7602516A643001FD067 /* APIServerResponseStatus.swift in Sources */, F8DE79E32515904400A6C2D5 /* NetworkService.swift in Sources */, + F865F76B2516C08C001FD067 /* NetworkValidResponse.swift in Sources */, F865F75025168F05001FD067 /* AppConfig.swift in Sources */, F865F7642516A9D5001FD067 /* APIServerResponse.swift in Sources */, F865F753251699AF001FD067 /* WebRepository.swift in Sources */, F865F74C251686D2001FD067 /* Authenticator.swift in Sources */, F865F75525169A2A001FD067 /* NewsHeadlinesRepository.swift in Sources */, F8DE79E5251594D700A6C2D5 /* APIClientService.swift in Sources */, + F865F76D2516C826001FD067 /* DefaultAPIValidResponse.swift in Sources */, F89B021E250D446000B41293 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/DutchNews/Classes/Data Layers/Networking/APIClientService.swift b/DutchNews/Classes/Data Layers/Networking/APIClientService.swift index f80f828..bd03f45 100644 --- a/DutchNews/Classes/Data Layers/Networking/APIClientService.swift +++ b/DutchNews/Classes/Data Layers/Networking/APIClientService.swift @@ -90,12 +90,23 @@ final class APIClientService: NetworkServiceInterceptable { /// - decoder: <#decoder description#> private func map (dataRequest: DataRequest, decoder: DataDecoder) -> Observable> { print(dataRequest) - return dataRequest.rx.decodable(decoder: decoder) + return dataRequest.rx.result(queue: workQueue, responseSerializer: DecodableResponseSerializer(decoder:decoder)) .map { value in return Result { value } - }.catchError { (error) -> Observable> in + }.catchError { (error) -> Observable> in .just(.failure(error)) - } + } + } + + private func validate(dataRequest: DataRequest, validator: NetworkValidResponse?) -> DataRequest { + + guard let validator = validator else { + return dataRequest.validate() + } + + return dataRequest.validate(statusCode: validator.statusCodes) + .validate(contentType: validator.contentTypes) + } //////////////////////////////////////////////////////////////// @@ -112,9 +123,10 @@ final class APIClientService: NetworkServiceInterceptable { /// - headers: <#headers description#> /// - completion: <#completion description#> func executeRequest(endpoint: EndPoint, - parameters: Parameters, - method: HTTPMethod, - headers: NetworkHeadersType, + parameters: Parameters = [:], + method: HTTPMethod = .get, + headers: NetworkHeadersType = [:], + validator: NetworkValidResponse? = nil, completion: @escaping ResponseCompletion) -> DataRequest? { do { @@ -126,16 +138,16 @@ final class APIClientService: NetworkServiceInterceptable { encoding: URLEncoding.default, headers: headers, interceptor: interceptor) - .validate() + + return validate(dataRequest: dataTask, validator: validator) .responseDecodable(queue: workQueue, decoder: decoder) { (response: DataResponse ) in let result = response.result.flatMapError { (error) -> Result in return .failure(error) } completion(result) - } + } - return dataTask }catch let error { completion(.failure(error)) return nil @@ -150,8 +162,10 @@ final class APIClientService: NetworkServiceInterceptable { /// - headers: <#headers description#> /// - completion: <#completion description#> func executeRequest(endpoint: EndPoint, - method: HTTPMethod, - parameter: P, headers: NetworkHeadersType, + method: HTTPMethod = .get, + parameter: P, + headers: NetworkHeadersType = [:], + validator: NetworkValidResponse? = nil , completion: @escaping ResponseCompletion) -> DataRequest? { do { @@ -162,16 +176,16 @@ final class APIClientService: NetworkServiceInterceptable { encoder: JSONParameterEncoder.prettyPrinted, headers: HTTPHeaders(headers), interceptor: interceptor) - .validate() + + return validate(dataRequest: dataTask, validator: validator) .responseDecodable(queue: workQueue, decoder: decoder) { (response: DataResponse ) in let result = response.result.flatMapError { (error) -> Result in return .failure(error) } completion(result) - } + } - return dataTask }catch let error { completion(.failure(error)) return nil @@ -191,19 +205,20 @@ final class APIClientService: NetworkServiceInterceptable { /// - method: <#method description#> /// - headers: <#headers description#> func executeRequest(endpoint: EndPoint, - parameters: Parameters, - method: HTTPMethod, - headers: NetworkHeadersType) -> Observable> { + parameters: Parameters = [:], + method: HTTPMethod = .get, + headers: NetworkHeadersType = [:], + validator: NetworkValidResponse? = nil) -> Observable> { do { let url = try attachBaseURL(into: endpoint) - let dataTask = session.request(url, + var dataTask = session.request(url, method: method, parameters: parameters, encoding: URLEncoding.default, headers: HTTPHeaders(headers), interceptor: interceptor) - .validate() + dataTask = validate(dataRequest: dataTask, validator: validator) return map(dataRequest: dataTask, decoder: decoder) @@ -219,18 +234,21 @@ final class APIClientService: NetworkServiceInterceptable { /// - parameter: <#parameter description#> /// - headers: <#headers description#> func executeRequest(endpoint: EndPoint, - method: HTTPMethod, - parameter: P, headers: NetworkHeadersType) -> Observable> { + method: HTTPMethod = .get, + parameter: P, + headers: NetworkHeadersType = [:], + validator: NetworkValidResponse? = nil) -> Observable> { do { let url = try attachBaseURL(into: endpoint) - let dataTask = session.request(url, + var dataTask = session.request(url, method: method, parameters: parameter, encoder: JSONParameterEncoder.prettyPrinted, headers: HTTPHeaders(headers), interceptor: interceptor) - .validate() + dataTask = validate(dataRequest: dataTask, validator: validator) + return map(dataRequest: dataTask, decoder: decoder) }catch let error { diff --git a/DutchNews/Classes/Data Layers/Networking/NetworkService.swift b/DutchNews/Classes/Data Layers/Networking/NetworkService.swift index 5abaed4..fe6f0ea 100644 --- a/DutchNews/Classes/Data Layers/Networking/NetworkService.swift +++ b/DutchNews/Classes/Data Layers/Networking/NetworkService.swift @@ -39,6 +39,7 @@ protocol NetworkService { parameters: Parameters, method: HTTPMethod, headers: NetworkHeadersType, + validator: NetworkValidResponse?, completion: @escaping ResponseCompletion) -> DataRequest? /// <#Description#> @@ -51,6 +52,7 @@ protocol NetworkService { func executeRequest(endpoint: EndPoint, method: HTTPMethod, parameter: P, headers: NetworkHeadersType, + validator: NetworkValidResponse?, completion: @escaping ResponseCompletion) -> DataRequest? //////////////////////////////////////////////////////////////// @@ -68,7 +70,8 @@ protocol NetworkService { func executeRequest(endpoint: EndPoint, parameters: Parameters, method: HTTPMethod, - headers: NetworkHeadersType) -> Observable> + headers: NetworkHeadersType, + validator: NetworkValidResponse?) -> Observable> /// <#Description#> /// - Parameters: @@ -78,7 +81,8 @@ protocol NetworkService { /// - headers: <#headers description#> func executeRequest(endpoint: EndPoint, method: HTTPMethod, - parameter: P, headers: NetworkHeadersType) -> Observable> + parameter: P, headers: NetworkHeadersType, + validator: NetworkValidResponse?) -> Observable> } @@ -96,6 +100,7 @@ extension NetworkService { parameters: Parameters, method: HTTPMethod, headers: NetworkHeadersType, + validator: NetworkValidResponse? = nil, completion: @escaping ResponseCompletion) -> DataRequest? { return nil } @@ -103,6 +108,7 @@ extension NetworkService { func executeRequest(endpoint: EndPoint, method: HTTPMethod, parameter: P, headers: NetworkHeadersType, + validator: NetworkValidResponse? = nil, completion: @escaping ResponseCompletion ) -> DataRequest? { return nil } @@ -110,13 +116,15 @@ extension NetworkService { func executeRequest(endpoint: EndPoint, parameters: Parameters, method: HTTPMethod, - headers: NetworkHeadersType) -> Observable> { + headers: NetworkHeadersType, + validator: NetworkValidResponse? = nil) -> Observable> { return .empty() } func executeRequest(endpoint: EndPoint, method: HTTPMethod, - parameter: P, headers: NetworkHeadersType) -> Observable> { + parameter: P, headers: NetworkHeadersType, + validator: NetworkValidResponse? = nil) -> Observable> { return .empty() } } diff --git a/DutchNews/Classes/Data Layers/Networking/NetworkValidResponse.swift b/DutchNews/Classes/Data Layers/Networking/NetworkValidResponse.swift new file mode 100644 index 0000000..cb8ec47 --- /dev/null +++ b/DutchNews/Classes/Data Layers/Networking/NetworkValidResponse.swift @@ -0,0 +1,22 @@ +// +// NetworkValidResponse.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import Alamofire + +/// NetworkValidResponse Abstract +protocol NetworkValidResponse { + + var statusCodes: Set { get } + var contentTypes: [String] { get } +} + +extension NetworkValidResponse { + var statusCodes: Set { Set(200..<300) } + var contentTypes: [String] { ["*/*" ] } +} diff --git a/DutchNews/Classes/Data Layers/Networking/Response/DefaultAPIValidResponse.swift b/DutchNews/Classes/Data Layers/Networking/Response/DefaultAPIValidResponse.swift new file mode 100644 index 0000000..1f87707 --- /dev/null +++ b/DutchNews/Classes/Data Layers/Networking/Response/DefaultAPIValidResponse.swift @@ -0,0 +1,14 @@ +// +// DefaultAPIValidResponse.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation + +struct DefaultAPIValidResponse: NetworkValidResponse { + var statusCodes: Set { Set((200..<300).map { $0 } + [400, 422, 429]) } + var contentTypes: [String] { ["application/json; charset=utf-8"] } +} diff --git a/DutchNewsTests/NetworkTests/APIAuthenticatorTests.swift b/DutchNewsTests/NetworkTests/APIAuthenticatorTests.swift index 47e60e9..63a7459 100644 --- a/DutchNewsTests/NetworkTests/APIAuthenticatorTests.swift +++ b/DutchNewsTests/NetworkTests/APIAuthenticatorTests.swift @@ -36,7 +36,9 @@ class APIAuthenticatorTests: XCTestCase { _ = networkService.executeRequest(endpoint: "v2/top-headlines", parameters: ["country": "nl"], method: .get, - headers: [:], completion: { (result: Result) in + headers: [:], + validator: nil, + completion: { (result: Result) in switch result { case .success(let value): print(value) @@ -63,7 +65,9 @@ class APIAuthenticatorTests: XCTestCase { _ = networkService.executeRequest(endpoint: "v2/top-headlines", parameters: ["country": "nl"], method: .get, - headers: [:], completion: { (result: Result) in + headers: [:], + validator: nil, + completion: { (result: Result) in switch result { case .success(let value): @@ -91,7 +95,9 @@ class APIAuthenticatorTests: XCTestCase { _ = networkService.executeRequest(endpoint: "v2/top-headlines", parameters: ["country": "nl"], method: .get, - headers: [:], completion: { (result: Result) in + headers: [:], + validator: nil, + completion: { (result: Result) in switch result { case .success(let value): print(value) diff --git a/DutchNewsTests/NetworkTests/APIClientServiceTests.swift b/DutchNewsTests/NetworkTests/APIClientServiceTests.swift index ff77e2f..f41b847 100644 --- a/DutchNewsTests/NetworkTests/APIClientServiceTests.swift +++ b/DutchNewsTests/NetworkTests/APIClientServiceTests.swift @@ -52,7 +52,9 @@ class APIClientServiceTests: XCTestCase { _ = networkService.executeRequest(endpoint: "api/user", parameters: [:], method: .get, - headers: [:], completion: { (result: Result) in + headers: [:], + validator: nil, + completion: { (result: Result) in switch result { case .success(let value): print(value) @@ -91,6 +93,7 @@ class APIClientServiceTests: XCTestCase { parameters: [:], method: .get, headers: ["Accept": "application/json"], + validator: nil, completion: { ( result: Result) in print("result => ", result) switch result { @@ -128,7 +131,9 @@ class APIClientServiceTests: XCTestCase { _ = networkService.executeRequest(endpoint: "api/user", parameters: [:], method: .get, - headers: [:], completion: { (result: Result) in + headers: [:], + validator: nil, + completion: { (result: Result) in switch result { case .success(_): XCTFail("SimpleResponse should not have a value response") @@ -165,7 +170,9 @@ class APIClientServiceTests: XCTestCase { _ = networkService.executeRequest(endpoint: "api/user", parameters: [:], method: .head, - headers: [:], completion: { (result: Result) in + headers: [:], + validator: nil, + completion: { (result: Result) in switch result { case .success(_): XCTFail("The response should had a value.") From 9b96771fb9b9436a7e4497d4031895c4fa3e768b Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sun, 20 Sep 2020 14:47:51 +0430 Subject: [PATCH 061/108] Bugfix/ci testing fastlane (#6) * -removed iOS 11 from github action.\ -Removed multidevice testing in fastlane. * --allow-empty-message * --allow-empty-message * -Fixed CI Testing Github Action. * Fixed matrix.devices * -Fixed Installing iOS Step at CI Testing Github Action. * -Fixed iOS 13.6 Simulator detection. * -Fixed last step if condition. * --allow-empty-message * - Fixed CI Integreation. --- .github/workflows/unit-test.yml | 30 +++++++++++++----- fastlane/Fastfile | 54 +++++++++++++++++++++------------ 2 files changed, 57 insertions(+), 27 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 5185081..d8683c4 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -8,12 +8,14 @@ on: push: branches: - feature/* + - feature/*/* - bugfix/* - hotfix/* pull_request: branches: - develop - feature/* + - feature/*/* - master - bugfix/* - hotfix/* @@ -25,12 +27,16 @@ jobs: runs-on: macOS-latest strategy: matrix: - xcode: ['/Applications/Xcode_11.4.app/Contents/Developer'] + devices: ["iPhone 8 (11.4)","iPhone SE (12.4)","iPhone X (13.6)","iPhone 11","iPad Air (11.4)","iPad Air 2 (12.4)","iPad Pro (10.5-inch) (13.6)","iPad Pro (12.9-inch)"] steps: - name: Checkout Branch uses: actions/checkout@v1 - name: Install Dependencies - run: bundle install + run: | + bundle install --verbose + bash <(curl -s https://raw.githubusercontent.com/TitouanVanBelle/XCTestHTMLReport/master/install.sh) + bundle exec pod install + # env: # BUNDLE_GITHUB__COM: x-access-token:${{ secrets.GITHUB_PERSONAL_ACCESS_TOKEN }} - name: Installing iOS Simulators @@ -38,8 +44,8 @@ jobs: hostname sudo mkdir -p /Library/Developer/CoreSimulator/Profiles/Runtimes sudo ln -s /Applications/Xcode_10.3.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 12.4.simruntime - bundle exec xcversion simulators --install="iOS 11.4 Simulator" --verbose --no-progress - bundle exec xcversion simulators --install="iOS 13.6 Simulator" --verbose --no-progress + sudo ln -s /Applications/Xcode_11.6.app//Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 13.6.simruntime + bundle exec xcversion simulators --install="iOS 11.4 Simulator" --verbose xcrun simctl list runtimes xcrun simctl list devicetypes - name: Creating iOS Simulators @@ -53,14 +59,24 @@ jobs: xcrun simctl list devices 11.4 xcrun simctl list devices 12.4 xcrun simctl list devices 13.6 - instruments -s - - name: Build and test on Devices + + - name: Build and test on Device run: bundle exec fastlane run_ci_tests devices:"${destination}" --verbose env: - destination: ${{ '["iPhone 8 (11.4)","iPhone SE (12.4)","iPhone X (13.6)","iPhone 11","iPad Air (11.4)","iPad Air 2 (12.4)","iPad Pro (10.5-inch) (13.6)","iPad Pro (12.9-inch)"]' }} + destination: ${{ matrix.devices }} + - name: Archive Failed Tests artifacts if: failure() uses: actions/upload-artifact@v1 with: name: FailureDiff path: fastlane/test_output + + - name: Archive Success Tests artifacts + if: success() + uses: actions/upload-artifact@v1 + with: + name: SuccessDiff + path: fastlane/test_output + + diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 943592f..72ca337 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -29,11 +29,16 @@ platform :ios do before_all do |lane| UI.message "prepare for builds" - xcversion(version: "~> 11.2") + begin + xcversion(version: "~> 11.6") + rescue + xcversion(version: "~> 11.2") + end + version = get_version_number(xcodeproj:projectspace,target:scheme) - if lane != :debugTestVersion + if lane != :run_ci_tests #for cocoapods install dependecy cocoapods() end @@ -61,29 +66,38 @@ platform :ios do desc "generate report after running tests" def generate_report puts "Generating Test Report ..." - # sh "xchtmlreport -r fastlane/test_output/#{scheme}-#{version}.test_result" + sh "xchtmlreport -r ./fastlane/out_put/TestResults" puts "Test Report Successfully generated" end desc "Run App Unit tests on given devices name" lane :run_ci_tests do |options| - - UI.message "The devices raw : #{options[:devices]}" - devices = eval options[:devices] - UI.message "The devices list : #{devices}" - - clean = false - - if options[:clean] - clean = options[:clean] - end - - scan(workspace: "#{workspace}", #workspace name - scheme: scheme, # Project scheme name - clean: clean, # clean project folder before test execution - devices: devices, # Devices for testing - result_bundle: true) # To generate test reports - xchtmlreport() + begin + UI.message "The device raw : #{options[:device]}" + device = options[:device] + UI.message "The device list : #{device}" + + clean = false + + if options[:clean] then + clean = options[:clean] + end + + open_report = false + + if options[:open_report] then + open_report = options[:open_report] + end + + scan(clean: clean, # clean project folder before test execution + device: device, # Devices for testing + configuration: "Debug", + open_report: open_report, + result_bundle: true) + + rescue => ex + UI.error "Failure on #{scheme} #{version} info : #{ex.to_s}" + end end end From 33a1656203aa1e38c91d19eab54c0c94fad537e6 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sun, 20 Sep 2020 15:24:45 +0430 Subject: [PATCH 062/108] -Fixed Build and Test Step in unit-test.yml. --- .github/workflows/unit-test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index d8683c4..82d4e03 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -61,7 +61,9 @@ jobs: xcrun simctl list devices 13.6 - name: Build and test on Device - run: bundle exec fastlane run_ci_tests devices:"${destination}" --verbose + run: | + echo "Destination => ${destination}" + bundle exec fastlane run_ci_tests device:"${destination}" --verbose env: destination: ${{ matrix.devices }} From a6ec6e48a0f82041229381fec43c29bd3f3b670d Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sun, 20 Sep 2020 15:51:12 +0430 Subject: [PATCH 063/108] -Added Articles Model. --- DutchNews.xcodeproj/project.pbxproj | 16 +++++++ DutchNews/Classes/Models/Article.swift | 44 ++++++++++++++++++++ DutchNews/Classes/Models/ArticleSource.swift | 21 ++++++++++ 3 files changed, 81 insertions(+) create mode 100644 DutchNews/Classes/Models/Article.swift create mode 100644 DutchNews/Classes/Models/ArticleSource.swift diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index bd0e87e..b249abc 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -12,6 +12,8 @@ F82C8EFD2516304D002B27B3 /* APIClientServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8EFC2516304D002B27B3 /* APIClientServiceTests.swift */; }; F82C8EFF25163073002B27B3 /* NetworkMocking.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8EFE25163073002B27B3 /* NetworkMocking.swift */; }; F82C8F0125163898002B27B3 /* NetworkMockingDataFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8F0025163898002B27B3 /* NetworkMockingDataFactory.swift */; }; + F858997B25176DC800A6BA2A /* Article.swift in Sources */ = {isa = PBXBuildFile; fileRef = F858997A25176DC800A6BA2A /* Article.swift */; }; + F858997D25176E8F00A6BA2A /* ArticleSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F858997C25176E8F00A6BA2A /* ArticleSource.swift */; }; F865F74C251686D2001FD067 /* Authenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F74B251686D2001FD067 /* Authenticator.swift */; }; F865F74E251687E4001FD067 /* APIAuthenticatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F74D251687E4001FD067 /* APIAuthenticatorTests.swift */; }; F865F75025168F05001FD067 /* AppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F74F25168F05001FD067 /* AppConfig.swift */; }; @@ -58,6 +60,8 @@ F82C8EFC2516304D002B27B3 /* APIClientServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClientServiceTests.swift; sourceTree = ""; }; F82C8EFE25163073002B27B3 /* NetworkMocking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMocking.swift; sourceTree = ""; }; F82C8F0025163898002B27B3 /* NetworkMockingDataFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMockingDataFactory.swift; sourceTree = ""; }; + F858997A25176DC800A6BA2A /* Article.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Article.swift; sourceTree = ""; }; + F858997C25176E8F00A6BA2A /* ArticleSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleSource.swift; sourceTree = ""; }; F865F74B251686D2001FD067 /* Authenticator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Authenticator.swift; sourceTree = ""; }; F865F74D251687E4001FD067 /* APIAuthenticatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIAuthenticatorTests.swift; sourceTree = ""; }; F865F74F25168F05001FD067 /* AppConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfig.swift; sourceTree = ""; }; @@ -133,6 +137,15 @@ path = Helper; sourceTree = ""; }; + F858997925176D7700A6BA2A /* Models */ = { + isa = PBXGroup; + children = ( + F858997A25176DC800A6BA2A /* Article.swift */, + F858997C25176E8F00A6BA2A /* ArticleSource.swift */, + ); + path = Models; + sourceTree = ""; + }; F865F74A251686C1001FD067 /* Authenticator */ = { isa = PBXGroup; children = ( @@ -207,6 +220,7 @@ F8F14C68250D70AA00C24FF5 /* Classes */ = { isa = PBXGroup; children = ( + F858997925176D7700A6BA2A /* Models */, F8F14C69250D70B400C24FF5 /* Data Layers */, F8F14C6F250D710100C24FF5 /* Extensions */, F8F14C6D250D70DC00C24FF5 /* Utilites */, @@ -494,6 +508,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F858997B25176DC800A6BA2A /* Article.swift in Sources */, F865F75725169ACF001FD067 /* SearchNewsRepository.swift in Sources */, F865F7622516A6C1001FD067 /* APIServerResponseError.swift in Sources */, F865F7602516A643001FD067 /* APIServerResponseStatus.swift in Sources */, @@ -506,6 +521,7 @@ F865F75525169A2A001FD067 /* NewsHeadlinesRepository.swift in Sources */, F8DE79E5251594D700A6C2D5 /* APIClientService.swift in Sources */, F865F76D2516C826001FD067 /* DefaultAPIValidResponse.swift in Sources */, + F858997D25176E8F00A6BA2A /* ArticleSource.swift in Sources */, F89B021E250D446000B41293 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/DutchNews/Classes/Models/Article.swift b/DutchNews/Classes/Models/Article.swift new file mode 100644 index 0000000..67f6ff6 --- /dev/null +++ b/DutchNews/Classes/Models/Article.swift @@ -0,0 +1,44 @@ +// +// Article.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation + +struct Article: Codable { + + let title: String + let author: String? + let description: String? + + let source: ArticleSource + + let url: URL + + let urlToImage: URL? + + let publishedAt: Date + + let content: String? + + var type: ArticleType = .news + +} + +enum ArticleType: Int, Codable { + case news + case mock +} + +extension Article: Hashable { + + func hash(into hasher: inout Hasher) { + hasher.combine(title) + hasher.combine(source) + hasher.combine(url) + hasher.combine(type) + } +} diff --git a/DutchNews/Classes/Models/ArticleSource.swift b/DutchNews/Classes/Models/ArticleSource.swift new file mode 100644 index 0000000..fc294f9 --- /dev/null +++ b/DutchNews/Classes/Models/ArticleSource.swift @@ -0,0 +1,21 @@ +// +// ArticleSource.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation + +struct ArticleSource: Codable, Hashable { + + let id: String? + let name: String + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(name) + } + +} From d0a225f33c1e542a5bf21e14e8a18c363836c76e Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sun, 20 Sep 2020 16:49:12 +0430 Subject: [PATCH 064/108] - Added UnitTest for decoding Models. - Resolved the decoding problem in Article Models. - Applied SwiftLint. --- DutchNews.xcodeproj/project.pbxproj | 20 ++ .../Networking/APIClientService.swift | 10 +- DutchNews/Classes/Models/Article.swift | 6 + DutchNewsTests/ModelsTests/Articles.json | 262 ++++++++++++++++++ DutchNewsTests/ModelsTests/ModelTests.swift | 94 +++++++ .../ModelsTests/ModelsDataFactory.swift | 127 +++++++++ .../NetworkTests/APIClientServiceTests.swift | 6 +- .../NetworkTests/APIServerResponseTests.swift | 1 - 8 files changed, 517 insertions(+), 9 deletions(-) create mode 100644 DutchNewsTests/ModelsTests/Articles.json create mode 100644 DutchNewsTests/ModelsTests/ModelTests.swift create mode 100644 DutchNewsTests/ModelsTests/ModelsDataFactory.swift diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index b249abc..9a6462f 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -14,6 +14,9 @@ F82C8F0125163898002B27B3 /* NetworkMockingDataFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8F0025163898002B27B3 /* NetworkMockingDataFactory.swift */; }; F858997B25176DC800A6BA2A /* Article.swift in Sources */ = {isa = PBXBuildFile; fileRef = F858997A25176DC800A6BA2A /* Article.swift */; }; F858997D25176E8F00A6BA2A /* ArticleSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F858997C25176E8F00A6BA2A /* ArticleSource.swift */; }; + F8589980251772CC00A6BA2A /* ModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F858997F251772CC00A6BA2A /* ModelTests.swift */; }; + F85899822517738C00A6BA2A /* ModelsDataFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85899812517738C00A6BA2A /* ModelsDataFactory.swift */; }; + F858998425177D6200A6BA2A /* Articles.json in Resources */ = {isa = PBXBuildFile; fileRef = F858998325177D6200A6BA2A /* Articles.json */; }; F865F74C251686D2001FD067 /* Authenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F74B251686D2001FD067 /* Authenticator.swift */; }; F865F74E251687E4001FD067 /* APIAuthenticatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F74D251687E4001FD067 /* APIAuthenticatorTests.swift */; }; F865F75025168F05001FD067 /* AppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F74F25168F05001FD067 /* AppConfig.swift */; }; @@ -62,6 +65,9 @@ F82C8F0025163898002B27B3 /* NetworkMockingDataFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMockingDataFactory.swift; sourceTree = ""; }; F858997A25176DC800A6BA2A /* Article.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Article.swift; sourceTree = ""; }; F858997C25176E8F00A6BA2A /* ArticleSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleSource.swift; sourceTree = ""; }; + F858997F251772CC00A6BA2A /* ModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelTests.swift; sourceTree = ""; }; + F85899812517738C00A6BA2A /* ModelsDataFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelsDataFactory.swift; sourceTree = ""; }; + F858998325177D6200A6BA2A /* Articles.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Articles.json; sourceTree = ""; }; F865F74B251686D2001FD067 /* Authenticator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Authenticator.swift; sourceTree = ""; }; F865F74D251687E4001FD067 /* APIAuthenticatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIAuthenticatorTests.swift; sourceTree = ""; }; F865F74F25168F05001FD067 /* AppConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfig.swift; sourceTree = ""; }; @@ -146,6 +152,16 @@ path = Models; sourceTree = ""; }; + F858997E251772AF00A6BA2A /* ModelsTests */ = { + isa = PBXGroup; + children = ( + F858997F251772CC00A6BA2A /* ModelTests.swift */, + F85899812517738C00A6BA2A /* ModelsDataFactory.swift */, + F858998325177D6200A6BA2A /* Articles.json */, + ); + path = ModelsTests; + sourceTree = ""; + }; F865F74A251686C1001FD067 /* Authenticator */ = { isa = PBXGroup; children = ( @@ -210,6 +226,7 @@ F89B022C250D446200B41293 /* DutchNewsTests */ = { isa = PBXGroup; children = ( + F858997E251772AF00A6BA2A /* ModelsTests */, F82C8EFB2516051D002B27B3 /* NetworkTests */, F89B022D250D446200B41293 /* DutchNewsTests.swift */, F89B022F250D446200B41293 /* Info.plist */, @@ -417,6 +434,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + F858998425177D6200A6BA2A /* Articles.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -530,11 +548,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F85899822517738C00A6BA2A /* ModelsDataFactory.swift in Sources */, F865F7662516AB66001FD067 /* APIServerResponseTests.swift in Sources */, F89B022E250D446200B41293 /* DutchNewsTests.swift in Sources */, F82C8F0125163898002B27B3 /* NetworkMockingDataFactory.swift in Sources */, F82C8EFF25163073002B27B3 /* NetworkMocking.swift in Sources */, F865F74E251687E4001FD067 /* APIAuthenticatorTests.swift in Sources */, + F8589980251772CC00A6BA2A /* ModelTests.swift in Sources */, F82C8EFD2516304D002B27B3 /* APIClientServiceTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/DutchNews/Classes/Data Layers/Networking/APIClientService.swift b/DutchNews/Classes/Data Layers/Networking/APIClientService.swift index bd03f45..e074d2e 100644 --- a/DutchNews/Classes/Data Layers/Networking/APIClientService.swift +++ b/DutchNews/Classes/Data Layers/Networking/APIClientService.swift @@ -90,12 +90,12 @@ final class APIClientService: NetworkServiceInterceptable { /// - decoder: <#decoder description#> private func map (dataRequest: DataRequest, decoder: DataDecoder) -> Observable> { print(dataRequest) - return dataRequest.rx.result(queue: workQueue, responseSerializer: DecodableResponseSerializer(decoder:decoder)) + return dataRequest.rx.result(queue: workQueue, responseSerializer: DecodableResponseSerializer(decoder: decoder)) .map { value in return Result { value } - }.catchError { (error) -> Observable> in + }.catchError { (error) -> Observable> in .just(.failure(error)) - } + } } private func validate(dataRequest: DataRequest, validator: NetworkValidResponse?) -> DataRequest { @@ -146,7 +146,7 @@ final class APIClientService: NetworkServiceInterceptable { } completion(result) - } + } }catch let error { completion(.failure(error)) @@ -184,7 +184,7 @@ final class APIClientService: NetworkServiceInterceptable { } completion(result) - } + } }catch let error { completion(.failure(error)) diff --git a/DutchNews/Classes/Models/Article.swift b/DutchNews/Classes/Models/Article.swift index 67f6ff6..d00f284 100644 --- a/DutchNews/Classes/Models/Article.swift +++ b/DutchNews/Classes/Models/Article.swift @@ -26,6 +26,12 @@ struct Article: Codable { var type: ArticleType = .news + enum CodingKeys: String, CodingKey { + case source, author, title + case description + case url, urlToImage, publishedAt, content + } + } enum ArticleType: Int, Codable { diff --git a/DutchNewsTests/ModelsTests/Articles.json b/DutchNewsTests/ModelsTests/Articles.json new file mode 100644 index 0000000..bcdc130 --- /dev/null +++ b/DutchNewsTests/ModelsTests/Articles.json @@ -0,0 +1,262 @@ +[ + { + "source": { + "id": null, + "name": "Telegraaf.nl" + }, + "author": "ANP", + "title": "Trump geeft 'zegen' aan bod Oracle op TikTok - Telegraaf.nl", + "description": "De Amerikaanse president Donald Trump staat achter het bod van Oracle op de Amerikaanse activiteiten van de Chinese filmpjesapp TikTok. „Ik heb de deal mijn zegen gegeven”, zei Trump zaterdag volgens Bloomberg tegen verslaggevers toen hij het Witte Huis verli…", + "url": "https://www.telegraaf.nl/financieel/1442448989/trump-geeft-zegen-aan-bod-oracle-op-tik-tok", + "urlToImage": "https://www.telegraaf.nl/images/1200x630/filters:format(jpeg):quality(80)/cdn-kiosk-api.telegraaf.nl/6409f6dc-fb2c-11ea-acba-02c309bc01c1.jpg", + "publishedAt": "2020-09-20T10:32:00Z", + "content": "Het nieuwe bedrijf, dat volgens minister van Financiën Steven Mnuchin TikTok Global zou gaan heten, is het resultaat van een transactie die vorige maand door Trump werd afgedwongen vanwege bezorgdhei… [+1099 chars]" + }, + { + "source": { + "id": null, + "name": "Nos.nl" + }, + "author": null, + "title": "Privégegevens Wit-Russische politiemensen online in aanloop naar nieuw protest - NOS", + "description": "Hackers dreigen de gegevens van nog veel meer politiemensen te publiceren zolang het hardhandige optreden tegen demonstranten doorgaat.", + "url": "https://nos.nl/l/2349125", + "urlToImage": "https://nos.nl/data/image/2020/09/20/677152/xxl.jpg", + "publishedAt": "2020-09-20T10:15:00Z", + "content": "Hackers hebben privégegevens van ongeveer 1000 Wit-Russische politiemensen gelekt als vergelding voor het arresteren van bijna 400 deelnemers aan de vrouwenmars van gisteren. Ook vandaag gaan Wit-Rus… [+514 chars]" + }, + { + "source": { + "id": null, + "name": "Tweakers.net" + }, + "author": "Mark Hendrikman", + "title": "Sony: pre-orderproces PlayStation 5 kon veel soepeler gaan - Tweakers", + "description": "Sony heeft op Twitter zijn excuses aangeboden voor de moeite die klanten hebben met een pre-order plaatsen van de PlayStation 5. \"Dat kon veel soepeler gaan\", stelt het Japanse bedrijf. Verder geeft het garanties wat betreft voorraden in de rest van het jaar.", + "url": "https://tweakers.net/nieuws/172394/sony-pre-orderproces-playstation-5-kon-veel-soepeler-gaan.html", + "urlToImage": "https://tweakers.net/i/nmiumjECtV-28XAWh4xU09LWx6c=/67x67/filters:strip_exif()/i/2002904894.png?f=fpa", + "publishedAt": "2020-09-20T09:51:39Z", + "content": "Met al die scalpers en bots maak je als normale consument geen schijn van kans.Alsof het normaal is te noemen dat bijv. Bol (en elke andere webwinkel op coolblue na) compleet uitverkocht raakte in lu… [+855 chars]" + }, + { + "source": { + "id": null, + "name": "Rtlboulevard.nl" + }, + "author": null, + "title": "Vriendin Tommie Christiaan in verwachting - RTL Boulevard", + "description": "'Vandaag op ons 2-jarige jubileum willen we graag met iedereen delen dat we papa en mama worden', zo maakt Tommie Christiaan (34) vandaag dolblij bekend dat hij binnenkort vader wordt.", + "url": "https://www.rtlboulevard.nl/entertainment/showbizz/artikel/5184898/vriendin-tommie-christiaan-verwachting-van-een-kleintje", + "urlToImage": "https://www.rtlboulevard.nl/sites/default/files/styles/liggend_hoge_resolutie/public/content/images/2020/09/20/Tommie%20Christiaan.jpg?h=a9edb586&itok=0sJngmbZ", + "publishedAt": "2020-09-20T09:50:39Z", + "content": "De musicalster deelt het leuke nieuwtje op zijn Instagram met daarbij een foto van de buik van zijn vriendin. 'Onze grootste wens komt uit en we zijn zo dankbaar en gelukkig dat we dit mogen meemaken… [+583 chars]" + }, + { + "source": { + "id": null, + "name": "Voetbalprimeur.nl" + }, + "author": null, + "title": "LIVE-discussie: gewijzigd ADO en Groningen jagen op eerste Eredivisie-zege - VoetbalPrimeur.nl", + "description": "ADO Den Haag begint zondag flink gewijzigd aan de eerste thuiswedstrijd van het seizoen in de Eredivisie. Trainer Aleksandr Rankovic geeft onder meer nieuweling Lassana Faye een basisplaats tegen FC Groningen, dat het nog moet stellen zonder Arjen Robben.", + "url": "https://www.voetbalprimeur.nl/nieuws/945567/live-discussie-gewijzigd-ado-en-groningen-jagen-op-eerste-eredivisie-zege.html", + "urlToImage": "https://files.voetbalprimeur.nl/social/2020/09/20/social_07e16002d68daebaf4ab8f699774e501d8928da7.jpg", + "publishedAt": "2020-09-20T09:45:51Z", + "content": "ADO Den Haag begint zondag flink gewijzigd aan de eerste thuiswedstrijd van het seizoen in de Eredivisie. Trainer Aleksandr Rankovic geeft onder meer nieuweling Lassana Faye een basisplaats tegen FC … [+1046 chars]" + }, + { + "source": { + "id": null, + "name": "Racesport.nl" + }, + "author": "https://www.facebook.com/asse.klein", + "title": "Van der Mark wint Superpole Race voor Rea en Baz in Catalunya - Racesport.nl", + "description": "Michael van der Mark rijdt een geweldige wedstrijd en wint de Superpole Race op het Circuit de Barcelona-Catalunya door Jonathan Rea achter zich te houden.", + "url": "https://www.racesport.nl/van-der-mark-wint-superpole-race-voor-rea-en-baz-in-catalunya/", + "urlToImage": "https://www.racesport.nl/wp-content/uploads/2020/09/IMG_6975.jpg", + "publishedAt": "2020-09-20T09:33:52Z", + "content": "Michael van der Mark rijdt een geweldige wedstrijd en wint de Superpole Race op het Circuit de Barcelona-Catalunya door Jonathan Rea achter zich te houden. Ten Kate Racing Yamaha scoort een podium fi… [+5138 chars]" + }, + { + "source": { + "id": null, + "name": "Www.nu.nl" + }, + "author": "NU.nl", + "title": "Bij dodelijke schietpartij Bas van Wijk buitgemaakt horloge was nep-Rolex - NU.nl", + "description": "Het horloge dat vlak voor het doodschieten van de 24-jarige Bas van Wijk in Amsterdam werd buitgemaakt, is een namaak-Rolex. Dat laat het Openbaar Ministerie (OM) zondag aan NU.nl weten na berichtgeving door het Parool. Mogelijk is Van Wijk doodgeschoten omda…", + "url": "https://www.nu.nl/binnenland/6078647/bij-dodelijke-schietpartij-bas-van-wijk-buitgemaakt-horloge-was-nep-rolex.html", + "urlToImage": "https://media.nu.nl/m/34wxnz3adstc_wd1280.jpg/bij-dodelijke-schietpartij-bas-van-wijk-buitgemaakt-horloge-was-nep-rolex.jpg", + "publishedAt": "2020-09-20T09:30:00Z", + "content": "Het horloge dat vlak voor het doodschieten van de 24-jarige Bas van Wijk in Amsterdam werd buitgemaakt, is een namaak-Rolex. Dat laat het Openbaar Ministerie (OM) zondag aan NU.nl weten na berichtgev… [+1614 chars]" + }, + { + "source": { + "id": null, + "name": "Www.nu.nl" + }, + "author": "NU.nl", + "title": "Soundos El Ahmadi rekent al op een tweede seizoen van All Stars & Zonen - NU.nl", + "description": "Het moet volgens comédienne Soundos El Ahmadi heel vreemd lopen wil All Stars & Zonen geen tweede seizoen krijgen. De actrice heeft een hoofdrol in het vervolg van All Stars en gaat er al voor de eerste uitzending van uit dat de nieuwe reeks aanslaat.", + "url": "https://www.nu.nl/film/6077372/soundos-el-ahmadi-rekent-al-op-een-tweede-seizoen-van-all-stars-zonen.html", + "urlToImage": "https://media.nu.nl/m/gp3xy7vat7vl_wd1280.jpg/soundos-el-ahmadi-rekent-al-op-een-tweede-seizoen-van-all-stars-zonen.jpg", + "publishedAt": "2020-09-20T09:17:00Z", + "content": "Het moet volgens comédienne Soundos El Ahmadi heel vreemd lopen wil All Stars & Zonen geen tweede seizoen krijgen. De actrice heeft een hoofdrol in het vervolg van All Stars en gaat er al voor de… [+671 chars]" + }, + { + "source": { + "id": "google-news", + "name": "Google News" + }, + "author": null, + "title": "'We zaten net naar Netflix te kijken en plotseling zit je zelf in zo'n film'; maar in Kiel-Windeweer is de rust na een schietpartij teruggekeerd - Dagblad van het Noorden", + "description": null, + "url": "https://news.google.com/__i/rss/rd/articles/CBMidWh0dHBzOi8vd3d3LmR2aG4ubmwvZ3JvbmluZ2VuL1dlLXphdGVuLW5ldC1uYWFyLU5ldGZsaXgtdGUta2lqa2VuLWVuLXBsb3RzZWxpbmcteml0LWplLXplbGYtaW4tem9uLWZpbG0tMjYwMzI3OTkuaHRtbNIBAA?oc=5", + "urlToImage": null, + "publishedAt": "2020-09-20T08:52:00Z", + "content": null + }, + { + "source": { + "id": null, + "name": "Dpgmedia.nl" + }, + "author": null, + "title": "Automobilist overlijdt door aanrijding tegen monument in Stadspark Groningen - AD.nl", + "description": null, + "url": "https://myprivacy.dpgmedia.nl/consent/?siteKey=V9f6VUvlHxq9wKIN&callbackUrl=https:%2f%2fwww.ad.nl%2fprivacy-gate%2faccept-tcf2%3fredirectUri%3d%252fgroningen%252fautomobilist-overlijdt-door-aanrijding-tegen-monument-in-stadspark-groningen%257eac916808%252f", + "urlToImage": null, + "publishedAt": "2020-09-20T08:13:17Z", + "content": null + }, + { + "source": { + "id": null, + "name": "Tpo.nl" + }, + "author": "Peil.nl", + "title": "Opiniepeiling Maurice de Hond: Wilders sterkste oppositieleider bij Algemene Beschouwingen - ThePostOnline", + "description": "Slechts 21% wil dat huidig kabinet na de verkiezingen doorgaat", + "url": "https://tpo.nl/2020/09/20/peiling-maurice-de-hond-wilders-na-rutte-het-sterkst/", + "urlToImage": "https://tpo.nl/wp-content/uploads/2020/09/geert-wilders-peil.png", + "publishedAt": "2020-09-20T07:55:45Z", + "content": "De peiling van deze week laat wederom kleine verschuivingen zien, maar ook na de Algemene Beschouwingen zien we geen structurele wijzigingen. De VVD blijft ruim voorop nummer 2 de PVV. De PvdA staat … [+483 chars]" + }, + { + "source": { + "id": null, + "name": "Voetbalzone.nl" + }, + "author": null, + "title": "Frank de Boer lijkt enige kandidaat voor Oranje met tweede gesprek op komst - Voetbalzone.nl", + "description": "", + "url": "https://www.voetbalzone.nl/doc.asp?uid=377347", + "urlToImage": "https://static.voetbalzone.nl/images/photos/ori_1152_648/132351093515435.jpg", + "publishedAt": "2020-09-20T07:53:00Z", + "content": "Er staat maandag een tweede gesprek tussen de KNVB en Frank de Boer op het programma. Pieter Zwart, hoofdredacteur van Voetbal International, vertelt in de liveshow van het weekblad dat er afgelopen … [+2136 chars]" + }, + { + "source": { + "id": null, + "name": "Nos.nl" + }, + "author": null, + "title": "Voor het eerst weer vorst aan de grond gemeten - NOS", + "description": "Bij een KNMI-weerstation in Twente werd vanmorgen om 07.20 uur kort -1,3 graden Celsius gemeten op klomphoogte.", + "url": "https://nos.nl/l/2349109", + "urlToImage": "https://nos.nl/data/image/2020/09/20/677123/xxl.jpg", + "publishedAt": "2020-09-20T07:10:00Z", + "content": "Weerplaza noemt het geen uitzondering dat op dit moment in het jaar de eerste vorst wordt gemeten. In 2012 werd half september zelfs bijna 5 graden vorst aan de grond gemeten.\r\nAfgelopen dinsdag was … [+572 chars]" + }, + { + "source": { + "id": null, + "name": "Dpgmedia.nl" + }, + "author": null, + "title": "Sylvie Meis geeft jawoord in romantisch Toscane: bekijk de foto's hier - AD.nl", + "description": null, + "url": "https://myprivacy.dpgmedia.nl/consent/?siteKey=V9f6VUvlHxq9wKIN&callbackUrl=https:%2f%2fwww.ad.nl%2fprivacy-gate%2faccept-tcf2%3fredirectUri%3d%252fhome%252fsylvie-meis-geeft-jawoord-in-romantisch-toscane-bekijk-de-foto-s-hier%257ea9e9c3d8%252f", + "urlToImage": null, + "publishedAt": "2020-09-20T07:01:35Z", + "content": null + }, + { + "source": { + "id": null, + "name": "Dagelijksestandaard.nl" + }, + "author": "", + "title": "Trump wil komende week nieuwe opvolger Hooggerechtshof kiezen: 'waarschijnlijk een vrouw' - De Dagelijkse Standaard", + "description": "Als het aan de Amerikaanse president Donald Trump ligt dan wordt er zo snel mogelijk een nieuwe opvolger voor het Hooggerechtshof aangewezen. Afgelopen week overleed rechter Ruth Bader Ginsburg, en voor de Trump-regering is dit een gouden kans om een progress…", + "url": "https://www.dagelijksestandaard.nl/2020/09/trump-wil-komende-week-nieuwe-opvolger-hooggerechtshof-kiezen-waarschijnlijk-een-vrouw/", + "urlToImage": "https://cdn-04.dagelijksestandaard.nl/wp-content/uploads/2020/08/vlcsnap-2020-08-04-07h55m32s542.jpg?x90889", + "publishedAt": "2020-09-20T07:00:43Z", + "content": "Als het aan de Amerikaanse president Donald Trump ligt dan wordt er zo snel mogelijk een nieuwe opvolger voor het Hooggerechtshof aangewezen. Afgelopen week overleed rechter Ruth Bader Ginsburg, en v… [+1101 chars]" + }, + { + "source": { + "id": null, + "name": "Dpgmedia.nl" + }, + "author": null, + "title": "In 7 foto's toont Marijn (22) de hel op Lesbos: 'Ik zat regelmatig even te huilen' - De Gelderlander", + "description": null, + "url": "https://myprivacy.dpgmedia.nl/consent/?siteKey=Oom2kBLTny5zJUeO&callbackUrl=https:%2f%2fwww.gelderlander.nl%2fprivacy-gate%2faccept-tcf2%3fredirectUri%3d%252fhome%252fin-7-foto-s-toont-marijn-22-de-hel-op-lesbos-ik-zat-regelmatig-even-te-huilen%257ea9f9da5a%252f", + "urlToImage": null, + "publishedAt": "2020-09-20T06:31:00Z", + "content": null + }, + { + "source": { + "id": null, + "name": "Www.nu.nl" + }, + "author": "NU.nl", + "title": "Pogacar zet Tour op zijn kop in sensationele tijdrit: 'Koers was al voorbij' - NU.nl", + "description": "De strijd om de eindzege leek al beslist voor de tijdrit op de voorlaatste dag van de Tour de France, maar op een sensationele zaterdag in de Vogezen brak Tadej Pogacar de gele droom van Jumbo-Visma en Primoz Roglic in duizend stukjes. Een terugblik op een va…", + "url": "https://www.nu.nl/tour-de-france/6078609/pogacar-zet-tour-op-zijn-kop-in-sensationele-tijdrit-koers-was-al-voorbij.html", + "urlToImage": "https://media.nu.nl/m/s3vx5u9auz1g_wd1280.jpg/pogacar-zet-tour-op-zijn-kop-in-sensationele-tijdrit-koers-was-al-voorbij.jpg", + "publishedAt": "2020-09-20T06:31:00Z", + "content": "De strijd om de eindzege leek al beslist voor de tijdrit op de voorlaatste dag van de Tour de France, maar op een sensationele zaterdag in de Vogezen brak Tadej Pogacar de gele droom van Jumbo-Visma … [+4978 chars]" + }, + { + "source": { + "id": null, + "name": "Nos.nl" + }, + "author": null, + "title": "VS kondigt op eigen houtje VN-sancties af tegen Iran - NOS", + "description": "Het land beroept zich op de Iran-deal uit 2015, maar volgens andere VN-diplomaten is dat niet meer mogelijk omdat Trump eruit is gestapt.", + "url": "https://nos.nl/l/2349102", + "urlToImage": "https://nos.nl/data/image/2020/09/20/677107/xxl.jpg", + "publishedAt": "2020-09-20T06:05:00Z", + "content": "De VS eist dat landen zich weer gaan houden aan VN-sancties die golden voor Iran. \"Dit is een stap op weg naar internationale vrede en veiligheid\", zegt minister Pompeo van Buitenlandse Zaken. De Rus… [+1354 chars]" + }, + { + "source": { + "id": "rtl-nieuws", + "name": "RTL Nieuws" + }, + "author": null, + "title": "Dit gebeurde er in de nacht op Le Mans! - RTL Nieuws", + "description": "De nachtelijke uren in de 24 uur van Le Mans brachten onder andere problemen voor de leidende Toyota, maar helaas ook voor Racing Team Nederland en het team van Job van Uitert. Een terugblik!", + "url": "https://www.rtlnieuws.nl/sport/gp/video/5184877/dit-gebeurde-er-de-nacht-op-le-mans", + "urlToImage": "https://screenshots.rtl.nl/system/thumb/sz=720x404/uuid=d3bff244-8f18-4814-be5c-d9e249db9632", + "publishedAt": "2020-09-20T05:52:44Z", + "content": "De nachtelijke uren in de 24 uur van Le Mans brachten onder andere problemen voor de leidende Toyota, maar helaas ook voor Racing Team Nederland en het team van Job van Uitert. Een terugblik! \r\n20 se… [+17 chars]" + }, + { + "source": { + "id": null, + "name": "Linda.nl" + }, + "author": null, + "title": "Molloot Ivo over aflevering drie: 'Ellie ontpopt zich als de Mol-Mata Hari' - LINDA.", + "description": "Tien kandidaten, van wie één Mol. Journalist Ivo over de derde aflevering van ‘Wie is de Mol?-Renaissance’:", + "url": "https://www.linda.nl/nieuws/fragment-gemist/molloot-ivo-aflevering-drie-ellie-mol-mata-hari/", + "urlToImage": "https://www.linda.nl/lindanl-assets/uploads/2020/09/20111952/ellie-lust-wie-is-de-mol-ivo-1800x1012.jpg", + "publishedAt": "2020-09-20T05:44:04Z", + "content": "Jeroen wist inmiddels dat Ellie zich alleen met hem in de echt wilde verbinden als hij met een flinke bruidsschat zou komen. Hij biechtte dan ook niet veel later op dat hij zichzelf verdacht had will… [+4849 chars]" + } +] diff --git a/DutchNewsTests/ModelsTests/ModelTests.swift b/DutchNewsTests/ModelsTests/ModelTests.swift new file mode 100644 index 0000000..6b10a01 --- /dev/null +++ b/DutchNewsTests/ModelsTests/ModelTests.swift @@ -0,0 +1,94 @@ +// +// ModelTests.swift +// DutchNewsTests +// +// Created by Farshad Mousalou on 9/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import XCTest +import Foundation + +@testable import DutchNews + +class ModelTests: XCTestCase { + + var decoder: JSONDecoder! + + override func setUp() { + decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + decoder = nil + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testDecodingArticles() { + + let data = ModelsDataFactory.createMockArticlesData() + + do { + let objects = try decoder.decode([Article].self, from: data) + XCTAssert(objects.count >= 0 , "data was not able to decode.") + print("Decoded Objc ", objects) + }catch let error { + XCTFail("Error Occurred info: \(error) \(error.localizedDescription)") + } + } + + func testDecodingArticleSource() { + + let data = ModelsDataFactory.createMockSource() + + do { + let object = try decoder.decode(ArticleSource.self, from: data) + XCTAssertNotNil(object, "Data was not able to decode.") + print("Decoded Objc ", object) + }catch let error { + XCTFail("Error Occurred info: \(error) \(error.localizedDescription)") + } + } + + func testHandleDecodingErrorWhenDataCorrupted() { + var corruptedData = ModelsDataFactory.createCorruptedMockArticlesData() + corruptedData = "G ".data(using: .utf8)! + corruptedData + + do { + let object = try decoder.decode([Article].self, from: corruptedData) + XCTAssertNil(object, "The Data was decoded successfully. detail: \(object)") + + }catch let error { + print("expected result -> ",error,error.localizedDescription) + } + } + + func testHandleDecodingErrorWhenArticlesCorrupted() { + + let corruptedData = ModelsDataFactory.createCorruptedMockArticlesData() + + do { + let object = try decoder.decode([Article].self, from: corruptedData) + XCTAssertNil(object, "Responsed was decoded successfully. detail: \(object)") + + }catch let error { + print("expected result -> ",error,error.localizedDescription) + } + } + + func testHandleDecodingErrorWhenArticleSourceCorrupted() { + + let data = ModelsDataFactory.createCorruptedMockSource() + do { + let object = try decoder.decode(ArticleSource.self, from: data) + XCTAssertNil(object, "Responsed was decoded successfully") + print("Decoded Objc ", object) + }catch let error { + print("expected result -> ",error,error.localizedDescription) + + } + } +} diff --git a/DutchNewsTests/ModelsTests/ModelsDataFactory.swift b/DutchNewsTests/ModelsTests/ModelsDataFactory.swift new file mode 100644 index 0000000..0d5f0f5 --- /dev/null +++ b/DutchNewsTests/ModelsTests/ModelsDataFactory.swift @@ -0,0 +1,127 @@ +// +// ModelsDataFactory.swift +// DutchNewsTests +// +// Created by Farshad Mousalou on 9/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation + +@testable import DutchNewsTests + +struct ModelsDataFactory { + + static func createMockArticlesData() -> Data { + let bundle = Bundle(for: ModelTests.self) + return try! Data(contentsOf: bundle.url(forResource: "Articles", withExtension: "json")!) + } + + static func createCorruptedMockArticlesData() -> Data { + return """ + [{ + "source": { + "id": null, + "name": "Rtlboulevard.nl" + }, + "author": null, + "title": "Vriendin Tommie Christiaan in verwachting - RTL Boulevard", + "description": "'Vandaag op ons 2-jarige jubileum willen we graag met iedereen delen dat we papa en mama worden', zo maakt Tommie Christiaan (34) vandaag dolblij bekend dat hij binnenkort vader wordt.", + "url": "https://www.rtlboulevشسیضصثضصثضصثard.nl/entertainment/showbizz/artikel/5184898/vriendin-tommie-christiaan-verwachting-van-een-kleintje", + "urlToImage": "https://www.rtlboulevard.nl/sites/default/files/styles/liggend_hoge_resolutie/public/content/images/2020/09/20/Tommie%20Christiaan.jpg?h=a9edb586&itok=0sJngmbZ", + "publishedAt": "2020-09-20T09:50:39Z", + "content": "De musicalster deelt het leuke nieuwtje op zijn Instagram met daarbij een foto van de buik van zijn vriendin. 'Onze grootste wens komt uit en we zijn zo dankbaar en gelukkig dat we dit mogen meemaken… [+583 chars]" + }, + { + "source": { + "id": null, + "name": "Voetbalprimeur.nl" + }, + "author": null, + "title": "LIVE-discussie: gewijzigd ADO en Groningen jagen op eerste Eredivisie-zege - VoetbalPrimeur.nl", + "description": "ADO Den Haag begint zondag flink gewijzigd aan de eerste thuiswedstrijd van het seizoen in de Eredivisie. Trainer Aleksandr Rankovic geeft onder meer nieuweling Lassana Faye een basisplaats tegen FC Groningen, dat het nog moet stellen zonder Arjen Robben.", + "url": "https://www.voetbalprimeur.nl/nieuws/945567/live-discussie-gewijzigd-ado-en-groningen-jagen-op-eerste-eredivisie-zege.html", + "urlToImage": "https://files.voetbalprimeur.nl/social/2020/09/20/social_07e16002d68daebaf4ab8f699774e501d8928da7.jpg", + "publishedAt": "2020-09-20T09:45:51Z", + "content": "ADO Den Haag begint zondag flink gewijzigd aan de eerste thuiswedstrijd van het seizoen in de Eredivisie. Trainer Aleksandr Rankovic geeft onder meer nieuweling Lassana Faye een basisplaats tegen FC … [+1046 chars]" + }, + { + "source": { + "id": null, + "name": "Racesport.nl" + }, + "author": "https://www.facebook.com/asse.klein", + "title": "Van der Mark wint Superpole Race voor Rea en Baz in Catalunya - Racesport.nl", + "description": "Michael van der Mark rijdt een geweldige wedstrijd en wint de Superpole Race op het Circuit de Barcelona-Catalunya door Jonathan Rea achter zich te houden.", + "url": "https://www.racesport.nl/van-der-mark-wint-superpole-race-voor-rea-en-baz-in-catalunya/", + "urlToImage": "https://www.racesport.nl/wp-content/uploads/2020/09/IMG_6975.jpg", + "publishedAt": "2020-09-20T09:33:52Z", + }, + { + "source": { + "id": null, + "name": "Www.nu.nl" + }, + "author": "NU.nl", + "description": "Het horloge dat vlak voor het doodschieten van de 24-jarige Bas van Wijk in Amsterdam werd buitgemaakt, is een namaak-Rolex. Dat laat het Openbaar Ministerie (OM) zondag aan NU.nl weten na berichtgeving door het Parool. Mogelijk is Van Wijk doodgeschoten omda…", + "url": "udp://www.nu.nl/binnenland/6078647/bij-dodelijke-schietpartij-bas-van-wijk-buitgemaakt-horloge-was-nep-rolex.html", + "urlToImage": "https://media.nu.nl/m/34wxnz3adstc_wd1280.jpg/bij-dodelijke-schietpartij-bas-van-wijk-buitgemaakt-horloge-was-nep-rolex.jpg", + "publishedAt": "2020-09-20T09:30:00Z", + "content": "Het horloge dat vlak voor het doodschieten van de 24-jarige Bas van Wijk in Amsterdam werd buitgemaakt, is een namaak-Rolex. Dat laat het Openbaar Ministerie (OM) zondag aan NU.nl weten na berichtgev… [+1614 chars]" + } + ] + """.data(using: .utf8)! + } + + static func createMockArticlesDataWithCorruptedSources() -> Data { + return """ + [{ + "source": { + "id": null + }, + "author": "ANP", + "title": "Trump geeft 'zegen' aan bod Oracle op TikTok - Telegraaf.nl", + "description": "De Amerikaanse president Donald Trump staat achter het bod van Oracle op de Amerikaanse activiteiten van de Chinese filmpjesapp TikTok. „Ik heb de deal mijn zegen gegeven”, zei Trump zaterdag volgens Bloomberg tegen verslaggevers toen hij het Witte Huis verli…", + "url": "https://www.telegraaf.nl/financieel/1442448989/trump-geeft-zegen-aan-bod-oracle-op-tik-tok", + "urlToImage": "https://www.telegraaf.nl/images/1200x630/filters:format(jpeg):quality(80)/cdn-kiosk-api.telegraaf.nl/6409f6dc-fb2c-11ea-acba-02c309bc01c1.jpg", + "publishedAt": "2020-09-20T10:32:00Z", + "content": "Het nieuwe bedrijf, dat volgens minister van Financiën Steven Mnuchin TikTok Global zou gaan heten, is het resultaat van een transactie die vorige maand door Trump werd afgedwongen vanwege bezorgdhei… [+1099 chars]" + }, + { + "source": { + "id": null, + "name": "Nos.nl" + }, + "author": null, + "title": "Privégegevens Wit-Russische politiemensen online in aanloop naar nieuw protest - NOS", + "description": "Hackers dreigen de gegevens van nog veel meer politiemensen te publiceren zolang het hardhandige optreden tegen demonstranten doorgaat.", + "url": "https://nos.nl/l/2349125", + "urlToImage": "https://nos.nl/data/image/2020/09/20/677152/xxl.jpg", + "publishedAt": "2020-09-20T10:15:00Z", + "content": "Hackers hebben privégegevens van ongeveer 1000 Wit-Russische politiemensen gelekt als vergelding voor het arresteren van bijna 400 deelnemers aan de vrouwenmars van gisteren. Ook vandaag gaan Wit-Rus… [+514 chars]" + }, + { + "source": { + "id": null, + }, + "author": "Mark Hendrikman", + "title": "Sony: pre-orderproces PlayStation 5 kon veel soepeler gaan - Tweakers", + "description": "Sony heeft op Twitter zijn excuses aangeboden voor de moeite die klanten hebben met een pre-order plaatsen van de PlayStation 5. \"Dat kon veel soepeler gaan\", stelt het Japanse bedrijf. Verder geeft het garanties wat betreft voorraden in de rest van het jaar.", + "url": "https://tweakers.net/nieuws/172394/sony-pre-orderproces-playstation-5-kon-veel-soepeler-gaan.html", + "urlToImage": "https://tweakers.net/i/nmiumjECtV-28XAWh4xU09LWx6c=/67x67/filters:strip_exif()/i/2002904894.png?f=fpa", + "publishedAt": "2020-09-20T09:51:39Z", + "content": "Met al die scalpers en bots maak je als normale consument geen schijn van kans.Alsof het normaal is te noemen dat bijv. Bol (en elke andere webwinkel op coolblue na) compleet uitverkocht raakte in lu… [+855 chars]" + }] + """ + .data(using: .utf8)! + } + + static func createMockSource() -> Data { + return "{ \"id\": null, \"name\": \"Telegraph\" }".data(using: .utf8)! + } + + static func createCorruptedMockSource() -> Data { + return "{ \"id\": null }".data(using: .utf8)! + } + +} diff --git a/DutchNewsTests/NetworkTests/APIClientServiceTests.swift b/DutchNewsTests/NetworkTests/APIClientServiceTests.swift index f41b847..16be3a5 100644 --- a/DutchNewsTests/NetworkTests/APIClientServiceTests.swift +++ b/DutchNewsTests/NetworkTests/APIClientServiceTests.swift @@ -97,7 +97,7 @@ class APIClientServiceTests: XCTestCase { completion: { ( result: Result) in print("result => ", result) switch result { - case .success(_): + case .success: XCTFail("SimpleResponse should not have a value response") case .failure(let error): print("Error Occured ",error.localizedDescription) @@ -135,7 +135,7 @@ class APIClientServiceTests: XCTestCase { validator: nil, completion: { (result: Result) in switch result { - case .success(_): + case .success: XCTFail("SimpleResponse should not have a value response") case .failure(let error): print("Error Occured ",error.localizedDescription) @@ -174,7 +174,7 @@ class APIClientServiceTests: XCTestCase { validator: nil, completion: { (result: Result) in switch result { - case .success(_): + case .success: XCTFail("The response should had a value.") case .failure(let error): print("error descripition :",error,error.localizedDescription) diff --git a/DutchNewsTests/NetworkTests/APIServerResponseTests.swift b/DutchNewsTests/NetworkTests/APIServerResponseTests.swift index 111174b..b2083c0 100644 --- a/DutchNewsTests/NetworkTests/APIServerResponseTests.swift +++ b/DutchNewsTests/NetworkTests/APIServerResponseTests.swift @@ -93,5 +93,4 @@ class APIServerResponseTests: XCTestCase { } } - } From 4e2e87b4068851f8b6b490d088fa308546b539f0 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sun, 20 Sep 2020 17:57:52 +0430 Subject: [PATCH 065/108] - refactored Repositories. - Added ArticleRepository - Added HeadlinesArticleLocalRespository. - Added HeadlinesArticleRemoteRepository. - Implemented HeadlinesArticleRemoteRepository.. --- DutchNews.xcodeproj/project.pbxproj | 24 ++++----- .../Repositories/ArticleRepository.swift | 23 ++++++++ .../HeadlinesArticleLocalRespository.swift | 13 +++++ .../HeadlinesArticleRemoteRepository.swift | 54 +++++++++++++++++++ .../NewsHeadlinesRepository.swift | 22 -------- .../Repositories/SearchNewsRepository.swift | 22 -------- .../Repositories/WebRepository.swift | 21 -------- 7 files changed, 102 insertions(+), 77 deletions(-) create mode 100644 DutchNews/Classes/Data Layers/Repositories/ArticleRepository.swift create mode 100644 DutchNews/Classes/Data Layers/Repositories/HeadlinesArticleLocalRespository.swift create mode 100644 DutchNews/Classes/Data Layers/Repositories/HeadlinesArticleRemoteRepository.swift delete mode 100644 DutchNews/Classes/Data Layers/Repositories/NewsHeadlinesRepository.swift delete mode 100644 DutchNews/Classes/Data Layers/Repositories/SearchNewsRepository.swift delete mode 100644 DutchNews/Classes/Data Layers/Repositories/WebRepository.swift diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index 9a6462f..9a58906 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -17,12 +17,12 @@ F8589980251772CC00A6BA2A /* ModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F858997F251772CC00A6BA2A /* ModelTests.swift */; }; F85899822517738C00A6BA2A /* ModelsDataFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85899812517738C00A6BA2A /* ModelsDataFactory.swift */; }; F858998425177D6200A6BA2A /* Articles.json in Resources */ = {isa = PBXBuildFile; fileRef = F858998325177D6200A6BA2A /* Articles.json */; }; + F8589986251784B200A6BA2A /* ArticleRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8589985251784B200A6BA2A /* ArticleRepository.swift */; }; + F85899882517894B00A6BA2A /* HeadlinesArticleRemoteRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85899872517894B00A6BA2A /* HeadlinesArticleRemoteRepository.swift */; }; + F858998A2517897700A6BA2A /* HeadlinesArticleLocalRespository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85899892517897700A6BA2A /* HeadlinesArticleLocalRespository.swift */; }; F865F74C251686D2001FD067 /* Authenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F74B251686D2001FD067 /* Authenticator.swift */; }; F865F74E251687E4001FD067 /* APIAuthenticatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F74D251687E4001FD067 /* APIAuthenticatorTests.swift */; }; F865F75025168F05001FD067 /* AppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F74F25168F05001FD067 /* AppConfig.swift */; }; - F865F753251699AF001FD067 /* WebRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F752251699AF001FD067 /* WebRepository.swift */; }; - F865F75525169A2A001FD067 /* NewsHeadlinesRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F75425169A2A001FD067 /* NewsHeadlinesRepository.swift */; }; - F865F75725169ACF001FD067 /* SearchNewsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F75625169ACF001FD067 /* SearchNewsRepository.swift */; }; F865F7602516A643001FD067 /* APIServerResponseStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F75F2516A643001FD067 /* APIServerResponseStatus.swift */; }; F865F7622516A6C1001FD067 /* APIServerResponseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F7612516A6C1001FD067 /* APIServerResponseError.swift */; }; F865F7642516A9D5001FD067 /* APIServerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F7632516A9D5001FD067 /* APIServerResponse.swift */; }; @@ -68,12 +68,12 @@ F858997F251772CC00A6BA2A /* ModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelTests.swift; sourceTree = ""; }; F85899812517738C00A6BA2A /* ModelsDataFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelsDataFactory.swift; sourceTree = ""; }; F858998325177D6200A6BA2A /* Articles.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Articles.json; sourceTree = ""; }; + F8589985251784B200A6BA2A /* ArticleRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleRepository.swift; sourceTree = ""; }; + F85899872517894B00A6BA2A /* HeadlinesArticleRemoteRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlinesArticleRemoteRepository.swift; sourceTree = ""; }; + F85899892517897700A6BA2A /* HeadlinesArticleLocalRespository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlinesArticleLocalRespository.swift; sourceTree = ""; }; F865F74B251686D2001FD067 /* Authenticator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Authenticator.swift; sourceTree = ""; }; F865F74D251687E4001FD067 /* APIAuthenticatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIAuthenticatorTests.swift; sourceTree = ""; }; F865F74F25168F05001FD067 /* AppConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfig.swift; sourceTree = ""; }; - F865F752251699AF001FD067 /* WebRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRepository.swift; sourceTree = ""; }; - F865F75425169A2A001FD067 /* NewsHeadlinesRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsHeadlinesRepository.swift; sourceTree = ""; }; - F865F75625169ACF001FD067 /* SearchNewsRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchNewsRepository.swift; sourceTree = ""; }; F865F75F2516A643001FD067 /* APIServerResponseStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIServerResponseStatus.swift; sourceTree = ""; }; F865F7612516A6C1001FD067 /* APIServerResponseError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIServerResponseError.swift; sourceTree = ""; }; F865F7632516A9D5001FD067 /* APIServerResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIServerResponse.swift; sourceTree = ""; }; @@ -173,9 +173,9 @@ F865F7512516998A001FD067 /* Repositories */ = { isa = PBXGroup; children = ( - F865F752251699AF001FD067 /* WebRepository.swift */, - F865F75425169A2A001FD067 /* NewsHeadlinesRepository.swift */, - F865F75625169ACF001FD067 /* SearchNewsRepository.swift */, + F8589985251784B200A6BA2A /* ArticleRepository.swift */, + F85899872517894B00A6BA2A /* HeadlinesArticleRemoteRepository.swift */, + F85899892517897700A6BA2A /* HeadlinesArticleLocalRespository.swift */, ); path = Repositories; sourceTree = ""; @@ -526,18 +526,18 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F85899882517894B00A6BA2A /* HeadlinesArticleRemoteRepository.swift in Sources */, F858997B25176DC800A6BA2A /* Article.swift in Sources */, - F865F75725169ACF001FD067 /* SearchNewsRepository.swift in Sources */, F865F7622516A6C1001FD067 /* APIServerResponseError.swift in Sources */, F865F7602516A643001FD067 /* APIServerResponseStatus.swift in Sources */, F8DE79E32515904400A6C2D5 /* NetworkService.swift in Sources */, F865F76B2516C08C001FD067 /* NetworkValidResponse.swift in Sources */, F865F75025168F05001FD067 /* AppConfig.swift in Sources */, F865F7642516A9D5001FD067 /* APIServerResponse.swift in Sources */, - F865F753251699AF001FD067 /* WebRepository.swift in Sources */, F865F74C251686D2001FD067 /* Authenticator.swift in Sources */, - F865F75525169A2A001FD067 /* NewsHeadlinesRepository.swift in Sources */, + F8589986251784B200A6BA2A /* ArticleRepository.swift in Sources */, F8DE79E5251594D700A6C2D5 /* APIClientService.swift in Sources */, + F858998A2517897700A6BA2A /* HeadlinesArticleLocalRespository.swift in Sources */, F865F76D2516C826001FD067 /* DefaultAPIValidResponse.swift in Sources */, F858997D25176E8F00A6BA2A /* ArticleSource.swift in Sources */, F89B021E250D446000B41293 /* AppDelegate.swift in Sources */, diff --git a/DutchNews/Classes/Data Layers/Repositories/ArticleRepository.swift b/DutchNews/Classes/Data Layers/Repositories/ArticleRepository.swift new file mode 100644 index 0000000..066d518 --- /dev/null +++ b/DutchNews/Classes/Data Layers/Repositories/ArticleRepository.swift @@ -0,0 +1,23 @@ +// +// Repository.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import RxSwift + +protocol ArticleRepository { + + associatedtype T: Codable + + /// <#Description#> + func fetchArticles() -> Observable<[T]> + + /// <#Description#> + /// - Parameter keyword: <#keyword description#> + func search(keyword: String) -> Observable<[T]> + +} diff --git a/DutchNews/Classes/Data Layers/Repositories/HeadlinesArticleLocalRespository.swift b/DutchNews/Classes/Data Layers/Repositories/HeadlinesArticleLocalRespository.swift new file mode 100644 index 0000000..05636d1 --- /dev/null +++ b/DutchNews/Classes/Data Layers/Repositories/HeadlinesArticleLocalRespository.swift @@ -0,0 +1,13 @@ +// +// HeadlinesArticleLocalRespository.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation + +class HeadlinesArticleLocalRespository: ArticleRepository { + +} diff --git a/DutchNews/Classes/Data Layers/Repositories/HeadlinesArticleRemoteRepository.swift b/DutchNews/Classes/Data Layers/Repositories/HeadlinesArticleRemoteRepository.swift new file mode 100644 index 0000000..a495a5e --- /dev/null +++ b/DutchNews/Classes/Data Layers/Repositories/HeadlinesArticleRemoteRepository.swift @@ -0,0 +1,54 @@ +// +// HeadlinesArticleRemoteRepository.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import RxSwift +import RxAlamofire +import Alamofire + +class HeadlinesArticleRemoteRepository: ArticleRepository { + + typealias T = Article + + let networkService: NetworkServiceInterceptable + let validator: NetworkValidResponse + + init(networkService: NetworkServiceInterceptable, + authentictor: RequestInterceptor, + validator: NetworkValidResponse) { + + self.networkService = networkService + self.networkService.addingRequest(interceptor: authentictor) + self.validator = validator + } + + private typealias ResponseResult = Result,Error> + + func fetchArticles() -> Observable<[Article]> { + + return networkService.executeRequest(endpoint: "v2/top-headlines", + parameters: ["country": "nl"], + method: .get, headers: [:], + validator: validator) + .map(map(response:)) + } + + func search(keyword: String) -> Observable<[Article]> { + networkService.executeRequest(endpoint: "v2/top-headlines", + parameters: ["q": keyword,"country": "nl"], + method: .get, headers: [:], + validator: validator) + .map(map(response:)) + } + + private func map(response: ResponseResult) throws -> [Article] { + let result = try response.get() + return result.articles ?? [] + } + +} diff --git a/DutchNews/Classes/Data Layers/Repositories/NewsHeadlinesRepository.swift b/DutchNews/Classes/Data Layers/Repositories/NewsHeadlinesRepository.swift deleted file mode 100644 index 2c5d041..0000000 --- a/DutchNews/Classes/Data Layers/Repositories/NewsHeadlinesRepository.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// NewsHeadlinesRepository.swift -// DutchNews -// -// Created by Farshad Mousalou on 9/20/20. -// Copyright © 2020 Farshad Mousalou. All rights reserved. -// - -import Foundation -import Alamofire - -/// The `NewsHeadlinesRepository` Class -final class NewsHeadlinesRepository: WebRepository { - - let networkService: NetworkServiceInterceptable - - init(networkService: NetworkServiceInterceptable, authenticator: RequestInterceptor) { - self.networkService = networkService - self.networkService.addingRequest(interceptor: authenticator) - } - -} diff --git a/DutchNews/Classes/Data Layers/Repositories/SearchNewsRepository.swift b/DutchNews/Classes/Data Layers/Repositories/SearchNewsRepository.swift deleted file mode 100644 index 4f75259..0000000 --- a/DutchNews/Classes/Data Layers/Repositories/SearchNewsRepository.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// SearchNewsRepository.swift -// DutchNews -// -// Created by Farshad Mousalou on 9/20/20. -// Copyright © 2020 Farshad Mousalou. All rights reserved. -// - -import Foundation -import Alamofire - -/// SearchNewsRepository -final class SearchNewsRepository: WebRepository { - - let networkService: NetworkServiceInterceptable - - init(networkService: NetworkServiceInterceptable, authenticator: RequestInterceptor) { - self.networkService = networkService - self.networkService.addingRequest(interceptor: authenticator) - } - -} diff --git a/DutchNews/Classes/Data Layers/Repositories/WebRepository.swift b/DutchNews/Classes/Data Layers/Repositories/WebRepository.swift deleted file mode 100644 index b1acaa8..0000000 --- a/DutchNews/Classes/Data Layers/Repositories/WebRepository.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// WebRepository.swift -// DutchNews -// -// Created by Farshad Mousalou on 9/20/20. -// Copyright © 2020 Farshad Mousalou. All rights reserved. -// - -import Foundation -import Alamofire - -/// The `WebRepository` Abstract -protocol WebRepository { - - /// <#Description#> - /// - Parameters: - /// - networkService: <#networkService description#> - /// - authenticator: <#authenticator description#> - init(networkService: NetworkServiceInterceptable, authenticator: RequestInterceptor) - -} From 29b5c77ca5085c2ae4cbbe35850768838a9a5db2 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sun, 20 Sep 2020 22:06:53 +0430 Subject: [PATCH 066/108] - Added Helper Classes for HeadlinesArticleRemoteRepositoryTests. - Added and Tested HeadlinesArticleRemoteRepositoryTests. - Fixed Bug while HeadlinesArticleRemoteRepositoryTests run. --- DutchNews.xcodeproj/project.pbxproj | 40 ++- DutchNews/AppConfig.swift | 1 + .../Networking/APIClientService.swift | 4 +- .../Response/APIServerResponse.swift | 12 +- .../Repositories/ArticleRepository.swift | 6 +- .../HeadlinesArticleLocalRespository.swift | 13 - .../HeadlinesArticleRemoteRepository.swift | 14 +- .../NetworkTests/APIServerResponseTests.swift | 2 +- .../HeadLines/HeadlineFailureResponse.json | 5 + .../HeadLines/HeadlineSuccessResponse.json | 266 ++++++++++++++++++ ...eadlinesArticleRemoteRepositoryTests.swift | 187 ++++++++++++ .../MockArticleValidResponse.swift | 17 ++ .../RepositoryDependenciesFactory.swift | 42 +++ 13 files changed, 573 insertions(+), 36 deletions(-) delete mode 100644 DutchNews/Classes/Data Layers/Repositories/HeadlinesArticleLocalRespository.swift create mode 100644 DutchNewsTests/Repositories/HeadLines/HeadlineFailureResponse.json create mode 100644 DutchNewsTests/Repositories/HeadLines/HeadlineSuccessResponse.json create mode 100644 DutchNewsTests/Repositories/HeadLines/HeadlinesArticleRemoteRepositoryTests.swift create mode 100644 DutchNewsTests/Repositories/MockArticleValidResponse.swift create mode 100644 DutchNewsTests/Repositories/RepositoryDependenciesFactory.swift diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index 9a58906..dc46169 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -19,7 +19,8 @@ F858998425177D6200A6BA2A /* Articles.json in Resources */ = {isa = PBXBuildFile; fileRef = F858998325177D6200A6BA2A /* Articles.json */; }; F8589986251784B200A6BA2A /* ArticleRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8589985251784B200A6BA2A /* ArticleRepository.swift */; }; F85899882517894B00A6BA2A /* HeadlinesArticleRemoteRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85899872517894B00A6BA2A /* HeadlinesArticleRemoteRepository.swift */; }; - F858998A2517897700A6BA2A /* HeadlinesArticleLocalRespository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85899892517897700A6BA2A /* HeadlinesArticleLocalRespository.swift */; }; + F858998D2517909A00A6BA2A /* MockArticleValidResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = F858998C2517909A00A6BA2A /* MockArticleValidResponse.swift */; }; + F85899902517910700A6BA2A /* HeadlinesArticleRemoteRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F858998F2517910700A6BA2A /* HeadlinesArticleRemoteRepositoryTests.swift */; }; F865F74C251686D2001FD067 /* Authenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F74B251686D2001FD067 /* Authenticator.swift */; }; F865F74E251687E4001FD067 /* APIAuthenticatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F74D251687E4001FD067 /* APIAuthenticatorTests.swift */; }; F865F75025168F05001FD067 /* AppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F74F25168F05001FD067 /* AppConfig.swift */; }; @@ -29,6 +30,9 @@ F865F7662516AB66001FD067 /* APIServerResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F7652516AB66001FD067 /* APIServerResponseTests.swift */; }; F865F76B2516C08C001FD067 /* NetworkValidResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F76A2516C08C001FD067 /* NetworkValidResponse.swift */; }; F865F76D2516C826001FD067 /* DefaultAPIValidResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F76C2516C826001FD067 /* DefaultAPIValidResponse.swift */; }; + F88800692517A423008DCC54 /* RepositoryDependenciesFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88800682517A423008DCC54 /* RepositoryDependenciesFactory.swift */; }; + F888006B2517A8EE008DCC54 /* HeadlineSuccessResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = F888006A2517A8EE008DCC54 /* HeadlineSuccessResponse.json */; }; + F888006D2517AA1F008DCC54 /* HeadlineFailureResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = F888006C2517AA1F008DCC54 /* HeadlineFailureResponse.json */; }; F89B021E250D446000B41293 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89B021D250D446000B41293 /* AppDelegate.swift */; }; F89B0220250D446200B41293 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F89B021F250D446200B41293 /* Assets.xcassets */; }; F89B0223250D446200B41293 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F89B0221250D446200B41293 /* LaunchScreen.storyboard */; }; @@ -70,7 +74,8 @@ F858998325177D6200A6BA2A /* Articles.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Articles.json; sourceTree = ""; }; F8589985251784B200A6BA2A /* ArticleRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleRepository.swift; sourceTree = ""; }; F85899872517894B00A6BA2A /* HeadlinesArticleRemoteRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlinesArticleRemoteRepository.swift; sourceTree = ""; }; - F85899892517897700A6BA2A /* HeadlinesArticleLocalRespository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlinesArticleLocalRespository.swift; sourceTree = ""; }; + F858998C2517909A00A6BA2A /* MockArticleValidResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockArticleValidResponse.swift; sourceTree = ""; }; + F858998F2517910700A6BA2A /* HeadlinesArticleRemoteRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlinesArticleRemoteRepositoryTests.swift; sourceTree = ""; }; F865F74B251686D2001FD067 /* Authenticator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Authenticator.swift; sourceTree = ""; }; F865F74D251687E4001FD067 /* APIAuthenticatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIAuthenticatorTests.swift; sourceTree = ""; }; F865F74F25168F05001FD067 /* AppConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfig.swift; sourceTree = ""; }; @@ -80,6 +85,9 @@ F865F7652516AB66001FD067 /* APIServerResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIServerResponseTests.swift; sourceTree = ""; }; F865F76A2516C08C001FD067 /* NetworkValidResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkValidResponse.swift; sourceTree = ""; }; F865F76C2516C826001FD067 /* DefaultAPIValidResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultAPIValidResponse.swift; sourceTree = ""; }; + F88800682517A423008DCC54 /* RepositoryDependenciesFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryDependenciesFactory.swift; sourceTree = ""; }; + F888006A2517A8EE008DCC54 /* HeadlineSuccessResponse.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = HeadlineSuccessResponse.json; sourceTree = ""; }; + F888006C2517AA1F008DCC54 /* HeadlineFailureResponse.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = HeadlineFailureResponse.json; sourceTree = ""; }; F89B021A250D446000B41293 /* DutchNews.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DutchNews.app; sourceTree = BUILT_PRODUCTS_DIR; }; F89B021D250D446000B41293 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F89B021F250D446200B41293 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -162,6 +170,26 @@ path = ModelsTests; sourceTree = ""; }; + F858998B2517906B00A6BA2A /* Repositories */ = { + isa = PBXGroup; + children = ( + F858998C2517909A00A6BA2A /* MockArticleValidResponse.swift */, + F88800682517A423008DCC54 /* RepositoryDependenciesFactory.swift */, + F858998E251790DF00A6BA2A /* HeadLines */, + ); + path = Repositories; + sourceTree = ""; + }; + F858998E251790DF00A6BA2A /* HeadLines */ = { + isa = PBXGroup; + children = ( + F888006A2517A8EE008DCC54 /* HeadlineSuccessResponse.json */, + F888006C2517AA1F008DCC54 /* HeadlineFailureResponse.json */, + F858998F2517910700A6BA2A /* HeadlinesArticleRemoteRepositoryTests.swift */, + ); + path = HeadLines; + sourceTree = ""; + }; F865F74A251686C1001FD067 /* Authenticator */ = { isa = PBXGroup; children = ( @@ -175,7 +203,6 @@ children = ( F8589985251784B200A6BA2A /* ArticleRepository.swift */, F85899872517894B00A6BA2A /* HeadlinesArticleRemoteRepository.swift */, - F85899892517897700A6BA2A /* HeadlinesArticleLocalRespository.swift */, ); path = Repositories; sourceTree = ""; @@ -226,6 +253,7 @@ F89B022C250D446200B41293 /* DutchNewsTests */ = { isa = PBXGroup; children = ( + F858998B2517906B00A6BA2A /* Repositories */, F858997E251772AF00A6BA2A /* ModelsTests */, F82C8EFB2516051D002B27B3 /* NetworkTests */, F89B022D250D446200B41293 /* DutchNewsTests.swift */, @@ -434,7 +462,9 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + F888006B2517A8EE008DCC54 /* HeadlineSuccessResponse.json in Resources */, F858998425177D6200A6BA2A /* Articles.json in Resources */, + F888006D2517AA1F008DCC54 /* HeadlineFailureResponse.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -537,7 +567,6 @@ F865F74C251686D2001FD067 /* Authenticator.swift in Sources */, F8589986251784B200A6BA2A /* ArticleRepository.swift in Sources */, F8DE79E5251594D700A6C2D5 /* APIClientService.swift in Sources */, - F858998A2517897700A6BA2A /* HeadlinesArticleLocalRespository.swift in Sources */, F865F76D2516C826001FD067 /* DefaultAPIValidResponse.swift in Sources */, F858997D25176E8F00A6BA2A /* ArticleSource.swift in Sources */, F89B021E250D446000B41293 /* AppDelegate.swift in Sources */, @@ -548,14 +577,17 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F85899902517910700A6BA2A /* HeadlinesArticleRemoteRepositoryTests.swift in Sources */, F85899822517738C00A6BA2A /* ModelsDataFactory.swift in Sources */, F865F7662516AB66001FD067 /* APIServerResponseTests.swift in Sources */, + F88800692517A423008DCC54 /* RepositoryDependenciesFactory.swift in Sources */, F89B022E250D446200B41293 /* DutchNewsTests.swift in Sources */, F82C8F0125163898002B27B3 /* NetworkMockingDataFactory.swift in Sources */, F82C8EFF25163073002B27B3 /* NetworkMocking.swift in Sources */, F865F74E251687E4001FD067 /* APIAuthenticatorTests.swift in Sources */, F8589980251772CC00A6BA2A /* ModelTests.swift in Sources */, F82C8EFD2516304D002B27B3 /* APIClientServiceTests.swift in Sources */, + F858998D2517909A00A6BA2A /* MockArticleValidResponse.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/DutchNews/AppConfig.swift b/DutchNews/AppConfig.swift index bc7f8ef..c2336f9 100644 --- a/DutchNews/AppConfig.swift +++ b/DutchNews/AppConfig.swift @@ -10,4 +10,5 @@ import Foundation struct AppConfig { static let APIKey = "56450901b0134dcbb5627035b12fca99" + static let BaseURL = URL(string: "https://newsapi.org/v2/")! } diff --git a/DutchNews/Classes/Data Layers/Networking/APIClientService.swift b/DutchNews/Classes/Data Layers/Networking/APIClientService.swift index e074d2e..ec5f5bb 100644 --- a/DutchNews/Classes/Data Layers/Networking/APIClientService.swift +++ b/DutchNews/Classes/Data Layers/Networking/APIClientService.swift @@ -89,8 +89,8 @@ final class APIClientService: NetworkServiceInterceptable { /// - dataRequest: <#dataRequest description#> /// - decoder: <#decoder description#> private func map (dataRequest: DataRequest, decoder: DataDecoder) -> Observable> { - print(dataRequest) - return dataRequest.rx.result(queue: workQueue, responseSerializer: DecodableResponseSerializer(decoder: decoder)) + + return dataRequest.rx.responseResult(queue: workQueue, responseSerializer: DecodableResponseSerializer(decoder: decoder)).map({ $1 }) .map { value in return Result { value } }.catchError { (error) -> Observable> in diff --git a/DutchNews/Classes/Data Layers/Networking/Response/APIServerResponse.swift b/DutchNews/Classes/Data Layers/Networking/Response/APIServerResponse.swift index a49481c..312926f 100644 --- a/DutchNews/Classes/Data Layers/Networking/Response/APIServerResponse.swift +++ b/DutchNews/Classes/Data Layers/Networking/Response/APIServerResponse.swift @@ -12,7 +12,7 @@ struct APIServerResponse where T: Decodable { var status: APIServerResponseStatus = .success var message: String? - var articles: T? + var data: T? enum CodingKeys: String, CodingKey { case status = "status" @@ -54,15 +54,15 @@ extension APIServerResponse: Decodable { throw APIServerResponseError.unknown } - self.articles = nil + self.data = nil return } do { - self.articles = try values.decodeIfPresent(T.self, forKey: .data) + self.data = try values.decodeIfPresent(T.self, forKey: .data) }catch { - self.articles = nil - throw APIServerResponseError.code(error.localizedDescription) + self.data = nil + throw APIServerResponseError.code("\(error)") } } @@ -72,6 +72,6 @@ extension APIServerResponse: Decodable { extension APIServerResponse: CustomDebugStringConvertible { var debugDescription: String { - return "[Server-Response] status = \(status) message= \(message ?? "no message") error = empty data = \(articles)" + return "[Server-Response] status = \(status) message= \(message ?? "no message") error = empty data = \(data)" } } diff --git a/DutchNews/Classes/Data Layers/Repositories/ArticleRepository.swift b/DutchNews/Classes/Data Layers/Repositories/ArticleRepository.swift index 066d518..116df87 100644 --- a/DutchNews/Classes/Data Layers/Repositories/ArticleRepository.swift +++ b/DutchNews/Classes/Data Layers/Repositories/ArticleRepository.swift @@ -11,13 +11,13 @@ import RxSwift protocol ArticleRepository { - associatedtype T: Codable + typealias DataType = Article /// <#Description#> - func fetchArticles() -> Observable<[T]> + func fetchArticles() -> Observable<[DataType]> /// <#Description#> /// - Parameter keyword: <#keyword description#> - func search(keyword: String) -> Observable<[T]> + func search(keyword: String) -> Observable<[DataType]> } diff --git a/DutchNews/Classes/Data Layers/Repositories/HeadlinesArticleLocalRespository.swift b/DutchNews/Classes/Data Layers/Repositories/HeadlinesArticleLocalRespository.swift deleted file mode 100644 index 05636d1..0000000 --- a/DutchNews/Classes/Data Layers/Repositories/HeadlinesArticleLocalRespository.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// HeadlinesArticleLocalRespository.swift -// DutchNews -// -// Created by Farshad Mousalou on 9/20/20. -// Copyright © 2020 Farshad Mousalou. All rights reserved. -// - -import Foundation - -class HeadlinesArticleLocalRespository: ArticleRepository { - -} diff --git a/DutchNews/Classes/Data Layers/Repositories/HeadlinesArticleRemoteRepository.swift b/DutchNews/Classes/Data Layers/Repositories/HeadlinesArticleRemoteRepository.swift index a495a5e..d49dc86 100644 --- a/DutchNews/Classes/Data Layers/Repositories/HeadlinesArticleRemoteRepository.swift +++ b/DutchNews/Classes/Data Layers/Repositories/HeadlinesArticleRemoteRepository.swift @@ -13,25 +13,25 @@ import Alamofire class HeadlinesArticleRemoteRepository: ArticleRepository { - typealias T = Article + typealias DataType = Article let networkService: NetworkServiceInterceptable - let validator: NetworkValidResponse + let validator: NetworkValidResponse? init(networkService: NetworkServiceInterceptable, authentictor: RequestInterceptor, - validator: NetworkValidResponse) { + validator: NetworkValidResponse? = nil) { self.networkService = networkService self.networkService.addingRequest(interceptor: authentictor) self.validator = validator } - private typealias ResponseResult = Result,Error> + private typealias ResponseResult = Result,Error> func fetchArticles() -> Observable<[Article]> { - return networkService.executeRequest(endpoint: "v2/top-headlines", + return networkService.executeRequest(endpoint: "top-headlines", parameters: ["country": "nl"], method: .get, headers: [:], validator: validator) @@ -39,7 +39,7 @@ class HeadlinesArticleRemoteRepository: ArticleRepository { } func search(keyword: String) -> Observable<[Article]> { - networkService.executeRequest(endpoint: "v2/top-headlines", + networkService.executeRequest(endpoint: "top-headlines", parameters: ["q": keyword,"country": "nl"], method: .get, headers: [:], validator: validator) @@ -48,7 +48,7 @@ class HeadlinesArticleRemoteRepository: ArticleRepository { private func map(response: ResponseResult) throws -> [Article] { let result = try response.get() - return result.articles ?? [] + return result.data ?? [] } } diff --git a/DutchNewsTests/NetworkTests/APIServerResponseTests.swift b/DutchNewsTests/NetworkTests/APIServerResponseTests.swift index b2083c0..cbfb948 100644 --- a/DutchNewsTests/NetworkTests/APIServerResponseTests.swift +++ b/DutchNewsTests/NetworkTests/APIServerResponseTests.swift @@ -32,7 +32,7 @@ class APIServerResponseTests: XCTestCase { do { let response = try JSONDecoder().decode(APIServerResponse<[String]>.self, from: data) XCTAssert(response.status == .success , "APIServerResponse data was not able to decode.") - XCTAssertNotNil(response.articles, "APIServerResponse data was not able to decode.") + XCTAssertNotNil(response.data, "APIServerResponse data was not able to decode.") print("Decoded Objc ", response) }catch let error { diff --git a/DutchNewsTests/Repositories/HeadLines/HeadlineFailureResponse.json b/DutchNewsTests/Repositories/HeadLines/HeadlineFailureResponse.json new file mode 100644 index 0000000..f601d0f --- /dev/null +++ b/DutchNewsTests/Repositories/HeadLines/HeadlineFailureResponse.json @@ -0,0 +1,5 @@ +{ + "status": "error", + "code": "parametersMissing", + "message": "Required parameters are missing. Please set any of the following parameters and try again: sources, q, language, country, category." +} diff --git a/DutchNewsTests/Repositories/HeadLines/HeadlineSuccessResponse.json b/DutchNewsTests/Repositories/HeadLines/HeadlineSuccessResponse.json new file mode 100644 index 0000000..f10af8f --- /dev/null +++ b/DutchNewsTests/Repositories/HeadLines/HeadlineSuccessResponse.json @@ -0,0 +1,266 @@ +{ + "status": "ok", + "totalResults": 34, + "articles": [ + { + "source": { + "id": null, + "name": "Telegraaf.nl" + }, + "author": "ANP", + "title": "Trump geeft 'zegen' aan bod Oracle op TikTok - Telegraaf.nl", + "description": "De Amerikaanse president Donald Trump staat achter het bod van Oracle op de Amerikaanse activiteiten van de Chinese filmpjesapp TikTok. „Ik heb de deal mijn zegen gegeven”, zei Trump zaterdag volgens Bloomberg tegen verslaggevers toen hij het Witte Huis verli…", + "url": "https://www.telegraaf.nl/financieel/1442448989/trump-geeft-zegen-aan-bod-oracle-op-tik-tok", + "urlToImage": "https://www.telegraaf.nl/images/1200x630/filters:format(jpeg):quality(80)/cdn-kiosk-api.telegraaf.nl/6409f6dc-fb2c-11ea-acba-02c309bc01c1.jpg", + "publishedAt": "2020-09-20T10:32:00Z", + "content": "Het nieuwe bedrijf, dat volgens minister van Financiën Steven Mnuchin TikTok Global zou gaan heten, is het resultaat van een transactie die vorige maand door Trump werd afgedwongen vanwege bezorgdhei… [+1099 chars]" + }, + { + "source": { + "id": null, + "name": "Nos.nl" + }, + "author": null, + "title": "Privégegevens Wit-Russische politiemensen online in aanloop naar nieuw protest - NOS", + "description": "Hackers dreigen de gegevens van nog veel meer politiemensen te publiceren zolang het hardhandige optreden tegen demonstranten doorgaat.", + "url": "https://nos.nl/l/2349125", + "urlToImage": "https://nos.nl/data/image/2020/09/20/677152/xxl.jpg", + "publishedAt": "2020-09-20T10:15:00Z", + "content": "Hackers hebben privégegevens van ongeveer 1000 Wit-Russische politiemensen gelekt als vergelding voor het arresteren van bijna 400 deelnemers aan de vrouwenmars van gisteren. Ook vandaag gaan Wit-Rus… [+514 chars]" + }, + { + "source": { + "id": null, + "name": "Tweakers.net" + }, + "author": "Mark Hendrikman", + "title": "Sony: pre-orderproces PlayStation 5 kon veel soepeler gaan - Tweakers", + "description": "Sony heeft op Twitter zijn excuses aangeboden voor de moeite die klanten hebben met een pre-order plaatsen van de PlayStation 5. \"Dat kon veel soepeler gaan\", stelt het Japanse bedrijf. Verder geeft het garanties wat betreft voorraden in de rest van het jaar.", + "url": "https://tweakers.net/nieuws/172394/sony-pre-orderproces-playstation-5-kon-veel-soepeler-gaan.html", + "urlToImage": "https://tweakers.net/i/nmiumjECtV-28XAWh4xU09LWx6c=/67x67/filters:strip_exif()/i/2002904894.png?f=fpa", + "publishedAt": "2020-09-20T09:51:39Z", + "content": "Met al die scalpers en bots maak je als normale consument geen schijn van kans.Alsof het normaal is te noemen dat bijv. Bol (en elke andere webwinkel op coolblue na) compleet uitverkocht raakte in lu… [+855 chars]" + }, + { + "source": { + "id": null, + "name": "Rtlboulevard.nl" + }, + "author": null, + "title": "Vriendin Tommie Christiaan in verwachting - RTL Boulevard", + "description": "'Vandaag op ons 2-jarige jubileum willen we graag met iedereen delen dat we papa en mama worden', zo maakt Tommie Christiaan (34) vandaag dolblij bekend dat hij binnenkort vader wordt.", + "url": "https://www.rtlboulevard.nl/entertainment/showbizz/artikel/5184898/vriendin-tommie-christiaan-verwachting-van-een-kleintje", + "urlToImage": "https://www.rtlboulevard.nl/sites/default/files/styles/liggend_hoge_resolutie/public/content/images/2020/09/20/Tommie%20Christiaan.jpg?h=a9edb586&itok=0sJngmbZ", + "publishedAt": "2020-09-20T09:50:39Z", + "content": "De musicalster deelt het leuke nieuwtje op zijn Instagram met daarbij een foto van de buik van zijn vriendin. 'Onze grootste wens komt uit en we zijn zo dankbaar en gelukkig dat we dit mogen meemaken… [+583 chars]" + }, + { + "source": { + "id": null, + "name": "Voetbalprimeur.nl" + }, + "author": null, + "title": "LIVE-discussie: gewijzigd ADO en Groningen jagen op eerste Eredivisie-zege - VoetbalPrimeur.nl", + "description": "ADO Den Haag begint zondag flink gewijzigd aan de eerste thuiswedstrijd van het seizoen in de Eredivisie. Trainer Aleksandr Rankovic geeft onder meer nieuweling Lassana Faye een basisplaats tegen FC Groningen, dat het nog moet stellen zonder Arjen Robben.", + "url": "https://www.voetbalprimeur.nl/nieuws/945567/live-discussie-gewijzigd-ado-en-groningen-jagen-op-eerste-eredivisie-zege.html", + "urlToImage": "https://files.voetbalprimeur.nl/social/2020/09/20/social_07e16002d68daebaf4ab8f699774e501d8928da7.jpg", + "publishedAt": "2020-09-20T09:45:51Z", + "content": "ADO Den Haag begint zondag flink gewijzigd aan de eerste thuiswedstrijd van het seizoen in de Eredivisie. Trainer Aleksandr Rankovic geeft onder meer nieuweling Lassana Faye een basisplaats tegen FC … [+1046 chars]" + }, + { + "source": { + "id": null, + "name": "Racesport.nl" + }, + "author": "https://www.facebook.com/asse.klein", + "title": "Van der Mark wint Superpole Race voor Rea en Baz in Catalunya - Racesport.nl", + "description": "Michael van der Mark rijdt een geweldige wedstrijd en wint de Superpole Race op het Circuit de Barcelona-Catalunya door Jonathan Rea achter zich te houden.", + "url": "https://www.racesport.nl/van-der-mark-wint-superpole-race-voor-rea-en-baz-in-catalunya/", + "urlToImage": "https://www.racesport.nl/wp-content/uploads/2020/09/IMG_6975.jpg", + "publishedAt": "2020-09-20T09:33:52Z", + "content": "Michael van der Mark rijdt een geweldige wedstrijd en wint de Superpole Race op het Circuit de Barcelona-Catalunya door Jonathan Rea achter zich te houden. Ten Kate Racing Yamaha scoort een podium fi… [+5138 chars]" + }, + { + "source": { + "id": null, + "name": "Www.nu.nl" + }, + "author": "NU.nl", + "title": "Bij dodelijke schietpartij Bas van Wijk buitgemaakt horloge was nep-Rolex - NU.nl", + "description": "Het horloge dat vlak voor het doodschieten van de 24-jarige Bas van Wijk in Amsterdam werd buitgemaakt, is een namaak-Rolex. Dat laat het Openbaar Ministerie (OM) zondag aan NU.nl weten na berichtgeving door het Parool. Mogelijk is Van Wijk doodgeschoten omda…", + "url": "https://www.nu.nl/binnenland/6078647/bij-dodelijke-schietpartij-bas-van-wijk-buitgemaakt-horloge-was-nep-rolex.html", + "urlToImage": "https://media.nu.nl/m/34wxnz3adstc_wd1280.jpg/bij-dodelijke-schietpartij-bas-van-wijk-buitgemaakt-horloge-was-nep-rolex.jpg", + "publishedAt": "2020-09-20T09:30:00Z", + "content": "Het horloge dat vlak voor het doodschieten van de 24-jarige Bas van Wijk in Amsterdam werd buitgemaakt, is een namaak-Rolex. Dat laat het Openbaar Ministerie (OM) zondag aan NU.nl weten na berichtgev… [+1614 chars]" + }, + { + "source": { + "id": null, + "name": "Www.nu.nl" + }, + "author": "NU.nl", + "title": "Soundos El Ahmadi rekent al op een tweede seizoen van All Stars & Zonen - NU.nl", + "description": "Het moet volgens comédienne Soundos El Ahmadi heel vreemd lopen wil All Stars & Zonen geen tweede seizoen krijgen. De actrice heeft een hoofdrol in het vervolg van All Stars en gaat er al voor de eerste uitzending van uit dat de nieuwe reeks aanslaat.", + "url": "https://www.nu.nl/film/6077372/soundos-el-ahmadi-rekent-al-op-een-tweede-seizoen-van-all-stars-zonen.html", + "urlToImage": "https://media.nu.nl/m/gp3xy7vat7vl_wd1280.jpg/soundos-el-ahmadi-rekent-al-op-een-tweede-seizoen-van-all-stars-zonen.jpg", + "publishedAt": "2020-09-20T09:17:00Z", + "content": "Het moet volgens comédienne Soundos El Ahmadi heel vreemd lopen wil All Stars & Zonen geen tweede seizoen krijgen. De actrice heeft een hoofdrol in het vervolg van All Stars en gaat er al voor de… [+671 chars]" + }, + { + "source": { + "id": "google-news", + "name": "Google News" + }, + "author": null, + "title": "'We zaten net naar Netflix te kijken en plotseling zit je zelf in zo'n film'; maar in Kiel-Windeweer is de rust na een schietpartij teruggekeerd - Dagblad van het Noorden", + "description": null, + "url": "https://news.google.com/__i/rss/rd/articles/CBMidWh0dHBzOi8vd3d3LmR2aG4ubmwvZ3JvbmluZ2VuL1dlLXphdGVuLW5ldC1uYWFyLU5ldGZsaXgtdGUta2lqa2VuLWVuLXBsb3RzZWxpbmcteml0LWplLXplbGYtaW4tem9uLWZpbG0tMjYwMzI3OTkuaHRtbNIBAA?oc=5", + "urlToImage": null, + "publishedAt": "2020-09-20T08:52:00Z", + "content": null + }, + { + "source": { + "id": null, + "name": "Dpgmedia.nl" + }, + "author": null, + "title": "Automobilist overlijdt door aanrijding tegen monument in Stadspark Groningen - AD.nl", + "description": null, + "url": "https://myprivacy.dpgmedia.nl/consent/?siteKey=V9f6VUvlHxq9wKIN&callbackUrl=https:%2f%2fwww.ad.nl%2fprivacy-gate%2faccept-tcf2%3fredirectUri%3d%252fgroningen%252fautomobilist-overlijdt-door-aanrijding-tegen-monument-in-stadspark-groningen%257eac916808%252f", + "urlToImage": null, + "publishedAt": "2020-09-20T08:13:17Z", + "content": null + }, + { + "source": { + "id": null, + "name": "Tpo.nl" + }, + "author": "Peil.nl", + "title": "Opiniepeiling Maurice de Hond: Wilders sterkste oppositieleider bij Algemene Beschouwingen - ThePostOnline", + "description": "Slechts 21% wil dat huidig kabinet na de verkiezingen doorgaat", + "url": "https://tpo.nl/2020/09/20/peiling-maurice-de-hond-wilders-na-rutte-het-sterkst/", + "urlToImage": "https://tpo.nl/wp-content/uploads/2020/09/geert-wilders-peil.png", + "publishedAt": "2020-09-20T07:55:45Z", + "content": "De peiling van deze week laat wederom kleine verschuivingen zien, maar ook na de Algemene Beschouwingen zien we geen structurele wijzigingen. De VVD blijft ruim voorop nummer 2 de PVV. De PvdA staat … [+483 chars]" + }, + { + "source": { + "id": null, + "name": "Voetbalzone.nl" + }, + "author": null, + "title": "Frank de Boer lijkt enige kandidaat voor Oranje met tweede gesprek op komst - Voetbalzone.nl", + "description": "", + "url": "https://www.voetbalzone.nl/doc.asp?uid=377347", + "urlToImage": "https://static.voetbalzone.nl/images/photos/ori_1152_648/132351093515435.jpg", + "publishedAt": "2020-09-20T07:53:00Z", + "content": "Er staat maandag een tweede gesprek tussen de KNVB en Frank de Boer op het programma. Pieter Zwart, hoofdredacteur van Voetbal International, vertelt in de liveshow van het weekblad dat er afgelopen … [+2136 chars]" + }, + { + "source": { + "id": null, + "name": "Nos.nl" + }, + "author": null, + "title": "Voor het eerst weer vorst aan de grond gemeten - NOS", + "description": "Bij een KNMI-weerstation in Twente werd vanmorgen om 07.20 uur kort -1,3 graden Celsius gemeten op klomphoogte.", + "url": "https://nos.nl/l/2349109", + "urlToImage": "https://nos.nl/data/image/2020/09/20/677123/xxl.jpg", + "publishedAt": "2020-09-20T07:10:00Z", + "content": "Weerplaza noemt het geen uitzondering dat op dit moment in het jaar de eerste vorst wordt gemeten. In 2012 werd half september zelfs bijna 5 graden vorst aan de grond gemeten.\r\nAfgelopen dinsdag was … [+572 chars]" + }, + { + "source": { + "id": null, + "name": "Dpgmedia.nl" + }, + "author": null, + "title": "Sylvie Meis geeft jawoord in romantisch Toscane: bekijk de foto's hier - AD.nl", + "description": null, + "url": "https://myprivacy.dpgmedia.nl/consent/?siteKey=V9f6VUvlHxq9wKIN&callbackUrl=https:%2f%2fwww.ad.nl%2fprivacy-gate%2faccept-tcf2%3fredirectUri%3d%252fhome%252fsylvie-meis-geeft-jawoord-in-romantisch-toscane-bekijk-de-foto-s-hier%257ea9e9c3d8%252f", + "urlToImage": null, + "publishedAt": "2020-09-20T07:01:35Z", + "content": null + }, + { + "source": { + "id": null, + "name": "Dagelijksestandaard.nl" + }, + "author": "", + "title": "Trump wil komende week nieuwe opvolger Hooggerechtshof kiezen: 'waarschijnlijk een vrouw' - De Dagelijkse Standaard", + "description": "Als het aan de Amerikaanse president Donald Trump ligt dan wordt er zo snel mogelijk een nieuwe opvolger voor het Hooggerechtshof aangewezen. Afgelopen week overleed rechter Ruth Bader Ginsburg, en voor de Trump-regering is dit een gouden kans om een progress…", + "url": "https://www.dagelijksestandaard.nl/2020/09/trump-wil-komende-week-nieuwe-opvolger-hooggerechtshof-kiezen-waarschijnlijk-een-vrouw/", + "urlToImage": "https://cdn-04.dagelijksestandaard.nl/wp-content/uploads/2020/08/vlcsnap-2020-08-04-07h55m32s542.jpg?x90889", + "publishedAt": "2020-09-20T07:00:43Z", + "content": "Als het aan de Amerikaanse president Donald Trump ligt dan wordt er zo snel mogelijk een nieuwe opvolger voor het Hooggerechtshof aangewezen. Afgelopen week overleed rechter Ruth Bader Ginsburg, en v… [+1101 chars]" + }, + { + "source": { + "id": null, + "name": "Dpgmedia.nl" + }, + "author": null, + "title": "In 7 foto's toont Marijn (22) de hel op Lesbos: 'Ik zat regelmatig even te huilen' - De Gelderlander", + "description": null, + "url": "https://myprivacy.dpgmedia.nl/consent/?siteKey=Oom2kBLTny5zJUeO&callbackUrl=https:%2f%2fwww.gelderlander.nl%2fprivacy-gate%2faccept-tcf2%3fredirectUri%3d%252fhome%252fin-7-foto-s-toont-marijn-22-de-hel-op-lesbos-ik-zat-regelmatig-even-te-huilen%257ea9f9da5a%252f", + "urlToImage": null, + "publishedAt": "2020-09-20T06:31:00Z", + "content": null + }, + { + "source": { + "id": null, + "name": "Www.nu.nl" + }, + "author": "NU.nl", + "title": "Pogacar zet Tour op zijn kop in sensationele tijdrit: 'Koers was al voorbij' - NU.nl", + "description": "De strijd om de eindzege leek al beslist voor de tijdrit op de voorlaatste dag van de Tour de France, maar op een sensationele zaterdag in de Vogezen brak Tadej Pogacar de gele droom van Jumbo-Visma en Primoz Roglic in duizend stukjes. Een terugblik op een va…", + "url": "https://www.nu.nl/tour-de-france/6078609/pogacar-zet-tour-op-zijn-kop-in-sensationele-tijdrit-koers-was-al-voorbij.html", + "urlToImage": "https://media.nu.nl/m/s3vx5u9auz1g_wd1280.jpg/pogacar-zet-tour-op-zijn-kop-in-sensationele-tijdrit-koers-was-al-voorbij.jpg", + "publishedAt": "2020-09-20T06:31:00Z", + "content": "De strijd om de eindzege leek al beslist voor de tijdrit op de voorlaatste dag van de Tour de France, maar op een sensationele zaterdag in de Vogezen brak Tadej Pogacar de gele droom van Jumbo-Visma … [+4978 chars]" + }, + { + "source": { + "id": null, + "name": "Nos.nl" + }, + "author": null, + "title": "VS kondigt op eigen houtje VN-sancties af tegen Iran - NOS", + "description": "Het land beroept zich op de Iran-deal uit 2015, maar volgens andere VN-diplomaten is dat niet meer mogelijk omdat Trump eruit is gestapt.", + "url": "https://nos.nl/l/2349102", + "urlToImage": "https://nos.nl/data/image/2020/09/20/677107/xxl.jpg", + "publishedAt": "2020-09-20T06:05:00Z", + "content": "De VS eist dat landen zich weer gaan houden aan VN-sancties die golden voor Iran. \"Dit is een stap op weg naar internationale vrede en veiligheid\", zegt minister Pompeo van Buitenlandse Zaken. De Rus… [+1354 chars]" + }, + { + "source": { + "id": "rtl-nieuws", + "name": "RTL Nieuws" + }, + "author": null, + "title": "Dit gebeurde er in de nacht op Le Mans! - RTL Nieuws", + "description": "De nachtelijke uren in de 24 uur van Le Mans brachten onder andere problemen voor de leidende Toyota, maar helaas ook voor Racing Team Nederland en het team van Job van Uitert. Een terugblik!", + "url": "https://www.rtlnieuws.nl/sport/gp/video/5184877/dit-gebeurde-er-de-nacht-op-le-mans", + "urlToImage": "https://screenshots.rtl.nl/system/thumb/sz=720x404/uuid=d3bff244-8f18-4814-be5c-d9e249db9632", + "publishedAt": "2020-09-20T05:52:44Z", + "content": "De nachtelijke uren in de 24 uur van Le Mans brachten onder andere problemen voor de leidende Toyota, maar helaas ook voor Racing Team Nederland en het team van Job van Uitert. Een terugblik! \r\n20 se… [+17 chars]" + }, + { + "source": { + "id": null, + "name": "Linda.nl" + }, + "author": null, + "title": "Molloot Ivo over aflevering drie: 'Ellie ontpopt zich als de Mol-Mata Hari' - LINDA.", + "description": "Tien kandidaten, van wie één Mol. Journalist Ivo over de derde aflevering van ‘Wie is de Mol?-Renaissance’:", + "url": "https://www.linda.nl/nieuws/fragment-gemist/molloot-ivo-aflevering-drie-ellie-mol-mata-hari/", + "urlToImage": "https://www.linda.nl/lindanl-assets/uploads/2020/09/20111952/ellie-lust-wie-is-de-mol-ivo-1800x1012.jpg", + "publishedAt": "2020-09-20T05:44:04Z", + "content": "Jeroen wist inmiddels dat Ellie zich alleen met hem in de echt wilde verbinden als hij met een flinke bruidsschat zou komen. Hij biechtte dan ook niet veel later op dat hij zichzelf verdacht had will… [+4849 chars]" + } + ] +} diff --git a/DutchNewsTests/Repositories/HeadLines/HeadlinesArticleRemoteRepositoryTests.swift b/DutchNewsTests/Repositories/HeadLines/HeadlinesArticleRemoteRepositoryTests.swift new file mode 100644 index 0000000..2e1b8e3 --- /dev/null +++ b/DutchNewsTests/Repositories/HeadLines/HeadlinesArticleRemoteRepositoryTests.swift @@ -0,0 +1,187 @@ +// +// HeadlinesArticleRemoteRepositoryTests.swift +// DutchNewsTests +// +// Created by Farshad Mousalou on 9/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import XCTest +import RxSwift +import RxTest +import RxBlocking +import Alamofire +import Mocker + +@testable import DutchNews + +class HeadlinesArticleRemoteRepositoryTests: XCTestCase { + + var articleRepository: ArticleRepository! + var disposeBag: DisposeBag! + + override func setUp() { + + articleRepository = HeadlinesArticleRemoteRepository(networkService: RepositoryDependenciesFactory.createMockAPIClient(), + authentictor: RepositoryDependenciesFactory.createAuthentictor(), + validator: RepositoryDependenciesFactory.createValidator()) + disposeBag = DisposeBag() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + disposeBag = nil + articleRepository = nil + } + + func testFetchMockArticles() { + do { + let exptection = self.expectation(description: "testFetchMockArticles") + exptection.expectedFulfillmentCount = 2 + + let data = mockResponserFetcher(name: "HeadlineSuccessResponse") + let mock = try NetworkMockBuilder(URL: "https://newsapi.org/v2/top-headlines?country=nl") + .set(method: .get) + .set(statusCode: 200) + .set(contentType: .json) + .set(data: [.get: data]) + .build() + + Mocker.register(mock) + + articleRepository.fetchArticles() + .observeOn(MainScheduler.asyncInstance) + .subscribe { (event) in + switch event { + case .next(let element): + print("element ", element) + case .error(let error): + XCTFail("Error Occured with info: \(error.localizedDescription)") + exptection.fulfill() + case .completed: + print("Observer completed") + } + + exptection.fulfill() + + }.disposed(by: disposeBag) + + self.waitForExpectations(timeout: 30.0) { (error) in + XCTAssertNil(error, "Error Occured with info: \(error!.localizedDescription)") + } + + } catch let error { + XCTFail("Error Occured with info: \(error.localizedDescription)") + } + } + + func testErrorOnFetchMockArticles() { + do { + let exptection = self.expectation(description: "testErrorOnFetchMockArticles") + exptection.expectedFulfillmentCount = 2 + let data = mockResponserFetcher(name: "HeadlineFailureResponse") + let mock = try NetworkMockBuilder(URL: AppConfig.BaseURL.absoluteString + "top-headlines?country=nl") + .set(method: .get) + .set(statusCode: 401) + .set(contentType: .json) + .set(data: [.get: data]) + .build() + + Mocker.register(mock) + + articleRepository.fetchArticles() + .observeOn(MainScheduler.asyncInstance) + .subscribe { (event) in + + switch event { + case .next(let element): + XCTFail("Stream emitted Element: \(element)") + case .error(let error): + print("Error Occured with info: \(error.localizedDescription)") + exptection.fulfill() + case .completed: + print("Observer completed") + } + + exptection.fulfill() + }.disposed(by: disposeBag) + + self.waitForExpectations(timeout: 30.0) { (error) in + XCTAssertNil(error, "Error Occured with info: \(error!.localizedDescription)") + } + + } catch let error { + XCTFail("Error Occured with info: \(error.localizedDescription)") + } + } + + func testRealFetchArticles() { + + let exptection = self.expectation(description: "testRealFetchArticles") + exptection.expectedFulfillmentCount = 2 + + let articleRepository = HeadlinesArticleRemoteRepository(networkService: RepositoryDependenciesFactory.createAPIClient(), + authentictor: RepositoryDependenciesFactory.createAuthentictor(), + validator: RepositoryDependenciesFactory.createValidator()) + + articleRepository.fetchArticles() + .observeOn(MainScheduler.asyncInstance) + .subscribe { (event) in + switch event { + case .next(let element): + print("element ", element) + case .error(let error): + XCTFail("Error Occured with info: \(error.localizedDescription)") + exptection.fulfill() + case .completed: + print("Observer completed") + } + + exptection.fulfill() + + }.disposed(by: disposeBag) + + self.waitForExpectations(timeout: 30.0) { (error) in + XCTAssertNil(error, "Error Occured with info: \(error!.localizedDescription)") + } + + } + + func testErrorOnFetchArticles() { + let exptection = self.expectation(description: "testErrorOnFetchArticles") + exptection.expectedFulfillmentCount = 2 + + let articleRepository = HeadlinesArticleRemoteRepository(networkService: RepositoryDependenciesFactory.createAPIClient(), + authentictor: APIAuthenticator(token: ""), + validator: RepositoryDependenciesFactory.createValidator()) + + articleRepository.fetchArticles() + .observeOn(MainScheduler.asyncInstance) + .subscribe { (event) in + + switch event { + case .next(let element): + XCTFail("Stream emitted Element: \(element)") + case .error(let error): + print("Error Occured with info: \(error.localizedDescription)") + exptection.fulfill() + case .completed: + print("Observer completed") + } + + exptection.fulfill() + }.disposed(by: disposeBag) + + self.waitForExpectations(timeout: 30.0) { (error) in + XCTAssertNil(error, "Error Occured with info: \(error!.localizedDescription)") + } + + } + + func mockResponserFetcher(name: String) -> Data { + let bundle = Bundle(for: type(of: self)) + return try! Data(contentsOf: bundle.url(forResource: name, withExtension: "json")!) + } + +} diff --git a/DutchNewsTests/Repositories/MockArticleValidResponse.swift b/DutchNewsTests/Repositories/MockArticleValidResponse.swift new file mode 100644 index 0000000..d431656 --- /dev/null +++ b/DutchNewsTests/Repositories/MockArticleValidResponse.swift @@ -0,0 +1,17 @@ +// +// MockArticleValidResponse.swift +// DutchNewsTests +// +// Created by Farshad Mousalou on 9/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +@testable import DutchNews + +struct MockArticleValidResponse: NetworkValidResponse { + var statusCodes: Set { + Set((200..<300).map { $0 } + [400, 422, 429, 401]) + } + var contentTypes: [String] { ["application/json"] } +} diff --git a/DutchNewsTests/Repositories/RepositoryDependenciesFactory.swift b/DutchNewsTests/Repositories/RepositoryDependenciesFactory.swift new file mode 100644 index 0000000..76ab8f3 --- /dev/null +++ b/DutchNewsTests/Repositories/RepositoryDependenciesFactory.swift @@ -0,0 +1,42 @@ +// +// RepositoryDependenciesFactory.swift +// DutchNewsTests +// +// Created by Farshad Mousalou on 9/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import Alamofire +import Mocker + +@testable import DutchNews + +struct RepositoryDependenciesFactory { + + static func createMockAPIClient() -> NetworkServiceInterceptable { + + let configuration = URLSessionConfiguration.af.default + configuration.protocolClasses = [MockingURLProtocol.self] + let sessionManager = Alamofire.Session(configuration: configuration) + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + return APIClientService(baseURL: AppConfig.BaseURL, + session: sessionManager, decoder: decoder) + } + + static func createAPIClient() -> NetworkServiceInterceptable { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + return APIClientService(baseURL: AppConfig.BaseURL, decoder: decoder) + } + + static func createAuthentictor() -> RequestInterceptor { + return APIAuthenticator(token: AppConfig.APIKey) + } + + static func createValidator() -> NetworkValidResponse { + return MockArticleValidResponse() + } + +} From 86b7f389e15227acfc910854182ae824f3c58b30 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Mon, 21 Sep 2020 00:31:15 +0330 Subject: [PATCH 067/108] - Implemented Headline Custom Layout. - Added HeadlinesViewController. - Added HeadlineLayoutConfiguration Abstract. - Added ArticleHeadlineLayoutConfiguration. - HeadlineLayoutConfiguration was implemented by ArticleHeadlineLayoutConfiguration. --- DutchNews.xcodeproj/project.pbxproj | 24 +++++++ .../ArticleHeadlineLayoutConfiguration.swift | 48 +++++++++++++ .../HeadlineLayoutConfiguration.swift | 67 +++++++++++++++++++ ...adlinesViewController+MagazineLayout.swift | 59 ++++++++++++++++ .../HeadlinesViewController.swift | 67 +++++++++++++++++++ .../Storyboards/Base.lproj/Main.storyboard | 30 +++------ 6 files changed, 274 insertions(+), 21 deletions(-) create mode 100644 DutchNews/Classes/Utilites/HeadlineLayoutConfiguration/ArticleHeadlineLayoutConfiguration.swift create mode 100644 DutchNews/Classes/Utilites/HeadlineLayoutConfiguration/HeadlineLayoutConfiguration.swift create mode 100644 DutchNews/Classes/ViewControllers/HeadlinesViewController+MagazineLayout.swift create mode 100644 DutchNews/Classes/ViewControllers/HeadlinesViewController.swift diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index c6e01d5..047e852 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -9,6 +9,10 @@ /* Begin PBXBuildFile section */ 21E07E636FF6F2984928E95F /* Pods_DutchNews.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6D6B9A18971CCB93B1E1B562 /* Pods_DutchNews.framework */; }; D9C19360BE6ACB6A2FE7DA86 /* Pods_DutchNewsTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4DDFCE580208FED7E50E9D2 /* Pods_DutchNewsTests.framework */; }; + F8154D4E2517D28700BFB42C /* HeadlinesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8154D4D2517D28700BFB42C /* HeadlinesViewController.swift */; }; + F8154D522517EBC700BFB42C /* HeadlinesViewController+MagazineLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8154D512517EBC700BFB42C /* HeadlinesViewController+MagazineLayout.swift */; }; + F8154D552517EC0D00BFB42C /* HeadlineLayoutConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8154D542517EC0D00BFB42C /* HeadlineLayoutConfiguration.swift */; }; + F8154D572517F03600BFB42C /* ArticleHeadlineLayoutConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8154D562517F03600BFB42C /* ArticleHeadlineLayoutConfiguration.swift */; }; F82C8EFD2516304D002B27B3 /* APIClientServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8EFC2516304D002B27B3 /* APIClientServiceTests.swift */; }; F82C8EFF25163073002B27B3 /* NetworkMocking.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8EFE25163073002B27B3 /* NetworkMocking.swift */; }; F82C8F0125163898002B27B3 /* NetworkMockingDataFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8F0025163898002B27B3 /* NetworkMockingDataFactory.swift */; }; @@ -46,6 +50,10 @@ CE0BB7F85E1175162F017DD0 /* Pods-DutchNews.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DutchNews.debug.xcconfig"; path = "Target Support Files/Pods-DutchNews/Pods-DutchNews.debug.xcconfig"; sourceTree = ""; }; DD3A1F2B03FE42DB1EAECC2E /* Pods-DutchNewsUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DutchNewsUITests.release.xcconfig"; path = "Target Support Files/Pods-DutchNewsUITests/Pods-DutchNewsUITests.release.xcconfig"; sourceTree = ""; }; E4DDFCE580208FED7E50E9D2 /* Pods_DutchNewsTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DutchNewsTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F8154D4D2517D28700BFB42C /* HeadlinesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlinesViewController.swift; sourceTree = ""; }; + F8154D512517EBC700BFB42C /* HeadlinesViewController+MagazineLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HeadlinesViewController+MagazineLayout.swift"; sourceTree = ""; }; + F8154D542517EC0D00BFB42C /* HeadlineLayoutConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlineLayoutConfiguration.swift; sourceTree = ""; }; + F8154D562517F03600BFB42C /* ArticleHeadlineLayoutConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleHeadlineLayoutConfiguration.swift; sourceTree = ""; }; F82C8EFC2516304D002B27B3 /* APIClientServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClientServiceTests.swift; sourceTree = ""; }; F82C8EFE25163073002B27B3 /* NetworkMocking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMocking.swift; sourceTree = ""; }; F82C8F0025163898002B27B3 /* NetworkMockingDataFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMockingDataFactory.swift; sourceTree = ""; }; @@ -95,6 +103,15 @@ name = Frameworks; sourceTree = ""; }; + F8154D532517EBE300BFB42C /* HeadlineLayoutConfiguration */ = { + isa = PBXGroup; + children = ( + F8154D542517EC0D00BFB42C /* HeadlineLayoutConfiguration.swift */, + F8154D562517F03600BFB42C /* ArticleHeadlineLayoutConfiguration.swift */, + ); + path = HeadlineLayoutConfiguration; + sourceTree = ""; + }; F82C8EFB2516051D002B27B3 /* NetworkTests */ = { isa = PBXGroup; children = ( @@ -203,6 +220,8 @@ F8F14C6C250D70CF00C24FF5 /* ViewControllers */ = { isa = PBXGroup; children = ( + F8154D4D2517D28700BFB42C /* HeadlinesViewController.swift */, + F8154D512517EBC700BFB42C /* HeadlinesViewController+MagazineLayout.swift */, ); path = ViewControllers; sourceTree = ""; @@ -210,6 +229,7 @@ F8F14C6D250D70DC00C24FF5 /* Utilites */ = { isa = PBXGroup; children = ( + F8154D532517EBE300BFB42C /* HeadlineLayoutConfiguration */, ); path = Utilites; sourceTree = ""; @@ -451,8 +471,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F8154D522517EBC700BFB42C /* HeadlinesViewController+MagazineLayout.swift in Sources */, F8DE79E32515904400A6C2D5 /* NetworkService.swift in Sources */, F865F75025168F05001FD067 /* AppConfig.swift in Sources */, + F8154D572517F03600BFB42C /* ArticleHeadlineLayoutConfiguration.swift in Sources */, + F8154D4E2517D28700BFB42C /* HeadlinesViewController.swift in Sources */, + F8154D552517EC0D00BFB42C /* HeadlineLayoutConfiguration.swift in Sources */, F865F74C251686D2001FD067 /* Authenticator.swift in Sources */, F8DE79E5251594D700A6C2D5 /* APIClientService.swift in Sources */, F89B021E250D446000B41293 /* AppDelegate.swift in Sources */, diff --git a/DutchNews/Classes/Utilites/HeadlineLayoutConfiguration/ArticleHeadlineLayoutConfiguration.swift b/DutchNews/Classes/Utilites/HeadlineLayoutConfiguration/ArticleHeadlineLayoutConfiguration.swift new file mode 100644 index 0000000..d17f029 --- /dev/null +++ b/DutchNews/Classes/Utilites/HeadlineLayoutConfiguration/ArticleHeadlineLayoutConfiguration.swift @@ -0,0 +1,48 @@ +// +// ArticleHeadlineLayoutConfiguration.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import MagazineLayout +import UIKit + +struct ArticleHeadlineLayoutConfiguration: HeadlineLayoutConfiguration { + + func itemSizeMode(forItemAt indexPath: IndexPath) -> MagazineLayoutItemSizeMode { + switch (indexPath.section, indexPath.row) { + case (_,0), + (_,3): + return sizeModeCreate(widthMode: .fullWidth(respectsHorizontalInsets: false), heightMode: .dynamic) + case (_,1), + (_,2): + return sizeModeCreate(widthMode: .halfWidth, heightMode: .dynamicAndStretchToTallestItemInRow) + default: + return sizeModeCreate() + } + } + + func verticalSpacing(forElementsInSectionAtIndex section: Int) -> CGFloat { + 2.5 + } + + func horizontalSpacing(forElementsInSectionAtIndex section: Int) -> CGFloat { + 2.5 + } + + + //////////////////////////////////////////////////////////////// + //MARK:- + //MARK:Private Methods + //MARK:- + //////////////////////////////////////////////////////////////// + + private func sizeModeCreate(widthMode: MagazineLayoutItemWidthMode = .fullWidth(respectsHorizontalInsets: false), + heightMode: MagazineLayoutItemHeightMode = .dynamic) -> MagazineLayoutItemSizeMode { + return MagazineLayoutItemSizeMode(widthMode: widthMode,heightMode: heightMode) + } + +} diff --git a/DutchNews/Classes/Utilites/HeadlineLayoutConfiguration/HeadlineLayoutConfiguration.swift b/DutchNews/Classes/Utilites/HeadlineLayoutConfiguration/HeadlineLayoutConfiguration.swift new file mode 100644 index 0000000..993d745 --- /dev/null +++ b/DutchNews/Classes/Utilites/HeadlineLayoutConfiguration/HeadlineLayoutConfiguration.swift @@ -0,0 +1,67 @@ +// +// HeadlineLayoutConfiguration.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import UIKit +import MagazineLayout + +/// `HeadlineLayoutConfiguration` Abstract +protocol HeadlineLayoutConfiguration { + + var defaultHeight: CGFloat { get } + + func itemSizeMode(forItemAt indexPath: IndexPath) -> MagazineLayoutItemSizeMode + func visibleMode(forFooterAt section: Int) -> MagazineLayoutFooterVisibilityMode + func visibleMode(forHeaderAt section: Int) -> MagazineLayoutHeaderVisibilityMode + func visibleMode(forBackground section: Int) -> MagazineLayoutBackgroundVisibilityMode + + func insets(forSectionAtIndex section: Int) -> UIEdgeInsets + func insets(forItemsInSectionAtIndex section: Int) -> UIEdgeInsets + + func verticalSpacing(forElementsInSectionAtIndex section: Int) -> CGFloat + func horizontalSpacing(forElementsInSectionAtIndex section: Int) -> CGFloat + +} + +extension HeadlineLayoutConfiguration { + + var defaultHeight: CGFloat { 44 } + + func itemSizeMode(forItemAt indexPath: IndexPath) -> MagazineLayoutItemSizeMode { + return MagazineLayoutItemSizeMode(widthMode: .fullWidth(respectsHorizontalInsets: true), + heightMode: MagazineLayoutItemHeightMode.static(height: defaultHeight)) + } + + func visibleMode(forFooterAt section: Int) -> MagazineLayoutFooterVisibilityMode { + .hidden + } + + func visibleMode(forHeaderAt section: Int) -> MagazineLayoutHeaderVisibilityMode { + .hidden + } + + func visibleMode(forBackground section: Int) -> MagazineLayoutBackgroundVisibilityMode { + .hidden + } + + func insets(forSectionAtIndex section: Int) -> UIEdgeInsets { + return .init(top: 8, left: 8, bottom: 8, right: 8) + } + + func insets(forItemsInSectionAtIndex section: Int) -> UIEdgeInsets { + return .zero + } + + func verticalSpacing(forElementsInSectionAtIndex section: Int) -> CGFloat { + return 0 + } + + func horizontalSpacing(forElementsInSectionAtIndex section: Int) -> CGFloat { + return 0 + } +} diff --git a/DutchNews/Classes/ViewControllers/HeadlinesViewController+MagazineLayout.swift b/DutchNews/Classes/ViewControllers/HeadlinesViewController+MagazineLayout.swift new file mode 100644 index 0000000..fe394cb --- /dev/null +++ b/DutchNews/Classes/ViewControllers/HeadlinesViewController+MagazineLayout.swift @@ -0,0 +1,59 @@ +// +// HeadlinesViewController+MagazineLayout.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import UIKit +import MagazineLayout + +extension HeadlinesViewController: UICollectionViewDelegateMagazineLayout { + + func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + visibilityModeForFooterInSectionAtIndex index: Int) -> MagazineLayoutFooterVisibilityMode { + + layoutConfiguration.visibleMode(forFooterAt: index) + } + + func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + visibilityModeForHeaderInSectionAtIndex index: Int) -> MagazineLayoutHeaderVisibilityMode { + layoutConfiguration.visibleMode(forHeaderAt: index) + } + + func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + visibilityModeForBackgroundInSectionAtIndex index: Int) -> MagazineLayoutBackgroundVisibilityMode { + layoutConfiguration.visibleMode(forBackground: index) + } + + func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeModeForItemAt indexPath: IndexPath) -> MagazineLayoutItemSizeMode { + return layoutConfiguration.itemSizeMode(forItemAt: indexPath) + } + + func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + horizontalSpacingForItemsInSectionAtIndex index: Int) -> CGFloat { + return layoutConfiguration.horizontalSpacing(forElementsInSectionAtIndex: index) + } + + func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + verticalSpacingForElementsInSectionAtIndex index: Int) -> CGFloat { + return layoutConfiguration.verticalSpacing(forElementsInSectionAtIndex: index) + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetsForSectionAtIndex index: Int) -> UIEdgeInsets { + layoutConfiguration.insets(forSectionAtIndex: index) + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetsForItemsInSectionAtIndex index: Int) -> UIEdgeInsets { + layoutConfiguration.insets(forItemsInSectionAtIndex: index) + } +} diff --git a/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift b/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift new file mode 100644 index 0000000..f4aedeb --- /dev/null +++ b/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift @@ -0,0 +1,67 @@ +// +// HeadlinesViewController.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/20/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import UIKit +import MagazineLayout +import RxCocoa +import RxSwift + +class HeadlinesViewController: UIViewController { + + @IBOutlet weak var collectionView: UICollectionView! + + var collectionLayout: MagazineLayout? { + return collectionView?.collectionViewLayout as? MagazineLayout + } + + var layoutConfiguration : HeadlineLayoutConfiguration = ArticleHeadlineLayoutConfiguration() { + didSet { + collectionView.reloadData() + } + } + + let disposeBag = DisposeBag() + + override func viewDidLoad() { + super.viewDidLoad() + + collectionView.register(MagazineLayoutCollectionViewCell.self, forCellWithReuseIdentifier: "MyCustomCellReuseIdentifier") + + // Do any additional setup after loading the view. + + Observable.from(optional: Array(repeating: 20, count: 10)) + .bind(to: collectionView.rx.items(cellIdentifier: "MyCustomCellReuseIdentifier", cellType: MagazineLayoutCollectionViewCell.self)) { model, index, cell in + cell.contentView.layer.borderColor = UIColor.lightGray.cgColor + cell.contentView.layer.borderWidth = 0.25 + cell.contentView.layer.cornerRadius = 5.0 + + }.disposed(by: disposeBag) + + collectionView.delegate = self + + } + + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ + +} + +extension HeadlinesViewController: UICollectionViewDelegate { + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + + } +} diff --git a/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard b/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard index a512696..51793dd 100644 --- a/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard +++ b/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard @@ -5,7 +5,6 @@ - @@ -28,30 +27,16 @@ - + - - + + - - - - - - - - - - - - - - - - + + @@ -64,10 +49,13 @@ + + + - + From ca82857ae7e7844a08a6dca783bb3338e4c785ff Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Mon, 21 Sep 2020 00:47:16 +0330 Subject: [PATCH 068/108] -tweaked unit-test workflow. (#7) * -tweaked unit-test workflow. * -tweaked unit-test workflow. * - Fixed Github action syntax error. --- .github/workflows/unit-test.yml | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 82d4e03..8459bcc 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -5,21 +5,13 @@ name: CI Testing # Controls when the action will run. Triggers the workflow on push or pull request # events but only for the master branch on: - push: - branches: - - feature/* - - feature/*/* - - bugfix/* - - hotfix/* + + repository_dispatch: + types: [test] pull_request: - branches: - - develop - - feature/* - - feature/*/* - - master - - bugfix/* - - hotfix/* - - + branches: + ['*'] + # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: test: From 9c85a684ca84efdbdd2c1d755c55d79627228645 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Mon, 21 Sep 2020 03:56:16 +0330 Subject: [PATCH 069/108] - Added Article Cells. - Added UIView+Nib Extension. - Added String+HTML Extension. - Fixed Bugs. --- DutchNews.xcodeproj/project.pbxproj | 66 +++++++++++ .../Classes/Extensions/String+HTML.swift | 25 +++++ .../Classes/Extensions/UI/UIView+Nib.swift | 67 +++++++++++ DutchNews/Classes/Models/Article.swift | 20 +++- .../ArticleHeadlineLayoutConfiguration.swift | 9 +- .../HeadlinesViewController+DataSource.swift | 104 ++++++++++++++++++ ...adlinesViewController+MagazineLayout.swift | 2 +- .../HeadlinesViewController.swift | 61 ++++++++-- .../Cells/ArticleRowCollectionViewCell.swift | 36 ++++++ .../Cells/ArticleRowCollectionViewCell.xib | 89 +++++++++++++++ ...rticleWebContainerCollectionViewCell.swift | 24 ++++ .../ArticleWebContainerCollectionViewCell.xib | 51 +++++++++ .../HalfWidthArticleCollectionViewCell.swift | 28 +++++ .../HalfWidthArticleCollectionViewCell.xib | 71 ++++++++++++ .../HeadlineBaseCollectionViewCell.swift | 14 +++ .../Cells/MainArticleCollectionViewCell.swift | 23 ++++ .../Cells/MainArticleCollectionViewCell.xib | 77 +++++++++++++ DutchNews/Info.plist | 2 - 18 files changed, 747 insertions(+), 22 deletions(-) create mode 100644 DutchNews/Classes/Extensions/String+HTML.swift create mode 100644 DutchNews/Classes/Extensions/UI/UIView+Nib.swift create mode 100644 DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift create mode 100644 DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.swift create mode 100644 DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.xib create mode 100644 DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.swift create mode 100644 DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.xib create mode 100644 DutchNews/Classes/Views/Cells/HalfWidthArticleCollectionViewCell.swift create mode 100644 DutchNews/Classes/Views/Cells/HalfWidthArticleCollectionViewCell.xib create mode 100644 DutchNews/Classes/Views/Cells/HeadlineBaseCollectionViewCell.swift create mode 100644 DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.swift create mode 100644 DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.xib diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index ae5d784..f72db2a 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -13,6 +13,19 @@ F8154D522517EBC700BFB42C /* HeadlinesViewController+MagazineLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8154D512517EBC700BFB42C /* HeadlinesViewController+MagazineLayout.swift */; }; F8154D552517EC0D00BFB42C /* HeadlineLayoutConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8154D542517EC0D00BFB42C /* HeadlineLayoutConfiguration.swift */; }; F8154D572517F03600BFB42C /* ArticleHeadlineLayoutConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8154D562517F03600BFB42C /* ArticleHeadlineLayoutConfiguration.swift */; }; + F8154D612518011500BFB42C /* MainArticleCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8154D5F2518011500BFB42C /* MainArticleCollectionViewCell.swift */; }; + F8154D622518011500BFB42C /* MainArticleCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F8154D602518011500BFB42C /* MainArticleCollectionViewCell.xib */; }; + F8154D652518012F00BFB42C /* ArticleRowCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8154D632518012F00BFB42C /* ArticleRowCollectionViewCell.swift */; }; + F8154D662518012F00BFB42C /* ArticleRowCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F8154D642518012F00BFB42C /* ArticleRowCollectionViewCell.xib */; }; + F8154D692518016800BFB42C /* HalfWidthArticleCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8154D672518016800BFB42C /* HalfWidthArticleCollectionViewCell.swift */; }; + F8154D6A2518016800BFB42C /* HalfWidthArticleCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F8154D682518016800BFB42C /* HalfWidthArticleCollectionViewCell.xib */; }; + F8154D6C25180A9000BFB42C /* HeadlineBaseCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8154D6B25180A9000BFB42C /* HeadlineBaseCollectionViewCell.swift */; }; + F8154D6F25180B0200BFB42C /* UIView+Nib.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8154D6E25180B0200BFB42C /* UIView+Nib.swift */; }; + F8154D7125180C3900BFB42C /* ArticleWebContainerCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8154D7025180C3900BFB42C /* ArticleWebContainerCollectionViewCell.swift */; }; + F8154D752518156000BFB42C /* HeadlinesViewController+DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8154D742518156000BFB42C /* HeadlinesViewController+DataSource.swift */; }; + F8154D7625181B1C00BFB42C /* ArticleWebContainerCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F8154D7225180F0E00BFB42C /* ArticleWebContainerCollectionViewCell.xib */; }; + F8154D7725181D4100BFB42C /* HeadlineSuccessResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = F888006A2517A8EE008DCC54 /* HeadlineSuccessResponse.json */; }; + F8154D792518207B00BFB42C /* String+HTML.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8154D782518207B00BFB42C /* String+HTML.swift */; }; F82C8EFD2516304D002B27B3 /* APIClientServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8EFC2516304D002B27B3 /* APIClientServiceTests.swift */; }; F82C8EFF25163073002B27B3 /* NetworkMocking.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8EFE25163073002B27B3 /* NetworkMocking.swift */; }; F82C8F0125163898002B27B3 /* NetworkMockingDataFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8F0025163898002B27B3 /* NetworkMockingDataFactory.swift */; }; @@ -72,6 +85,18 @@ F8154D512517EBC700BFB42C /* HeadlinesViewController+MagazineLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HeadlinesViewController+MagazineLayout.swift"; sourceTree = ""; }; F8154D542517EC0D00BFB42C /* HeadlineLayoutConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlineLayoutConfiguration.swift; sourceTree = ""; }; F8154D562517F03600BFB42C /* ArticleHeadlineLayoutConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleHeadlineLayoutConfiguration.swift; sourceTree = ""; }; + F8154D5F2518011500BFB42C /* MainArticleCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainArticleCollectionViewCell.swift; sourceTree = ""; }; + F8154D602518011500BFB42C /* MainArticleCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainArticleCollectionViewCell.xib; sourceTree = ""; }; + F8154D632518012F00BFB42C /* ArticleRowCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleRowCollectionViewCell.swift; sourceTree = ""; }; + F8154D642518012F00BFB42C /* ArticleRowCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ArticleRowCollectionViewCell.xib; sourceTree = ""; }; + F8154D672518016800BFB42C /* HalfWidthArticleCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HalfWidthArticleCollectionViewCell.swift; sourceTree = ""; }; + F8154D682518016800BFB42C /* HalfWidthArticleCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HalfWidthArticleCollectionViewCell.xib; sourceTree = ""; }; + F8154D6B25180A9000BFB42C /* HeadlineBaseCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlineBaseCollectionViewCell.swift; sourceTree = ""; }; + F8154D6E25180B0200BFB42C /* UIView+Nib.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Nib.swift"; sourceTree = ""; }; + F8154D7025180C3900BFB42C /* ArticleWebContainerCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleWebContainerCollectionViewCell.swift; sourceTree = ""; }; + F8154D7225180F0E00BFB42C /* ArticleWebContainerCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ArticleWebContainerCollectionViewCell.xib; sourceTree = ""; }; + F8154D742518156000BFB42C /* HeadlinesViewController+DataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HeadlinesViewController+DataSource.swift"; sourceTree = ""; }; + F8154D782518207B00BFB42C /* String+HTML.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+HTML.swift"; sourceTree = ""; }; F82C8EFC2516304D002B27B3 /* APIClientServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClientServiceTests.swift; sourceTree = ""; }; F82C8EFE25163073002B27B3 /* NetworkMocking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMocking.swift; sourceTree = ""; }; F82C8F0025163898002B27B3 /* NetworkMockingDataFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMockingDataFactory.swift; sourceTree = ""; }; @@ -148,6 +173,30 @@ path = HeadlineLayoutConfiguration; sourceTree = ""; }; + F8154D5A251800E400BFB42C /* Cells */ = { + isa = PBXGroup; + children = ( + F8154D6B25180A9000BFB42C /* HeadlineBaseCollectionViewCell.swift */, + F8154D5F2518011500BFB42C /* MainArticleCollectionViewCell.swift */, + F8154D602518011500BFB42C /* MainArticleCollectionViewCell.xib */, + F8154D632518012F00BFB42C /* ArticleRowCollectionViewCell.swift */, + F8154D642518012F00BFB42C /* ArticleRowCollectionViewCell.xib */, + F8154D672518016800BFB42C /* HalfWidthArticleCollectionViewCell.swift */, + F8154D682518016800BFB42C /* HalfWidthArticleCollectionViewCell.xib */, + F8154D7025180C3900BFB42C /* ArticleWebContainerCollectionViewCell.swift */, + F8154D7225180F0E00BFB42C /* ArticleWebContainerCollectionViewCell.xib */, + ); + path = Cells; + sourceTree = ""; + }; + F8154D6D25180AF100BFB42C /* UI */ = { + isa = PBXGroup; + children = ( + F8154D6E25180B0200BFB42C /* UIView+Nib.swift */, + ); + path = UI; + sourceTree = ""; + }; F82C8EFB2516051D002B27B3 /* NetworkTests */ = { isa = PBXGroup; children = ( @@ -306,6 +355,7 @@ F8F14C6A250D70BD00C24FF5 /* Views */ = { isa = PBXGroup; children = ( + F8154D5A251800E400BFB42C /* Cells */, ); path = Views; sourceTree = ""; @@ -321,6 +371,7 @@ isa = PBXGroup; children = ( F8154D4D2517D28700BFB42C /* HeadlinesViewController.swift */, + F8154D742518156000BFB42C /* HeadlinesViewController+DataSource.swift */, F8154D512517EBC700BFB42C /* HeadlinesViewController+MagazineLayout.swift */, ); path = ViewControllers; @@ -348,6 +399,8 @@ F8F14C6F250D710100C24FF5 /* Extensions */ = { isa = PBXGroup; children = ( + F8154D6D25180AF100BFB42C /* UI */, + F8154D782518207B00BFB42C /* String+HTML.swift */, ); path = Extensions; sourceTree = ""; @@ -472,8 +525,13 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + F8154D7625181B1C00BFB42C /* ArticleWebContainerCollectionViewCell.xib in Resources */, F8F14C74250D719800C24FF5 /* Main.storyboard in Resources */, + F8154D6A2518016800BFB42C /* HalfWidthArticleCollectionViewCell.xib in Resources */, F89B0220250D446200B41293 /* Assets.xcassets in Resources */, + F8154D622518011500BFB42C /* MainArticleCollectionViewCell.xib in Resources */, + F8154D662518012F00BFB42C /* ArticleRowCollectionViewCell.xib in Resources */, + F8154D7725181D4100BFB42C /* HeadlineSuccessResponse.json in Resources */, F89B0223250D446200B41293 /* LaunchScreen.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -577,20 +635,28 @@ buildActionMask = 2147483647; files = ( F85899882517894B00A6BA2A /* HeadlinesArticleRemoteRepository.swift in Sources */, + F8154D6F25180B0200BFB42C /* UIView+Nib.swift in Sources */, F858997B25176DC800A6BA2A /* Article.swift in Sources */, F865F7622516A6C1001FD067 /* APIServerResponseError.swift in Sources */, F865F7602516A643001FD067 /* APIServerResponseStatus.swift in Sources */, F8154D522517EBC700BFB42C /* HeadlinesViewController+MagazineLayout.swift in Sources */, F8DE79E32515904400A6C2D5 /* NetworkService.swift in Sources */, + F8154D752518156000BFB42C /* HeadlinesViewController+DataSource.swift in Sources */, + F8154D6C25180A9000BFB42C /* HeadlineBaseCollectionViewCell.swift in Sources */, F865F76B2516C08C001FD067 /* NetworkValidResponse.swift in Sources */, + F8154D612518011500BFB42C /* MainArticleCollectionViewCell.swift in Sources */, + F8154D7125180C3900BFB42C /* ArticleWebContainerCollectionViewCell.swift in Sources */, F865F75025168F05001FD067 /* AppConfig.swift in Sources */, F865F7642516A9D5001FD067 /* APIServerResponse.swift in Sources */, F8154D572517F03600BFB42C /* ArticleHeadlineLayoutConfiguration.swift in Sources */, F8154D4E2517D28700BFB42C /* HeadlinesViewController.swift in Sources */, + F8154D652518012F00BFB42C /* ArticleRowCollectionViewCell.swift in Sources */, + F8154D792518207B00BFB42C /* String+HTML.swift in Sources */, F8154D552517EC0D00BFB42C /* HeadlineLayoutConfiguration.swift in Sources */, F865F74C251686D2001FD067 /* Authenticator.swift in Sources */, F8589986251784B200A6BA2A /* ArticleRepository.swift in Sources */, F8DE79E5251594D700A6C2D5 /* APIClientService.swift in Sources */, + F8154D692518016800BFB42C /* HalfWidthArticleCollectionViewCell.swift in Sources */, F865F76D2516C826001FD067 /* DefaultAPIValidResponse.swift in Sources */, F858997D25176E8F00A6BA2A /* ArticleSource.swift in Sources */, F89B021E250D446000B41293 /* AppDelegate.swift in Sources */, diff --git a/DutchNews/Classes/Extensions/String+HTML.swift b/DutchNews/Classes/Extensions/String+HTML.swift new file mode 100644 index 0000000..8250f0f --- /dev/null +++ b/DutchNews/Classes/Extensions/String+HTML.swift @@ -0,0 +1,25 @@ +// +// String+HTML.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation + +extension String { + + func convertToAttributedFromHTML() -> NSAttributedString? { + + var attributedText: NSAttributedString? + let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue] + + if let data = data(using: .unicode, allowLossyConversion: true), let attrStr = try? NSAttributedString(data: data, options: options, documentAttributes: nil) { + attributedText = attrStr + } + + return attributedText + + } +} diff --git a/DutchNews/Classes/Extensions/UI/UIView+Nib.swift b/DutchNews/Classes/Extensions/UI/UIView+Nib.swift new file mode 100644 index 0000000..c0ce178 --- /dev/null +++ b/DutchNews/Classes/Extensions/UI/UIView+Nib.swift @@ -0,0 +1,67 @@ +// +// UIView+Nib.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import UIKit + +extension UIView { + + class func fromNib(nibNameOrNil: String? = nil) -> Self { + return fromNib(nibNameOrNil: nibNameOrNil, type: self) + } + + class func fromNib(nibNameOrNil: String? = nil, type: T.Type) -> T { + let view: T? = fromNib(nibNameOrNil: nibNameOrNil, type: T.self) + return view! + } + + class func fromNib(nibNameOrNil: String? = nil, type: T.Type) -> T? { + var view: T? + let name: String + if let nibName = nibNameOrNil { + name = nibName + } else { + // Most nibs are demangled by practice, if not, just declare string explicitly + name = String(describing: T.self) + } + + let nibViews = Bundle.main.loadNibNamed(name, owner: nil, options: nil) + + nibViews?.forEach({ (nibView) in + if let tog = nibView as? T { + view = tog + } + }) + + return view + } + +} + +extension UIView { + + class func nib(nibNameOrNil: String? = nil) -> UINib { + return nib(nibNameOrNil: nibNameOrNil, type: self) + } + + class func nib(nibNameOrNil: String? = nil, type: T.Type) -> UINib { + + let name: String + + if let nibName = nibNameOrNil { + name = nibName + } else { + // Most nibs are demangled by practice, if not, just declare string explicitly + name = String(describing: type) + } + + let bundle = Bundle(for: type) + + return UINib(nibName: name, bundle: bundle) + } +} diff --git a/DutchNews/Classes/Models/Article.swift b/DutchNews/Classes/Models/Article.swift index d00f284..c80f349 100644 --- a/DutchNews/Classes/Models/Article.swift +++ b/DutchNews/Classes/Models/Article.swift @@ -17,11 +17,11 @@ struct Article: Codable { let source: ArticleSource let url: URL - + let urlToImage: URL? - + let publishedAt: Date - + let content: String? var type: ArticleType = .news @@ -48,3 +48,17 @@ extension Article: Hashable { hasher.combine(type) } } + +extension Article { + + static func htmlArticle() -> Article { + + return .init(title: "", author: "", description: "", source: ArticleSource(id: "", name: ""), + url: URL(string:"https://domain.com")!, + urlToImage: nil, publishedAt: Date(), + content: """ +
\n \n \n \n
+ """,type: .mock) + } + +} diff --git a/DutchNews/Classes/Utilites/HeadlineLayoutConfiguration/ArticleHeadlineLayoutConfiguration.swift b/DutchNews/Classes/Utilites/HeadlineLayoutConfiguration/ArticleHeadlineLayoutConfiguration.swift index d17f029..5d2cfff 100644 --- a/DutchNews/Classes/Utilites/HeadlineLayoutConfiguration/ArticleHeadlineLayoutConfiguration.swift +++ b/DutchNews/Classes/Utilites/HeadlineLayoutConfiguration/ArticleHeadlineLayoutConfiguration.swift @@ -19,7 +19,7 @@ struct ArticleHeadlineLayoutConfiguration: HeadlineLayoutConfiguration { return sizeModeCreate(widthMode: .fullWidth(respectsHorizontalInsets: false), heightMode: .dynamic) case (_,1), (_,2): - return sizeModeCreate(widthMode: .halfWidth, heightMode: .dynamicAndStretchToTallestItemInRow) + return sizeModeCreate(widthMode: .halfWidth, heightMode: .dynamic) default: return sizeModeCreate() } @@ -33,11 +33,10 @@ struct ArticleHeadlineLayoutConfiguration: HeadlineLayoutConfiguration { 2.5 } - //////////////////////////////////////////////////////////////// - //MARK:- - //MARK:Private Methods - //MARK:- + // MARK: - + // MARK: Private Methods + // MARK: - //////////////////////////////////////////////////////////////// private func sizeModeCreate(widthMode: MagazineLayoutItemWidthMode = .fullWidth(respectsHorizontalInsets: false), diff --git a/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift b/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift new file mode 100644 index 0000000..565dea4 --- /dev/null +++ b/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift @@ -0,0 +1,104 @@ +// +// HeadlinesViewController+DataSource.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import UIKit +import RxCocoa +import RxSwift +import RxDataSources + +extension HeadlinesViewController { + + typealias SectionType = SectionModel + + func buildDataSource() -> RxCollectionViewSectionedReloadDataSource { + + return RxCollectionViewSectionedReloadDataSource(configureCell: {[weak self] (_, collectionView, indexPath, item) -> UICollectionViewCell in + + guard let `self` = self else { + return HeadlineBaseCollectionViewCell() + } + + let reuseId = self.reuseItentifier(forCellAt: indexPath).id + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseId, for: indexPath) + + self.fill(cell: cell, withArticle:item) + + cell.contentView.layer.borderColor = UIColor.lightGray.cgColor + cell.contentView.layer.borderWidth = 0.25 + cell.contentView.layer.cornerRadius = 5.0 + + return cell + }) + } + + /// <#Description#> + /// - Parameter index: <#index description#> + private func reuseItentifier(forCellAt index: IndexPath) -> HeadlinesCellIdentifier { + + switch (index.section, index.item) { + case (_,0): + return .main + case (_,3): + return .web + case (_,1), + (_,2): + return .halfWidth + default: + return .row + } + } + + private func fill(cell: UICollectionViewCell, withArticle article: Article) { + switch (cell) { + case (let cell as MainArticleCollectionViewCell): + cell.titleLabel.text = article.title + case (let cell as HalfWidthArticleCollectionViewCell): + cell.titleLabel.text = article.title + case (let cell as ArticleWebContainerCollectionViewCell): + + if cell.contentLabel.attributedText == nil, let attribute = article.content?.convertToAttributedFromHTML() { + cell.contentLabel.attributedText = attribute + } + case (let cell as ArticleRowCollectionViewCell): + + cell.titleLabel.text = article.title + cell.descriptionLabel.text = article.description + cell.sourceLabel.text = article.source.name + cell.dateLabel.text = article.publishedAt.description(with: Locale.current) + + default: + break + } + } + + func buildMockData() -> SectionType { + + let bundle = Bundle(for: type(of: self)) + guard let data = try? Data(contentsOf: bundle.url(forResource: "HeadlineSuccessResponse", withExtension: "json")!) else { + return SectionType(model: 0, items: []) + } + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + decoder.dataDecodingStrategy = .deferredToData + + guard let response = try? decoder.decode(APIServerResponse<[Article]>.self, from: data), + var items = response.data else { + return SectionType(model: 0, items: []) + } + + if items.count > 3 { + items.insert(Article.htmlArticle(), at: 3) + } + + return SectionType(model: 0, items: items) + + } + +} diff --git a/DutchNews/Classes/ViewControllers/HeadlinesViewController+MagazineLayout.swift b/DutchNews/Classes/ViewControllers/HeadlinesViewController+MagazineLayout.swift index fe394cb..4a9ce9a 100644 --- a/DutchNews/Classes/ViewControllers/HeadlinesViewController+MagazineLayout.swift +++ b/DutchNews/Classes/ViewControllers/HeadlinesViewController+MagazineLayout.swift @@ -10,7 +10,7 @@ import Foundation import UIKit import MagazineLayout -extension HeadlinesViewController: UICollectionViewDelegateMagazineLayout { +extension HeadlinesViewController: UICollectionViewDelegateMagazineLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, diff --git a/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift b/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift index f4aedeb..a4ed2c6 100644 --- a/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift +++ b/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift @@ -10,16 +10,28 @@ import UIKit import MagazineLayout import RxCocoa import RxSwift +import RxDataSources class HeadlinesViewController: UIViewController { + enum HeadlinesCellIdentifier: String { + case main + case halfWidth + case web + case row + + var id: String { + return self.rawValue + } + } + @IBOutlet weak var collectionView: UICollectionView! var collectionLayout: MagazineLayout? { return collectionView?.collectionViewLayout as? MagazineLayout } - var layoutConfiguration : HeadlineLayoutConfiguration = ArticleHeadlineLayoutConfiguration() { + var layoutConfiguration: HeadlineLayoutConfiguration = ArticleHeadlineLayoutConfiguration() { didSet { collectionView.reloadData() } @@ -27,25 +39,33 @@ class HeadlinesViewController: UIViewController { let disposeBag = DisposeBag() + lazy var dataSource: RxCollectionViewSectionedReloadDataSource = { + return self.buildDataSource() + }() + override func viewDidLoad() { super.viewDidLoad() - collectionView.register(MagazineLayoutCollectionViewCell.self, forCellWithReuseIdentifier: "MyCustomCellReuseIdentifier") - // Do any additional setup after loading the view. - Observable.from(optional: Array(repeating: 20, count: 10)) - .bind(to: collectionView.rx.items(cellIdentifier: "MyCustomCellReuseIdentifier", cellType: MagazineLayoutCollectionViewCell.self)) { model, index, cell in - cell.contentView.layer.borderColor = UIColor.lightGray.cgColor - cell.contentView.layer.borderWidth = 0.25 - cell.contentView.layer.cornerRadius = 5.0 - - }.disposed(by: disposeBag) - + setupLayouts() collectionView.delegate = self } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + let block : () -> () = {[unowned self] in + let items = self.buildMockData() + Observable.from(optional: [items]) + .bind(to:self.collectionView.rx.items(dataSource: self.dataSource)) + .disposed(by: self.disposeBag) + } + + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2), execute: block) + + } /* // MARK: - Navigation @@ -57,6 +77,25 @@ class HeadlinesViewController: UIViewController { } */ + func setupLayouts() { + setupColletionView() + } + + func setupColletionView() { + + collectionView.register(MainArticleCollectionViewCell.nib(), + forCellWithReuseIdentifier: HeadlinesCellIdentifier.main.id) + collectionView.register(ArticleRowCollectionViewCell.nib(), + forCellWithReuseIdentifier: HeadlinesCellIdentifier.row.id) + collectionView.register(HalfWidthArticleCollectionViewCell.nib(), + forCellWithReuseIdentifier: HeadlinesCellIdentifier.halfWidth.id) + collectionView.register(ArticleWebContainerCollectionViewCell.nib(), + forCellWithReuseIdentifier: HeadlinesCellIdentifier.web.id) + + collectionView.delegate = self + + } + } extension HeadlinesViewController: UICollectionViewDelegate { diff --git a/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.swift b/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.swift new file mode 100644 index 0000000..39a8d61 --- /dev/null +++ b/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.swift @@ -0,0 +1,36 @@ +// +// ArticleRowCollectionViewCell.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import UIKit + +class ArticleRowCollectionViewCell: HeadlineBaseCollectionViewCell { + + @IBOutlet weak var cellContentView: UIView! + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var descriptionLabel: UILabel! + @IBOutlet weak var sourceLabel: UILabel! + @IBOutlet weak var dateLabel: UILabel! + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override func prepareForReuse() { + super.prepareForReuse() + + titleLabel.text = nil + descriptionLabel.text = nil + sourceLabel.text = nil + dateLabel.text = nil + imageView.image = nil + + } + +} diff --git a/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.xib b/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.xib new file mode 100644 index 0000000..b7706fd --- /dev/null +++ b/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.xib @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.swift b/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.swift new file mode 100644 index 0000000..9170b43 --- /dev/null +++ b/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.swift @@ -0,0 +1,24 @@ +// +// ArticleWebContainerCollectionViewCell.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import UIKit + +class ArticleWebContainerCollectionViewCell: HeadlineBaseCollectionViewCell { + + @IBOutlet var contentLabel: UILabel! + + override func awakeFromNib() { + super.awakeFromNib() + contentLabel.text = nil + } + + override func prepareForReuse() { + super.prepareForReuse() + } + +} diff --git a/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.xib b/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.xib new file mode 100644 index 0000000..90c79ad --- /dev/null +++ b/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.xib @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DutchNews/Classes/Views/Cells/HalfWidthArticleCollectionViewCell.swift b/DutchNews/Classes/Views/Cells/HalfWidthArticleCollectionViewCell.swift new file mode 100644 index 0000000..3a76574 --- /dev/null +++ b/DutchNews/Classes/Views/Cells/HalfWidthArticleCollectionViewCell.swift @@ -0,0 +1,28 @@ +// +// HalfWidthArticleCollectionViewCell.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import UIKit + +class HalfWidthArticleCollectionViewCell: HeadlineBaseCollectionViewCell { + + @IBOutlet weak var cellContentView: UIView! + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var titleLabel: UILabel! + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override func prepareForReuse() { + super.prepareForReuse() + titleLabel.text = nil + imageView.image = nil + } + +} diff --git a/DutchNews/Classes/Views/Cells/HalfWidthArticleCollectionViewCell.xib b/DutchNews/Classes/Views/Cells/HalfWidthArticleCollectionViewCell.xib new file mode 100644 index 0000000..6a231e3 --- /dev/null +++ b/DutchNews/Classes/Views/Cells/HalfWidthArticleCollectionViewCell.xib @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DutchNews/Classes/Views/Cells/HeadlineBaseCollectionViewCell.swift b/DutchNews/Classes/Views/Cells/HeadlineBaseCollectionViewCell.swift new file mode 100644 index 0000000..bbf3cc8 --- /dev/null +++ b/DutchNews/Classes/Views/Cells/HeadlineBaseCollectionViewCell.swift @@ -0,0 +1,14 @@ +// +// HeadlineBaseCollectionViewCell.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import MagazineLayout + +class HeadlineBaseCollectionViewCell: MagazineLayoutCollectionViewCell { + +} diff --git a/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.swift b/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.swift new file mode 100644 index 0000000..6ea3a93 --- /dev/null +++ b/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.swift @@ -0,0 +1,23 @@ +// +// MainArticleCollectionViewCell.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import UIKit + +class MainArticleCollectionViewCell: HeadlineBaseCollectionViewCell { + + @IBOutlet weak var cellContentView: UIView! + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var titleLabel: UILabel! + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + + } + +} diff --git a/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.xib b/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.xib new file mode 100644 index 0000000..7c655f4 --- /dev/null +++ b/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.xib @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DutchNews/Info.plist b/DutchNews/Info.plist index af8868b..952074c 100644 --- a/DutchNews/Info.plist +++ b/DutchNews/Info.plist @@ -31,8 +31,6 @@ UISupportedInterfaceOrientations UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad From f02724e42fb91ec8d66b1ec9ce061119f687b595 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Mon, 21 Sep 2020 07:03:12 +0330 Subject: [PATCH 070/108] - Fixed Auto layout Crash. - Fixed some bug and adjusted Cell Layouts. --- .../ArticleHeadlineLayoutConfiguration.swift | 10 +- .../HeadlinesViewController+DataSource.swift | 19 ++- .../HeadlinesViewController.swift | 11 +- .../Cells/ArticleRowCollectionViewCell.xib | 108 ++++++++++-------- .../ArticleWebContainerCollectionViewCell.xib | 20 ++-- .../HalfWidthArticleCollectionViewCell.swift | 2 + .../HalfWidthArticleCollectionViewCell.xib | 72 ++++++------ .../Cells/MainArticleCollectionViewCell.swift | 5 + .../Cells/MainArticleCollectionViewCell.xib | 74 +++++++----- 9 files changed, 193 insertions(+), 128 deletions(-) diff --git a/DutchNews/Classes/Utilites/HeadlineLayoutConfiguration/ArticleHeadlineLayoutConfiguration.swift b/DutchNews/Classes/Utilites/HeadlineLayoutConfiguration/ArticleHeadlineLayoutConfiguration.swift index 5d2cfff..93eb674 100644 --- a/DutchNews/Classes/Utilites/HeadlineLayoutConfiguration/ArticleHeadlineLayoutConfiguration.swift +++ b/DutchNews/Classes/Utilites/HeadlineLayoutConfiguration/ArticleHeadlineLayoutConfiguration.swift @@ -16,21 +16,21 @@ struct ArticleHeadlineLayoutConfiguration: HeadlineLayoutConfiguration { switch (indexPath.section, indexPath.row) { case (_,0), (_,3): - return sizeModeCreate(widthMode: .fullWidth(respectsHorizontalInsets: false), heightMode: .dynamic) + return sizeModeCreate(widthMode: .fullWidth(respectsHorizontalInsets: true), heightMode: .dynamic) case (_,1), (_,2): - return sizeModeCreate(widthMode: .halfWidth, heightMode: .dynamic) + return sizeModeCreate(widthMode: .halfWidth, heightMode: .dynamicAndStretchToTallestItemInRow) default: return sizeModeCreate() } } func verticalSpacing(forElementsInSectionAtIndex section: Int) -> CGFloat { - 2.5 + 5.0 } func horizontalSpacing(forElementsInSectionAtIndex section: Int) -> CGFloat { - 2.5 + 5.0 } //////////////////////////////////////////////////////////////// @@ -39,7 +39,7 @@ struct ArticleHeadlineLayoutConfiguration: HeadlineLayoutConfiguration { // MARK: - //////////////////////////////////////////////////////////////// - private func sizeModeCreate(widthMode: MagazineLayoutItemWidthMode = .fullWidth(respectsHorizontalInsets: false), + private func sizeModeCreate(widthMode: MagazineLayoutItemWidthMode = .fullWidth(respectsHorizontalInsets: true), heightMode: MagazineLayoutItemHeightMode = .dynamic) -> MagazineLayoutItemSizeMode { return MagazineLayoutItemSizeMode(widthMode: widthMode,heightMode: heightMode) } diff --git a/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift b/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift index 565dea4..552ee89 100644 --- a/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift +++ b/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift @@ -18,16 +18,18 @@ extension HeadlinesViewController { func buildDataSource() -> RxCollectionViewSectionedReloadDataSource { - return RxCollectionViewSectionedReloadDataSource(configureCell: {[weak self] (_, collectionView, indexPath, item) -> UICollectionViewCell in + return RxCollectionViewSectionedReloadDataSource(configureCell: {[weak self] (dataSource, collectionView, indexPath, item) -> UICollectionViewCell in guard let `self` = self else { return HeadlineBaseCollectionViewCell() } - let reuseId = self.reuseItentifier(forCellAt: indexPath).id + let item2 = dataSource[indexPath] + + let reuseId = self.reuseItentifier(forCellAt: indexPath, item: item2).id let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseId, for: indexPath) - self.fill(cell: cell, withArticle:item) + self.fill(cell: cell, withArticle: item2) cell.contentView.layer.borderColor = UIColor.lightGray.cgColor cell.contentView.layer.borderWidth = 0.25 @@ -39,12 +41,12 @@ extension HeadlinesViewController { /// <#Description#> /// - Parameter index: <#index description#> - private func reuseItentifier(forCellAt index: IndexPath) -> HeadlinesCellIdentifier { + private func reuseItentifier(forCellAt index: IndexPath, item: Article) -> HeadlinesCellIdentifier { switch (index.section, index.item) { case (_,0): return .main - case (_,3): + case (_,3) where item.type == .mock : return .web case (_,1), (_,2): @@ -55,18 +57,23 @@ extension HeadlinesViewController { } private func fill(cell: UICollectionViewCell, withArticle article: Article) { + switch (cell) { case (let cell as MainArticleCollectionViewCell): cell.titleLabel.text = article.title case (let cell as HalfWidthArticleCollectionViewCell): cell.titleLabel.text = article.title - case (let cell as ArticleWebContainerCollectionViewCell): + cell.sourceLabel.text = article.source.name + + case (let cell as ArticleWebContainerCollectionViewCell) where article.type == .mock : if cell.contentLabel.attributedText == nil, let attribute = article.content?.convertToAttributedFromHTML() { cell.contentLabel.attributedText = attribute } + case (let cell as ArticleRowCollectionViewCell): + print(article) cell.titleLabel.text = article.title cell.descriptionLabel.text = article.description cell.sourceLabel.text = article.source.name diff --git a/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift b/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift index a4ed2c6..7f87589 100644 --- a/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift +++ b/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift @@ -31,6 +31,12 @@ class HeadlinesViewController: UIViewController { return collectionView?.collectionViewLayout as? MagazineLayout } + lazy var refreshControl: UIRefreshControl = { + let refreshControl = UIRefreshControl() + refreshControl.tintColor = .black + return refreshControl + }() + var layoutConfiguration: HeadlineLayoutConfiguration = ArticleHeadlineLayoutConfiguration() { didSet { collectionView.reloadData() @@ -49,7 +55,6 @@ class HeadlinesViewController: UIViewController { // Do any additional setup after loading the view. setupLayouts() - collectionView.delegate = self } @@ -61,9 +66,10 @@ class HeadlinesViewController: UIViewController { Observable.from(optional: [items]) .bind(to:self.collectionView.rx.items(dataSource: self.dataSource)) .disposed(by: self.disposeBag) + self.collectionView.delegate = self } - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2), execute: block) + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: block) } @@ -93,6 +99,7 @@ class HeadlinesViewController: UIViewController { forCellWithReuseIdentifier: HeadlinesCellIdentifier.web.id) collectionView.delegate = self + collectionView.refreshControl = refreshControl } diff --git a/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.xib b/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.xib index b7706fd..ce0218e 100644 --- a/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.xib +++ b/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.xib @@ -9,81 +9,95 @@ - - - + + - + - - + + - + - - + + - - - + + - + + + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + - - - - + + + + - - + + + + + + + + + +
diff --git a/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.xib b/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.xib index 90c79ad..8bc1037 100644 --- a/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.xib +++ b/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.xib @@ -2,24 +2,27 @@ + - - - + + - + - + - diff --git a/DutchNews/Classes/Views/Cells/HalfWidthArticleCollectionViewCell.swift b/DutchNews/Classes/Views/Cells/HalfWidthArticleCollectionViewCell.swift index 3a76574..38c5e12 100644 --- a/DutchNews/Classes/Views/Cells/HalfWidthArticleCollectionViewCell.swift +++ b/DutchNews/Classes/Views/Cells/HalfWidthArticleCollectionViewCell.swift @@ -13,6 +13,7 @@ class HalfWidthArticleCollectionViewCell: HeadlineBaseCollectionViewCell { @IBOutlet weak var cellContentView: UIView! @IBOutlet weak var imageView: UIImageView! @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var sourceLabel: UILabel! override func awakeFromNib() { super.awakeFromNib() @@ -23,6 +24,7 @@ class HalfWidthArticleCollectionViewCell: HeadlineBaseCollectionViewCell { super.prepareForReuse() titleLabel.text = nil imageView.image = nil + sourceLabel.text = nil } } diff --git a/DutchNews/Classes/Views/Cells/HalfWidthArticleCollectionViewCell.xib b/DutchNews/Classes/Views/Cells/HalfWidthArticleCollectionViewCell.xib index 6a231e3..5379b7a 100644 --- a/DutchNews/Classes/Views/Cells/HalfWidthArticleCollectionViewCell.xib +++ b/DutchNews/Classes/Views/Cells/HalfWidthArticleCollectionViewCell.xib @@ -4,65 +4,69 @@ + - - - + + - + - - + + - - + + - - + + - - - - - - + + - - - - - - - - - + + + + + + + + + + - - - - + + + + + + diff --git a/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.swift b/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.swift index 6ea3a93..33a1052 100644 --- a/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.swift +++ b/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.swift @@ -13,11 +13,16 @@ class MainArticleCollectionViewCell: HeadlineBaseCollectionViewCell { @IBOutlet weak var cellContentView: UIView! @IBOutlet weak var imageView: UIImageView! @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var sourceLabel: UILabel! override func awakeFromNib() { super.awakeFromNib() // Initialization code } + + override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { + super.preferredLayoutAttributesFitting(layoutAttributes) + } } diff --git a/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.xib b/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.xib index 7c655f4..0ff6880 100644 --- a/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.xib +++ b/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.xib @@ -4,6 +4,7 @@ + @@ -16,52 +17,72 @@ - + - - - - - + - + + + + + + + + + + + + + - - - - + + + + - - - - - - - - - + + + + + + + + + + + - + @@ -69,6 +90,7 @@ + From 2bffdaf8be439298887524f224919b3ff60a69aa Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Mon, 21 Sep 2020 07:06:35 +0330 Subject: [PATCH 071/108] Update unit-test.yml --- .github/workflows/unit-test.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 8459bcc..8e140a1 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -10,7 +10,12 @@ on: types: [test] pull_request: branches: - ['*'] + - develop + - feature/* + - feature/*/* + - bugfix/* + - hotfix/* + - master # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: From 945f86522b318b03195632af07ff119c835557da Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Mon, 21 Sep 2020 09:41:49 +0330 Subject: [PATCH 072/108] -Fixed The web embeded in WebRowcell. --- DutchNews/Classes/Models/Article.swift | 2 +- .../HeadlinesViewController+DataSource.swift | 13 ++++++++++-- .../HeadlinesViewController.swift | 19 +++++------------- ...rticleWebContainerCollectionViewCell.swift | 20 ++++++++++++++++++- 4 files changed, 36 insertions(+), 18 deletions(-) diff --git a/DutchNews/Classes/Models/Article.swift b/DutchNews/Classes/Models/Article.swift index c80f349..322f858 100644 --- a/DutchNews/Classes/Models/Article.swift +++ b/DutchNews/Classes/Models/Article.swift @@ -54,7 +54,7 @@ extension Article { static func htmlArticle() -> Article { return .init(title: "", author: "", description: "", source: ArticleSource(id: "", name: ""), - url: URL(string:"https://domain.com")!, + url: URL(string: "https://domain.com")!, urlToImage: nil, publishedAt: Date(), content: """
\n \n \n \n
diff --git a/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift b/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift index 552ee89..9f294c4 100644 --- a/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift +++ b/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift @@ -67,8 +67,17 @@ extension HeadlinesViewController { case (let cell as ArticleWebContainerCollectionViewCell) where article.type == .mock : - if cell.contentLabel.attributedText == nil, let attribute = article.content?.convertToAttributedFromHTML() { - cell.contentLabel.attributedText = attribute + if cell.contentLabel.text == nil, + let content = article.content { + + cell.webView.loadHTMLString(content, baseURL: nil) + cell.contentLabel.text = content + + //FIXME: determind a observation for callback purpose when loading was finished. + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) {[weak self] in + self?.collectionView.reloadData() + self?.collectionLayout?.invalidateLayout() + } } case (let cell as ArticleRowCollectionViewCell): diff --git a/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift b/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift index 7f87589..2966f54 100644 --- a/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift +++ b/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift @@ -56,20 +56,11 @@ class HeadlinesViewController: UIViewController { setupLayouts() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - let block : () -> () = {[unowned self] in - let items = self.buildMockData() - Observable.from(optional: [items]) - .bind(to:self.collectionView.rx.items(dataSource: self.dataSource)) - .disposed(by: self.disposeBag) - self.collectionView.delegate = self - } - - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: block) + let items = self.buildMockData() + Observable.from(optional: [items]) + .bind(to: self.collectionView.rx.items(dataSource: self.dataSource)) + .disposed(by: self.disposeBag) + self.collectionView.delegate = self } diff --git a/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.swift b/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.swift index 9170b43..70d50e6 100644 --- a/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.swift +++ b/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.swift @@ -7,18 +7,36 @@ // import UIKit +import WebKit +import PureLayout class ArticleWebContainerCollectionViewCell: HeadlineBaseCollectionViewCell { @IBOutlet var contentLabel: UILabel! + lazy var webView = WKWebView() + override func awakeFromNib() { super.awakeFromNib() - contentLabel.text = nil + contentLabel.text = nil + contentView.addSubview(webView) + webView.autoPinEdgesToSuperviewSafeArea() + webView.scrollView.isScrollEnabled = false + contentLabel.isHidden = true } override func prepareForReuse() { super.prepareForReuse() } + + override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { + + let layoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes) + + layoutAttributes.size.height = max(webView.scrollView.contentSize.height, 60.0) + + return layoutAttributes + } + } From cf04fa1e0847eea327d0b204925312a206976849 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Mon, 21 Sep 2020 10:05:34 +0330 Subject: [PATCH 073/108] - done some enhancement. --- .../HeadlinesViewController+DataSource.swift | 6 +++--- .../Classes/Views/Cells/ArticleRowCollectionViewCell.swift | 2 ++ .../Views/Cells/HalfWidthArticleCollectionViewCell.swift | 3 +++ .../Classes/Views/Cells/MainArticleCollectionViewCell.swift | 1 + 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift b/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift index 9f294c4..d93696a 100644 --- a/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift +++ b/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift @@ -31,9 +31,9 @@ extension HeadlinesViewController { self.fill(cell: cell, withArticle: item2) - cell.contentView.layer.borderColor = UIColor.lightGray.cgColor - cell.contentView.layer.borderWidth = 0.25 - cell.contentView.layer.cornerRadius = 5.0 + cell.contentView.layer.borderColor = UIColor.darkGray.cgColor + cell.contentView.layer.borderWidth = 0.4 + cell.contentView.layer.cornerRadius = 8.0 return cell }) diff --git a/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.swift b/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.swift index 39a8d61..48d97ec 100644 --- a/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.swift +++ b/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.swift @@ -19,6 +19,8 @@ class ArticleRowCollectionViewCell: HeadlineBaseCollectionViewCell { override func awakeFromNib() { super.awakeFromNib() + imageView.clipsToBounds = true + imageView.layer.cornerRadius = 5.0 // Initialization code } diff --git a/DutchNews/Classes/Views/Cells/HalfWidthArticleCollectionViewCell.swift b/DutchNews/Classes/Views/Cells/HalfWidthArticleCollectionViewCell.swift index 38c5e12..750ccf3 100644 --- a/DutchNews/Classes/Views/Cells/HalfWidthArticleCollectionViewCell.swift +++ b/DutchNews/Classes/Views/Cells/HalfWidthArticleCollectionViewCell.swift @@ -17,6 +17,9 @@ class HalfWidthArticleCollectionViewCell: HeadlineBaseCollectionViewCell { override func awakeFromNib() { super.awakeFromNib() + imageView.clipsToBounds = true + imageView.layer.cornerRadius = 5.0 + // Initialization code } diff --git a/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.swift b/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.swift index 33a1052..d71e058 100644 --- a/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.swift +++ b/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.swift @@ -18,6 +18,7 @@ class MainArticleCollectionViewCell: HeadlineBaseCollectionViewCell { override func awakeFromNib() { super.awakeFromNib() // Initialization code + contentView.clipsToBounds = true } From 903bbf30a8ad84d011af101cea51046a15277bcb Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Mon, 21 Sep 2020 10:12:11 +0330 Subject: [PATCH 074/108] - defined `AlertableView` Abstract. - implemented `AlertableView` for UIViewController. --- DutchNews.xcodeproj/project.pbxproj | 16 ++++++++ .../UI/UIViewController+AlertableView.swift | 26 ++++++++++++ .../Views/AlertView/AlertableView.swift | 40 +++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 DutchNews/Classes/Extensions/UI/UIViewController+AlertableView.swift create mode 100644 DutchNews/Classes/Views/AlertView/AlertableView.swift diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index f72db2a..9dc9d27 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -56,6 +56,8 @@ F89B022E250D446200B41293 /* DutchNewsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89B022D250D446200B41293 /* DutchNewsTests.swift */; }; F8DE79E32515904400A6C2D5 /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DE79E22515904400A6C2D5 /* NetworkService.swift */; }; F8DE79E5251594D700A6C2D5 /* APIClientService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DE79E4251594D700A6C2D5 /* APIClientService.swift */; }; + F8E5C0E9251881C80083D2B1 /* AlertableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0E8251881C80083D2B1 /* AlertableView.swift */; }; + F8E5C0EB251882560083D2B1 /* UIViewController+AlertableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0EA251882560083D2B1 /* UIViewController+AlertableView.swift */; }; F8F14C74250D719800C24FF5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F8F14C72250D719800C24FF5 /* Main.storyboard */; }; /* End PBXBuildFile section */ @@ -131,6 +133,8 @@ F89B022F250D446200B41293 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F8DE79E22515904400A6C2D5 /* NetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = ""; }; F8DE79E4251594D700A6C2D5 /* APIClientService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClientService.swift; sourceTree = ""; }; + F8E5C0E8251881C80083D2B1 /* AlertableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertableView.swift; sourceTree = ""; }; + F8E5C0EA251882560083D2B1 /* UIViewController+AlertableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+AlertableView.swift"; sourceTree = ""; }; F8F14C73250D719800C24FF5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; /* End PBXFileReference section */ @@ -193,6 +197,7 @@ isa = PBXGroup; children = ( F8154D6E25180B0200BFB42C /* UIView+Nib.swift */, + F8E5C0EA251882560083D2B1 /* UIViewController+AlertableView.swift */, ); path = UI; sourceTree = ""; @@ -328,6 +333,14 @@ path = DutchNewsTests; sourceTree = ""; }; + F8E5C0E7251881400083D2B1 /* AlertView */ = { + isa = PBXGroup; + children = ( + F8E5C0E8251881C80083D2B1 /* AlertableView.swift */, + ); + path = AlertView; + sourceTree = ""; + }; F8F14C68250D70AA00C24FF5 /* Classes */ = { isa = PBXGroup; children = ( @@ -355,6 +368,7 @@ F8F14C6A250D70BD00C24FF5 /* Views */ = { isa = PBXGroup; children = ( + F8E5C0E7251881400083D2B1 /* AlertView */, F8154D5A251800E400BFB42C /* Cells */, ); path = Views; @@ -652,7 +666,9 @@ F8154D4E2517D28700BFB42C /* HeadlinesViewController.swift in Sources */, F8154D652518012F00BFB42C /* ArticleRowCollectionViewCell.swift in Sources */, F8154D792518207B00BFB42C /* String+HTML.swift in Sources */, + F8E5C0EB251882560083D2B1 /* UIViewController+AlertableView.swift in Sources */, F8154D552517EC0D00BFB42C /* HeadlineLayoutConfiguration.swift in Sources */, + F8E5C0E9251881C80083D2B1 /* AlertableView.swift in Sources */, F865F74C251686D2001FD067 /* Authenticator.swift in Sources */, F8589986251784B200A6BA2A /* ArticleRepository.swift in Sources */, F8DE79E5251594D700A6C2D5 /* APIClientService.swift in Sources */, diff --git a/DutchNews/Classes/Extensions/UI/UIViewController+AlertableView.swift b/DutchNews/Classes/Extensions/UI/UIViewController+AlertableView.swift new file mode 100644 index 0000000..3953b78 --- /dev/null +++ b/DutchNews/Classes/Extensions/UI/UIViewController+AlertableView.swift @@ -0,0 +1,26 @@ +// +// UIViewController+AlertableView.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import UIKit + +// MARK: - UIViewController extension for AlertableView Abstract. +extension AlertableView where Self: UIViewController { + + /// <#Description#> + /// + /// - Parameters: + /// - message: <#message description#> + /// - actionTitle: <#actionTitle description#> + /// - actionHandler: <#actionHandler description#> + func presentAlertView(withMessage message: String, actionTitle: String? = nil, actionHandler: @escaping () -> () = {}) { + presentAlert(message: message, actionTitle: actionTitle, actionHandler: actionHandler) + } + +} + diff --git a/DutchNews/Classes/Views/AlertView/AlertableView.swift b/DutchNews/Classes/Views/AlertView/AlertableView.swift new file mode 100644 index 0000000..e9037f1 --- /dev/null +++ b/DutchNews/Classes/Views/AlertView/AlertableView.swift @@ -0,0 +1,40 @@ +// +// AlertableView.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import MaterialComponents +import UIKit + +/// Abstract `AlertableView` +protocol AlertableView: class { + + /// <#Description#> + /// + /// - Parameters: + /// - message: <#message description#> + /// - actionTitle: <#actionTitle description#> + /// - actionHandler: <#actionHandler description#> + func presentAlert(message: String, actionTitle: String?, actionHandler:@escaping () -> ()) +} + +// Abstract `AlertableView` impelementation +extension AlertableView { + + func presentAlert(message: String, actionTitle: String?, actionHandler:@escaping () -> ()) { + let alert = MDCSnackbarMessage(text: message) + + // if actionTitle has a value, create snackbar action and assign to alert + if let actionTitle = actionTitle { + alert.action = MDCSnackbarMessageAction() + alert.action?.title = actionTitle + alert.action?.handler = actionHandler + } + + MDCSnackbarManager.show(alert) + } +} From 3a6837a78850cb0f742b29ea28daa291b394b834 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Mon, 21 Sep 2020 21:10:57 +0330 Subject: [PATCH 075/108] - Added Headlines feature ViewModels. --- DutchNews.xcodeproj/project.pbxproj | 69 ++++++++++- .../Domains/HeadlinesFetchingUseCase.swift | 24 ++++ .../Domains/HeadlinesSearchingUseCases.swift | 24 ++++ .../Classes/Domains/HeadlinesUseCases.swift | 30 +++++ .../Classes/ViewModels/ArticleViewModel.swift | 58 +++++++++ .../ViewModels/ArticlesViewModel.swift | 38 ++++++ .../ViewModels/HeadlineCellViewModel.swift | 69 +++++++++++ .../ViewModels/HeadlineSearchViewModel.swift | 9 ++ .../ViewModels/HeadlinesViewModel.swift | 116 ++++++++++++++++++ .../Classes/ViewModels/ViewModelState.swift | 40 ++++++ .../Views/AlertView/AlertableView.swift | 4 +- ...rticleWebContainerCollectionViewCell.swift | 3 +- 12 files changed, 477 insertions(+), 7 deletions(-) create mode 100644 DutchNews/Classes/Domains/HeadlinesFetchingUseCase.swift create mode 100644 DutchNews/Classes/Domains/HeadlinesSearchingUseCases.swift create mode 100644 DutchNews/Classes/Domains/HeadlinesUseCases.swift create mode 100644 DutchNews/Classes/ViewModels/ArticleViewModel.swift create mode 100644 DutchNews/Classes/ViewModels/ArticlesViewModel.swift create mode 100644 DutchNews/Classes/ViewModels/HeadlineCellViewModel.swift create mode 100644 DutchNews/Classes/ViewModels/HeadlineSearchViewModel.swift create mode 100644 DutchNews/Classes/ViewModels/HeadlinesViewModel.swift create mode 100644 DutchNews/Classes/ViewModels/ViewModelState.swift diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index 9dc9d27..a465e5e 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -58,6 +58,15 @@ F8DE79E5251594D700A6C2D5 /* APIClientService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DE79E4251594D700A6C2D5 /* APIClientService.swift */; }; F8E5C0E9251881C80083D2B1 /* AlertableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0E8251881C80083D2B1 /* AlertableView.swift */; }; F8E5C0EB251882560083D2B1 /* UIViewController+AlertableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0EA251882560083D2B1 /* UIViewController+AlertableView.swift */; }; + F8E5C0ED2518833B0083D2B1 /* ArticlesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0EC2518833B0083D2B1 /* ArticlesViewModel.swift */; }; + F8E5C0EF2518848D0083D2B1 /* HeadlinesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0EE2518848D0083D2B1 /* HeadlinesViewModel.swift */; }; + F8E5C0F1251884A20083D2B1 /* HeadlineSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0F0251884A20083D2B1 /* HeadlineSearchViewModel.swift */; }; + F8E5C0F32518D8AC0083D2B1 /* ArticleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0F22518D8AC0083D2B1 /* ArticleViewModel.swift */; }; + F8E5C0F52518DD3E0083D2B1 /* ViewModelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0F42518DD3E0083D2B1 /* ViewModelState.swift */; }; + F8E5C0F82518E7770083D2B1 /* HeadlinesUseCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0F72518E7770083D2B1 /* HeadlinesUseCases.swift */; }; + F8E5C0FA2518E8100083D2B1 /* HeadlinesFetchingUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0F92518E8100083D2B1 /* HeadlinesFetchingUseCase.swift */; }; + F8E5C0FC2518E9150083D2B1 /* HeadlinesSearchingUseCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0FB2518E9150083D2B1 /* HeadlinesSearchingUseCases.swift */; }; + F8E5C0FE25190CDD0083D2B1 /* HeadlineCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0FD25190CDD0083D2B1 /* HeadlineCellViewModel.swift */; }; F8F14C74250D719800C24FF5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F8F14C72250D719800C24FF5 /* Main.storyboard */; }; /* End PBXBuildFile section */ @@ -135,6 +144,15 @@ F8DE79E4251594D700A6C2D5 /* APIClientService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClientService.swift; sourceTree = ""; }; F8E5C0E8251881C80083D2B1 /* AlertableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertableView.swift; sourceTree = ""; }; F8E5C0EA251882560083D2B1 /* UIViewController+AlertableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+AlertableView.swift"; sourceTree = ""; }; + F8E5C0EC2518833B0083D2B1 /* ArticlesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticlesViewModel.swift; sourceTree = ""; }; + F8E5C0EE2518848D0083D2B1 /* HeadlinesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlinesViewModel.swift; sourceTree = ""; }; + F8E5C0F0251884A20083D2B1 /* HeadlineSearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlineSearchViewModel.swift; sourceTree = ""; }; + F8E5C0F22518D8AC0083D2B1 /* ArticleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleViewModel.swift; sourceTree = ""; }; + F8E5C0F42518DD3E0083D2B1 /* ViewModelState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelState.swift; sourceTree = ""; }; + F8E5C0F72518E7770083D2B1 /* HeadlinesUseCases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlinesUseCases.swift; sourceTree = ""; }; + F8E5C0F92518E8100083D2B1 /* HeadlinesFetchingUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlinesFetchingUseCase.swift; sourceTree = ""; }; + F8E5C0FB2518E9150083D2B1 /* HeadlinesSearchingUseCases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlinesSearchingUseCases.swift; sourceTree = ""; }; + F8E5C0FD25190CDD0083D2B1 /* HeadlineCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlineCellViewModel.swift; sourceTree = ""; }; F8F14C73250D719800C24FF5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; /* End PBXFileReference section */ @@ -341,16 +359,27 @@ path = AlertView; sourceTree = ""; }; + F8E5C0F62518E7300083D2B1 /* Domains */ = { + isa = PBXGroup; + children = ( + F8E5C0F72518E7770083D2B1 /* HeadlinesUseCases.swift */, + F8E5C0F92518E8100083D2B1 /* HeadlinesFetchingUseCase.swift */, + F8E5C0FB2518E9150083D2B1 /* HeadlinesSearchingUseCases.swift */, + ); + path = Domains; + sourceTree = ""; + }; F8F14C68250D70AA00C24FF5 /* Classes */ = { isa = PBXGroup; children = ( + F8F14C6C250D70CF00C24FF5 /* ViewControllers */, + F8F14C6B250D70C700C24FF5 /* ViewModels */, + F8F14C6A250D70BD00C24FF5 /* Views */, F858997925176D7700A6BA2A /* Models */, + F8E5C0F62518E7300083D2B1 /* Domains */, F8F14C69250D70B400C24FF5 /* Data Layers */, F8F14C6F250D710100C24FF5 /* Extensions */, F8F14C6D250D70DC00C24FF5 /* Utilites */, - F8F14C6C250D70CF00C24FF5 /* ViewControllers */, - F8F14C6B250D70C700C24FF5 /* ViewModels */, - F8F14C6A250D70BD00C24FF5 /* Views */, ); path = Classes; sourceTree = ""; @@ -377,6 +406,12 @@ F8F14C6B250D70C700C24FF5 /* ViewModels */ = { isa = PBXGroup; children = ( + F8E5C0F42518DD3E0083D2B1 /* ViewModelState.swift */, + F8E5C0F22518D8AC0083D2B1 /* ArticleViewModel.swift */, + F8E5C0EC2518833B0083D2B1 /* ArticlesViewModel.swift */, + F8E5C0EE2518848D0083D2B1 /* HeadlinesViewModel.swift */, + F8E5C0F0251884A20083D2B1 /* HeadlineSearchViewModel.swift */, + F8E5C0FD25190CDD0083D2B1 /* HeadlineCellViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -460,6 +495,7 @@ buildConfigurationList = F89B023D250D446200B41293 /* Build configuration list for PBXNativeTarget "DutchNews" */; buildPhases = ( ED77C5D597B0DE2AE4BC3C93 /* [CP] Check Pods Manifest.lock */, + F8E5C105251915CE0083D2B1 /* Swift Lint Run Script */, F89B0216250D446000B41293 /* Sources */, F89B0217250D446000B41293 /* Frameworks */, F89B0218250D446000B41293 /* Resources */, @@ -641,6 +677,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; }; + F8E5C105251915CE0083D2B1 /* Swift Lint Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Swift Lint Run Script"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nif which \"${PODS_ROOT}/SwiftLint/swiftlint\" >/dev/null; then\n \"${PODS_ROOT}/SwiftLint/swiftlint\" autocorrect\n \"${PODS_ROOT}/SwiftLint/swiftlint\"\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n\n"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -649,20 +703,29 @@ buildActionMask = 2147483647; files = ( F85899882517894B00A6BA2A /* HeadlinesArticleRemoteRepository.swift in Sources */, + F8E5C0F32518D8AC0083D2B1 /* ArticleViewModel.swift in Sources */, + F8E5C0ED2518833B0083D2B1 /* ArticlesViewModel.swift in Sources */, F8154D6F25180B0200BFB42C /* UIView+Nib.swift in Sources */, F858997B25176DC800A6BA2A /* Article.swift in Sources */, F865F7622516A6C1001FD067 /* APIServerResponseError.swift in Sources */, F865F7602516A643001FD067 /* APIServerResponseStatus.swift in Sources */, + F8E5C0FA2518E8100083D2B1 /* HeadlinesFetchingUseCase.swift in Sources */, F8154D522517EBC700BFB42C /* HeadlinesViewController+MagazineLayout.swift in Sources */, F8DE79E32515904400A6C2D5 /* NetworkService.swift in Sources */, F8154D752518156000BFB42C /* HeadlinesViewController+DataSource.swift in Sources */, F8154D6C25180A9000BFB42C /* HeadlineBaseCollectionViewCell.swift in Sources */, F865F76B2516C08C001FD067 /* NetworkValidResponse.swift in Sources */, F8154D612518011500BFB42C /* MainArticleCollectionViewCell.swift in Sources */, + F8E5C0F1251884A20083D2B1 /* HeadlineSearchViewModel.swift in Sources */, F8154D7125180C3900BFB42C /* ArticleWebContainerCollectionViewCell.swift in Sources */, + F8E5C0F82518E7770083D2B1 /* HeadlinesUseCases.swift in Sources */, F865F75025168F05001FD067 /* AppConfig.swift in Sources */, + F8E5C0FE25190CDD0083D2B1 /* HeadlineCellViewModel.swift in Sources */, F865F7642516A9D5001FD067 /* APIServerResponse.swift in Sources */, + F8E5C0F52518DD3E0083D2B1 /* ViewModelState.swift in Sources */, + F8E5C0FC2518E9150083D2B1 /* HeadlinesSearchingUseCases.swift in Sources */, F8154D572517F03600BFB42C /* ArticleHeadlineLayoutConfiguration.swift in Sources */, + F8E5C0EF2518848D0083D2B1 /* HeadlinesViewModel.swift in Sources */, F8154D4E2517D28700BFB42C /* HeadlinesViewController.swift in Sources */, F8154D652518012F00BFB42C /* ArticleRowCollectionViewCell.swift in Sources */, F8154D792518207B00BFB42C /* String+HTML.swift in Sources */, diff --git a/DutchNews/Classes/Domains/HeadlinesFetchingUseCase.swift b/DutchNews/Classes/Domains/HeadlinesFetchingUseCase.swift new file mode 100644 index 0000000..80ef832 --- /dev/null +++ b/DutchNews/Classes/Domains/HeadlinesFetchingUseCase.swift @@ -0,0 +1,24 @@ +// +// HeadlinesFetchingUseCase.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import RxSwift + +class HeadlinesFetchingUseCase: HeadlinesUseCases { + + let repository: ArticleRepository + + init(repository: ArticleRepository) { + self.repository = repository + } + + func fetchArticles() -> Observable<[T]> { + return repository.fetchArticles() + } + +} diff --git a/DutchNews/Classes/Domains/HeadlinesSearchingUseCases.swift b/DutchNews/Classes/Domains/HeadlinesSearchingUseCases.swift new file mode 100644 index 0000000..5920297 --- /dev/null +++ b/DutchNews/Classes/Domains/HeadlinesSearchingUseCases.swift @@ -0,0 +1,24 @@ +// +// HeadlinesSearchingUseCases.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import RxSwift + +class HeadlinesSearchingUseCases: HeadlinesUseCases { + + let repository: ArticleRepository + + init(repository: ArticleRepository) { + self.repository = repository + } + + func searchInArticle(keyword: String) -> Observable<[T]> { + return repository.search(keyword: keyword) + } + +} diff --git a/DutchNews/Classes/Domains/HeadlinesUseCases.swift b/DutchNews/Classes/Domains/HeadlinesUseCases.swift new file mode 100644 index 0000000..2155038 --- /dev/null +++ b/DutchNews/Classes/Domains/HeadlinesUseCases.swift @@ -0,0 +1,30 @@ +// +// HeadlinesUseCases.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import RxSwift + +/// Abstract `HeadlinesUseCases` +protocol HeadlinesUseCases { + + typealias T = Article + + func fetchArticles() -> Observable<[T]> + func searchInArticle(keyword: String) -> Observable<[T]> +} + +extension HeadlinesUseCases { + + func fetchArticles() -> Observable<[T]> { + return .empty() + } + + func searchInArticle(keyword: String) -> Observable<[T]> { + return .empty() + } +} diff --git a/DutchNews/Classes/ViewModels/ArticleViewModel.swift b/DutchNews/Classes/ViewModels/ArticleViewModel.swift new file mode 100644 index 0000000..16a77b2 --- /dev/null +++ b/DutchNews/Classes/ViewModels/ArticleViewModel.swift @@ -0,0 +1,58 @@ +// +// ArticleViewModel.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import RxSwift +import RxCocoa + +/// Abstract `ArtileViewModel` +protocol ArticleViewModel: class { + + typealias T = Article + + var state: Driver { get } + + var output: Driver { get } + + var model: T { get } + + func buildURLContent() -> Observable + +} + +extension ArticleViewModel { + + func buildURLContent() -> Observable { + return output.map({ + URLRequest(url: $0.url, + cachePolicy: .reloadRevalidatingCacheData, + timeoutInterval: 30.0) + }) .asObservable() + } +} + +/// `ArticleRepresentable` is representive of article output +protocol ArticleRepresentable { + + var title: String { get } + var author: String? { get } + var description: String? { get } + + var source: String? { get } + + var url: URL { get } + + var urlToImage: URL? { get } + + var publishedAt: String { get } + + var content: String? { get } + + var type: ArticleType { get } + +} diff --git a/DutchNews/Classes/ViewModels/ArticlesViewModel.swift b/DutchNews/Classes/ViewModels/ArticlesViewModel.swift new file mode 100644 index 0000000..dc2d2e0 --- /dev/null +++ b/DutchNews/Classes/ViewModels/ArticlesViewModel.swift @@ -0,0 +1,38 @@ +// +// ArticlesViewModel.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import RxSwift +import RxCocoa +import RxDataSources + +/// ArticleViewModel Interface +protocol ArticlesViewModel: class { + + typealias T = SectionModel + + var state: Driver { get } + + var output: Driver<[T]> { get } + + func fetchArticles() + + func refreshArticles() + + func article(atIndex: IndexPath) -> T.Item? + + func didSelect(article: T.Item) + + func didSelect(articleAtIndex: IndexPath) + +} + +protocol ArticlesSearchViewModel: ArticlesViewModel { + + func searchArticles(keyword: String) -> Observable<[T]> +} diff --git a/DutchNews/Classes/ViewModels/HeadlineCellViewModel.swift b/DutchNews/Classes/ViewModels/HeadlineCellViewModel.swift new file mode 100644 index 0000000..0462361 --- /dev/null +++ b/DutchNews/Classes/ViewModels/HeadlineCellViewModel.swift @@ -0,0 +1,69 @@ +// +// HeadlineCellViewModel.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import RxSwift +import RxCocoa + +class HeadlineCellViewModel: ArticleViewModel { + + var state: Driver { + return .empty() + } + + var output: Driver { + return _output.asDriver() + } + + private var _output: BehaviorRelay + + var model: T { + didSet { + guard model != oldValue else { + return + } + _output.accept(Self.convert(model: model)) + } + } + + init(model: T) { + self.model = model + _output = BehaviorRelay(value: Self.convert(model: model)) + } + + private static func convert(model: T) -> ArticleRepresentable { + return HeadlineCellOutput(title: model.title, + author: model.author, + description: model.title, + source: model.source.name, + url: model.url, + urlToImage: model.urlToImage, + publishedAt: "", + content: model.content, type: model.type) + } + +} + +private extension HeadlineCellViewModel { + + struct HeadlineCellOutput: ArticleRepresentable { + + var title: String + var author: String? + var description: String? + var source: String? + var url: URL + var urlToImage: URL? + var publishedAt: String + var content: String? + + var type: ArticleType + + } + +} diff --git a/DutchNews/Classes/ViewModels/HeadlineSearchViewModel.swift b/DutchNews/Classes/ViewModels/HeadlineSearchViewModel.swift new file mode 100644 index 0000000..299c625 --- /dev/null +++ b/DutchNews/Classes/ViewModels/HeadlineSearchViewModel.swift @@ -0,0 +1,9 @@ +// +// HeadlineSearchViewModel.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation diff --git a/DutchNews/Classes/ViewModels/HeadlinesViewModel.swift b/DutchNews/Classes/ViewModels/HeadlinesViewModel.swift new file mode 100644 index 0000000..a813e76 --- /dev/null +++ b/DutchNews/Classes/ViewModels/HeadlinesViewModel.swift @@ -0,0 +1,116 @@ +// +// HeadlinesViewModel.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import RxSwift +import RxCocoa + +final class HeadlinesViewModel: ArticlesViewModel { + + private var statePublisher: BehaviorRelay + + var state: Driver { + return statePublisher.asDriver { + return .just(.error($0) ) + }.distinctUntilChanged() + } + + private var outputPublisher: BehaviorRelay<[T]> + + var output: Driver<[T]> { + outputPublisher.asDriver { _ in return .never() } + } + + let useCase: HeadlinesUseCases + + let disposeBag = DisposeBag() + + private init(useCase: HeadlinesUseCases, + state: BehaviorRelay, + output: BehaviorRelay<[T]>) { + + self.useCase = useCase + self.statePublisher = state + self.outputPublisher = output + } + + required convenience init(useCase: HeadlinesUseCases) { + self.init(useCase: useCase, + state: .init(value: .idle), + output: .init(value: [])) + } + + //////////////////////////////////////////////////////////////// + // MARK: - + // MARK: Article View Model Implementation + // MARK: - + //////////////////////////////////////////////////////////////// + + func fetchArticles() { + fetchArticlesFromRepository() + } + + func refreshArticles() { + fetchArticlesFromRepository(isRefreshing: true) + } + + func article(atIndex index: IndexPath) -> T.Item? { + //TODO: Implement method + return nil + } + + func didSelect(article: T.Item) { + //TODO: Implement method + } + + func didSelect(articleAtIndex: IndexPath) { + //TODO: Implement method + } + + //////////////////////////////////////////////////////////////// + // MARK: - + // MARK: Private Methods + // MARK: - + //////////////////////////////////////////////////////////////// + + func fetchArticlesFromRepository(isRefreshing: Bool = false) { + statePublisher.accept(.loading(isRefreshing: isRefreshing)) + + useCase.fetchArticles() + .subscribe {[weak self] event in + + switch event { + case .next(let items): + guard let `self` = self else { + break + } + + // let mapped = newItems.map { + // ArticleViewModel(repository: $0, userRepositoryUseCases: AppDIContainer.userRepositoryUseCases) + // } + // .filter { item in + // !self.items.contains(where: { $0.model.id == item.model.id }) + // } + // .reversed() as Array + // + // self.items += mapped + // + self.statePublisher.accept(.loaded) + + case .error(let error): + self?.statePublisher.accept(.error(error)) + self?.statePublisher.accept(.idle) + case .completed: + self?.statePublisher.accept(.idle) + } + + }.disposed(by: disposeBag) + + } + +} diff --git a/DutchNews/Classes/ViewModels/ViewModelState.swift b/DutchNews/Classes/ViewModels/ViewModelState.swift new file mode 100644 index 0000000..5cfdb9e --- /dev/null +++ b/DutchNews/Classes/ViewModels/ViewModelState.swift @@ -0,0 +1,40 @@ +// +// ViewModelState.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation + +enum ViewModelState { + + case idle + case loading(isRefreshing: Bool) + case loaded + case error(Error) + +} + +extension ViewModelState: Hashable { + + static func == (lhs: ViewModelState, rhs: ViewModelState) -> Bool { + switch (lhs, rhs) { + case (idle,idle), + (loaded,loaded): + return true + case (error(let lhs),error(let rhs)) where type(of: lhs.self) == type(of: rhs.self): + return true + case (loading(isRefreshing: let lValue),loading(isRefreshing: let rValue)) where rValue == lValue: + return true + default: + return false + } + } + + func hash(into hasher: inout Hasher) { + hasher.combine(self) + } + +} diff --git a/DutchNews/Classes/Views/AlertView/AlertableView.swift b/DutchNews/Classes/Views/AlertView/AlertableView.swift index e9037f1..b62b906 100644 --- a/DutchNews/Classes/Views/AlertView/AlertableView.swift +++ b/DutchNews/Classes/Views/AlertView/AlertableView.swift @@ -19,13 +19,13 @@ protocol AlertableView: class { /// - message: <#message description#> /// - actionTitle: <#actionTitle description#> /// - actionHandler: <#actionHandler description#> - func presentAlert(message: String, actionTitle: String?, actionHandler:@escaping () -> ()) + func presentAlert(message: String, actionTitle: String?, actionHandler:@escaping () -> Void) } // Abstract `AlertableView` impelementation extension AlertableView { - func presentAlert(message: String, actionTitle: String?, actionHandler:@escaping () -> ()) { + func presentAlert(message: String, actionTitle: String?, actionHandler:@escaping () -> Void) { let alert = MDCSnackbarMessage(text: message) // if actionTitle has a value, create snackbar action and assign to alert diff --git a/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.swift b/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.swift index 70d50e6..1b22f08 100644 --- a/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.swift +++ b/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.swift @@ -22,14 +22,13 @@ class ArticleWebContainerCollectionViewCell: HeadlineBaseCollectionViewCell { contentView.addSubview(webView) webView.autoPinEdgesToSuperviewSafeArea() webView.scrollView.isScrollEnabled = false - contentLabel.isHidden = true + contentLabel.isHidden = true } override func prepareForReuse() { super.prepareForReuse() } - override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { let layoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes) From 0745c13509b1f598585a45712a1207a787a094b9 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Mon, 21 Sep 2020 21:16:52 +0330 Subject: [PATCH 076/108] - Added UIKit framework additional extensions. - Added Foundation framework additional extensions. --- DutchNews.xcodeproj/project.pbxproj | 48 +++ .../Extensions/Bundle+Extensions.swift | 20 + .../Extensions/Collection+Additionals.swift | 101 +++++ .../Classes/Extensions/Date+Convertor.swift | 50 +++ .../Classes/Extensions/Date+TimeAgo.swift | 123 ++++++ .../Extensions/String+EmptyChecking.swift | 58 +++ .../Extensions/UI/UIColor+Extension.swift | 121 ++++++ .../Extensions/UI/UIImage+Additionals.swift | 383 ++++++++++++++++++ .../UI/UIImageView+SDWebImage.swift | 29 ++ .../Extensions/UI/UILabel+Localization.swift | 35 ++ .../UI/UINavigationBar+Additionals.swift | 25 ++ .../UI/UIViewController+AlertableView.swift | 3 +- .../UI/UIViewController+StoryboardName.swift | 18 + .../Extensions/URL+ApplicationPath.swift | 35 ++ 14 files changed, 1047 insertions(+), 2 deletions(-) create mode 100644 DutchNews/Classes/Extensions/Bundle+Extensions.swift create mode 100644 DutchNews/Classes/Extensions/Collection+Additionals.swift create mode 100644 DutchNews/Classes/Extensions/Date+Convertor.swift create mode 100644 DutchNews/Classes/Extensions/Date+TimeAgo.swift create mode 100644 DutchNews/Classes/Extensions/String+EmptyChecking.swift create mode 100644 DutchNews/Classes/Extensions/UI/UIColor+Extension.swift create mode 100644 DutchNews/Classes/Extensions/UI/UIImage+Additionals.swift create mode 100644 DutchNews/Classes/Extensions/UI/UIImageView+SDWebImage.swift create mode 100644 DutchNews/Classes/Extensions/UI/UILabel+Localization.swift create mode 100644 DutchNews/Classes/Extensions/UI/UINavigationBar+Additionals.swift create mode 100644 DutchNews/Classes/Extensions/UI/UIViewController+StoryboardName.swift create mode 100644 DutchNews/Classes/Extensions/URL+ApplicationPath.swift diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index a465e5e..086175e 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -67,6 +67,18 @@ F8E5C0FA2518E8100083D2B1 /* HeadlinesFetchingUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0F92518E8100083D2B1 /* HeadlinesFetchingUseCase.swift */; }; F8E5C0FC2518E9150083D2B1 /* HeadlinesSearchingUseCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0FB2518E9150083D2B1 /* HeadlinesSearchingUseCases.swift */; }; F8E5C0FE25190CDD0083D2B1 /* HeadlineCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0FD25190CDD0083D2B1 /* HeadlineCellViewModel.swift */; }; + F8E5C11E25191D5E0083D2B1 /* Date+Convertor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C11825191D5D0083D2B1 /* Date+Convertor.swift */; }; + F8E5C11F25191D5E0083D2B1 /* Collection+Additionals.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C11925191D5D0083D2B1 /* Collection+Additionals.swift */; }; + F8E5C12025191D5E0083D2B1 /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C11A25191D5D0083D2B1 /* Date+TimeAgo.swift */; }; + F8E5C12125191D5E0083D2B1 /* Bundle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C11B25191D5D0083D2B1 /* Bundle+Extensions.swift */; }; + F8E5C12225191D5E0083D2B1 /* String+EmptyChecking.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C11C25191D5D0083D2B1 /* String+EmptyChecking.swift */; }; + F8E5C12325191D5E0083D2B1 /* URL+ApplicationPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C11D25191D5D0083D2B1 /* URL+ApplicationPath.swift */; }; + F8E5C12A25191DA60083D2B1 /* UIImage+Additionals.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C12425191DA50083D2B1 /* UIImage+Additionals.swift */; }; + F8E5C12B25191DA60083D2B1 /* UINavigationBar+Additionals.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C12525191DA50083D2B1 /* UINavigationBar+Additionals.swift */; }; + F8E5C12C25191DA60083D2B1 /* UILabel+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C12625191DA50083D2B1 /* UILabel+Localization.swift */; }; + F8E5C12D25191DA60083D2B1 /* UIViewController+StoryboardName.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C12725191DA50083D2B1 /* UIViewController+StoryboardName.swift */; }; + F8E5C12E25191DA60083D2B1 /* UIImageView+SDWebImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C12825191DA60083D2B1 /* UIImageView+SDWebImage.swift */; }; + F8E5C12F25191DA60083D2B1 /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C12925191DA60083D2B1 /* UIColor+Extension.swift */; }; F8F14C74250D719800C24FF5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F8F14C72250D719800C24FF5 /* Main.storyboard */; }; /* End PBXBuildFile section */ @@ -153,6 +165,18 @@ F8E5C0F92518E8100083D2B1 /* HeadlinesFetchingUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlinesFetchingUseCase.swift; sourceTree = ""; }; F8E5C0FB2518E9150083D2B1 /* HeadlinesSearchingUseCases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlinesSearchingUseCases.swift; sourceTree = ""; }; F8E5C0FD25190CDD0083D2B1 /* HeadlineCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlineCellViewModel.swift; sourceTree = ""; }; + F8E5C11825191D5D0083D2B1 /* Date+Convertor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+Convertor.swift"; sourceTree = ""; }; + F8E5C11925191D5D0083D2B1 /* Collection+Additionals.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Collection+Additionals.swift"; sourceTree = ""; }; + F8E5C11A25191D5D0083D2B1 /* Date+TimeAgo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = ""; }; + F8E5C11B25191D5D0083D2B1 /* Bundle+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bundle+Extensions.swift"; sourceTree = ""; }; + F8E5C11C25191D5D0083D2B1 /* String+EmptyChecking.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+EmptyChecking.swift"; sourceTree = ""; }; + F8E5C11D25191D5D0083D2B1 /* URL+ApplicationPath.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+ApplicationPath.swift"; sourceTree = ""; }; + F8E5C12425191DA50083D2B1 /* UIImage+Additionals.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+Additionals.swift"; sourceTree = ""; }; + F8E5C12525191DA50083D2B1 /* UINavigationBar+Additionals.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+Additionals.swift"; sourceTree = ""; }; + F8E5C12625191DA50083D2B1 /* UILabel+Localization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UILabel+Localization.swift"; sourceTree = ""; }; + F8E5C12725191DA50083D2B1 /* UIViewController+StoryboardName.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+StoryboardName.swift"; sourceTree = ""; }; + F8E5C12825191DA60083D2B1 /* UIImageView+SDWebImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImageView+SDWebImage.swift"; sourceTree = ""; }; + F8E5C12925191DA60083D2B1 /* UIColor+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extension.swift"; sourceTree = ""; }; F8F14C73250D719800C24FF5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; /* End PBXFileReference section */ @@ -214,6 +238,12 @@ F8154D6D25180AF100BFB42C /* UI */ = { isa = PBXGroup; children = ( + F8E5C12925191DA60083D2B1 /* UIColor+Extension.swift */, + F8E5C12425191DA50083D2B1 /* UIImage+Additionals.swift */, + F8E5C12825191DA60083D2B1 /* UIImageView+SDWebImage.swift */, + F8E5C12625191DA50083D2B1 /* UILabel+Localization.swift */, + F8E5C12525191DA50083D2B1 /* UINavigationBar+Additionals.swift */, + F8E5C12725191DA50083D2B1 /* UIViewController+StoryboardName.swift */, F8154D6E25180B0200BFB42C /* UIView+Nib.swift */, F8E5C0EA251882560083D2B1 /* UIViewController+AlertableView.swift */, ); @@ -449,6 +479,12 @@ isa = PBXGroup; children = ( F8154D6D25180AF100BFB42C /* UI */, + F8E5C11B25191D5D0083D2B1 /* Bundle+Extensions.swift */, + F8E5C11925191D5D0083D2B1 /* Collection+Additionals.swift */, + F8E5C11825191D5D0083D2B1 /* Date+Convertor.swift */, + F8E5C11A25191D5D0083D2B1 /* Date+TimeAgo.swift */, + F8E5C11C25191D5D0083D2B1 /* String+EmptyChecking.swift */, + F8E5C11D25191D5D0083D2B1 /* URL+ApplicationPath.swift */, F8154D782518207B00BFB42C /* String+HTML.swift */, ); path = Extensions; @@ -702,23 +738,30 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F8E5C12F25191DA60083D2B1 /* UIColor+Extension.swift in Sources */, F85899882517894B00A6BA2A /* HeadlinesArticleRemoteRepository.swift in Sources */, F8E5C0F32518D8AC0083D2B1 /* ArticleViewModel.swift in Sources */, F8E5C0ED2518833B0083D2B1 /* ArticlesViewModel.swift in Sources */, F8154D6F25180B0200BFB42C /* UIView+Nib.swift in Sources */, F858997B25176DC800A6BA2A /* Article.swift in Sources */, F865F7622516A6C1001FD067 /* APIServerResponseError.swift in Sources */, + F8E5C11E25191D5E0083D2B1 /* Date+Convertor.swift in Sources */, F865F7602516A643001FD067 /* APIServerResponseStatus.swift in Sources */, F8E5C0FA2518E8100083D2B1 /* HeadlinesFetchingUseCase.swift in Sources */, + F8E5C12125191D5E0083D2B1 /* Bundle+Extensions.swift in Sources */, F8154D522517EBC700BFB42C /* HeadlinesViewController+MagazineLayout.swift in Sources */, F8DE79E32515904400A6C2D5 /* NetworkService.swift in Sources */, F8154D752518156000BFB42C /* HeadlinesViewController+DataSource.swift in Sources */, F8154D6C25180A9000BFB42C /* HeadlineBaseCollectionViewCell.swift in Sources */, F865F76B2516C08C001FD067 /* NetworkValidResponse.swift in Sources */, + F8E5C12325191D5E0083D2B1 /* URL+ApplicationPath.swift in Sources */, F8154D612518011500BFB42C /* MainArticleCollectionViewCell.swift in Sources */, F8E5C0F1251884A20083D2B1 /* HeadlineSearchViewModel.swift in Sources */, + F8E5C12D25191DA60083D2B1 /* UIViewController+StoryboardName.swift in Sources */, F8154D7125180C3900BFB42C /* ArticleWebContainerCollectionViewCell.swift in Sources */, F8E5C0F82518E7770083D2B1 /* HeadlinesUseCases.swift in Sources */, + F8E5C12A25191DA60083D2B1 /* UIImage+Additionals.swift in Sources */, + F8E5C12B25191DA60083D2B1 /* UINavigationBar+Additionals.swift in Sources */, F865F75025168F05001FD067 /* AppConfig.swift in Sources */, F8E5C0FE25190CDD0083D2B1 /* HeadlineCellViewModel.swift in Sources */, F865F7642516A9D5001FD067 /* APIServerResponse.swift in Sources */, @@ -729,16 +772,21 @@ F8154D4E2517D28700BFB42C /* HeadlinesViewController.swift in Sources */, F8154D652518012F00BFB42C /* ArticleRowCollectionViewCell.swift in Sources */, F8154D792518207B00BFB42C /* String+HTML.swift in Sources */, + F8E5C11F25191D5E0083D2B1 /* Collection+Additionals.swift in Sources */, F8E5C0EB251882560083D2B1 /* UIViewController+AlertableView.swift in Sources */, F8154D552517EC0D00BFB42C /* HeadlineLayoutConfiguration.swift in Sources */, F8E5C0E9251881C80083D2B1 /* AlertableView.swift in Sources */, F865F74C251686D2001FD067 /* Authenticator.swift in Sources */, + F8E5C12E25191DA60083D2B1 /* UIImageView+SDWebImage.swift in Sources */, F8589986251784B200A6BA2A /* ArticleRepository.swift in Sources */, F8DE79E5251594D700A6C2D5 /* APIClientService.swift in Sources */, F8154D692518016800BFB42C /* HalfWidthArticleCollectionViewCell.swift in Sources */, + F8E5C12025191D5E0083D2B1 /* Date+TimeAgo.swift in Sources */, F865F76D2516C826001FD067 /* DefaultAPIValidResponse.swift in Sources */, F858997D25176E8F00A6BA2A /* ArticleSource.swift in Sources */, + F8E5C12225191D5E0083D2B1 /* String+EmptyChecking.swift in Sources */, F89B021E250D446000B41293 /* AppDelegate.swift in Sources */, + F8E5C12C25191DA60083D2B1 /* UILabel+Localization.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/DutchNews/Classes/Extensions/Bundle+Extensions.swift b/DutchNews/Classes/Extensions/Bundle+Extensions.swift new file mode 100644 index 0000000..0821d51 --- /dev/null +++ b/DutchNews/Classes/Extensions/Bundle+Extensions.swift @@ -0,0 +1,20 @@ +// +// Bundle+Extensions.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation + +extension Bundle { + + var releaseVersionNumber: String? { + return self.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String + } + var buildVersionNumber: String? { + return self.object(forInfoDictionaryKey: "CFBundleVersion") as? String + } + +} diff --git a/DutchNews/Classes/Extensions/Collection+Additionals.swift b/DutchNews/Classes/Extensions/Collection+Additionals.swift new file mode 100644 index 0000000..b66a6c9 --- /dev/null +++ b/DutchNews/Classes/Extensions/Collection+Additionals.swift @@ -0,0 +1,101 @@ +// +// Collection+Additionals.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation + +// MARK: - Equatable +extension Array where Element: Equatable { + + /// Remove first collection element that is equal to the given `object` + /// + /// - Parameter object: the object to remove from collection + /// - Returns: if find object return true, otherwise return false + @discardableResult + mutating func remove(object: Element) -> Bool { + if let index = firstIndex(of: object) { + self.remove(at: index) + return true + } + return false + } + + /// Remove first collection element that predicate block return true + /// + /// - Parameter predicate: the predicate block + /// - Returns: a true value if operation meet success, otherwise return false. + @discardableResult + mutating func remove(where predicate: (Array.Iterator.Element) -> Bool) -> Bool { + if let index = self.firstIndex(where: { (element) -> Bool in + return predicate(element) + }) { + self.remove(at: index) + return true + } + return false + } + + /// append the object which does not exist in the collection. + /// + /// - Parameter object: the object that insert into a collection. + /// - Returns: a true value if operation meet success, otherwise return false. + @discardableResult + mutating func append(unique object: Element) -> Bool { + guard contains(object) == false else { + return false + } + + append(object) + return true + } + +} + +extension Optional where Wrapped: Collection { + + /// indicate the current optional Collection is empty or not. + var isEmpty: Bool { + switch self { + case .some(let collection): + return collection.isEmpty + case .none : + return true + } + } + +} + +extension Collection { + + /// Returns the element at the specified index if it is within bounds, otherwise nil. + subscript (safe index: Index) -> Element? { + return indices.contains(index) ? self[index] : nil + } +} + +/// append right dictionary into left dictionary +/// +/// - Parameters: +/// - left: the `Dictionary` object that append. +/// - right: the `Dictionary` object insert into left collection. +func += (left: inout [K: V], right: [K: V]) { + for (key, value) in right { + left[key] = value + } +} + +/// Contact the two same dictionary into the new one. +/// +/// - Parameters: +/// - left: the left operand `Dictionary` object. +/// - right: the right operand `Dictionray' object. +/// - Returns: <#return value description#> +func + (left: [K: V], right: [K: V]) -> [K: V] { + var newValue = left + newValue += right + return newValue +} diff --git a/DutchNews/Classes/Extensions/Date+Convertor.swift b/DutchNews/Classes/Extensions/Date+Convertor.swift new file mode 100644 index 0000000..29d3e4c --- /dev/null +++ b/DutchNews/Classes/Extensions/Date+Convertor.swift @@ -0,0 +1,50 @@ +// +// Date+PersianConvertor.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import UIKit + +extension Date { + + static var today: Date { + return Date() + } + + static var now: Date { + return today + } + +} + +extension DateFormatter { + + static let standardFormatter: DateFormatter = { + + let calendar = Calendar.current + let locale = NSLocale.system + + let formatter = DateFormatter() + formatter.locale = locale + formatter.calendar = calendar + formatter.timeZone = NSTimeZone.system + + return formatter + }() + + static func currentZoneFormatter() -> DateFormatter { + let calendar = Calendar.current + let locale = Locale.current + + let formatter = DateFormatter() + formatter.locale = locale + formatter.calendar = calendar + + return formatter + } + +} diff --git a/DutchNews/Classes/Extensions/Date+TimeAgo.swift b/DutchNews/Classes/Extensions/Date+TimeAgo.swift new file mode 100644 index 0000000..b1e2d0e --- /dev/null +++ b/DutchNews/Classes/Extensions/Date+TimeAgo.swift @@ -0,0 +1,123 @@ +// +// Date+TimeAgo.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation + +private struct DateComponentUnitFormatter { + + private struct DateComponentUnitFormat { + let unit: Calendar.Component + + let singularUnit: String + let pluralUnit: String + + let futureSingular: String + let pastSingular: String + } + + private let formats: [DateComponentUnitFormat] = [ + + DateComponentUnitFormat(unit: .year, + singularUnit: NSLocalizedString("year", comment: ""), + pluralUnit: NSLocalizedString("years", comment: ""), + futureSingular: NSLocalizedString("next-year", comment: ""), + pastSingular: NSLocalizedString("last-year", comment: "")), + + DateComponentUnitFormat(unit: .month, + singularUnit: NSLocalizedString("month", comment: ""), + pluralUnit: NSLocalizedString("months", comment: ""), + futureSingular: NSLocalizedString("next-month", comment: ""), + pastSingular: NSLocalizedString("last-month", comment: "")), + + DateComponentUnitFormat(unit: .weekOfYear, + singularUnit: NSLocalizedString("week", comment: ""), + pluralUnit: NSLocalizedString("weeks", comment: ""), + futureSingular: NSLocalizedString("next-week", comment: ""), + pastSingular: NSLocalizedString("last-week", comment: "")), + + DateComponentUnitFormat(unit: .day, + singularUnit: NSLocalizedString("day", comment: ""), + pluralUnit: NSLocalizedString("days", comment: ""), + futureSingular: NSLocalizedString("Tomorrow", comment: ""), + pastSingular: NSLocalizedString("Yesterday", comment: "")), + + DateComponentUnitFormat(unit: .hour, + singularUnit: NSLocalizedString("hour", comment: ""), + pluralUnit: NSLocalizedString("hours", comment: ""), + futureSingular: NSLocalizedString("In-an-hour", comment: ""), + pastSingular: NSLocalizedString("An hour ago", comment: "")), + + DateComponentUnitFormat(unit: .minute, + singularUnit: NSLocalizedString("minute", comment: ""), + pluralUnit: NSLocalizedString("minutes", comment: ""), + futureSingular: NSLocalizedString("In a minute", comment: ""), + pastSingular: NSLocalizedString("A minute ago", comment: "")), + + DateComponentUnitFormat(unit: .second, + singularUnit: NSLocalizedString("second", comment: ""), + pluralUnit: NSLocalizedString("seconds", comment: ""), + futureSingular: NSLocalizedString("just-now", comment: ""), + pastSingular: NSLocalizedString("just-now", comment: "")) + + ] + + func string(forDateComponents dateComponents: DateComponents, useNumericDates: Bool) -> String { + for format in self.formats { + let unitValue: Int + + switch format.unit { + case .year: + unitValue = dateComponents.year ?? 0 + case .month: + unitValue = dateComponents.month ?? 0 + case .weekOfYear: + unitValue = dateComponents.weekOfYear ?? 0 + case .day: + unitValue = dateComponents.day ?? 0 + case .hour: + unitValue = dateComponents.hour ?? 0 + case .minute: + unitValue = dateComponents.minute ?? 0 + case .second: + unitValue = dateComponents.second ?? 0 + default: + assertionFailure("Date does not have requried components") + return "" + } + + switch unitValue { + case 2 ..< Int.max: + return "\(unitValue) \(format.pluralUnit) \(NSLocalizedString("ago", comment: ""))" + case 1: + return useNumericDates ? "\(unitValue) \(format.singularUnit) \(NSLocalizedString("ago", comment: ""))" : format.pastSingular + case -1: + return useNumericDates ? "In \(-unitValue) \(format.singularUnit)" : format.futureSingular + case Int.min ..< -1: + return "\(NSLocalizedString("In", comment: "")) \(-unitValue) \(format.pluralUnit)" + default: + break + } + } + + return (NSLocalizedString("just-now", comment: "")) + } +} + +extension Date { + + func timeAgoSinceNow(useNumericDates: Bool = false) -> String { + + let calendar = Calendar.current + let unitFlags: Set = [.minute, .hour, .day, .weekOfYear, .month, .year, .second] + let now = Date() + let components = calendar.dateComponents(unitFlags, from: self, to: now) + + let formatter = DateComponentUnitFormatter() + return formatter.string(forDateComponents: components, useNumericDates: useNumericDates) + } +} diff --git a/DutchNews/Classes/Extensions/String+EmptyChecking.swift b/DutchNews/Classes/Extensions/String+EmptyChecking.swift new file mode 100644 index 0000000..3e38857 --- /dev/null +++ b/DutchNews/Classes/Extensions/String+EmptyChecking.swift @@ -0,0 +1,58 @@ +// +// String+EmptyChecking.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation + +// swiftlint:disable empty_first_line + +protocol OptionalString {} + +extension String: OptionalString {} +// swiftlint:enable empty_first_line + +extension Optional where Wrapped: OptionalString { + + var isEmptyOrBlank: Bool { + return String.isEmptyOrBlankString(self as? String) + } + +} + +extension String { + + func toBool() -> Bool? { + return NSString(string: self).boolValue + } + + static func isEmptyOrBlankString(_ aString: String?) -> Bool { + + if aString == nil { + return true + } + + if aString!.isEmpty { + return true + } + + if aString!.trimmingCharacters(in: CharacterSet.whitespaces).count == 0 { + return true + } + if aString!.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).count == 0 { + return true + } + + return false + } + + var isEmptyOrBlank: Bool { + return String.isEmptyOrBlankString(self) + } + +} + +// swiftlint:disable all diff --git a/DutchNews/Classes/Extensions/UI/UIColor+Extension.swift b/DutchNews/Classes/Extensions/UI/UIColor+Extension.swift new file mode 100644 index 0000000..223e0cf --- /dev/null +++ b/DutchNews/Classes/Extensions/UI/UIColor+Extension.swift @@ -0,0 +1,121 @@ +// +// UIColor+String.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import UIKit +import Foundation + +extension UIColor { + + convenience init?(hexString: String?) { + + guard hexString != nil else { + return nil + } + + let scanner = Scanner(string: hexString!) + scanner.scanLocation = 1 + + var rgbValue: UInt64 = 0 + + if scanner.scanHexInt64(&rgbValue) { + + self.init(rgb: rgbValue) + }else { + return nil + } + + } + + convenience init(rgb rgbValue: UInt64 = 0) { + + let red = CGFloat((rgbValue & 0xFF0000) >> 16) + let green = CGFloat((rgbValue & 0xFF00) >> 8) + let blue = CGFloat(rgbValue & 0xFF) + + self.init( + red: red / 255.0, + green: green / 255.0, + blue: blue / 255.0, + alpha: 1 + ) + + } + + static func transactionColor(for type: Int) -> UIColor { + + let color: UIColor + + switch type { + case 1, 3: + color = UIColor(rgb: 0x004f80) + + default: + color = UIColor(rgb: 0xd2a645) + } + + return color + } + + static func discountColor(for discountValue: Int) -> UIColor { + + let discountColor: UIColor + + switch discountValue { + case 1..<20: + discountColor = UIColor(rgb: 0x13b878) + + case 20..<40: + discountColor = UIColor(rgb: 0x276aae) + + case 40..<60: + discountColor = UIColor(rgb: 0x673ab7) + + case 60..<80: + discountColor = UIColor(rgb: 0xe91e63) + + case 80..<100: + discountColor = UIColor(rgb: 0xf44336) + + default: + discountColor = .clear + + } + return discountColor + } + + var hexString: String { + + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + var alpha: CGFloat = 0 + + getRed(&red, green: &green, blue: &blue, alpha: &alpha) + + let rgb: Int = (Int)(red * 255) << 16 | (Int)(green * 255) << 8 | (Int)(blue * 255) << 0 + + return String(format: "#%06x", rgb) + + } + + var hexAlphaString: String { + + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + var alpha: CGFloat = 0 + + getRed(&red, green: &green, blue: &blue, alpha: &alpha) + + let rgba: Int = (Int)(red * 255) << 32 | (Int)(green * 255) << 16 | (Int)(blue * 255) << 8 | (Int)(alpha * 255) << 0 + + return String(format: "#%08x", rgba) + + } + +} diff --git a/DutchNews/Classes/Extensions/UI/UIImage+Additionals.swift b/DutchNews/Classes/Extensions/UI/UIImage+Additionals.swift new file mode 100644 index 0000000..b7c221f --- /dev/null +++ b/DutchNews/Classes/Extensions/UI/UIImage+Additionals.swift @@ -0,0 +1,383 @@ +// +// UIImage+Additionals.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import UIKit + +protocol OptionalImage { + +} + +extension UIImage: OptionalImage { + +} + +extension Optional where Wrapped: OptionalImage { + + var isEmpty: Bool { + return UIImage.isEmpty(self as? UIImage) + } + +} + +public enum UIImageContentMode { + case scaleToFill, scaleAspectFit, scaleAspectFill +} + +extension UIImage { + + static func isEmpty(_ aImage: UIImage?) -> Bool { + + guard let image = aImage else { + return true + } + + return image.isEmpty + } + + var isEmpty: Bool { + return self.size == .zero + } + + func resize(toSize: CGSize, contentMode: UIImageContentMode = .scaleToFill) -> UIImage? { + let horizontalRatio = size.width / self.size.width + let verticalRatio = size.height / self.size.height + var ratio: CGFloat! + + switch contentMode { + case .scaleToFill: + ratio = 1 + case .scaleAspectFill: + ratio = max(horizontalRatio, verticalRatio) + case .scaleAspectFit: + ratio = min(horizontalRatio, verticalRatio) + } + + let rect = CGRect(x: 0, y: 0, width: size.width * ratio, height: size.height * ratio) + + // Fix for a colorspace / transparency issue that affects some types of + // images. See here: http://vocaro.com/trevor/blog/2009/10/12/resize-a-uiimage-the-right-way/comment-page-2/#comment-39951 + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue) + let context = CGContext(data: nil, width: Int(rect.size.width), height: Int(rect.size.height), bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo.rawValue) + + let transform = CGAffineTransform.identity + + // Rotate and/or flip the image if required by its orientation + context?.concatenate(transform) + + // Set the quality level to use when rescaling + context!.interpolationQuality = CGInterpolationQuality(rawValue: 3)! + + //CGContextSetInterpolationQuality(context, CGInterpolationQuality(kCGInterpolationHigh.value)) + + // Draw into the context; this scales the image + context?.draw(self.cgImage!, in: rect) + + // Get the resized image from the context and a UIImage + let newImage = UIImage(cgImage: (context?.makeImage()!)!, scale: self.scale, orientation: self.imageOrientation) + return newImage + } + +} + +extension UIImage { + + /// Returns base64 string + var base64: String { + return jpegData(compressionQuality: 1.0)!.base64EncodedString() + } + + /// Returns compressed image to rate from 0 to 1 + func compressImage(rate: CGFloat) -> Data? { + return jpegData(compressionQuality: rate) + } + + /// Returns Image size in Bytes + func getSizeAsBytes() -> Int { + return jpegData(compressionQuality: 1.0)?.count ?? 0 + } + + /// Returns Image size in Kylobites + func getSizeAsKilobytes() -> Int { + let sizeAsBytes = getSizeAsBytes() + return sizeAsBytes != 0 ? sizeAsBytes / 1024 : 0 + } + + /// scales image + // swiftlint:disable:next identifier_name + class func scaleTo(image: UIImage, w: CGFloat, h: CGFloat) -> UIImage { + let newSize = CGSize(width: w, height: h) + UIGraphicsBeginImageContextWithOptions(newSize, false, 0.0) + image.draw(in: CGRect(x: 0, y: 0, width: newSize.width, height: newSize.height)) + let newImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext() + return newImage + } + + /// Returns resized image with width. Might return low quality + func resizeWithWidth(_ width: CGFloat) -> UIImage { + let aspectSize = CGSize(width: width, height: aspectHeightForWidth(width)) + + UIGraphicsBeginImageContextWithOptions(aspectSize, false, UIScreen.main.scale) + self.draw(in: CGRect(origin: CGPoint.zero, size: aspectSize)) + let img = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return img! + } + + /// Returns resized image with height. Might return low quality + func resizeWithHeight(_ height: CGFloat) -> UIImage { + let aspectSize = CGSize(width: aspectWidthForHeight(height), height: height) + +// UIGraphicsBeginImageContext(aspectSize) + UIGraphicsBeginImageContextWithOptions(aspectSize, false, UIScreen.main.scale) + self.draw(in: CGRect(origin: CGPoint.zero, size: aspectSize)) + let img = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return img! + } + + func aspectHeightForWidth(_ width: CGFloat) -> CGFloat { + return (width * self.size.height) / self.size.width + } + + func aspectWidthForHeight(_ height: CGFloat) -> CGFloat { + return (height * self.size.width) / self.size.height + } + + func croppedImage(_ bound: CGRect) -> UIImage? { + guard self.size.width > bound.origin.x else { + Logger.errorLog(": Your cropping X coordinate is larger than the image width", tag: "\(#function) \(#line)") + return nil + } + + guard self.size.height > bound.origin.y else { + Logger.errorLog(": Your cropping Y coordinate is larger than the image height", tag: "\(#function) \(#line)") + return nil + } + + let scaledBounds = CGRect(x: bound.minX * self.scale, y: bound.midY * self.scale, width: bound.width * self.scale, height: bound.height * self.scale) + + let imageRef = self.cgImage?.cropping(to: scaledBounds) + let croppedImage = UIImage(cgImage: imageRef!, scale: self.scale, orientation: UIImage.Orientation.up) + return croppedImage + } + + /// Use current image for pattern of color + func withColor(_ tintColor: UIColor) -> UIImage { + UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale) + + let context = UIGraphicsGetCurrentContext() + context?.translateBy(x: 0, y: self.size.height) + context?.scaleBy(x: 1.0, y: -1.0) + context?.setBlendMode(CGBlendMode.normal) + + let rect = CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height) as CGRect + context?.clip(to: rect, mask: self.cgImage!) + tintColor.setFill() + context?.fill(rect) + + let newImage = UIGraphicsGetImageFromCurrentImageContext()! as UIImage + UIGraphicsEndImageContext() + + return newImage + } + + /// Returns the image associated with the URL + convenience init?(urlString: String) { + guard let url = URL(string: urlString) else { + self.init(data: Data()) + return + } + guard let data = try? Data(contentsOf: url) else { + self.init(data: Data()) + return + } + self.init(data: data) + } + + class func blankImage() -> UIImage { + UIGraphicsBeginImageContextWithOptions(CGSize(width: 1, height: 1), false, UIScreen.main.scale) + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image! + } +} + +extension UIImage { + + /// SwifterSwift: Size in bytes of UIImage + var bytesSize: Int { + return jpegData(compressionQuality: 1.0)?.count ?? 0 + } + + /// SwifterSwift: Size in kilo bytes of UIImage + var kilobytesSize: Int { + return bytesSize / 1024 + } + + /// SwifterSwift: UIImage with .alwaysOriginal rendering mode. + var original: UIImage { + return withRenderingMode(.alwaysOriginal) + } + + /// SwifterSwift: UIImage with .alwaysTemplate rendering mode. + var template: UIImage { + return withRenderingMode(.alwaysTemplate) + } + +} + +// MARK: - Methods +extension UIImage { + + /// SwifterSwift: Compressed UIImage from original UIImage. + /// + /// - Parameter quality: The quality of the resulting JPEG image, expressed as a value from 0.0 to 1.0. The value 0.0 represents the maximum compression (or lowest quality) while the value 1.0 represents the least compression (or best quality), (default is 0.5). + /// - Returns: optional UIImage (if applicable). + func compressed(quality: CGFloat = 0.5) -> UIImage? { + guard let data = compressedData(quality: quality) else { + return nil + } + return UIImage(data: data) + } + + /// SwifterSwift: Compressed UIImage data from original UIImage. + /// + /// - Parameter quality: The quality of the resulting JPEG image, expressed as a value from 0.0 to 1.0. The value 0.0 represents the maximum compression (or lowest quality) while the value 1.0 represents the least compression (or best quality), (default is 0.5). + /// - Returns: optional Data (if applicable). + func compressedData(quality: CGFloat = 0.5) -> Data? { + return jpegData(compressionQuality: quality) + } + + /// SwifterSwift: UIImage Cropped to CGRect. + /// + /// - Parameter rect: CGRect to crop UIImage to. + /// - Returns: cropped UIImage + func cropped(to rect: CGRect) -> UIImage { + guard rect.size.height < size.height && rect.size.height < size.height else { + return self + } + guard let image: CGImage = cgImage?.cropping(to: rect) else { + return self + } + return UIImage(cgImage: image) + } + + /// SwifterSwift: UIImage scaled to height with respect to aspect ratio. + /// + /// - Parameters: + /// - toHeight: new height. + /// - orientation: optional UIImage orientation (default is nil). + /// - Returns: optional scaled UIImage (if applicable). + func scaled(toHeight: CGFloat, with orientation: UIImage.Orientation? = nil) -> UIImage? { + let scale = toHeight / size.height + let newWidth = size.width * scale + UIGraphicsBeginImageContextWithOptions(CGSize(width: newWidth, height: toHeight), false, UIScreen.main.scale) +// UIGraphicsBeginImageContext(CGSize(width: newWidth, height: toHeight)) + draw(in: CGRect(x: 0, y: 0, width: newWidth, height: toHeight)) + let newImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return newImage + } + + /// SwifterSwift: UIImage scaled to width with respect to aspect ratio. + /// + /// - Parameters: + /// - toWidth: new width. + /// - orientation: optional UIImage orientation (default is nil). + /// - Returns: optional scaled UIImage (if applicable). + func scaled(toWidth: CGFloat, with orientation: UIImage.Orientation? = nil) -> UIImage? { + let scale = toWidth / size.width + let newHeight = size.height * scale + + UIGraphicsBeginImageContextWithOptions(CGSize(width: toWidth, height: newHeight), false, UIScreen.main.scale) + draw(in: CGRect(x: 0, y: 0, width: toWidth, height: newHeight)) + + let newImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return newImage + } + + /// SwifterSwift: UIImage filled with color + /// + /// - Parameter color: color to fill image with. + /// - Returns: UIImage filled with given color. + func filled(withColor color: UIColor) -> UIImage { + UIGraphicsBeginImageContextWithOptions(size, false, scale) + color.setFill() + guard let context = UIGraphicsGetCurrentContext() else { + return self + } + + context.translateBy(x: 0, y: size.height) + context.scaleBy(x: 1.0, y: -1.0) + context.setBlendMode(CGBlendMode.normal) + + let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) + guard let mask = self.cgImage else { + return self + } + context.clip(to: rect, mask: mask) + context.fill(rect) + + let newImage = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext() + return newImage + } + + /// SwifterSwift: UIImage tinted with color + /// + /// - Parameters: + /// - color: color to tint image with. + /// - blendMode: how to blend the tint + /// - Returns: UIImage tinted with given color. + func tint(_ color: UIColor, blendMode: CGBlendMode) -> UIImage { + let drawRect = CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height) + UIGraphicsBeginImageContextWithOptions(size, false, scale) + let context = UIGraphicsGetCurrentContext() + context!.clip(to: drawRect, mask: cgImage!) + color.setFill() + UIRectFill(drawRect) + draw(in: drawRect, blendMode: blendMode, alpha: 1.0) + let tintedImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return tintedImage! + } +} + +// MARK: - Initializers +extension UIImage { + + /// SwifterSwift: Create UIImage from color and size. + /// + /// - Parameters: + /// - color: image fill color. + /// - size: image size. + convenience init(color: UIColor, size: CGSize) { + + UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale) + color.setFill() + UIRectFill(CGRect(x: 0, y: 0, width: size.width, height: size.height)) + guard let image = UIGraphicsGetImageFromCurrentImageContext() else { + self.init() + return + } + UIGraphicsEndImageContext() + guard let aCgImage = image.cgImage else { + self.init() + return + } + self.init(cgImage: aCgImage) + } + +} diff --git a/DutchNews/Classes/Extensions/UI/UIImageView+SDWebImage.swift b/DutchNews/Classes/Extensions/UI/UIImageView+SDWebImage.swift new file mode 100644 index 0000000..9ba96e4 --- /dev/null +++ b/DutchNews/Classes/Extensions/UI/UIImageView+SDWebImage.swift @@ -0,0 +1,29 @@ +// +// UIImageView+SDWebImage.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import SDWebImage + +extension UIImageView { + + static let defaultSDWebImageOptions: SDWebImageOptions = [.lowPriority,.scaleDownLargeImages,.retryFailed,.refreshCached] + static let cacheSDWebImageOptions: SDWebImageOptions = [.lowPriority,.scaleDownLargeImages,.queryMemoryData,.refreshCached] + + func setImage(url: URL?,placeHolderImage: UIImage? = #imageLiteral(resourceName: "placeHolderImage"), + options: SDWebImageOptions = UIImageView.defaultSDWebImageOptions, + completed: SDExternalCompletionBlock? = nil) { + self.contentMode = .scaleAspectFill + self.sd_setImage(with: url, placeholderImage: placeHolderImage, + options: options, completed: completed) + } + + func cancelCurrentImageLoad() { + sd_cancelCurrentImageLoad() + } + +} diff --git a/DutchNews/Classes/Extensions/UI/UILabel+Localization.swift b/DutchNews/Classes/Extensions/UI/UILabel+Localization.swift new file mode 100644 index 0000000..27d442f --- /dev/null +++ b/DutchNews/Classes/Extensions/UI/UILabel+Localization.swift @@ -0,0 +1,35 @@ +// +// UILabel+Localization.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import UIKit + +extension UILabel { + + // swiftlint:disable:next empty_first_line + private struct AssociatedKey { + static var localizedStringKey = "localizedStringKey" + } + + /// <#Description#> + @IBInspectable + var localizedText: String? { + get { + return objc_getAssociatedObject(self, &AssociatedKey.localizedStringKey) as? String + } + + set { + objc_setAssociatedObject(self, &AssociatedKey.localizedStringKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC) + guard let value = newValue else { + return + } + self.text = NSLocalizedString(value, comment: "") + } + } + +} diff --git a/DutchNews/Classes/Extensions/UI/UINavigationBar+Additionals.swift b/DutchNews/Classes/Extensions/UI/UINavigationBar+Additionals.swift new file mode 100644 index 0000000..c2f7f92 --- /dev/null +++ b/DutchNews/Classes/Extensions/UI/UINavigationBar+Additionals.swift @@ -0,0 +1,25 @@ +// +// UINavigationBar+Additionals.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import UIKit + +extension UINavigationItem { + + @IBInspectable + var localizedTitle: String? { + get { + return nil + } + set { + guard let newValue = newValue else { self.title = nil; return } + self.title = NSLocalizedString(newValue, comment: "") + } + } + +} diff --git a/DutchNews/Classes/Extensions/UI/UIViewController+AlertableView.swift b/DutchNews/Classes/Extensions/UI/UIViewController+AlertableView.swift index 3953b78..24b7f2d 100644 --- a/DutchNews/Classes/Extensions/UI/UIViewController+AlertableView.swift +++ b/DutchNews/Classes/Extensions/UI/UIViewController+AlertableView.swift @@ -18,9 +18,8 @@ extension AlertableView where Self: UIViewController { /// - message: <#message description#> /// - actionTitle: <#actionTitle description#> /// - actionHandler: <#actionHandler description#> - func presentAlertView(withMessage message: String, actionTitle: String? = nil, actionHandler: @escaping () -> () = {}) { + func presentAlertView(withMessage message: String, actionTitle: String? = nil, actionHandler: @escaping () -> Void = {}) { presentAlert(message: message, actionTitle: actionTitle, actionHandler: actionHandler) } } - diff --git a/DutchNews/Classes/Extensions/UI/UIViewController+StoryboardName.swift b/DutchNews/Classes/Extensions/UI/UIViewController+StoryboardName.swift new file mode 100644 index 0000000..4cf4b8f --- /dev/null +++ b/DutchNews/Classes/Extensions/UI/UIViewController+StoryboardName.swift @@ -0,0 +1,18 @@ +// +// UIViewController+StoryboardName.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import UIKit + +extension UIViewController { + + class var className: String { + return String(describing: self) + } + +} diff --git a/DutchNews/Classes/Extensions/URL+ApplicationPath.swift b/DutchNews/Classes/Extensions/URL+ApplicationPath.swift new file mode 100644 index 0000000..0af95ba --- /dev/null +++ b/DutchNews/Classes/Extensions/URL+ApplicationPath.swift @@ -0,0 +1,35 @@ +// +// URL+ApplicationPath.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation + +extension URL { + + static func applicationSupportDirectoryURL() throws -> URL { + return try getSearchingPath(for: .applicationSupportDirectory) + } + + static func documentDirectoryURL() throws -> URL { + return try getSearchingPath(for: .documentDirectory) + } + + static func cacheDirectoryURL() throws -> URL { + return try getSearchingPath(for: .cachesDirectory) + } + + static func getSearchingPath(for searchingPath: FileManager.SearchPathDirectory = .applicationSupportDirectory) throws -> URL { + guard let path = FileManager.default.urls(for: searchingPath, in: .userDomainMask).last else { + throw POSIXError(.ENOENT) + } + + try FileManager.default.createDirectory(at: path, withIntermediateDirectories: true, attributes: nil) + + return path + } + +} From a3a332ce73b1f332a59dd333ec4b86cec0f93522 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Tue, 22 Sep 2020 01:34:56 +0330 Subject: [PATCH 077/108] - Added Image Place holder. --- .../image-placeHolder.imageset/Contents.json | 21 ++++++++++++++++++ .../image-placeHolder.imageset/Image.png | Bin 0 -> 16513 bytes 2 files changed, 21 insertions(+) create mode 100644 DutchNews/Resources/Assets.xcassets/image-placeHolder.imageset/Contents.json create mode 100644 DutchNews/Resources/Assets.xcassets/image-placeHolder.imageset/Image.png diff --git a/DutchNews/Resources/Assets.xcassets/image-placeHolder.imageset/Contents.json b/DutchNews/Resources/Assets.xcassets/image-placeHolder.imageset/Contents.json new file mode 100644 index 0000000..d58d092 --- /dev/null +++ b/DutchNews/Resources/Assets.xcassets/image-placeHolder.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "Image.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/DutchNews/Resources/Assets.xcassets/image-placeHolder.imageset/Image.png b/DutchNews/Resources/Assets.xcassets/image-placeHolder.imageset/Image.png new file mode 100644 index 0000000000000000000000000000000000000000..0ed1f35af211f30eea2c13884310c6cf25dca253 GIT binary patch literal 16513 zcmeIZ`9GBX`#;PyT^X*jBoc{)l&ujGhNvM+$`XS?S;jV$HOpX%bVZghc2mhNyOCX5 z>?YZbC6#sT%V03RFYou~^S$qX;C|e{Tt9d)<~+~Wc^a-LOo;1{!}p_T?yQMbSx_;AzS?6!lxKGP-eHwzPU zxDylf@DT8G2K+EF9eKmdbOiinKK$Ps$p3x|F?)05fB$A~K0L6jfgH}nq(;4c^M>&g z=EX5;)ddrr;tC|;%Nu6CFU)`am4u&sr!Ms7Tkby&QU`f;PZ$N3PmcU00{z0Az?sJ9 z@KLXc?O}XKqT?Pd?psE7O+h}n2v<{!2#^{`^Q;Ll?a$HKt)-1`7F-?=KG;{gdWIPS zW#JG~4^h8@zZ|joUgUp1GU1a$LZ&pp7fgpoaPx4Kus)lQ`p*XrAt9UUzy5O;_#Ac= zTG;%iM&y5vWHBrHzn6T#%v>OwyY#mzS@y^Wdj{)k9Wsi=b!$57;c9zx2UZAr zPqf0;sHcHvhg8*7Q_IJ<5^$xQfB^^)LPEUF)=0WG>_>yv;)qu#4j5ah2aM$dD?cwH zy)P$)j9c>T5?9-4OITio0D9qrR&N^Flyf|Qx#F9OgZHV^UwN%vrO`EgF7EKB-oL*5 zGySJ6u1gEC7HdUZ6$5K5ybcMe)Wvi(a1Funddv+BI<+fV4wEv$pjAEFTAD8?c0=qA zlNAQ7gZF=u>4$+wLV-o5%WWrETuYmZgEkAM115H@)@c;?b5CTV&nhce5WV|NOwWFl zIeTaXekimMdyS2TZ#n_nV{A1N$Id=k8m$&nD|Sg{tpf&2A?`0=sN)I&Qj?b=e!4g8 zI7IwZuB^i|;jz3NC8Yhoht(h!7h>MT-&)|SpLa%e1|95gDH6B8h^HVTCT8}}DtXwE zSP;G8hn&sF1l(M|9HtRw%-u@QFD&<8tQ@>vMK6uU6;|&q)v6gs2~Y6{ZDgL4pCJ<) z3j;MR!cDFG>^*1FeHDC1A8W-C11%_O0In4ckf){*z0l+>aCvQcdsk0 zUT)YO;NjM1{!uvQ4^F(kt1A&6@73yBpFB==K6kL) zg7G~v{^;{@w0nAeMlHLHRozhC#t+@D<)E$FZnA({P@R(Zym4I5l~=&ZwSY@5Hlxa- zp_f?gCjIkEa?S)0Z!J`f`A`48&hGFU3uEKGUX{{LO)lzaHEF}Jx_w4?3DzlsjeV0764HON zox-wE&_sGAXwNMb@w5DNLqUSTE$Rqi5XK=SdsDsX%)6<2C{0_a_c7`VW5B}QThL^2 z(_r1(MuHl%c7ke-?188F45EGIQwxt+3|0*SPF#`)Y@e4#<{Y|*N7p=a?^NdV-Pjn$ zg3SdN9Em+?uLz6h80tS`oaOR3p``B^*!BS!iy76c4ryZmrM-Pgk8xg0zSQY?+`W2= z@Q!VD>yO8y(@+*;4vvybLKGC|GfV0W5RWs%RqP$}x8PU}b|t-yNU z*+={jIfz!?jV9+>vc@~a-#OfiAqZq(sF^N)_nr`5ME*Ve)j&VjH8{@aPit&ZmD?uO zh&A2+g7!j-(x{j7wHgJ#9p+*UJ`B=ts=C>ARj^F$m3; z60*_X-BYHka$O-`G!`*vsCTR%{%;u+eGpGGRPmxxPzZaqQ|r@5O^9 z$~(rr7%!cWrde=DvtJa{Z3OVb_H1wuudOj()5{hs?U#!?d4{Ua?&PTL&kdXBHf~2W zItEm?PSD;;-r{6Q=MZ}22u^+5Ly^UI9VOMTI0!yIM8RT}rqM3n!^o?e2nz6=N-!-7 zoVZ~<)Zd(cOx2AjuKsfc`0{79W3&);cRbq9t5YUuHDbC=oVcJ;)VSBtm?l+0KJ%e; zL|XNXqvVl&E}^^D>P`9XZV}MG*cN_$jyAe<-sN+R;DA7+O5sjlo>3;Ea0kRqs=h_T z&U9)zqw<{FSheOjFm^XEp|WrL!b%@>+;1iwHMM5VSW8R~59*R1>_mpHEh|)nri2zA zhH-lB`b;omrVC*Hpn$4>#d`fQKTXI0d(g_W>HV#V<%Q|z{BB7fcz3s3)W&zL+(7}? z^bWXm?j#1_*h=lhev#i?R$>Ypue8^a$-q^6=tYqmhp<1htBBJ?{wTB7jCjpBTtc!> za@^#j^Es{(t_rS?-<&Cq$^)hIiA?yMF=B57q&E`vhHJce!Ju612CSi+`M`bxfJF9zd(+DR6~^iEl7V=@_R1y}bI( z0gJ@O?`swxyfu=?I}r9Y?$x49qAbTHgy`$UgFuC;;T$(pp2i7K9Rn-+@eWo=5sx*V z6e_`iou#Fp47hp&kFD#K>h(hv#Lv}xW8>Z=-t}bc?F{Fd1z8Qkr~F#Z`gXQokK8Gl zMptWkrYC?{9};9{b_wVXQ%%loBK14#3p}k*}8Vx4k|K3ra)%#Ao!n`)fN&_1Zg;ZKk;sPQPyb;tEa;SOlNaiGAUp( zj5kH^H}(g-*$bdd7wZce+LdT)kgMc5trw@Y;8h8NxVm~KBOoO*?N$GC<2JH?=>l1* zY7{>2Us@Vv(gFjigfluJ})sa{+$ViguXFQS0cdNx9zqg!Op!1V{kZ(90}d3VK% zXgN@GzFiWc_YxUiBsJ8@{{AwVWQDyi;)tHjfmhaKZQG|9h$C_Hq4=FN$2%D+!;#fi zuD_qH-T)*7IC_6B_Jw+{-OUS=m+aXGDkD2DSRVhNSXo4#8iYTLcd9nFTz2kYj}R;q z3XFri(?s^c?Nij_Y~*MgPEQ@XADDJH+CH^Lo&$+8TLR$8dY$sTCWm%cRt_=9ANw(X z^=q0$lWwAPSN3mupP=o}21P z+_j9mM=CX6;L<*+w>!#oSL9-}cOhL={ih9Zuupq|De5x#!s_KBTe)&A=X*Jv8r(@?_;mvaQJ4W6Ld*`@;J^S>) zU7NW_KO;Hmd?8F8?OpvaN%?QNc|uRs6^a3%W+dXBZ;c!Q#{kPgUyAxc0efwnuRaS6ios|KY3DZC#)Y zm=LU&MC8lMcrIl=wylKDsvs{ilc*;;&8HN;UB3???mK|EdH1Pw0C9hkpGvfRAK|RP{nqqrRB}7)jWCGthu72NX zz5TS46dK7c%UBNQzS?y*(oev^v?HY=-tu%AkK`nLAM@(IX~8LJxm#&XsZIU;3VsAz zQ=-*jq5)X)xx8vhm1ld&w~^w{7R;j*yjfr^)i8VAEpgl*}zpBjKh)kA1ZV+$w~hw#<=@2$32 zM2q=$PQC5QqS!{Ol1Sg~KONf>?zFwHyc}T3_qE;82w7QUzQojdep{H@|GaN{#Lm3T zg-Ee7cso3LZ)D4@JM!mL(VbUJ_%ZJ7RyD?NHAdsusCR(Ajq*{Cj-Up;LCCYEzNrTzBDX^6#adr$BoopIZ-deepQ4?)te@L%%3oK#NZPoH=;bbKsGE zFxMN~curYh;l;z+<3M2e^I5Le4I7HK;r!X-67Q8ZVon;;gC5LusQ6addtDHbw?&i} zFF&@^k@P`t7IMidcne&YNi3tu3@f8rBkujrp&fDQi-82`HOIu%{pjy%BurZBMrrwrU75>sd0m zycf0*{S22Rj{bfw$F-GN+m@(TXPnD^s@*b@^B(HwiY#XdvOo&g(gvx}?27=JR|}P= z??K`H>x=HeyP7!6nb~E=(lTOWt0A~`_72i;&HlQb|3xD-j(8(!t|agQrXt&6cg`Sq zL}h=YH$Anpra-UMK~0zBs)j@x6%0=wrXI4qWIcOCOI%FGYyO$;bQg3>)%JL#P5QOR z5AT=w6mpWjF76~V;U9p#l9gE&D=`$Zekrk0J?gPF4!BqC-i%daPLN^4qh78eL+447 z8bYXgdE;(FiP4lwuRC>Mr_)!At-JiRTCbD*$U0d~&Eh1<*#dSV&wpYjNe)7|n_SBj z90X2JPlFOZ&o45)cvui@5iP}xn7qb(S47<=07Uho5--iJ<>>uW zytwVja7@IbGWGd( zbhO(Vm1rBa`S!<6Y05JX3vaE!s`&otKxBp91c5VpD+Slim{Ey78)Z7Yt;`HL0I9=6 zl;h%sn7O(sr;mYc_Qnt5WNP(J+S@spbf80aH1t2@G(@DYH;h&i_FXSOule{PG(NOW zSlE7b9T(KyfVn9x;(DcK^&5X`?ltP5Henj#QP zIr}z(KmQ)>ywT^!ZOt_3(pRDCk1{)cf>G-Yb-|sF>guq7JwB=qk!bEv-9y^g;)Cfq z!SwIBTF8@($#aYcnFynU*+Z)DZHl^-HuD&c)A$mI;VSXivNClFty7~0!t>*uYOyYl z+cTdIyt-yjR7}?B(6-9HU+av?I?(UFflz!es&21}hS&op6pjm@jj>&~g6xXfKwT!f zbAKyc#j6xKREyf!$L@+r^_nLSP>n<+GOs0bbztC-Ud#rDF-y>Evwx2nB)-B+EKSS$w>z~bfwl6wYEsI2dlj`6MXdz=_pbf>t? zDGMuBHhHKdU$^Y{Kp4@5g|B3WQr42*o9t4Tp)`F^(pJgMKRKZAxQP&U7U{_jCuZ5)u5t`zK1~EF4)DW)o9+udz(kH*bFA>6q=VU~r*Ympd z^l7l5f++W~3D3V_1ehmZk9q&Jks~Y~m-C&=c}?$gZTAiGE8+y(Hb-wnUuy!1Y58TTYfa=T4Ile%#7E?Qss>h&dxC zEWJC3!J&|QKe(>bp^ZB)tdYwPE8M=guOdSW#dWnWkCl;E4X$;cAr(Qg);Aw&4lE>$ zWA-_d4v9%t`J$QDsn(h#Qfz3S{-qvZ4jF3T+v$#qf@{-zZ1T*x%V_&`iPF&UqL{Mn zM}I(V)#-fOKGe~q<4mqF^JQjf`8qC#wQ7V>)i>7inM3YvK^&aR{1NP zea_7jF|Rob{-!Vx>@7;qiyoz|#|fpmhzuH@>m_07{LGLs2;_cMtS`r`XpAzQu)L!4 zvz_}vn^SV2#55ZMPj0Z4xfJ76PP&&>IL2p-f7~!yenQ;(Cts}DASfVnzDx^? z9*Etnc-8%ZGQC#`t&O*uwSWsz+KhkyqQT!0j#4zWQn>`_1k2WUtA!@*#x}@j^okuN z?UJSI$50Up){TU`A;gVRGD5OJT;xgh_?k6QKA@UUcyKX^`juykOonD(01h(esyuM- z=-Scrp0p`KY*?gaWDgt?E7L34+|3&}2wCvczAZZ|hgLi%uMwd11wePc~l)!qkr$%~%m3qg-1zbQY*iyae`p36|ES*qzdz{Q~l2c7! zwSBtoD}@7%MNd;yc3~Nrul{ry3qq7PN>KHB!DD%LPS~kvl{M{Hr|iIbbCW1&IgH= zREKCyxw_57K@k^X9%f=sjh!rF(A*Dbn#<>Q+4-2;8J63&d|}>3_lKh+L+o#g+KWDq zB#DODJ@$r7pO%{Z3EIQsg&uyU2g!eRvW&#U#DpZZOkQyGsf0EbUe--op!{0@6(Q5z zE-x804Qj8nu#&Hh2@ItfwNLZEzDA^@#l3=dI;;gNtcuEw+=XPp;KQX^eBx~)Z|mZ6 zv>M}yON;g1FWzIg)heH|$#ZgN2{Avsb1Z7exw<^riD{SGwNcG8S-Oc1lTt2k;r_iC ztPxFOQdYQh5kKOGtIj%z4?A7<5z+e!W}aL_bxwXLKwoHy|1kHei$9-&gc4&tGeBN+8m85{7&FZ>m!-FCO3CF8tF)EU^S7cTt(BF`nYG7mcQa;2m9gezK8vZr@u}o zjMS1E=Qo9sN>#9zw$dc)N+RQq%>-zK;nY4y71yX|r)1!0!vl+AHm_7YdTJ)a)Eq*^ z@ZzWHYt6Iw%WPCdG;o?a7Q_QEY#6OMA+f9)wD-hwzAx`a#INwk?k~!ja;P^GRmYP- zZj0DmZVbMAccJ#2=D6Zjb@yKgyWqOSrarJ(o;7n+<45&|*^DQ@{u!8`Mv26GOH*Wj z#VWV2*-Y=8-}L6-vj-K+1SBm2_w~0mW6JveUxjtPap1XovLoB=`Kji0b)L+|6fxiU|&o)coF z>4-CW^V6mv{`_ua-CI%K>4cvrGw7jj{e~l;nYg5W>k4(ei@wac%(CN8TGcqK~`;fS4U!7U(KKnu! z89CA}_&)k@0CdZZ)eIB&Jwx6LR)faIg)z`Lf6#Oa0;x<*eEEDytop#UN*rHKd7pE8 zt_Ja5{40fq$?&zKLDQYd?EP40s&vH98Q&k9$0bmdcxBddl7yOXWvQ;TU5$dv>*tbB z4W8V&t5-?$(>1I89i=?o$aCQ9$khEixy*pV{VO7KHl~ROREQRV9t$H&Y<68g8uuxU zUD*vIQDR04?4VSABd?F$k6f)Pl+bxG>;D{0O4rDTNoeNx=S=ja&&z$SY;!bP*yTZW z#s3il^Mz%UH6e!7cAN=Ewm|NANW&qZ>f!d3COt^E9&GSW|G)JpU%(+-r?ZG$iY{J`>yTkchDLz>{m5HC$BkM6qAO0gU0PDBp5Jp zG_~=^0VymliKrStN(@GFDCT`3{vH4bAOjFUv0ntw4zt4=Ep3?hmyKTp?yX(_N=oEn z@{*qXS`k4WC9$IP{>c-Mp zx72h~nT4-hXNDX*34CyRrWfkZX7|_&DRRFuG)#H9&XW|C9;oo`W$S^=dQq~O-KmrBFeMExo5Yo7b^o*y1-<|9IWwsv1(#GAbmKnLIUq) zayzuzgwcf#;k&|LmZaQI^OUZ6SvW1-rWY{a3@d?!;2^F@mhxaxL3?BBwrhA(JgB-P z(y=Z6vk(}2wWj+{bs$#Rhx(za8!7G_{ib43moo7@-g4-|Rk6JbP|?4=D^E{FRqHKo z)~A+|&u{D9atL)?ARIkNKGFH=C|XJsVJMFf3Dq<&s*mKD?rzyj@1sg4ZE2xjb9*>k|nWA82@?cYp#1HA2@Fh+gex3-qb?W%v^T z(F-`qJst6LH>&%>tXPPhIh>+%PJ$A1#@enFX{`Tg8Qc6Z2G;X zBIT>)&oA$TtRD}Yp&d2<4`3UC$uqjDBIanR2g0HpB)^IW+HGO+Iogf_hCk(^*V~NE zn{S(TaGMQWJf5u6vD11sZf7Az9*y2D+xCCunK9Q79Y@H2o8Bd0xE)@ah^W7b1e2|A z_hg_aF35UawROBlKC(F4#Lg~NEnCrqL8e-Pw)Swk59~5pD&iBMZLK1>8v5wQ=4gEq zXI2pgk$u4ZR$pBJ?#!3E6K_l8hw?;=PJrZPQ2=)8H~6sUS< zK+YSd6j>H_@O)aXm zs3H_DUkP4~uuhW-T;2^@2IYJu*np95m!T(a*|hp+>n~p{@V0@yN@jG5xYTIa(wyQT z#N@v^a_^4Tm@hh&OjUyC_uboG9lp8}qpT(OQt?l$I?6r4={xCNDeSE+!l0D8U4O@h z2;3ESJvGX{7+RJg@#(=%cQNVNC@n=>6Zxo&CXvMnd{z5NN2~P`K{prGe>hf~*0rYp z$WM%5s8YX{y{IRP_=BcjZ2DP`@TrHBgOPMqFwQ@BeLS=vR1!e@GZ1XY6#I_gdkz=y zE~guBW*9ufdYUW?yTlk~O1N}AS7C_y{;}uWBL_3q2RvWbt{SG}ItLUN?9VBD7$?s4 zgf1ga&B;wA$vY~JdG#I(Pq(2hl#_#2cK5a*shW+1|g`L~7Cz ziEfTkxjpPQ500$Y&KmMO7;`j2lsUD9fmuF|NAKiaUdoV>jf7Eb?G|0n)Xjs}zV5l? zinWQaByE^HMMyETC1jh8XH38r%7NXO9Wie|p6hh-i4N_1NQ_6zLsy+D*UM~6avv!D@S`bj61DdJ9!@Okss6g?&?h6T zoEd^$+?eYvpV5(SydUstWR14}|Jn;K2wuZjwU;oV*yCl%nbsVON#sBmUp**+!jTcNItOZG8}@!qSZX#k1SK8V{k1<>cvIQ@a}6Q!!t6K4 zE1K*-{1&WvvG+Dy(Am(d(yjyjBAK)x_pajhr$IeRmbx=**7S_Gl!%YSUnw4}n#ctY z{lg~Whn5(G5q}}gm|F9D;|v}Lrd_m2$?JbXbRjUYJn#6qHwbX`Kb%s!sREmW4K7_= zTn&3uGQ7iyA4SdV+lvaY*Pjxe=H|H*j;^Dzz491TOTxSN1x3#&+`NnLwyxt}f$)7b z6!@aajm56?$-La=hwb!B<5^lEIyzHDHK7?ME^j=dZ* zkob-ebm%yXYyqPY;ai%u=JYOfoWz-w#Z7GWx~x&u6^nk4mIuh<=aJ7p%y&${I6i_Z ztY-AHJCtT<(<_05c0=)E}+D;Ihz8%U=XiT%-Mo};~KWST2RCohr69IurgFu$vTH(jn zpGs1DH{Be@mfn)Y`N}BgP8SmMJTk@0$8zO3Oapdloil|TI4#%pj2&^!@q&||?+*;m z(_%KY)Db&l$xL^NTRr_;x$u;v5c&7@>)p!b5-mlsRRW(gqY*V_KXzlV{xZyvMc{O` z5sh3!7WfA1X6Dw}PB6l5>}Pv(@;Mh|oh9X2MWh)H49CosJrZV)eY0(ywHo-`r4tEUEM#6JF^(1A>7a zQD1*jCKx_(Ga{tQ({^W#o!yS}_3*FNeUS{nL zO^oy99MZ*0hIUt&uqOCa>MzGr=Y9FD$eFhHP4o@AO5%gHugy{5?=}vVRmo9ZECV%! zxf;pW=gy3SdzAQ5HSX_%-YOsD$}KB$n~k@{v>cB7b--L&N!`eTJAM#&M& zW3=;S5xesk$S&yAFaAb(>p@Ql$n%o?-`z|!`b zOunk{?tYviSU8Z`J4L+dq(V;MT|6DW`bta9wlQ?Usx8hTd+=n5}z$NPcxsI zu$9>a#pGKP|1%D*HadFQXk2)+XvBPpN00AW(a}ZCjKm~=Ba}_hnD<~M)?;eoIigjW zavDDM(c`m~kor$`fGGIwz@(-iw)fJ;`S_U|pU{gyUXXG0g@{&^SN-O9Yx2eKaR!KC zyLN1KmP)f}BfZ}N{g@nivFnHJD|DrwRA}*y^e6Xrn@D_dsi?rF=e#B~qxwK;#l%y|cx|40B^Y1#3#5 z%~FYq=X7Vg$BBqPDC-1pUtOdIo{8G+^@ga^(t?>pnBFb*rj+(%pEM{d>~HkRc+NDv zb8fmFUYM6JNtK1Q=F)93oLTW6+8^hQzMb#&f|+W*^tvdV*`#zPDk_JhZCmhj4e*ZxS z!G1Zr_EMK!FqE_r09O+*t>;P#Rp8lu?wQv4M`t-^m^e^I$RrDs7->z!YF0 zaU8AOBVQwUQxmQH^X80>&2O*%E)VBp&pTqmy7nVWx68M2cZ}UTQTeY%T4gbs$4W4G zZ&gghjB1|2pLA;#RV)8qzuJSrAr5GG2jtw?RC?86>S$0$8oSjll7`FabQOM?2a@a; z8^H6X9BSw0nG5~hCe|KqL_Ye!W-2lD2y`aI9z1S+VYsBy33|dUH6sYM-BRihU}onI znc207D0~wS%LR@<)i{^|%p9nI@(IdZMqC9Sqb5)=k`#+uum)Ha{RJ za{SDSeR}U!>zUq30|k+#kcjS~1h+d~d9VQ4*A^#^ajAJ8rGlGLo=EPWH6QO)36>=b3;gie z#}8^o^#{N>hV+_P1{~ZFw?fo~@t}oquPa`+)p3C0-dJBYQ|R5E9^&^$f8O{|S2`7t zXl>@5XfKEg=4C|w6vyi znBDssAd*a1)yN@h-y5*s2NE*3B``G|W*Mr9eP@!1-U&pOj^tM#yB6IscLtS8d{I!` z;J?y3vRBR!Rkvw`M%pY6?s#2=(sXQQ&2#klvjx87)4rTEs6bfD65B2gJZx|sEI>$UQWSE|YkS{fhH9(Lsddh2>T5XtIkG*;s zFA=70TB&kGmRW)ULe!hKBpF2V#I)098hl0Is zoIz9jNZ}MgnMdwr0+aW5+Mya(8&~vfDVz)}mkx4^PX=!Q*pCQxWQnv@-L-%h9)j6* z`Sus6{1e;AxZp&KCBwk^yqxP=p9yMJ+W~E?>k<2OFlF8cN;uk#<#jGd9OvwMXCt)G zggx?3ayXD8u%AGo3KF!o;scSt?Czu-5tY3H@bsbh=p@&WI3DO`JxSTle)Yynr}vt9 z8%k(*@t&_J>r1kh-=qZo;P_R6u#U`8QnIWFwMCnOuU?4*wqcEBJ4a2zWrYKwQ z{mnr|c9j+N-WlO;lsYloV5ZR*$Op@G)?g+uXRT<#^!-7Fv5#>!J}FH2OCX|7Iq(as z+qmLG3-+p<%}ZQ%cU&)3&P+(!EF0BT6c`kr6p}@QwQ6R@V^99~#V-+9;5JWctipQ> zASXS1A+5<6pkiK{Q_JoDz87T-I7t3I3I|ZZg70NdfzvNxMjmth_w;^nuydcn&HoMj^f<|DD1|1Jxp!~YujUzY*__HO|F s3m5;v*?+i_@% literal 0 HcmV?d00001 From 9189bda68dec1589895a1dfc1d86ba775b27c5cb Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Tue, 22 Sep 2020 01:35:58 +0330 Subject: [PATCH 078/108] - Adjusted the Headline cells. - Added Localization title to Headline cells. --- .../Cells/ArticleRowCollectionViewCell.swift | 10 ++- .../Cells/ArticleRowCollectionViewCell.xib | 3 + ...rticleWebContainerCollectionViewCell.swift | 15 ++++- .../ArticleWebContainerCollectionViewCell.xib | 9 +-- .../HalfWidthArticleCollectionViewCell.swift | 13 +++- .../HeadlineBaseCollectionViewCell.swift | 17 +++++ .../Cells/MainArticleCollectionViewCell.swift | 16 ++++- .../Cells/MainArticleCollectionViewCell.xib | 62 +++++++++++++------ 8 files changed, 113 insertions(+), 32 deletions(-) diff --git a/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.swift b/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.swift index 48d97ec..eb8ac6a 100644 --- a/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.swift +++ b/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.swift @@ -32,7 +32,15 @@ class ArticleRowCollectionViewCell: HeadlineBaseCollectionViewCell { sourceLabel.text = nil dateLabel.text = nil imageView.image = nil - + imageView.cancelCurrentImageLoad() + } + + override func config(viewModel article: ArticleRepresentable) { + titleLabel.text = article.title + descriptionLabel.text = article.description + sourceLabel.text = article.source + imageView.setImage(url: article.urlToImage) + dateLabel.text = article.publishedAt } } diff --git a/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.xib b/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.xib index ce0218e..530a164 100644 --- a/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.xib +++ b/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.xib @@ -50,6 +50,9 @@ + + +
+ From 9ad15436b7a23b3a8f5c0a3faf9e57089ff19604 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Tue, 22 Sep 2020 01:38:37 +0330 Subject: [PATCH 079/108] - Added AppDIContainer. - Added String+Localization Extensions. - Added GradientViews. - Added SDWebImage library in pod file. --- DutchNews.xcodeproj/project.pbxproj | 28 +++ DutchNews/AppDIContainer.swift | 67 ++++++++ DutchNews/AppDelegate.swift | 2 +- .../Extensions/String+Localization.swift | 24 +++ .../UI/UIImageView+SDWebImage.swift | 2 +- .../Extensions/UI/UILabel+Localization.swift | 3 +- .../UI/UINavigationBar+Additionals.swift | 2 +- DutchNews/Classes/Utilites/Logger.swift | 105 ++++++++++++ .../Views/GradientView/GradientView.swift | 133 +++++++++++++++ .../GradientView/LinearGradientView.swift | 159 ++++++++++++++++++ Podfile | 1 + Podfile.lock | 8 +- 12 files changed, 529 insertions(+), 5 deletions(-) create mode 100644 DutchNews/AppDIContainer.swift create mode 100644 DutchNews/Classes/Extensions/String+Localization.swift create mode 100644 DutchNews/Classes/Utilites/Logger.swift create mode 100644 DutchNews/Classes/Views/GradientView/GradientView.swift create mode 100644 DutchNews/Classes/Views/GradientView/LinearGradientView.swift diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index 086175e..777c630 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -29,6 +29,8 @@ F82C8EFD2516304D002B27B3 /* APIClientServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8EFC2516304D002B27B3 /* APIClientServiceTests.swift */; }; F82C8EFF25163073002B27B3 /* NetworkMocking.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8EFE25163073002B27B3 /* NetworkMocking.swift */; }; F82C8F0125163898002B27B3 /* NetworkMockingDataFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8F0025163898002B27B3 /* NetworkMockingDataFactory.swift */; }; + F841DD0A25195415006E7E90 /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F841DD0925195415006E7E90 /* GradientView.swift */; }; + F841DD0C25195429006E7E90 /* LinearGradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F841DD0B25195429006E7E90 /* LinearGradientView.swift */; }; F858997B25176DC800A6BA2A /* Article.swift in Sources */ = {isa = PBXBuildFile; fileRef = F858997A25176DC800A6BA2A /* Article.swift */; }; F858997D25176E8F00A6BA2A /* ArticleSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F858997C25176E8F00A6BA2A /* ArticleSource.swift */; }; F8589980251772CC00A6BA2A /* ModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F858997F251772CC00A6BA2A /* ModelTests.swift */; }; @@ -79,6 +81,9 @@ F8E5C12D25191DA60083D2B1 /* UIViewController+StoryboardName.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C12725191DA50083D2B1 /* UIViewController+StoryboardName.swift */; }; F8E5C12E25191DA60083D2B1 /* UIImageView+SDWebImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C12825191DA60083D2B1 /* UIImageView+SDWebImage.swift */; }; F8E5C12F25191DA60083D2B1 /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C12925191DA60083D2B1 /* UIColor+Extension.swift */; }; + F8E5C1312519250E0083D2B1 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C1302519250E0083D2B1 /* Logger.swift */; }; + F8E5C13325193A290083D2B1 /* String+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C13225193A290083D2B1 /* String+Localization.swift */; }; + F8E5C135251943CF0083D2B1 /* AppDIContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C134251943CF0083D2B1 /* AppDIContainer.swift */; }; F8F14C74250D719800C24FF5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F8F14C72250D719800C24FF5 /* Main.storyboard */; }; /* End PBXBuildFile section */ @@ -123,6 +128,8 @@ F82C8EFC2516304D002B27B3 /* APIClientServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClientServiceTests.swift; sourceTree = ""; }; F82C8EFE25163073002B27B3 /* NetworkMocking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMocking.swift; sourceTree = ""; }; F82C8F0025163898002B27B3 /* NetworkMockingDataFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMockingDataFactory.swift; sourceTree = ""; }; + F841DD0925195415006E7E90 /* GradientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientView.swift; sourceTree = ""; }; + F841DD0B25195429006E7E90 /* LinearGradientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinearGradientView.swift; sourceTree = ""; }; F858997A25176DC800A6BA2A /* Article.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Article.swift; sourceTree = ""; }; F858997C25176E8F00A6BA2A /* ArticleSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleSource.swift; sourceTree = ""; }; F858997F251772CC00A6BA2A /* ModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelTests.swift; sourceTree = ""; }; @@ -177,6 +184,9 @@ F8E5C12725191DA50083D2B1 /* UIViewController+StoryboardName.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+StoryboardName.swift"; sourceTree = ""; }; F8E5C12825191DA60083D2B1 /* UIImageView+SDWebImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImageView+SDWebImage.swift"; sourceTree = ""; }; F8E5C12925191DA60083D2B1 /* UIColor+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extension.swift"; sourceTree = ""; }; + F8E5C1302519250E0083D2B1 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; + F8E5C13225193A290083D2B1 /* String+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Localization.swift"; sourceTree = ""; }; + F8E5C134251943CF0083D2B1 /* AppDIContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDIContainer.swift; sourceTree = ""; }; F8F14C73250D719800C24FF5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; /* End PBXFileReference section */ @@ -270,6 +280,15 @@ path = Helper; sourceTree = ""; }; + F841DD08251953F9006E7E90 /* GradientView */ = { + isa = PBXGroup; + children = ( + F841DD0925195415006E7E90 /* GradientView.swift */, + F841DD0B25195429006E7E90 /* LinearGradientView.swift */, + ); + path = GradientView; + sourceTree = ""; + }; F858997925176D7700A6BA2A /* Models */ = { isa = PBXGroup; children = ( @@ -365,6 +384,7 @@ F89B021D250D446000B41293 /* AppDelegate.swift */, F89B0224250D446200B41293 /* Info.plist */, F865F74F25168F05001FD067 /* AppConfig.swift */, + F8E5C134251943CF0083D2B1 /* AppDIContainer.swift */, ); path = DutchNews; sourceTree = ""; @@ -427,6 +447,7 @@ F8F14C6A250D70BD00C24FF5 /* Views */ = { isa = PBXGroup; children = ( + F841DD08251953F9006E7E90 /* GradientView */, F8E5C0E7251881400083D2B1 /* AlertView */, F8154D5A251800E400BFB42C /* Cells */, ); @@ -460,6 +481,7 @@ isa = PBXGroup; children = ( F8154D532517EBE300BFB42C /* HeadlineLayoutConfiguration */, + F8E5C1302519250E0083D2B1 /* Logger.swift */, ); path = Utilites; sourceTree = ""; @@ -486,6 +508,7 @@ F8E5C11C25191D5D0083D2B1 /* String+EmptyChecking.swift */, F8E5C11D25191D5D0083D2B1 /* URL+ApplicationPath.swift */, F8154D782518207B00BFB42C /* String+HTML.swift */, + F8E5C13225193A290083D2B1 /* String+Localization.swift */, ); path = Extensions; sourceTree = ""; @@ -754,6 +777,7 @@ F8154D752518156000BFB42C /* HeadlinesViewController+DataSource.swift in Sources */, F8154D6C25180A9000BFB42C /* HeadlineBaseCollectionViewCell.swift in Sources */, F865F76B2516C08C001FD067 /* NetworkValidResponse.swift in Sources */, + F841DD0A25195415006E7E90 /* GradientView.swift in Sources */, F8E5C12325191D5E0083D2B1 /* URL+ApplicationPath.swift in Sources */, F8154D612518011500BFB42C /* MainArticleCollectionViewCell.swift in Sources */, F8E5C0F1251884A20083D2B1 /* HeadlineSearchViewModel.swift in Sources */, @@ -765,6 +789,8 @@ F865F75025168F05001FD067 /* AppConfig.swift in Sources */, F8E5C0FE25190CDD0083D2B1 /* HeadlineCellViewModel.swift in Sources */, F865F7642516A9D5001FD067 /* APIServerResponse.swift in Sources */, + F8E5C13325193A290083D2B1 /* String+Localization.swift in Sources */, + F841DD0C25195429006E7E90 /* LinearGradientView.swift in Sources */, F8E5C0F52518DD3E0083D2B1 /* ViewModelState.swift in Sources */, F8E5C0FC2518E9150083D2B1 /* HeadlinesSearchingUseCases.swift in Sources */, F8154D572517F03600BFB42C /* ArticleHeadlineLayoutConfiguration.swift in Sources */, @@ -783,7 +809,9 @@ F8154D692518016800BFB42C /* HalfWidthArticleCollectionViewCell.swift in Sources */, F8E5C12025191D5E0083D2B1 /* Date+TimeAgo.swift in Sources */, F865F76D2516C826001FD067 /* DefaultAPIValidResponse.swift in Sources */, + F8E5C1312519250E0083D2B1 /* Logger.swift in Sources */, F858997D25176E8F00A6BA2A /* ArticleSource.swift in Sources */, + F8E5C135251943CF0083D2B1 /* AppDIContainer.swift in Sources */, F8E5C12225191D5E0083D2B1 /* String+EmptyChecking.swift in Sources */, F89B021E250D446000B41293 /* AppDelegate.swift in Sources */, F8E5C12C25191DA60083D2B1 /* UILabel+Localization.swift in Sources */, diff --git a/DutchNews/AppDIContainer.swift b/DutchNews/AppDIContainer.swift new file mode 100644 index 0000000..7c02237 --- /dev/null +++ b/DutchNews/AppDIContainer.swift @@ -0,0 +1,67 @@ +// +// AppDIContainer.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import RxSwift +import Alamofire + +struct AppDIContainer { + + //////////////////////////////////////////////////////////////// + // MARK: - + // MARK: Authroization DI Container + // MARK: - + //////////////////////////////////////////////////////////////// + + static let authenticator: RequestInterceptor = { + return APIAuthenticator(token: AppConfig.APIKey) + }() + + static let authorizedNetworkService: NetworkServiceInterceptable = { + let session = Session() + session.sessionConfiguration.requestCachePolicy = .reloadIgnoringLocalCacheData + let apiClient = APIClientService(baseURL: AppConfig.BaseURL, session: session, decoder: decoder) + apiClient.addingRequest(interceptor: authenticator) + return apiClient + }() + + static let networkService: NetworkServiceInterceptable = { + let session = Session() + session.sessionConfiguration.requestCachePolicy = .reloadIgnoringLocalCacheData + return APIClientService(baseURL: AppConfig.BaseURL, session: session, decoder: decoder) + }() + + static let decoder: DataDecoder = { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + return decoder + }() + + //////////////////////////////////////////////////////////////// + // MARK: - + // MARK: Repository DI Container + // MARK: - + //////////////////////////////////////////////////////////////// + + static var headlineArticleRepository: ArticleRepository { + return HeadlinesArticleRemoteRepository(networkService: authorizedNetworkService, + authentictor: authenticator, + validator: DefaultAPIValidResponse()) + } + + //////////////////////////////////////////////////////////////// + // MARK: - + // MARK: Use Cases DI Container + // MARK: - + //////////////////////////////////////////////////////////////// + + static var headlineFetchingUseCase: HeadlinesUseCases { + return HeadlinesFetchingUseCase(repository: headlineArticleRepository) + } + +} diff --git a/DutchNews/AppDelegate.swift b/DutchNews/AppDelegate.swift index 4ffa404..f81b12f 100644 --- a/DutchNews/AppDelegate.swift +++ b/DutchNews/AppDelegate.swift @@ -15,7 +15,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. - + _ = Logger() return true } diff --git a/DutchNews/Classes/Extensions/String+Localization.swift b/DutchNews/Classes/Extensions/String+Localization.swift new file mode 100644 index 0000000..65f61bc --- /dev/null +++ b/DutchNews/Classes/Extensions/String+Localization.swift @@ -0,0 +1,24 @@ +// +// String+Localization.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation + +extension String { + + var localized: String { + return localized(withComment: "") + } + + func localized(withComment: String) -> String { + return NSLocalizedString(self, + tableName: nil, + bundle: Bundle.main, + value: "", comment: withComment) + } + +} diff --git a/DutchNews/Classes/Extensions/UI/UIImageView+SDWebImage.swift b/DutchNews/Classes/Extensions/UI/UIImageView+SDWebImage.swift index 9ba96e4..ab770df 100644 --- a/DutchNews/Classes/Extensions/UI/UIImageView+SDWebImage.swift +++ b/DutchNews/Classes/Extensions/UI/UIImageView+SDWebImage.swift @@ -14,7 +14,7 @@ extension UIImageView { static let defaultSDWebImageOptions: SDWebImageOptions = [.lowPriority,.scaleDownLargeImages,.retryFailed,.refreshCached] static let cacheSDWebImageOptions: SDWebImageOptions = [.lowPriority,.scaleDownLargeImages,.queryMemoryData,.refreshCached] - func setImage(url: URL?,placeHolderImage: UIImage? = #imageLiteral(resourceName: "placeHolderImage"), + func setImage(url: URL?,placeHolderImage: UIImage? = #imageLiteral(resourceName: "image-placeHolder"), options: SDWebImageOptions = UIImageView.defaultSDWebImageOptions, completed: SDExternalCompletionBlock? = nil) { self.contentMode = .scaleAspectFill diff --git a/DutchNews/Classes/Extensions/UI/UILabel+Localization.swift b/DutchNews/Classes/Extensions/UI/UILabel+Localization.swift index 27d442f..60f176d 100644 --- a/DutchNews/Classes/Extensions/UI/UILabel+Localization.swift +++ b/DutchNews/Classes/Extensions/UI/UILabel+Localization.swift @@ -28,7 +28,8 @@ extension UILabel { guard let value = newValue else { return } - self.text = NSLocalizedString(value, comment: "") + + self.text = value.localized } } diff --git a/DutchNews/Classes/Extensions/UI/UINavigationBar+Additionals.swift b/DutchNews/Classes/Extensions/UI/UINavigationBar+Additionals.swift index c2f7f92..898a1e1 100644 --- a/DutchNews/Classes/Extensions/UI/UINavigationBar+Additionals.swift +++ b/DutchNews/Classes/Extensions/UI/UINavigationBar+Additionals.swift @@ -18,7 +18,7 @@ extension UINavigationItem { } set { guard let newValue = newValue else { self.title = nil; return } - self.title = NSLocalizedString(newValue, comment: "") + self.title = newValue.localized } } diff --git a/DutchNews/Classes/Utilites/Logger.swift b/DutchNews/Classes/Utilites/Logger.swift new file mode 100644 index 0000000..ea69a95 --- /dev/null +++ b/DutchNews/Classes/Utilites/Logger.swift @@ -0,0 +1,105 @@ +// +// Logger.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/21/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import CocoaLumberjack + +/// App Logger class, in order to log async in xcode project. +struct Logger { + + /// Log the message passed into function with debug level. + /// + /// - Parameters: + /// - message: The message will be log + /// - file: The file name which the method called by. + /// - function: The function name which log method called in. + /// - line: The line number of file + /// - tag: The external tags which provide more information. + static func debugLog(_ message: @autoclosure () -> String, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, tag: Any? = nil) { + DDLogDebug(message(), file: file, function: function, line: line, tag: tag) + } + + /// Log the message passed into function with info level. + /// + /// - Parameters: + /// - message: The message will be log + /// - file: The file name which the method called by. + /// - function: The function name which log method called in. + /// - line: The line number of file + /// - tag: The external tags which provide more information. + static func infoLog(_ message: @autoclosure () -> String, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, tag: Any? = nil) { + DDLogInfo(message(), file: file, function: function, line: line, tag: tag) + } + + /// Log the message passed into function with warn level. + /// + /// - Parameters: + /// - message: The message will be log + /// - file: The file name which the method called by. + /// - function: The function name which log method called in. + /// - line: The line number of file + /// - tag: The external tags which provide more information. + static func warnLog(_ message: @autoclosure () -> String, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, tag: Any? = nil) { + DDLogWarn(message(), file: file, function: function, line: line, tag: tag) + } + + /// Log the message passed into function with verbose level. + /// + /// - Parameters: + /// - message: The message will be log + /// - file: The file name which the method called by. + /// - function: The function name which log method called in. + /// - line: The line number of file + /// - tag: The external tags which provide more information. + static func verboseLog(_ message: @autoclosure () -> String, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, tag: Any? = nil) { + DDLogVerbose(message(), file: file, function: function, line: line, tag: tag) + } + + /// Log the message passed into function with error level. + /// - Note: this method log and effect immediately after called. + /// - Parameters: + /// - message: The message will be log + /// - file: The file name which the method called by. + /// - function: The function name which log method called in. + /// - line: The line number of file + /// - tag: The external tags which provide more information. + static func errorLog(_ message: @autoclosure () -> String, file: StaticString = #file, function: StaticString = #function, line: UInt = #line, tag: Any? = nil) { + DDLogError(message(), file: file, function: function, line: line, tag: tag) + } + + /** File Logger variable */ + let fileLogger = DDFileLogger() + + /// default instance of Logger + static let `default` = Logger() + + /// Default Constructor for Logger + /// Set Log Level depend on Scheme Configuration + /// Note: Debug Configuration level is all, otherwise leve is waring + init() { + + #if DEBUG + dynamicLogLevel = DDLogLevel.all + #else + dynamicLogLevel = DDLogLevel.warning + #endif + + if let ddttyLogger = DDTTYLogger.sharedInstance { + DDLog.add(ddttyLogger) + } + + fileLogger.rollingFrequency = TimeInterval(60 * 60 * 24) // 24 hours + fileLogger.logFileManager.maximumNumberOfLogFiles = 3 + DDLog.add(fileLogger) + + } + +} + +/// Load Logger +private let logger = Logger.default diff --git a/DutchNews/Classes/Views/GradientView/GradientView.swift b/DutchNews/Classes/Views/GradientView/GradientView.swift new file mode 100644 index 0000000..e36596a --- /dev/null +++ b/DutchNews/Classes/Views/GradientView/GradientView.swift @@ -0,0 +1,133 @@ +// +// GradientView.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/22/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import UIKit + +@IBDesignable +class GradientView: UIView { + + open override class var layerClass: Swift.AnyClass { + return CAGradientLayer.self + } + + var gradientLayer: CAGradientLayer { + // swiftlint:disable:next force_cast + return self.layer as! CAGradientLayer + } + + @IBInspectable + var topColor: UIColor? = .white { + didSet { + self.updateGradiant(colors: (top: topColor, bottom: bottomColor)) + self.setNeedsDisplay() + } + } + + @IBInspectable + var bottomColor: UIColor? = .gray { + + didSet { + self.updateGradiant(colors: (top: topColor, bottom: bottomColor)) + self.setNeedsDisplay() + } + } + + @IBInspectable + var startPoint: CGPoint = .zero { + didSet { + self.gradientLayer.startPoint = startPoint + self.setNeedsDisplay() + self.setNeedsLayout() + } + } + + @IBInspectable + var endPoint: CGPoint = .zero { + didSet { + self.gradientLayer.endPoint = endPoint + self.setNeedsLayout() + self.setNeedsDisplay() + } + } + + @IBInspectable + var startLocation: Float = 0.0 { + didSet { + self.update(locations: [self.startLocation, endLocation]) + } + } + + @IBInspectable + var endLocation: Float = 1.0 { + didSet { + + self.update(locations: [self.startLocation, endLocation]) + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + self.commonInit() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + self.commonInit() + } + + override func prepareForInterfaceBuilder() { +// self.commonInit() + super.prepareForInterfaceBuilder() + + } + + override func layoutSubviews() { + super.layoutSubviews() +// self.gradientLayer.frame = self.bounds + } + + private func commonInit() { + + self.gradientLayer.startPoint = startPoint + self.gradientLayer.endPoint = endPoint + self.update(locations: [self.startLocation, self.endLocation]) + self.updateGradiant(colors: (top: topColor, bottom: bottomColor)) + + } + + private func updateGradiant(colors : (top: UIColor?, bottom: UIColor?)) { + + switch colors { + case (.some(let up), .some(let down)): + self.setGradientColor(colors: [up, down]) + break + case (.some(let color), .none), + (.none, .some(let color)): + self.setGradientColor(colors: [color]) + break + + default: + self.self.setGradientColor(colors: nil) + } + + } + + private func setGradientColor(colors: [UIColor]?) { + self.gradientLayer.colors = colors?.map { $0.cgColor } + self.setNeedsDisplay() + self.setNeedsLayout() + } + + private func update(locations: [Float]) { + self.gradientLayer.locations = locations.map { NSNumber(value: $0) } + self.setNeedsDisplay() + self.setNeedsLayout() + } + +} diff --git a/DutchNews/Classes/Views/GradientView/LinearGradientView.swift b/DutchNews/Classes/Views/GradientView/LinearGradientView.swift new file mode 100644 index 0000000..5886900 --- /dev/null +++ b/DutchNews/Classes/Views/GradientView/LinearGradientView.swift @@ -0,0 +1,159 @@ +// +// LinearGradientView.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/22/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import UIKit + +class LinearGradientLayer: CALayer { + + /// <#Description#> + /// + /// - vertical: <#vertical description#> + /// - horizontal: <#horizontal description#> + /// - custom: <#custom description#> + /// - let.custom:: <#let.custom: description#> + enum Direction { + case vertical + case horizontal + case custom(start: CGPoint, end: CGPoint) + + var points: (start: CGPoint, end: CGPoint) { + switch self { + case .vertical: + return (CGPoint(x: 0.5, y: 0.0), CGPoint(x: 0.5, y: 1.0)) + case .horizontal: + return (CGPoint(x: 0.0, y: 0.5), CGPoint(x: 1.0, y: 0.5)) + case let .custom(start, end): + return (start, end) + } + } + } + + var direction: Direction = .vertical + + var colorSpace = CGColorSpaceCreateDeviceRGB() + var colors: [CGColor]? + var locations: [CGFloat]? + + /// <#Description#> + var options = CGGradientDrawingOptions(rawValue: 0) + + // MARK: - Lifecycle + + /// <#Description#> + required override init() { + super.init() + masksToBounds = true + needsDisplayOnBoundsChange = true + } + + /// <#Description#> + /// + /// - Parameter aDecoder: <#aDecoder description#> + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + /// <#Description#> + /// + /// - Parameter layer: <#layer description#> + required override init(layer: Any) { + super.init(layer: layer) + } + + /// <#Description#> + /// + /// - Parameter ctx: <#ctx description#> + override func draw(in ctx: CGContext) { + ctx.saveGState() + + guard let colors = colors, let gradient = CGGradient(colorsSpace: colorSpace, + colors: colors as CFArray, locations: locations) else { return } + + let points = direction.points + ctx.drawLinearGradient( + gradient, + start: transform(points.start), + end: transform(points.end), + options: options + ) + } + + // MARK: - Private + + /// <#Description#> + /// + /// - Parameter point: <#point description#> + /// - Returns: <#return value description#> + private func transform(_ point: CGPoint) -> CGPoint { + return CGPoint(x: bounds.width * point.x, y: bounds.height * point.y) + } +} + +/// <#Description#> +class LinearGradientView: UIView { + + /// <#Description#> + lazy var gradientLayer = layer as? LinearGradientLayer + + /// <#Description#> + override class var layerClass: AnyClass { + return LinearGradientLayer.self + } + + /// <#Description#> + var direction: LinearGradientLayer.Direction = .vertical { + didSet { + updateGradient(with: direction, colors: colors) + } + } + + /// <#Description#> + var colors: [UIColor] = [UIColor.white, UIColor.blue] { + didSet { + guard colors != oldValue else { + return + } + updateGradient(with: direction, colors: colors) + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + commonInit() + } + + /// <#Description#> + /// + /// - Parameters: + /// - direction: <#direction description#> + /// - colors: <#colors description#> + func updateGradient(with direction: LinearGradientLayer.Direction, colors: UIColor...) { + gradientLayer?.direction = direction + gradientLayer?.colors = colors.compactMap { color in + color.cgColor + } + } + + func updateGradient(with direction: LinearGradientLayer.Direction, colors: [UIColor]) { + gradientLayer?.direction = direction + gradientLayer?.colors = colors.compactMap { color in + color.cgColor + } + } + + private func commonInit() { + updateGradient(with: direction, colors: colors) + } + +} diff --git a/Podfile b/Podfile index f83db76..044b2d8 100644 --- a/Podfile +++ b/Podfile @@ -30,6 +30,7 @@ target 'DutchNews' do pod 'MaterialComponents' pod 'SwiftLint' pod 'CryptoSwift', '1.1.2' + pod 'SDWebImage' #Logger Framework pod 'CocoaLumberjack/Swift' diff --git a/Podfile.lock b/Podfile.lock index fc6bf4d..fb8f898 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -676,6 +676,9 @@ PODS: - RxSwift (5.1.1) - RxTest (5.1.1): - RxSwift (~> 5) + - SDWebImage (5.9.1): + - SDWebImage/Core (= 5.9.1) + - SDWebImage/Core (5.9.1) - SwiftLint (0.39.2) DEPENDENCIES: @@ -697,6 +700,7 @@ DEPENDENCIES: - RxDataSources - RxSwift - RxTest + - SDWebImage - SwiftLint SPEC REPOS: @@ -725,6 +729,7 @@ SPEC REPOS: - RxRelay - RxSwift - RxTest + - SDWebImage - SwiftLint EXTERNAL SOURCES: @@ -762,8 +767,9 @@ SPEC CHECKSUMS: RxRelay: d77f7d771495f43c556cbc43eebd1bb54d01e8e9 RxSwift: 81470a2074fa8780320ea5fe4102807cb7118178 RxTest: 711632d5644dffbeb62c936a521b5b008a1e1faa + SDWebImage: a990c053fff71e388a10f3357edb0be17929c9c5 SwiftLint: 22ccbbe3b8008684be5955693bab135e0ed6a447 -PODFILE CHECKSUM: 642d3a8ac7db7cbfe1ddc2c86cd18cf4177143c5 +PODFILE CHECKSUM: ceb1a84305b4be47ad7dd206a4d7f422db78790b COCOAPODS: 1.9.3 From 3ff761a8bf1aa166ef45b9542a56dbc35733de2b Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Tue, 22 Sep 2020 01:51:52 +0330 Subject: [PATCH 080/108] - Implemented Headline feature logic. - Attached Headline Logic to Views. --- .../HeadlinesViewController+DataSource.swift | 67 +++-------- .../HeadlinesViewController.swift | 110 +++++++++++++++++- .../Classes/ViewModels/ArticleViewModel.swift | 29 +++++ .../ViewModels/HeadlineCellViewModel.swift | 19 ++- .../ViewModels/HeadlinesViewModel.swift | 46 ++++++-- .../ModelsTests/ModelsDataFactory.swift | 6 +- .../Helper/NetworkMockingDataFactory.swift | 4 + 7 files changed, 212 insertions(+), 69 deletions(-) diff --git a/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift b/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift index d93696a..8cc0694 100644 --- a/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift +++ b/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift @@ -14,22 +14,22 @@ import RxDataSources extension HeadlinesViewController { - typealias SectionType = SectionModel + typealias SectionType = ArticlesViewModel.T func buildDataSource() -> RxCollectionViewSectionedReloadDataSource { - return RxCollectionViewSectionedReloadDataSource(configureCell: {[weak self] (dataSource, collectionView, indexPath, item) -> UICollectionViewCell in + return RxCollectionViewSectionedReloadDataSource(configureCell: {[weak self] (dataSource, collectionView, indexPath, _) -> UICollectionViewCell in guard let `self` = self else { return HeadlineBaseCollectionViewCell() } - let item2 = dataSource[indexPath] + let item = dataSource[indexPath] - let reuseId = self.reuseItentifier(forCellAt: indexPath, item: item2).id + let reuseId = self.reuseItentifier(forCellAt: indexPath, item: item.model).id let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseId, for: indexPath) - self.fill(cell: cell, withArticle: item2) + self.fill(cell: cell, withArticle: item) cell.contentView.layer.borderColor = UIColor.darkGray.cgColor cell.contentView.layer.borderWidth = 0.4 @@ -56,65 +56,32 @@ extension HeadlinesViewController { } } - private func fill(cell: UICollectionViewCell, withArticle article: Article) { + private func fill(cell: UICollectionViewCell, withArticle article: ArticleViewModel) { switch (cell) { - case (let cell as MainArticleCollectionViewCell): - cell.titleLabel.text = article.title - case (let cell as HalfWidthArticleCollectionViewCell): - cell.titleLabel.text = article.title - cell.sourceLabel.text = article.source.name - - case (let cell as ArticleWebContainerCollectionViewCell) where article.type == .mock : + case (let cell as ArticleWebContainerCollectionViewCell) where article.model.type == .mock : if cell.contentLabel.text == nil, - let content = article.content { - + let content = article.model.content { + cell.webView.loadHTMLString(content, baseURL: nil) cell.contentLabel.text = content - //FIXME: determind a observation for callback purpose when loading was finished. - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) {[weak self] in + cell.rx.didLoadContent.bind {[weak self] _ in self?.collectionView.reloadData() self?.collectionLayout?.invalidateLayout() } + .disposed(by: cell.disposeBag) } - - case (let cell as ArticleRowCollectionViewCell): - - print(article) - cell.titleLabel.text = article.title - cell.descriptionLabel.text = article.description - cell.sourceLabel.text = article.source.name - cell.dateLabel.text = article.publishedAt.description(with: Locale.current) - + case (let cell as HeadlineBaseCollectionViewCell): + article.output + .debug() + .drive(onNext: {[weak cell] viewModel in + cell?.config(viewModel: viewModel) + }).disposed(by: cell.disposeBag) default: break } } - func buildMockData() -> SectionType { - - let bundle = Bundle(for: type(of: self)) - guard let data = try? Data(contentsOf: bundle.url(forResource: "HeadlineSuccessResponse", withExtension: "json")!) else { - return SectionType(model: 0, items: []) - } - - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .iso8601 - decoder.dataDecodingStrategy = .deferredToData - - guard let response = try? decoder.decode(APIServerResponse<[Article]>.self, from: data), - var items = response.data else { - return SectionType(model: 0, items: []) - } - - if items.count > 3 { - items.insert(Article.htmlArticle(), at: 3) - } - - return SectionType(model: 0, items: items) - - } - } diff --git a/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift b/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift index 2966f54..64a7803 100644 --- a/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift +++ b/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift @@ -11,6 +11,7 @@ import MagazineLayout import RxCocoa import RxSwift import RxDataSources +import PureLayout class HeadlinesViewController: UIViewController { @@ -37,6 +38,14 @@ class HeadlinesViewController: UIViewController { return refreshControl }() + lazy var loadingIndicator: UIActivityIndicatorView = { + if #available(iOS 13.0, *) { + return UIActivityIndicatorView(style: .large) + } else { + return UIActivityIndicatorView(style: .gray) + } + }() + var layoutConfiguration: HeadlineLayoutConfiguration = ArticleHeadlineLayoutConfiguration() { didSet { collectionView.reloadData() @@ -49,6 +58,8 @@ class HeadlinesViewController: UIViewController { return self.buildDataSource() }() + var viewModel: ArticlesViewModel? = HeadlinesViewModel(useCase: AppDIContainer.headlineFetchingUseCase) + override func viewDidLoad() { super.viewDidLoad() @@ -56,11 +67,8 @@ class HeadlinesViewController: UIViewController { setupLayouts() - let items = self.buildMockData() - Observable.from(optional: [items]) - .bind(to: self.collectionView.rx.items(dataSource: self.dataSource)) - .disposed(by: self.disposeBag) - self.collectionView.delegate = self + bindViewModels() + loadContentsIfNeeded() } @@ -74,8 +82,18 @@ class HeadlinesViewController: UIViewController { } */ + //////////////////////////////////////////////////////////////// + // MARK: - + // MARK: UI Methods + // MARK: - + //////////////////////////////////////////////////////////////// + func setupLayouts() { setupColletionView() + view.addSubview(loadingIndicator) + loadingIndicator.autoCenterInSuperview() + loadingIndicator.hidesWhenStopped = true + } func setupColletionView() { @@ -94,11 +112,93 @@ class HeadlinesViewController: UIViewController { } + func updateLayoutsBase(onState state: ViewModelState) { + switch state { + case .loading(isRefreshing: let isRefreshing) where isRefreshing == false : + loadingIndicator.stopAnimating() + + case .loaded: + guard refreshControl.isRefreshing else { + fallthrough + } + refreshControl.endRefreshing() + fallthrough + case .idle: + loadingIndicator.stopAnimating() + + case .error(let error): + refreshControl.endRefreshing() + loadingIndicator.stopAnimating() + + let message: String + if let err = error as? URLError { + message = err.localizedDescription + }else { + message = error.localizedDescription + } + + presentAlert(message: message, + actionTitle: "retry".localized) {[weak self] in + self?.loadContentsIfNeeded() + } + default: + break + } + } + + //////////////////////////////////////////////////////////////// + // MARK: - + // MARK: View Model Methods + // MARK: - + //////////////////////////////////////////////////////////////// + + func bindViewModels() { + + guard let viewModel = self.viewModel else { + return + } + + bind(viewModel: viewModel) + } + + func bind(viewModel: ArticlesViewModel) { + + viewModel.output.drive(collectionView.rx.items(dataSource: dataSource)).disposed(by: disposeBag) + + // RxSwift assigned another delelgate object after running the upper code + // we have to make sure that current vc present as delegate + collectionView.delegate = self + + viewModel.state.drive(onNext: {[weak self] (state) in + self?.updateLayoutsBase(onState: state) + }).disposed(by: disposeBag) + + refreshControl.rx.controlEvent(.valueChanged) + .bind { [weak self] _ in + self?.loadContentsIfNeeded() + }.disposed(by: disposeBag) + } + + func loadContentsIfNeeded() { + guard refreshControl.isRefreshing else { + viewModel?.fetchArticles() + return + } + + viewModel?.refreshArticles() + } + } extension HeadlinesViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + collectionView.deselectItem(at: indexPath, animated: true) + + let item = dataSource[indexPath] + viewModel?.didSelect(article: item) } } + +extension HeadlinesViewController: AlertableView {} diff --git a/DutchNews/Classes/ViewModels/ArticleViewModel.swift b/DutchNews/Classes/ViewModels/ArticleViewModel.swift index 16a77b2..43ad38b 100644 --- a/DutchNews/Classes/ViewModels/ArticleViewModel.swift +++ b/DutchNews/Classes/ViewModels/ArticleViewModel.swift @@ -10,6 +10,8 @@ import Foundation import RxSwift import RxCocoa +//swiftlint:disable type_name + /// Abstract `ArtileViewModel` protocol ArticleViewModel: class { @@ -24,6 +26,7 @@ protocol ArticleViewModel: class { func buildURLContent() -> Observable } +//swiftlint:enable type_name extension ArticleViewModel { @@ -36,6 +39,24 @@ extension ArticleViewModel { } } +extension ArticleViewModel where Self: Equatable { + + static func ==(lhs: ArticleViewModel, rhs: ArticleViewModel) -> Bool { + guard type(of: lhs) == type(of: rhs) else { + return false + } + return lhs.model.publishedAt == lhs.model.publishedAt + } +} + +func ==(lhs: ArticleViewModel, rhs: ArticleViewModel) -> Bool { + + guard type(of: lhs) == type(of: rhs) else { + return false + } + return lhs.model.publishedAt == lhs.model.publishedAt +} + /// `ArticleRepresentable` is representive of article output protocol ArticleRepresentable { @@ -56,3 +77,11 @@ protocol ArticleRepresentable { var type: ArticleType { get } } + +func ==(lhs: ArticleRepresentable, rhs: ArticleRepresentable) -> Bool { + + guard type(of: lhs) == type(of: rhs) else { + return false + } + return lhs.publishedAt == lhs.publishedAt +} diff --git a/DutchNews/Classes/ViewModels/HeadlineCellViewModel.swift b/DutchNews/Classes/ViewModels/HeadlineCellViewModel.swift index 0462361..9406ae4 100644 --- a/DutchNews/Classes/ViewModels/HeadlineCellViewModel.swift +++ b/DutchNews/Classes/ViewModels/HeadlineCellViewModel.swift @@ -37,18 +37,35 @@ class HeadlineCellViewModel: ArticleViewModel { } private static func convert(model: T) -> ArticleRepresentable { + + let dateFormatter = DateFormatter.currentZoneFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .short + return HeadlineCellOutput(title: model.title, author: model.author, description: model.title, source: model.source.name, url: model.url, urlToImage: model.urlToImage, - publishedAt: "", + publishedAt: dateFormatter.string(from: model.publishedAt), content: model.content, type: model.type) } } +extension HeadlineCellViewModel: Hashable { + + static func == (lhs: HeadlineCellViewModel, rhs: HeadlineCellViewModel) -> Bool { + return lhs.hashValue == rhs.hashValue + } + + func hash(into hasher: inout Hasher) { + hasher.combine(model.title) + hasher.combine(model.publishedAt) + } +} + private extension HeadlineCellViewModel { struct HeadlineCellOutput: ArticleRepresentable { diff --git a/DutchNews/Classes/ViewModels/HeadlinesViewModel.swift b/DutchNews/Classes/ViewModels/HeadlinesViewModel.swift index a813e76..7063d52 100644 --- a/DutchNews/Classes/ViewModels/HeadlinesViewModel.swift +++ b/DutchNews/Classes/ViewModels/HeadlinesViewModel.swift @@ -22,6 +22,12 @@ final class HeadlinesViewModel: ArticlesViewModel { private var outputPublisher: BehaviorRelay<[T]> + private var items: [T.Item] = [] { + didSet { + outputPublisher.accept([T(model: "Headlines", items: items)]) + } + } + var output: Driver<[T]> { outputPublisher.asDriver { _ in return .never() } } @@ -85,21 +91,39 @@ final class HeadlinesViewModel: ArticlesViewModel { .subscribe {[weak self] event in switch event { - case .next(let items): + case .next(let newItems): guard let `self` = self else { break } - // let mapped = newItems.map { - // ArticleViewModel(repository: $0, userRepositoryUseCases: AppDIContainer.userRepositoryUseCases) - // } - // .filter { item in - // !self.items.contains(where: { $0.model.id == item.model.id }) - // } - // .reversed() as Array - // - // self.items += mapped - // + var mapped = newItems.map { + HeadlineCellViewModel(model: $0) + } + + guard case let .loading(isRefreshing:isRefreshing) = self.statePublisher.value, + isRefreshing == true else { + + if mapped.count > 3 { + mapped.insert(HeadlineCellViewModel(model: Article.htmlArticle()), + at: 3) + } + self.items = mapped + self.statePublisher.accept(.loaded) + return + } + + let output = mapped.filter { viewModel in + !self.items.contains(where: { $0 == viewModel }) + } + + var result = output + self.items.filter({ $0.model.type == .news }) + + if result.count > 3 { + result.insert(HeadlineCellViewModel(model: Article.htmlArticle()), + at: 3) + } + self.items = result + self.statePublisher.accept(.loaded) case .error(let error): diff --git a/DutchNewsTests/ModelsTests/ModelsDataFactory.swift b/DutchNewsTests/ModelsTests/ModelsDataFactory.swift index 0d5f0f5..7799e63 100644 --- a/DutchNewsTests/ModelsTests/ModelsDataFactory.swift +++ b/DutchNewsTests/ModelsTests/ModelsDataFactory.swift @@ -12,13 +12,14 @@ import Foundation struct ModelsDataFactory { + //swiftlint:disable all static func createMockArticlesData() -> Data { let bundle = Bundle(for: ModelTests.self) return try! Data(contentsOf: bundle.url(forResource: "Articles", withExtension: "json")!) } static func createCorruptedMockArticlesData() -> Data { - return """ + return """ [{ "source": { "id": null, @@ -123,5 +124,6 @@ struct ModelsDataFactory { static func createCorruptedMockSource() -> Data { return "{ \"id\": null }".data(using: .utf8)! } - + //swiftlint: disable enable + } diff --git a/DutchNewsTests/NetworkTests/Helper/NetworkMockingDataFactory.swift b/DutchNewsTests/NetworkTests/Helper/NetworkMockingDataFactory.swift index c44738a..10c62d8 100644 --- a/DutchNewsTests/NetworkTests/Helper/NetworkMockingDataFactory.swift +++ b/DutchNewsTests/NetworkTests/Helper/NetworkMockingDataFactory.swift @@ -12,6 +12,7 @@ import Mocker import Alamofire @testable import DutchNews +// swiftlint:disable all struct NetworkMockingDataFactory { static func createSimpleJSONData() -> Data { @@ -26,6 +27,7 @@ struct NetworkMockingDataFactory { return data } + static func createRealJSONData() -> Data { fatalError("") } @@ -37,7 +39,9 @@ struct NetworkMockingDataFactory { } struct StubPerson: Codable { + let name: String let age: Int let email: String? } +// swiftlint:enable all From 50c9f6152ea4128bb401960027316f74d884e5e7 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Tue, 22 Sep 2020 02:41:47 +0330 Subject: [PATCH 081/108] - Fixed Bugs. --- .../ArticleHeadlineLayoutConfiguration.swift | 5 +++-- .../HeadlinesViewController+DataSource.swift | 5 ----- DutchNews/Classes/ViewModels/ArticleViewModel.swift | 11 ++++++++--- .../Classes/ViewModels/HeadlineCellViewModel.swift | 1 + .../ArticleWebContainerCollectionViewCell.swift | 13 +++++-------- .../Views/Cells/MainArticleCollectionViewCell.xib | 6 +++--- 6 files changed, 20 insertions(+), 21 deletions(-) diff --git a/DutchNews/Classes/Utilites/HeadlineLayoutConfiguration/ArticleHeadlineLayoutConfiguration.swift b/DutchNews/Classes/Utilites/HeadlineLayoutConfiguration/ArticleHeadlineLayoutConfiguration.swift index 93eb674..78d0d0b 100644 --- a/DutchNews/Classes/Utilites/HeadlineLayoutConfiguration/ArticleHeadlineLayoutConfiguration.swift +++ b/DutchNews/Classes/Utilites/HeadlineLayoutConfiguration/ArticleHeadlineLayoutConfiguration.swift @@ -14,9 +14,10 @@ struct ArticleHeadlineLayoutConfiguration: HeadlineLayoutConfiguration { func itemSizeMode(forItemAt indexPath: IndexPath) -> MagazineLayoutItemSizeMode { switch (indexPath.section, indexPath.row) { - case (_,0), - (_,3): + case (_,0): return sizeModeCreate(widthMode: .fullWidth(respectsHorizontalInsets: true), heightMode: .dynamic) + case (_,3): + return sizeModeCreate(widthMode: .fullWidth(respectsHorizontalInsets: true), heightMode: .static(height: 80)) case (_,1), (_,2): return sizeModeCreate(widthMode: .halfWidth, heightMode: .dynamicAndStretchToTallestItemInRow) diff --git a/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift b/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift index 8cc0694..00533a7 100644 --- a/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift +++ b/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift @@ -67,11 +67,6 @@ extension HeadlinesViewController { cell.webView.loadHTMLString(content, baseURL: nil) cell.contentLabel.text = content - cell.rx.didLoadContent.bind {[weak self] _ in - self?.collectionView.reloadData() - self?.collectionLayout?.invalidateLayout() - } - .disposed(by: cell.disposeBag) } case (let cell as HeadlineBaseCollectionViewCell): article.output diff --git a/DutchNews/Classes/ViewModels/ArticleViewModel.swift b/DutchNews/Classes/ViewModels/ArticleViewModel.swift index 43ad38b..d5dd78c 100644 --- a/DutchNews/Classes/ViewModels/ArticleViewModel.swift +++ b/DutchNews/Classes/ViewModels/ArticleViewModel.swift @@ -45,7 +45,9 @@ extension ArticleViewModel where Self: Equatable { guard type(of: lhs) == type(of: rhs) else { return false } - return lhs.model.publishedAt == lhs.model.publishedAt + return lhs.model.publishedAt == rhs.model.publishedAt && + lhs.model.url == rhs.model.url && + lhs.model.title == rhs.model.title } } @@ -54,7 +56,10 @@ func ==(lhs: ArticleViewModel, rhs: ArticleViewModel) -> Bool { guard type(of: lhs) == type(of: rhs) else { return false } - return lhs.model.publishedAt == lhs.model.publishedAt + + return lhs.model.publishedAt == rhs.model.publishedAt && + lhs.model.url == rhs.model.url && + lhs.model.title == rhs.model.title } /// `ArticleRepresentable` is representive of article output @@ -83,5 +88,5 @@ func ==(lhs: ArticleRepresentable, rhs: ArticleRepresentable) -> Bool { guard type(of: lhs) == type(of: rhs) else { return false } - return lhs.publishedAt == lhs.publishedAt + return lhs.publishedAt == rhs.publishedAt && lhs.url == rhs.url && lhs.title == rhs.title } diff --git a/DutchNews/Classes/ViewModels/HeadlineCellViewModel.swift b/DutchNews/Classes/ViewModels/HeadlineCellViewModel.swift index 9406ae4..38cee4b 100644 --- a/DutchNews/Classes/ViewModels/HeadlineCellViewModel.swift +++ b/DutchNews/Classes/ViewModels/HeadlineCellViewModel.swift @@ -63,6 +63,7 @@ extension HeadlineCellViewModel: Hashable { func hash(into hasher: inout Hasher) { hasher.combine(model.title) hasher.combine(model.publishedAt) + hasher.combine(model.url) } } diff --git a/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.swift b/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.swift index 8c89282..b892c94 100644 --- a/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.swift +++ b/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.swift @@ -22,7 +22,7 @@ class ArticleWebContainerCollectionViewCell: HeadlineBaseCollectionViewCell { super.awakeFromNib() contentLabel.text = nil contentView.addSubview(webView) - webView.autoPinEdgesToSuperviewSafeArea() + webView.scrollView.isScrollEnabled = false contentLabel.isHidden = true } @@ -31,13 +31,9 @@ class ArticleWebContainerCollectionViewCell: HeadlineBaseCollectionViewCell { super.prepareForReuse() } - override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { - - let layoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes) - - layoutAttributes.size.height = max(webView.scrollView.contentSize.height, 44.0) - Logger.debugLog("Height \(layoutAttributes.size.height)") - return layoutAttributes + override func layoutSubviews() { + super.layoutSubviews() + webView.frame = contentView.bounds } } @@ -46,6 +42,7 @@ extension Reactive where Base: ArticleWebContainerCollectionViewCell { var didLoadContent: ControlEvent { let source = self.base.webView.rx.didFinishLoad.map { _ in () } + .delay(.milliseconds(500), scheduler: MainScheduler.instance) return ControlEvent(events: source) } diff --git a/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.xib b/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.xib index abe2a12..34eb04d 100644 --- a/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.xib +++ b/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.xib @@ -24,10 +24,10 @@ - + - + @@ -97,7 +97,7 @@ - + From 8c9637a5ac5df82842e176a1e3c1f50b5b2454ac Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Tue, 22 Sep 2020 03:18:55 +0330 Subject: [PATCH 083/108] -Fixed some autolayout constraints. --- DutchNews/Classes/Utilites/Logger.swift | 2 ++ .../Views/Cells/ArticleRowCollectionViewCell.xib | 14 +++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/DutchNews/Classes/Utilites/Logger.swift b/DutchNews/Classes/Utilites/Logger.swift index ea69a95..7ce03f2 100644 --- a/DutchNews/Classes/Utilites/Logger.swift +++ b/DutchNews/Classes/Utilites/Logger.swift @@ -93,6 +93,8 @@ struct Logger { DDLog.add(ddttyLogger) } + DDLog.add(DDOSLogger.sharedInstance) + fileLogger.rollingFrequency = TimeInterval(60 * 60 * 24) // 24 hours fileLogger.logFileManager.maximumNumberOfLogFiles = 3 DDLog.add(fileLogger) diff --git a/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.xib b/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.xib index 1c66ac5..2686c4b 100644 --- a/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.xib +++ b/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.xib @@ -25,24 +25,24 @@ - - + + - - + From 996630408b02ad76064dc7df603906a82b5592cc Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Tue, 22 Sep 2020 03:33:12 +0330 Subject: [PATCH 084/108] - removed unneed pods. --- Podfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Podfile b/Podfile index 044b2d8..c6785b8 100644 --- a/Podfile +++ b/Podfile @@ -23,7 +23,7 @@ target 'DutchNews' do pod 'PureLayout' pod 'MXParallaxHeader' pod 'Pageboy' - pod 'JEKScrollableSectionCollectionViewLayout', :git => 'https://github.com/farshadmb/JEKScrollableSectionCollectionViewLayout.git' +# pod 'JEKScrollableSectionCollectionViewLayout', :git => 'https://github.com/farshadmb/JEKScrollableSectionCollectionViewLayout.git' pod 'MagazineLayout' pod 'RealmSwift' From 9dbf349df87e094142ed5efab50e30a3c083b5a7 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Wed, 23 Sep 2020 17:01:14 +0330 Subject: [PATCH 085/108] - Added ArticleDetailViewController and ArticlePageViewController. - Added UI Objects relate to article details. --- DutchNews.xcodeproj/project.pbxproj | 8 +++++ .../ArticleDetailViewController.swift | 31 +++++++++++++++++++ .../ArticlesPageViewController.swift | 31 +++++++++++++++++++ .../Storyboards/Base.lproj/Main.storyboard | 30 ++++++++++++++++++ 4 files changed, 100 insertions(+) create mode 100644 DutchNews/Classes/ViewControllers/ArticleDetailViewController.swift create mode 100644 DutchNews/Classes/ViewControllers/ArticlesPageViewController.swift diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index 777c630..ef3dc77 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -40,6 +40,8 @@ F85899882517894B00A6BA2A /* HeadlinesArticleRemoteRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85899872517894B00A6BA2A /* HeadlinesArticleRemoteRepository.swift */; }; F858998D2517909A00A6BA2A /* MockArticleValidResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = F858998C2517909A00A6BA2A /* MockArticleValidResponse.swift */; }; F85899902517910700A6BA2A /* HeadlinesArticleRemoteRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F858998F2517910700A6BA2A /* HeadlinesArticleRemoteRepositoryTests.swift */; }; + F85E318C251B8462002753AC /* ArticlesPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85E318B251B8462002753AC /* ArticlesPageViewController.swift */; }; + F85E318E251B8484002753AC /* ArticleDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85E318D251B8484002753AC /* ArticleDetailViewController.swift */; }; F865F74C251686D2001FD067 /* Authenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F74B251686D2001FD067 /* Authenticator.swift */; }; F865F74E251687E4001FD067 /* APIAuthenticatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F74D251687E4001FD067 /* APIAuthenticatorTests.swift */; }; F865F75025168F05001FD067 /* AppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F74F25168F05001FD067 /* AppConfig.swift */; }; @@ -139,6 +141,8 @@ F85899872517894B00A6BA2A /* HeadlinesArticleRemoteRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlinesArticleRemoteRepository.swift; sourceTree = ""; }; F858998C2517909A00A6BA2A /* MockArticleValidResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockArticleValidResponse.swift; sourceTree = ""; }; F858998F2517910700A6BA2A /* HeadlinesArticleRemoteRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlinesArticleRemoteRepositoryTests.swift; sourceTree = ""; }; + F85E318B251B8462002753AC /* ArticlesPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticlesPageViewController.swift; sourceTree = ""; }; + F85E318D251B8484002753AC /* ArticleDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleDetailViewController.swift; sourceTree = ""; }; F865F74B251686D2001FD067 /* Authenticator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Authenticator.swift; sourceTree = ""; }; F865F74D251687E4001FD067 /* APIAuthenticatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIAuthenticatorTests.swift; sourceTree = ""; }; F865F74F25168F05001FD067 /* AppConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfig.swift; sourceTree = ""; }; @@ -473,6 +477,8 @@ F8154D4D2517D28700BFB42C /* HeadlinesViewController.swift */, F8154D742518156000BFB42C /* HeadlinesViewController+DataSource.swift */, F8154D512517EBC700BFB42C /* HeadlinesViewController+MagazineLayout.swift */, + F85E318B251B8462002753AC /* ArticlesPageViewController.swift */, + F85E318D251B8484002753AC /* ArticleDetailViewController.swift */, ); path = ViewControllers; sourceTree = ""; @@ -762,6 +768,7 @@ buildActionMask = 2147483647; files = ( F8E5C12F25191DA60083D2B1 /* UIColor+Extension.swift in Sources */, + F85E318C251B8462002753AC /* ArticlesPageViewController.swift in Sources */, F85899882517894B00A6BA2A /* HeadlinesArticleRemoteRepository.swift in Sources */, F8E5C0F32518D8AC0083D2B1 /* ArticleViewModel.swift in Sources */, F8E5C0ED2518833B0083D2B1 /* ArticlesViewModel.swift in Sources */, @@ -798,6 +805,7 @@ F8154D4E2517D28700BFB42C /* HeadlinesViewController.swift in Sources */, F8154D652518012F00BFB42C /* ArticleRowCollectionViewCell.swift in Sources */, F8154D792518207B00BFB42C /* String+HTML.swift in Sources */, + F85E318E251B8484002753AC /* ArticleDetailViewController.swift in Sources */, F8E5C11F25191D5E0083D2B1 /* Collection+Additionals.swift in Sources */, F8E5C0EB251882560083D2B1 /* UIViewController+AlertableView.swift in Sources */, F8154D552517EC0D00BFB42C /* HeadlineLayoutConfiguration.swift in Sources */, diff --git a/DutchNews/Classes/ViewControllers/ArticleDetailViewController.swift b/DutchNews/Classes/ViewControllers/ArticleDetailViewController.swift new file mode 100644 index 0000000..99aaa78 --- /dev/null +++ b/DutchNews/Classes/ViewControllers/ArticleDetailViewController.swift @@ -0,0 +1,31 @@ +// +// ArticleDetailViewController.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/23/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation + +class ArticleDetailViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + + } + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ + + +} diff --git a/DutchNews/Classes/ViewControllers/ArticlesPageViewController.swift b/DutchNews/Classes/ViewControllers/ArticlesPageViewController.swift new file mode 100644 index 0000000..bb0f506 --- /dev/null +++ b/DutchNews/Classes/ViewControllers/ArticlesPageViewController.swift @@ -0,0 +1,31 @@ +// +// ArticlesPageViewController.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/23/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import Pageboy + +class ArticlePageViewController: PageboyViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + + } + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ + +} diff --git a/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard b/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard index 51793dd..7577418 100644 --- a/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard +++ b/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard @@ -57,5 +57,35 @@
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
From 544ab26d38a7c0f5326b74710242960eae81b67b Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Wed, 23 Sep 2020 18:26:23 +0330 Subject: [PATCH 086/108] - Added Rx Web Kit Extensions. - Added ArticleDetailHeaderView. - Added and implement ArticleDetailViewController and ArticlePageViewController. --- DutchNews.xcodeproj/project.pbxproj | 48 ++++ .../Classes/Extensions/WebKit/Rx+WebKit.swift | 78 ++++++ .../WebKit/RxWKNavigationDelegateProxy.swift | 44 +++ .../WebKit/RxWKUIDelegateEvents+Rx.swift | 99 +++++++ .../WebKit/RxWKUIDelegateProxy.swift | 45 ++++ .../WebKit/RxWKUserContentController.swift | 47 ++++ .../WKNavigationDelegateEvents+Rx.swift | 253 ++++++++++++++++++ .../ArticleDetailViewController.swift | 51 +++- .../ArticlesPageViewController.swift | 17 ++ .../HeadlinesViewController+DataSource.swift | 4 - .../Headers/ArticleDetailHeaderView.swift | 34 +++ .../Views/Headers/ArticleDetailHeaderView.xib | 130 +++++++++ 12 files changed, 845 insertions(+), 5 deletions(-) create mode 100644 DutchNews/Classes/Extensions/WebKit/Rx+WebKit.swift create mode 100644 DutchNews/Classes/Extensions/WebKit/RxWKNavigationDelegateProxy.swift create mode 100644 DutchNews/Classes/Extensions/WebKit/RxWKUIDelegateEvents+Rx.swift create mode 100644 DutchNews/Classes/Extensions/WebKit/RxWKUIDelegateProxy.swift create mode 100644 DutchNews/Classes/Extensions/WebKit/RxWKUserContentController.swift create mode 100644 DutchNews/Classes/Extensions/WebKit/WKNavigationDelegateEvents+Rx.swift create mode 100644 DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.swift create mode 100644 DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.xib diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index ef3dc77..296ef47 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -42,6 +42,14 @@ F85899902517910700A6BA2A /* HeadlinesArticleRemoteRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F858998F2517910700A6BA2A /* HeadlinesArticleRemoteRepositoryTests.swift */; }; F85E318C251B8462002753AC /* ArticlesPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85E318B251B8462002753AC /* ArticlesPageViewController.swift */; }; F85E318E251B8484002753AC /* ArticleDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85E318D251B8484002753AC /* ArticleDetailViewController.swift */; }; + F85E3192251B85E4002753AC /* ArticleDetailHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85E3190251B85E4002753AC /* ArticleDetailHeaderView.swift */; }; + F85E3195251B8665002753AC /* ArticleDetailHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = F85E3194251B8665002753AC /* ArticleDetailHeaderView.xib */; }; + F85E319D251B8D61002753AC /* Rx+WebKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85E3197251B8D60002753AC /* Rx+WebKit.swift */; }; + F85E319E251B8D61002753AC /* RxWKUIDelegateProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85E3198251B8D60002753AC /* RxWKUIDelegateProxy.swift */; }; + F85E319F251B8D61002753AC /* WKNavigationDelegateEvents+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85E3199251B8D60002753AC /* WKNavigationDelegateEvents+Rx.swift */; }; + F85E31A0251B8D61002753AC /* RxWKUIDelegateEvents+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85E319A251B8D61002753AC /* RxWKUIDelegateEvents+Rx.swift */; }; + F85E31A1251B8D61002753AC /* RxWKUserContentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85E319B251B8D61002753AC /* RxWKUserContentController.swift */; }; + F85E31A2251B8D61002753AC /* RxWKNavigationDelegateProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85E319C251B8D61002753AC /* RxWKNavigationDelegateProxy.swift */; }; F865F74C251686D2001FD067 /* Authenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F74B251686D2001FD067 /* Authenticator.swift */; }; F865F74E251687E4001FD067 /* APIAuthenticatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F74D251687E4001FD067 /* APIAuthenticatorTests.swift */; }; F865F75025168F05001FD067 /* AppConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F74F25168F05001FD067 /* AppConfig.swift */; }; @@ -143,6 +151,14 @@ F858998F2517910700A6BA2A /* HeadlinesArticleRemoteRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlinesArticleRemoteRepositoryTests.swift; sourceTree = ""; }; F85E318B251B8462002753AC /* ArticlesPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticlesPageViewController.swift; sourceTree = ""; }; F85E318D251B8484002753AC /* ArticleDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleDetailViewController.swift; sourceTree = ""; }; + F85E3190251B85E4002753AC /* ArticleDetailHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleDetailHeaderView.swift; sourceTree = ""; }; + F85E3194251B8665002753AC /* ArticleDetailHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ArticleDetailHeaderView.xib; sourceTree = ""; }; + F85E3197251B8D60002753AC /* Rx+WebKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Rx+WebKit.swift"; sourceTree = ""; }; + F85E3198251B8D60002753AC /* RxWKUIDelegateProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RxWKUIDelegateProxy.swift; sourceTree = ""; }; + F85E3199251B8D60002753AC /* WKNavigationDelegateEvents+Rx.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "WKNavigationDelegateEvents+Rx.swift"; sourceTree = ""; }; + F85E319A251B8D61002753AC /* RxWKUIDelegateEvents+Rx.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "RxWKUIDelegateEvents+Rx.swift"; sourceTree = ""; }; + F85E319B251B8D61002753AC /* RxWKUserContentController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RxWKUserContentController.swift; sourceTree = ""; }; + F85E319C251B8D61002753AC /* RxWKNavigationDelegateProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RxWKNavigationDelegateProxy.swift; sourceTree = ""; }; F865F74B251686D2001FD067 /* Authenticator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Authenticator.swift; sourceTree = ""; }; F865F74D251687E4001FD067 /* APIAuthenticatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIAuthenticatorTests.swift; sourceTree = ""; }; F865F74F25168F05001FD067 /* AppConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfig.swift; sourceTree = ""; }; @@ -332,6 +348,28 @@ path = HeadLines; sourceTree = ""; }; + F85E318F251B85B0002753AC /* Headers */ = { + isa = PBXGroup; + children = ( + F85E3190251B85E4002753AC /* ArticleDetailHeaderView.swift */, + F85E3194251B8665002753AC /* ArticleDetailHeaderView.xib */, + ); + path = Headers; + sourceTree = ""; + }; + F85E3196251B8D56002753AC /* WebKit */ = { + isa = PBXGroup; + children = ( + F85E3197251B8D60002753AC /* Rx+WebKit.swift */, + F85E319C251B8D61002753AC /* RxWKNavigationDelegateProxy.swift */, + F85E319A251B8D61002753AC /* RxWKUIDelegateEvents+Rx.swift */, + F85E3198251B8D60002753AC /* RxWKUIDelegateProxy.swift */, + F85E319B251B8D61002753AC /* RxWKUserContentController.swift */, + F85E3199251B8D60002753AC /* WKNavigationDelegateEvents+Rx.swift */, + ); + path = WebKit; + sourceTree = ""; + }; F865F74A251686C1001FD067 /* Authenticator */ = { isa = PBXGroup; children = ( @@ -451,6 +489,7 @@ F8F14C6A250D70BD00C24FF5 /* Views */ = { isa = PBXGroup; children = ( + F85E318F251B85B0002753AC /* Headers */, F841DD08251953F9006E7E90 /* GradientView */, F8E5C0E7251881400083D2B1 /* AlertView */, F8154D5A251800E400BFB42C /* Cells */, @@ -506,6 +545,7 @@ F8F14C6F250D710100C24FF5 /* Extensions */ = { isa = PBXGroup; children = ( + F85E3196251B8D56002753AC /* WebKit */, F8154D6D25180AF100BFB42C /* UI */, F8E5C11B25191D5D0083D2B1 /* Bundle+Extensions.swift */, F8E5C11925191D5D0083D2B1 /* Collection+Additionals.swift */, @@ -643,6 +683,7 @@ F8154D7625181B1C00BFB42C /* ArticleWebContainerCollectionViewCell.xib in Resources */, F8F14C74250D719800C24FF5 /* Main.storyboard in Resources */, F8154D6A2518016800BFB42C /* HalfWidthArticleCollectionViewCell.xib in Resources */, + F85E3195251B8665002753AC /* ArticleDetailHeaderView.xib in Resources */, F89B0220250D446200B41293 /* Assets.xcassets in Resources */, F8154D622518011500BFB42C /* MainArticleCollectionViewCell.xib in Resources */, F8154D662518012F00BFB42C /* ArticleRowCollectionViewCell.xib in Resources */, @@ -773,7 +814,9 @@ F8E5C0F32518D8AC0083D2B1 /* ArticleViewModel.swift in Sources */, F8E5C0ED2518833B0083D2B1 /* ArticlesViewModel.swift in Sources */, F8154D6F25180B0200BFB42C /* UIView+Nib.swift in Sources */, + F85E319E251B8D61002753AC /* RxWKUIDelegateProxy.swift in Sources */, F858997B25176DC800A6BA2A /* Article.swift in Sources */, + F85E319D251B8D61002753AC /* Rx+WebKit.swift in Sources */, F865F7622516A6C1001FD067 /* APIServerResponseError.swift in Sources */, F8E5C11E25191D5E0083D2B1 /* Date+Convertor.swift in Sources */, F865F7602516A643001FD067 /* APIServerResponseStatus.swift in Sources */, @@ -782,6 +825,9 @@ F8154D522517EBC700BFB42C /* HeadlinesViewController+MagazineLayout.swift in Sources */, F8DE79E32515904400A6C2D5 /* NetworkService.swift in Sources */, F8154D752518156000BFB42C /* HeadlinesViewController+DataSource.swift in Sources */, + F85E319F251B8D61002753AC /* WKNavigationDelegateEvents+Rx.swift in Sources */, + F85E31A1251B8D61002753AC /* RxWKUserContentController.swift in Sources */, + F85E31A0251B8D61002753AC /* RxWKUIDelegateEvents+Rx.swift in Sources */, F8154D6C25180A9000BFB42C /* HeadlineBaseCollectionViewCell.swift in Sources */, F865F76B2516C08C001FD067 /* NetworkValidResponse.swift in Sources */, F841DD0A25195415006E7E90 /* GradientView.swift in Sources */, @@ -808,7 +854,9 @@ F85E318E251B8484002753AC /* ArticleDetailViewController.swift in Sources */, F8E5C11F25191D5E0083D2B1 /* Collection+Additionals.swift in Sources */, F8E5C0EB251882560083D2B1 /* UIViewController+AlertableView.swift in Sources */, + F85E3192251B85E4002753AC /* ArticleDetailHeaderView.swift in Sources */, F8154D552517EC0D00BFB42C /* HeadlineLayoutConfiguration.swift in Sources */, + F85E31A2251B8D61002753AC /* RxWKNavigationDelegateProxy.swift in Sources */, F8E5C0E9251881C80083D2B1 /* AlertableView.swift in Sources */, F865F74C251686D2001FD067 /* Authenticator.swift in Sources */, F8E5C12E25191DA60083D2B1 /* UIImageView+SDWebImage.swift in Sources */, diff --git a/DutchNews/Classes/Extensions/WebKit/Rx+WebKit.swift b/DutchNews/Classes/Extensions/WebKit/Rx+WebKit.swift new file mode 100644 index 0000000..e8c4929 --- /dev/null +++ b/DutchNews/Classes/Extensions/WebKit/Rx+WebKit.swift @@ -0,0 +1,78 @@ +// +// Rx+WebKit.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/23/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import WebKit +import RxSwift +import RxCocoa + +extension Reactive where Base: WKWebView { + /** + Reactive wrapper for `title` property + */ + public var title: Observable { + return self.observeWeakly(String.self, "title") + } + + /** + Reactive wrapper for `loading` property. + */ + public var loading: Observable { + return self.observeWeakly(Bool.self, "loading") + .map { $0 ?? false } + } + + /** + Reactive wrapper for `estimatedProgress` property. + */ + public var estimatedProgress: Observable { + return self.observeWeakly(Double.self, "estimatedProgress") + .map { $0 ?? 0.0 } + } + + /** + Reactive wrapper for `url` property. + */ + public var url: Observable { + return self.observeWeakly(URL.self, "URL") + } + + /** + Reactive wrapper for `canGoBack` property. + */ + public var canGoBack: Observable { + return self.observeWeakly(Bool.self, "canGoBack") + .map { $0 ?? false } + } + + /** + Reactive wrapper for `canGoForward` property. + */ + public var canGoForward: Observable { + return self.observeWeakly(Bool.self, "canGoForward") + .map { $0 ?? false } + } + + /// Reactive wrapper for `evaluateJavaScript(_:completionHandler:)` method. + /// + /// - Parameter javaScriptString: The JavaScript string to evaluate. + /// - Returns: Observable sequence of result of the script evaluation. + public func evaluateJavaScript(_ javaScriptString: String) -> Observable { + return Observable.create { [weak base] observer in + base?.evaluateJavaScript(javaScriptString) { value, error in + if let error = error { + observer.onError(error) + } else { + observer.onNext(value) + observer.onCompleted() + } + } + return Disposables.create() + } + } +} diff --git a/DutchNews/Classes/Extensions/WebKit/RxWKNavigationDelegateProxy.swift b/DutchNews/Classes/Extensions/WebKit/RxWKNavigationDelegateProxy.swift new file mode 100644 index 0000000..df87aa9 --- /dev/null +++ b/DutchNews/Classes/Extensions/WebKit/RxWKNavigationDelegateProxy.swift @@ -0,0 +1,44 @@ +// +// RxWKNavigationDelegateProxy.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/23/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import WebKit +#if !RX_NO_MODULE + import RxSwift + import RxCocoa +#endif + +public typealias RxWKNavigationDelegate = DelegateProxy + +open class RxWKNavigationDelegateProxy: RxWKNavigationDelegate, DelegateProxyType, WKNavigationDelegate { + + /// Type of parent object + public weak private(set) var webView: WKWebView? + + /// Init with ParentObject + public init(parentObject: ParentObject) { + webView = parentObject + super.init(parentObject: parentObject, delegateProxy: RxWKNavigationDelegateProxy.self) + } + + /// Register self to known implementations + public static func registerKnownImplementations() { + self.register { parent -> RxWKNavigationDelegateProxy in + RxWKNavigationDelegateProxy(parentObject: parent) + } + } + + /// Gets the current `WKNavigationDelegate` on `WKWebView` + open class func currentDelegate(for object: ParentObject) -> WKNavigationDelegate? { + return object.navigationDelegate + } + + /// Set the navigationDelegate for `WKWebView` + open class func setCurrentDelegate(_ delegate: WKNavigationDelegate?, to object: ParentObject) { + object.navigationDelegate = delegate + } +} diff --git a/DutchNews/Classes/Extensions/WebKit/RxWKUIDelegateEvents+Rx.swift b/DutchNews/Classes/Extensions/WebKit/RxWKUIDelegateEvents+Rx.swift new file mode 100644 index 0000000..202e179 --- /dev/null +++ b/DutchNews/Classes/Extensions/WebKit/RxWKUIDelegateEvents+Rx.swift @@ -0,0 +1,99 @@ +// +// RxWKUIDelegateEvents+Rx.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/23/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +#if !RX_NO_MODULE + import RxSwift + import RxCocoa +#endif + +import WebKit + +private func castOrThrow(_ resultType: T.Type, _ object: Any) throws -> T { + guard let returnValue = object as? T else { + throw RxCocoaError.castingError(object: object, targetType: resultType) + } + + return returnValue +} +//swiftlint:disable all +extension Reactive where Base: WKWebView { + public typealias JSAlertEvent = (webView: WKWebView, message: String, frame: WKFrameInfo, handler: () -> Void) + public typealias JSConfirmEvent = (webView: WKWebView, message: String, frame: WKFrameInfo, handler: (Bool) -> Void) + public typealias CommitPreviewEvent = (webView: WKWebView, controller: UIViewController) + + /// Reactive wrapper for `navigationDelegate`. + public var uiDelegate: DelegateProxy { + return RxWKUIDelegateProxy.proxy(for: base) + } + + /// Reactive wrapper for `func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Swift.Void)` + public var javaScriptAlertPanel: ControlEvent { + typealias __CompletionHandler = @convention(block) () -> Void + let source: Observable = uiDelegate + .methodInvoked(.jsAlert).map { args in + let view = try castOrThrow(WKWebView.self, args[0]) + let message = try castOrThrow(String.self, args[1]) + let frame = try castOrThrow(WKFrameInfo.self, args[2]) + var closureObject: AnyObject? + var mutableArgs = args + mutableArgs.withUnsafeMutableBufferPointer { ptr in + closureObject = ptr[3] as AnyObject + } + let __completionBlockPtr = UnsafeRawPointer(Unmanaged.passUnretained(closureObject as AnyObject).toOpaque()) + let handler = unsafeBitCast(__completionBlockPtr, to: __CompletionHandler.self) + return (view, message, frame, handler) + } + + return ControlEvent(events: source) + } + + /// Reactive wrapper for `func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Swift.Void)` + public var javaScriptConfirmPanel: ControlEvent { + typealias __ConfirmHandler = @convention(block) (Bool) -> Void + let source: Observable = uiDelegate + .methodInvoked(.jsConfirm).map { args in + let view = try castOrThrow(WKWebView.self, args[0]) + let message = try castOrThrow(String.self, args[1]) + let frame = try castOrThrow(WKFrameInfo.self, args[2]) + var closureObject: AnyObject? + var mutableArgs = args + mutableArgs.withUnsafeMutableBufferPointer { ptr in + closureObject = ptr[3] as AnyObject + } + let __confirmBlockPtr = UnsafeRawPointer(Unmanaged.passUnretained(closureObject as AnyObject).toOpaque()) + let handler = unsafeBitCast(__confirmBlockPtr, to: __ConfirmHandler.self) + return (view, message, frame, handler) + } + + return ControlEvent(events: source) + } + + /// Reactive wrappper for `func webView(_ webView: WKWebView, commitPreviewingViewController previewingViewController: UIViewController)` + @available(iOS 10.0, *) + public var commitPreviewing: ControlEvent { + let source: Observable = uiDelegate + .methodInvoked(.commitPreviewing) + .map { args in + let view = try castOrThrow(WKWebView.self, args[0]) + let controller = try castOrThrow(UIViewController.self, args[1]) + return (view, controller) + } + + return ControlEvent(events: source) + } +} + +//swiftlint:enable all + +fileprivate extension Selector { + + static let jsAlert = #selector(WKUIDelegate.webView(_:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:)) + static let jsConfirm = #selector(WKUIDelegate.webView(_:runJavaScriptConfirmPanelWithMessage:initiatedByFrame:completionHandler:)) + @available(iOS 10.0, *) + static let commitPreviewing = #selector(WKUIDelegate.webView(_:commitPreviewingViewController:)) +} diff --git a/DutchNews/Classes/Extensions/WebKit/RxWKUIDelegateProxy.swift b/DutchNews/Classes/Extensions/WebKit/RxWKUIDelegateProxy.swift new file mode 100644 index 0000000..28e71ea --- /dev/null +++ b/DutchNews/Classes/Extensions/WebKit/RxWKUIDelegateProxy.swift @@ -0,0 +1,45 @@ +// +// RxWKUIDelegateProxy.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/23/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import WebKit +#if !RX_NO_MODULE + import RxSwift + import RxCocoa +#endif + +public typealias RxWKUIDelegate = DelegateProxy + +open class RxWKUIDelegateProxy: RxWKUIDelegate, DelegateProxyType, WKUIDelegate { + + /// Type of parent object + /// must be WKWebView! + public weak private(set) var webView: WKWebView? + + /// Init with ParentObject + public init(parentObject: ParentObject) { + webView = parentObject + super.init(parentObject: parentObject, delegateProxy: RxWKUIDelegateProxy.self) + } + + /// Register self to known implementations + public static func registerKnownImplementations() { + self.register { parent -> RxWKUIDelegateProxy in + RxWKUIDelegateProxy(parentObject: parent) + } + } + + /// Gets the current `WKUIDelegate` on `WKWebView` + open class func currentDelegate(for object: ParentObject) -> WKUIDelegate? { + return object.uiDelegate + } + + /// Set the uiDelegate for `WKWebView` + open class func setCurrentDelegate(_ delegate: WKUIDelegate?, to object: ParentObject) { + object.uiDelegate = delegate + } +} diff --git a/DutchNews/Classes/Extensions/WebKit/RxWKUserContentController.swift b/DutchNews/Classes/Extensions/WebKit/RxWKUserContentController.swift new file mode 100644 index 0000000..cc4a3d8 --- /dev/null +++ b/DutchNews/Classes/Extensions/WebKit/RxWKUserContentController.swift @@ -0,0 +1,47 @@ +// +// RxWKUserContentController.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/23/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import WebKit +#if !RX_NO_MODULE + import RxSwift + import RxCocoa +#endif + +extension WKUserContentController { + fileprivate class MessageHandler: NSObject, WKScriptMessageHandler { + typealias MessageReceiveHandler = (WKScriptMessage) -> Void + private var messageReceiveHandler: MessageReceiveHandler? + + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + self.messageReceiveHandler?(message) + } + + func onReceive(_ handler:@escaping MessageReceiveHandler) { + self.messageReceiveHandler = handler + } + } +} + +public extension Reactive where Base: WKUserContentController { + /// Observable sequence of script message. + /// + /// - Parameter name: The name of the message handler + /// - Returns: Observable sequence of script message. + func scriptMessage(forName name: String) -> ControlEvent { + return ControlEvent(events: Observable.create { [weak base] observer in + let handler = WKUserContentController.MessageHandler() + base?.add(handler, name: name) + handler.onReceive { + observer.onNext($0) + } + return Disposables.create { + base?.removeScriptMessageHandler(forName: name) + } + }) + } +} diff --git a/DutchNews/Classes/Extensions/WebKit/WKNavigationDelegateEvents+Rx.swift b/DutchNews/Classes/Extensions/WebKit/WKNavigationDelegateEvents+Rx.swift new file mode 100644 index 0000000..f598cea --- /dev/null +++ b/DutchNews/Classes/Extensions/WebKit/WKNavigationDelegateEvents+Rx.swift @@ -0,0 +1,253 @@ +// +// WKNavigationDelegateEvents+Rx.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/23/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +#if !RX_NO_MODULE + import RxSwift + import RxCocoa +#endif + +import WebKit + +private func castOrThrow(_ resultType: T.Type, _ object: Any) throws -> T { + guard let returnValue = object as? T else { + throw RxCocoaError.castingError(object: object, targetType: resultType) + } + + return returnValue +} +//swiftlint:disable all +extension Reactive where Base: WKWebView { + /// WKNavigationEvent emits a tuple that contains both + /// WKWebView + WKNavigation + public typealias WKNavigationEvent = (webView: WKWebView, navigation: WKNavigation) + + /// WKNavigationFailedEvent emits a tuple that contains both + /// WKWebView + WKNavigation + Swift.Error + public typealias WKNavigationFailEvent = (webView: WKWebView, navigation: WKNavigation, error: Error) + + /// ChallengeHandler this is exposed to the user on subscription + public typealias ChallengeHandler = (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + /// WKNavigationChallengeEvent emits a tuple event of WKWebView + challenge + ChallengeHandler + public typealias WKNavigationChallengeEvent = (webView: WKWebView, challenge: URLAuthenticationChallenge, handler: ChallengeHandler) + + /// DecisionHandler this is the block exposed to the user on subscription + public typealias DecisionHandler = (WKNavigationResponsePolicy) -> Void + /// WKNavigationResponsePolicyEvent emits a tuple event of WKWebView + WKNavigationResponse + DecisionHandler + public typealias WKNavigationResponsePolicyEvent = ( webView: WKWebView, reponse: WKNavigationResponse, handler: DecisionHandler) + /// ActionHandler this is the block exposed to the user on subscription + public typealias ActionHandler = (WKNavigationActionPolicy) -> Void + /// WKNavigationActionPolicyEvent emits a tuple event of WKWebView + WKNavigationAction + ActionHandler + public typealias WKNavigationActionPolicyEvent = ( webView: WKWebView, action: WKNavigationAction, handler: ActionHandler) + + private func navigationEventWith(_ arg: [Any]) throws -> WKNavigationEvent { + let view = try castOrThrow(WKWebView.self, arg[0]) + let nav = try castOrThrow(WKNavigation.self, arg[1]) + return (view, nav) + } + + private func navigationFailEventWith(_ arg: [Any]) throws -> WKNavigationFailEvent { + let view = try castOrThrow(WKWebView.self, arg[0]) + let nav = try castOrThrow(WKNavigation.self, arg[1]) + let error = try castOrThrow(Swift.Error.self, arg[2]) + return (view, nav, error) + } + + /// Reactive wrapper for `navigationDelegate`. + public var delegate: DelegateProxy { + return RxWKNavigationDelegateProxy.proxy(for: base) + } + + /// Reactive wrapper for delegate method `webView(_ webView: WKWebView, didCommit navigation: WKNavigation!)`. + public var didCommitNavigation: ControlEvent { + let source: Observable = delegate + .methodInvoked(.didCommitNavigation) + .map(navigationEventWith) + return ControlEvent(events: source) + } + + /// Reactive wrapper for delegate method `webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!)`. + public var didStartProvisionalNavigation: ControlEvent { + let source: Observable = delegate + .methodInvoked(.didStartProvisionalNavigation) + .map(navigationEventWith) + return ControlEvent(events: source) + } + + /// Reactive wrapper for delegate method `webView(_ webView: WKWebView, didFinish navigation: WKNavigation!)` + public var didFinishNavigation: ControlEvent { + let source: Observable = delegate + .methodInvoked(.didFinishNavigation) + .map(navigationEventWith) + return ControlEvent(events: source) + } + + /// Reactive wrapper for delegate method `webViewWebContentProcessDidTerminate(_ webView: WKWebView)`. + @available(iOS 9.0, *) + public var didTerminate: ControlEvent { + let source: Observable = delegate + .methodInvoked(.didTerminate) + .map { try castOrThrow(WKWebView.self, $0[0]) } + return ControlEvent(events: source) + } + + /// Reactive wrapper for delegate method `webView(_ webView: WKWebView, didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!)`. + public var didReceiveServerRedirectForProvisionalNavigation: ControlEvent { + let source: Observable = delegate + .methodInvoked(.didReceiveServerRedirectForProvisionalNavigation) + .map(navigationEventWith) + return ControlEvent(events: source) + } + + /// Reactive wrapper for delegate method `webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error)`. + public var didFailNavigation: ControlEvent { + let source: Observable = delegate + .methodInvoked(.didFailNavigation) + .map(navigationFailEventWith) + return ControlEvent(events: source) + } + + /// Reactive wrapper for delegate method `webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error)`. + public var didFailProvisionalNavigation: ControlEvent { + let source: Observable = delegate + .methodInvoked(.didFailProvisionalNavigation) + .map(navigationFailEventWith) + return ControlEvent(events: source) + } + + /// Reactive wrapper for delegate method `webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)` + public var didReceiveChallenge: ControlEvent { + /// __ChallengeHandler is same as ChallengeHandler + /// They are interchangeable, __ChallengeHandler is for internal use. + /// ChallengeHandler is exposed to the user on subscription. + /// @convention attribute makes the swift closure compatible with Objc blocks + typealias __ChallengeHandler = @convention(block) (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + /*! @abstract Invoked when the web view needs to respond to an authentication challenge. + @param webView The web view that received the authentication challenge. + @param challenge The authentication challenge. + @param completionHandler The completion handler you must invoke to respond to the challenge. The + disposition argument is one of the constants of the enumerated type + NSURLSessionAuthChallengeDisposition. When disposition is NSURLSessionAuthChallengeUseCredential, + the credential argument is the credential to use, or nil to indicate continuing without a + credential. + @discussion If you do not implement this method, the web view will respond to the authentication challenge with the NSURLSessionAuthChallengeRejectProtectionSpace disposition. + */ + let source: Observable = delegate + .sentMessage(.didReceiveChallenge) + .map { arg in + /// Extracting the WKWebView from the array at index zero + /// which is the first argument of the function signature + let view = try castOrThrow(WKWebView.self, arg[0]) + /// Extracting the URLAuthenticationChallenge from the array at index one + /// which is the second argument of the function signature + let challenge = try castOrThrow(URLAuthenticationChallenge.self, arg[1]) + /// Now you `Can't` transform closure easily because they are excuted + /// in the stack if try it you will get the famous error + /// `Could not cast value of type '__NSStackBlock__' (0x12327d1a8) to` + /// this is because closures are transformed into a system type which is `__NSStackBlock__` + /// the above mentioned type is not exposed to `developer`. So everytime + /// you execute a closure the compiler transforms it into this Object. + /// So you go through the following steps to get a human readable type + /// of the closure signature: + /// 1. closureObject is type of AnyObject to that holds the raw value from + /// the array. + var closureObject: AnyObject? + /// 2. make the array mutable in order to access the `withUnsafeMutableBufferPointer` + /// fuctionalities + var mutableArg = arg + /// 3. Grab the closure at index 3 of the array, but we have to use the C-style + /// approach to access the raw memory underpinning the array and store it in closureObject + /// Now the object stored in the `closureObject` is `Unmanaged` and `some unspecified type` + /// the intelligent swift compiler doesn't know what sort of type it contains. It is Raw. + mutableArg.withUnsafeMutableBufferPointer { ptr in + closureObject = ptr[2] as AnyObject + } + /// 4. instantiate an opaque pointer to referenc the value of the `unspecified type` + let __challengeBlockPtr = UnsafeRawPointer(Unmanaged.passUnretained(closureObject as AnyObject).toOpaque()) + /// 5. Here the magic happen we forcefully tell the compiler that anything + /// found at this memory address that is refrenced should be a type of + /// `__ChallengeHandler`! + let handler = unsafeBitCast(__challengeBlockPtr, to: __ChallengeHandler.self) + return (view, challenge, handler) + } + + return ControlEvent(events: source) + + /** + Reference: + + This is a holy grail part for more information please read the following articles. + 1: http://codejaxy.com/q/332345/ios-objective-c-memory-management-automatic-ref-counting-objective-c-blocks-understand-one-edge-case-of-block-memory-management-in-objc + 2: http://www.galloway.me.uk/2012/10/a-look-inside-blocks-episode-2/ + 3: https://maniacdev.com/2013/11/tutorial-an-in-depth-guide-to-objective-c-block-debugging + 4: get know how [__NSStackBlock__ + UnsafeRawPointer + unsafeBitCast] works under the hood + 5: https://en.wikipedia.org/wiki/Opaque_pointer + 6: https://stackoverflow.com/questions/43662363/cast-objective-c-block-nsstackblock-into-swift-3 + */ + } + + /// Reactive wrapper for `func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Swift.Void)` + public var decidePolicyNavigationResponse: ControlEvent { + typealias __DecisionHandler = @convention(block) (WKNavigationResponsePolicy) -> Void + let source: Observable = delegate + .methodInvoked(.decidePolicyNavigationResponse).map { args in + let view = try castOrThrow(WKWebView.self, args[0]) + let response = try castOrThrow(WKNavigationResponse.self, args[1]) + var closureObject: AnyObject? + var mutableArgs = args + mutableArgs.withUnsafeMutableBufferPointer { ptr in + closureObject = ptr[2] as AnyObject + } + let __decisionBlockPtr = UnsafeRawPointer(Unmanaged.passUnretained(closureObject as AnyObject).toOpaque()) + let handler = unsafeBitCast(__decisionBlockPtr, to: __DecisionHandler.self) + return (view, response, handler) + } + + return ControlEvent(events: source) + } + + /// Reactive wrapper for `func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Swift.Void)` + public var decidePolicyNavigationAction: ControlEvent { + typealias __ActionHandler = @convention(block) (WKNavigationActionPolicy) -> Void + let source: Observable = delegate + .methodInvoked(.decidePolicyNavigationAction).map { args in + let view = try castOrThrow(WKWebView.self, args[0]) + let action = try castOrThrow(WKNavigationAction.self, args[1]) + var closureObject: AnyObject? + var mutableArgs = args + mutableArgs.withUnsafeMutableBufferPointer { ptr in + closureObject = ptr[2] as AnyObject + } + let __actionBlockPtr = UnsafeRawPointer(Unmanaged.passUnretained(closureObject as AnyObject).toOpaque()) + let handler = unsafeBitCast(__actionBlockPtr, to: __ActionHandler.self) + return (view, action, handler) + } + + return ControlEvent(events: source) + } +} + +extension Selector { + static let didCommitNavigation = #selector(WKNavigationDelegate.webView(_:didCommit:)) + static let didStartProvisionalNavigation = #selector(WKNavigationDelegate.webView(_:didStartProvisionalNavigation:)) + static let didFinishNavigation = #selector(WKNavigationDelegate.webView(_:didFinish:)) + static let didReceiveServerRedirectForProvisionalNavigation = #selector(WKNavigationDelegate.webView(_:didReceiveServerRedirectForProvisionalNavigation:)) + static let didFailNavigation = #selector(WKNavigationDelegate.webView(_:didFail:withError:)) + static let didFailProvisionalNavigation = #selector(WKNavigationDelegate.webView(_:didFailProvisionalNavigation:withError:)) + static let didReceiveChallenge = #selector(WKNavigationDelegate.webView(_:didReceive:completionHandler:)) + @available(iOS 9.0, *) + static let didTerminate = #selector(WKNavigationDelegate.webViewWebContentProcessDidTerminate(_:)) + /// Xcode give error when selectors results into having same signature + /// because of swift style you get for example: + /// Ambiguous use of 'webView(_:decidePolicyFor:decisionHandler:)' + /// please see this link for further understanding + /// https://bugs.swift.org/browse/SR-3062 + static let decidePolicyNavigationResponse = #selector(WKNavigationDelegate.webView(_:decidePolicyFor:decisionHandler:) as ((WKNavigationDelegate) -> (WKWebView, WKNavigationResponse, @escaping(WKNavigationResponsePolicy) -> Void) -> Void)?) + static let decidePolicyNavigationAction = #selector(WKNavigationDelegate.webView(_:decidePolicyFor:decisionHandler:) as ((WKNavigationDelegate) -> (WKWebView, WKNavigationAction, @escaping(WKNavigationActionPolicy) -> Void) -> Void)?) +} + +//swiftlint:enable all diff --git a/DutchNews/Classes/ViewControllers/ArticleDetailViewController.swift b/DutchNews/Classes/ViewControllers/ArticleDetailViewController.swift index 99aaa78..a838267 100644 --- a/DutchNews/Classes/ViewControllers/ArticleDetailViewController.swift +++ b/DutchNews/Classes/ViewControllers/ArticleDetailViewController.swift @@ -7,12 +7,38 @@ // import Foundation +import UIKit +import RxSwift +import RxCocoa +import WebKit +import MaterialComponents.MaterialColor +import MXParallaxHeader -class ArticleDetailViewController: UIViewController { +class ArticleDetailViewController: UIViewController, AlertableView { + + lazy var progressView: UIProgressView = { + let view = UIProgressView(progressViewStyle: .bar) + view.progress = 0 + view.progressTintColor = MDCPalette.blue.tint300 + return view + }() + + lazy var contentView: WKWebView = { + return WKWebView(forAutoLayout: ()) + }() + + lazy var headerView: ArticleDetailHeaderView = { + let view = ArticleDetailHeaderView.fromNib() + return view + }() + + let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() + setupViews() + // Do any additional setup after loading the view. } @@ -27,5 +53,28 @@ class ArticleDetailViewController: UIViewController { } */ + //////////////////////////////////////////////////////////////// + // MARK: - + // MARK: UI Methods + // MARK: - + //////////////////////////////////////////////////////////////// + + func setupViews() { + setupContentView() + setupHeaderView() + } + + func setupHeaderView() { + contentView.scrollView.parallaxHeader.view = headerView + contentView.scrollView.parallaxHeader.mode = .topFill + contentView.scrollView.parallaxHeader.minimumHeight = 70 + contentView.scrollView.parallaxHeader.height = 180 + headerView.backgroundColor = .cyan + } + + func setupContentView() { + view.addSubview(contentView) + contentView.autoPinEdgesToSuperviewSafeArea() + } } diff --git a/DutchNews/Classes/ViewControllers/ArticlesPageViewController.swift b/DutchNews/Classes/ViewControllers/ArticlesPageViewController.swift index bb0f506..78c4d27 100644 --- a/DutchNews/Classes/ViewControllers/ArticlesPageViewController.swift +++ b/DutchNews/Classes/ViewControllers/ArticlesPageViewController.swift @@ -29,3 +29,20 @@ class ArticlePageViewController: PageboyViewController { */ } + +extension ArticlePageViewController: PageboyViewControllerDataSource { + + func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int { + 10 + } + + func viewController(for pageboyViewController: PageboyViewController, + at index: PageboyViewController.PageIndex) -> UIViewController? { + UIViewController() + } + + func defaultPage(for pageboyViewController: PageboyViewController) -> PageboyViewController.Page? { + return .first + } + +} diff --git a/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift b/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift index 00533a7..e375851 100644 --- a/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift +++ b/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift @@ -31,10 +31,6 @@ extension HeadlinesViewController { self.fill(cell: cell, withArticle: item) - cell.contentView.layer.borderColor = UIColor.darkGray.cgColor - cell.contentView.layer.borderWidth = 0.4 - cell.contentView.layer.cornerRadius = 8.0 - return cell }) } diff --git a/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.swift b/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.swift new file mode 100644 index 0000000..4d57ef8 --- /dev/null +++ b/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.swift @@ -0,0 +1,34 @@ +// +// ArticleDetailHeaderView.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/23/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import UIKit +import SDWebImage + +class ArticleDetailHeaderView: UIView { + + @IBOutlet weak var backgroundImageView: UIImageView! + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var sourceLabel: UILabel! + @IBOutlet weak var publishDateLabel: UILabel! + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + func config(content article: ArticleRepresentable) { + + titleLabel.text = article.title + sourceLabel.text = article.source + publishDateLabel.text = article.publishedAt + + backgroundImageView.setImage(url: article.urlToImage) + + } + +} diff --git a/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.xib b/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.xib new file mode 100644 index 0000000..5f61cae --- /dev/null +++ b/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.xib @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 254d6a589d8d2a99e1224b8c36e2845a454f56bc Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Thu, 24 Sep 2020 01:17:55 +0330 Subject: [PATCH 087/108] - Implemented Article Detail UI. --- .../ArticleDetailViewController.swift | 40 ++++++----- .../ArticlesPageViewController.swift | 9 ++- .../HeadlinesViewController.swift | 1 + .../Views/GradientView/GradientView.swift | 3 +- .../Headers/ArticleDetailHeaderView.swift | 1 + .../Views/Headers/ArticleDetailHeaderView.xib | 8 +-- .../Storyboards/Base.lproj/Main.storyboard | 72 ++++++++++++++++--- 7 files changed, 101 insertions(+), 33 deletions(-) diff --git a/DutchNews/Classes/ViewControllers/ArticleDetailViewController.swift b/DutchNews/Classes/ViewControllers/ArticleDetailViewController.swift index a838267..5a5c61b 100644 --- a/DutchNews/Classes/ViewControllers/ArticleDetailViewController.swift +++ b/DutchNews/Classes/ViewControllers/ArticleDetailViewController.swift @@ -13,19 +13,12 @@ import RxCocoa import WebKit import MaterialComponents.MaterialColor import MXParallaxHeader +import AVFoundation class ArticleDetailViewController: UIViewController, AlertableView { - lazy var progressView: UIProgressView = { - let view = UIProgressView(progressViewStyle: .bar) - view.progress = 0 - view.progressTintColor = MDCPalette.blue.tint300 - return view - }() - - lazy var contentView: WKWebView = { - return WKWebView(forAutoLayout: ()) - }() + @IBOutlet weak var containerScrollView: UIScrollView! + @IBOutlet weak var contentView: WKWebView! lazy var headerView: ArticleDetailHeaderView = { let view = ArticleDetailHeaderView.fromNib() @@ -38,7 +31,7 @@ class ArticleDetailViewController: UIViewController, AlertableView { super.viewDidLoad() setupViews() - + contentView.load(URLRequest(url: URL(string: "https://news.google.com/topstories?hl=en-US&gl=US&ceid=US:en")!)) // Do any additional setup after loading the view. } @@ -65,16 +58,27 @@ class ArticleDetailViewController: UIViewController, AlertableView { } func setupHeaderView() { - contentView.scrollView.parallaxHeader.view = headerView - contentView.scrollView.parallaxHeader.mode = .topFill - contentView.scrollView.parallaxHeader.minimumHeight = 70 - contentView.scrollView.parallaxHeader.height = 180 - headerView.backgroundColor = .cyan + containerScrollView.parallaxHeader.view = headerView + containerScrollView.parallaxHeader.mode = .fill + containerScrollView.parallaxHeader.minimumHeight = 70 + + let height = AVMakeRect(aspectRatio: CGSize(width: 16, height: 9), + insideRect: view.bounds).size.height + + containerScrollView.parallaxHeader.height = height } func setupContentView() { - view.addSubview(contentView) - contentView.autoPinEdgesToSuperviewSafeArea() +// contentView.scrollView.isScrollEnabled = true +// contentView.scrollView.rx.observe(CGSize.self, #keyPath(UIScrollView.contentSize), +// options: [.initial,.new], retainSelf: false) +// .filter { $0 != nil }.map { $0! } +// .distinctUntilChanged() +// .bind {[weak self ] (size) in +// self?.contentView.autoSetDimension(.height, toSize: size.height) +// self?.view.layoutIfNeeded() +// } + } } diff --git a/DutchNews/Classes/ViewControllers/ArticlesPageViewController.swift b/DutchNews/Classes/ViewControllers/ArticlesPageViewController.swift index 78c4d27..eb82316 100644 --- a/DutchNews/Classes/ViewControllers/ArticlesPageViewController.swift +++ b/DutchNews/Classes/ViewControllers/ArticlesPageViewController.swift @@ -13,6 +13,8 @@ class ArticlePageViewController: PageboyViewController { override func viewDidLoad() { super.viewDidLoad() + interPageSpacing = 8.0 + self.dataSource = self // Do any additional setup after loading the view. @@ -38,7 +40,12 @@ extension ArticlePageViewController: PageboyViewControllerDataSource { func viewController(for pageboyViewController: PageboyViewController, at index: PageboyViewController.PageIndex) -> UIViewController? { - UIViewController() + let id = String(describing: ArticleDetailViewController.self) + if #available(iOS 13.0, *) { + return self.storyboard?.instantiateViewController(identifier: id) + } else { + return self.storyboard?.instantiateViewController(withIdentifier: id) + } } func defaultPage(for pageboyViewController: PageboyViewController) -> PageboyViewController.Page? { diff --git a/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift b/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift index 64a7803..ce2bea2 100644 --- a/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift +++ b/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift @@ -198,6 +198,7 @@ extension HeadlinesViewController: UICollectionViewDelegate { let item = dataSource[indexPath] viewModel?.didSelect(article: item) + self.performSegue(withIdentifier: "showDetails", sender: item) } } diff --git a/DutchNews/Classes/Views/GradientView/GradientView.swift b/DutchNews/Classes/Views/GradientView/GradientView.swift index e36596a..4b1f3e7 100644 --- a/DutchNews/Classes/Views/GradientView/GradientView.swift +++ b/DutchNews/Classes/Views/GradientView/GradientView.swift @@ -106,11 +106,10 @@ class GradientView: UIView { switch colors { case (.some(let up), .some(let down)): self.setGradientColor(colors: [up, down]) - break + case (.some(let color), .none), (.none, .some(let color)): self.setGradientColor(colors: [color]) - break default: self.self.setGradientColor(colors: nil) diff --git a/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.swift b/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.swift index 4d57ef8..b76d309 100644 --- a/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.swift +++ b/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.swift @@ -18,6 +18,7 @@ class ArticleDetailHeaderView: UIView { override func awakeFromNib() { super.awakeFromNib() + backgroundImageView.image = #imageLiteral(resourceName: "image-placeHolder") // Initialization code } diff --git a/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.xib b/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.xib index 5f61cae..e349ffc 100644 --- a/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.xib +++ b/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.xib @@ -93,9 +93,7 @@ - - @@ -107,11 +105,13 @@ - + + + @@ -122,7 +122,7 @@ - + diff --git a/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard b/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard index 7577418..29a9ea6 100644 --- a/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard +++ b/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard @@ -5,13 +5,14 @@ + - + @@ -27,16 +28,25 @@ - + - + - + + + + + + + + + + @@ -51,22 +61,24 @@ + - + - + + @@ -75,17 +87,61 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + From 7e4e694cf2d01d975185c44d51a002980baff0f3 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Thu, 24 Sep 2020 01:20:32 +0330 Subject: [PATCH 088/108] - Fixed bug in decoding Article Models. - Fixed UI Layout for headlines - Added Logger for networking responses. --- .../Networking/APIClientService.swift | 24 +++++-- DutchNews/Classes/Models/Article.swift | 16 ++++- .../Cells/ArticleRowCollectionViewCell.xib | 66 ++++++++++--------- .../ArticleWebContainerCollectionViewCell.xib | 2 +- .../HalfWidthArticleCollectionViewCell.xib | 5 +- .../HeadlineBaseCollectionViewCell.swift | 34 ++++++++++ .../Cells/MainArticleCollectionViewCell.swift | 11 +++- .../Cells/MainArticleCollectionViewCell.xib | 18 +++-- 8 files changed, 126 insertions(+), 50 deletions(-) diff --git a/DutchNews/Classes/Data Layers/Networking/APIClientService.swift b/DutchNews/Classes/Data Layers/Networking/APIClientService.swift index ec5f5bb..5a1cc26 100644 --- a/DutchNews/Classes/Data Layers/Networking/APIClientService.swift +++ b/DutchNews/Classes/Data Layers/Networking/APIClientService.swift @@ -138,8 +138,11 @@ final class APIClientService: NetworkServiceInterceptable { encoding: URLEncoding.default, headers: headers, interceptor: interceptor) - - return validate(dataRequest: dataTask, validator: validator) + dataTask.responseString { (result) in + + Logger.debugLog(result.debugDescription,tag: "Networking") + } + return validate(dataRequest: dataTask, validator: validator) .responseDecodable(queue: workQueue, decoder: decoder) { (response: DataResponse ) in let result = response.result.flatMapError { (error) -> Result in return .failure(error) @@ -176,15 +179,19 @@ final class APIClientService: NetworkServiceInterceptable { encoder: JSONParameterEncoder.prettyPrinted, headers: HTTPHeaders(headers), interceptor: interceptor) + dataTask.responseString { (result) in + Logger.debugLog(result.debugDescription,tag: "Networking") + } - return validate(dataRequest: dataTask, validator: validator) + return validate(dataRequest: dataTask, validator: validator) + .responseDecodable(queue: workQueue, decoder: decoder) { (response: DataResponse ) in let result = response.result.flatMapError { (error) -> Result in return .failure(error) } completion(result) - } + } }catch let error { completion(.failure(error)) @@ -219,8 +226,11 @@ final class APIClientService: NetworkServiceInterceptable { headers: HTTPHeaders(headers), interceptor: interceptor) dataTask = validate(dataRequest: dataTask, validator: validator) + dataTask.responseString { (result) in + Logger.debugLog(result.debugDescription,tag: "Networking") + } - return map(dataRequest: dataTask, decoder: decoder) + return map(dataRequest: dataTask, decoder: decoder).debug() }catch let error { return .just(.failure(error)) @@ -249,6 +259,10 @@ final class APIClientService: NetworkServiceInterceptable { interceptor: interceptor) dataTask = validate(dataRequest: dataTask, validator: validator) + dataTask.responseString { (result) in + Logger.debugLog(result.debugDescription,tag: "Networking") + } + return map(dataRequest: dataTask, decoder: decoder) }catch let error { diff --git a/DutchNews/Classes/Models/Article.swift b/DutchNews/Classes/Models/Article.swift index 322f858..3100bfe 100644 --- a/DutchNews/Classes/Models/Article.swift +++ b/DutchNews/Classes/Models/Article.swift @@ -18,7 +18,14 @@ struct Article: Codable { let url: URL - let urlToImage: URL? + var urlToImage: URL? { + guard let urlStr = imageUrl, let url = URL(string: urlStr) else { + return nil + } + return url + } + + private let imageUrl: String? let publishedAt: Date @@ -29,7 +36,9 @@ struct Article: Codable { enum CodingKeys: String, CodingKey { case source, author, title case description - case url, urlToImage, publishedAt, content + case url + case imageUrl = "urlToImage" + case publishedAt, content } } @@ -46,6 +55,7 @@ extension Article: Hashable { hasher.combine(source) hasher.combine(url) hasher.combine(type) + } } @@ -55,7 +65,7 @@ extension Article { return .init(title: "", author: "", description: "", source: ArticleSource(id: "", name: ""), url: URL(string: "https://domain.com")!, - urlToImage: nil, publishedAt: Date(), + imageUrl: nil, publishedAt: Date(), content: """
\n \n \n \n
""",type: .mock) diff --git a/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.xib b/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.xib index 2686c4b..df886fa 100644 --- a/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.xib +++ b/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.xib @@ -9,13 +9,14 @@ - + + - + @@ -25,28 +26,23 @@ - - + + + + - - - - - - - - - - + + + + - - + + + + - + + + diff --git a/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.xib b/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.xib index 0a7e886..92b727f 100644 --- a/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.xib +++ b/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.xib @@ -15,7 +15,7 @@ - + diff --git a/DutchNews/Classes/Views/Cells/HeadlineBaseCollectionViewCell.swift b/DutchNews/Classes/Views/Cells/HeadlineBaseCollectionViewCell.swift index ecc5b21..48d5ee1 100644 --- a/DutchNews/Classes/Views/Cells/HeadlineBaseCollectionViewCell.swift +++ b/DutchNews/Classes/Views/Cells/HeadlineBaseCollectionViewCell.swift @@ -9,6 +9,7 @@ import Foundation import MagazineLayout import RxSwift +import UIKit class HeadlineBaseCollectionViewCell: MagazineLayoutCollectionViewCell { @@ -24,8 +25,41 @@ class HeadlineBaseCollectionViewCell: MagazineLayoutCollectionViewCell { disposeBag = DisposeBag() } + override func awakeFromNib() { + super.awakeFromNib() + self.clipsToBounds = false + } + open func config(viewModel: ArticleRepresentable) { } + override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { + contentView.bounds = layoutAttributes.bounds + } + + override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { + contentView.layoutIfNeeded() + + let attributes = super.preferredLayoutAttributesFitting(layoutAttributes) + + let size: CGSize + + if (attributes as? MagazineLayoutCollectionViewLayoutAttributes)?.shouldVerticallySelfSize == true { + // Self-sizing is required in the vertical dimension. + layoutIfNeeded() + size = super.systemLayoutSizeFitting( + layoutAttributes.size, + withHorizontalFittingPriority: .required, + verticalFittingPriority: .required) + } else { + // No self-sizing is required; respect whatever size the layout determined. + size = layoutAttributes.size + } + + layoutAttributes.size = size + + return layoutAttributes + } + } diff --git a/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.swift b/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.swift index 431084a..28d226e 100644 --- a/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.swift +++ b/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.swift @@ -7,6 +7,7 @@ // import UIKit +import AVFoundation class MainArticleCollectionViewCell: HeadlineBaseCollectionViewCell { @@ -37,7 +38,15 @@ class MainArticleCollectionViewCell: HeadlineBaseCollectionViewCell { } override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { - super.preferredLayoutAttributesFitting(layoutAttributes) + let attribute = super.preferredLayoutAttributesFitting(layoutAttributes) + + //make sure that aspect ratio applied on size calculation. + let size = AVMakeRect(aspectRatio: CGSize(width: 16, height: 9), + insideRect: attribute.bounds).size + + attribute.size = size + return attribute + } } diff --git a/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.xib b/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.xib index 34eb04d..b878611 100644 --- a/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.xib +++ b/DutchNews/Classes/Views/Cells/MainArticleCollectionViewCell.xib @@ -17,17 +17,17 @@ - + - + - - + + - - + + From 4b86237a6e2c2993e35db2b198be03e288114f76 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Thu, 24 Sep 2020 06:42:59 +0330 Subject: [PATCH 089/108] - Added and defined Storage and Storable Abstract. - Added and implemented CodableDataManager. - Article conformed Storable Abstract. - Added and Run UnitTest. --- DutchNews.xcodeproj/project.pbxproj | 52 ++++++ .../Persistence/Abstracts/Storable.swift | 17 ++ .../Persistence/Abstracts/Storage.swift | 91 ++++++++++ .../Database/CodableDataManager.swift | 158 ++++++++++++++++++ .../Extensions/DispatchQueue+Additonals.swift | 51 ++++++ DutchNews/Classes/Models/Article.swift | 8 +- .../ModelsTests/ModelsDataFactory.swift | 2 - .../Presistence/CodableDataManagerTests.swift | 134 +++++++++++++++ Podfile.lock | 38 ++--- 9 files changed, 526 insertions(+), 25 deletions(-) create mode 100644 DutchNews/Classes/Data Layers/Persistence/Abstracts/Storable.swift create mode 100644 DutchNews/Classes/Data Layers/Persistence/Abstracts/Storage.swift create mode 100644 DutchNews/Classes/Data Layers/Persistence/Database/CodableDataManager.swift create mode 100644 DutchNews/Classes/Extensions/DispatchQueue+Additonals.swift create mode 100644 DutchNewsTests/Presistence/CodableDataManagerTests.swift diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index 296ef47..01eab1a 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -66,6 +66,11 @@ F89B0220250D446200B41293 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F89B021F250D446200B41293 /* Assets.xcassets */; }; F89B0223250D446200B41293 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F89B0221250D446200B41293 /* LaunchScreen.storyboard */; }; F89B022E250D446200B41293 /* DutchNewsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89B022D250D446200B41293 /* DutchNewsTests.swift */; }; + F8C83460251C04020051A0FD /* CodableDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C8345B251C04010051A0FD /* CodableDataManager.swift */; }; + F8C83462251C04020051A0FD /* Storable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C8345E251C04010051A0FD /* Storable.swift */; }; + F8C83463251C04020051A0FD /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C8345F251C04010051A0FD /* Storage.swift */; }; + F8C83466251C1F480051A0FD /* CodableDataManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C83465251C1F480051A0FD /* CodableDataManagerTests.swift */; }; + F8C83468251C22C80051A0FD /* DispatchQueue+Additonals.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C83467251C22C80051A0FD /* DispatchQueue+Additonals.swift */; }; F8DE79E32515904400A6C2D5 /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DE79E22515904400A6C2D5 /* NetworkService.swift */; }; F8DE79E5251594D700A6C2D5 /* APIClientService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DE79E4251594D700A6C2D5 /* APIClientService.swift */; }; F8E5C0E9251881C80083D2B1 /* AlertableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0E8251881C80083D2B1 /* AlertableView.swift */; }; @@ -179,6 +184,11 @@ F89B0229250D446200B41293 /* DutchNewsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DutchNewsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F89B022D250D446200B41293 /* DutchNewsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DutchNewsTests.swift; sourceTree = ""; }; F89B022F250D446200B41293 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F8C8345B251C04010051A0FD /* CodableDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CodableDataManager.swift; sourceTree = ""; }; + F8C8345E251C04010051A0FD /* Storable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Storable.swift; sourceTree = ""; }; + F8C8345F251C04010051A0FD /* Storage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; + F8C83465251C1F480051A0FD /* CodableDataManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableDataManagerTests.swift; sourceTree = ""; }; + F8C83467251C22C80051A0FD /* DispatchQueue+Additonals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+Additonals.swift"; sourceTree = ""; }; F8DE79E22515904400A6C2D5 /* NetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = ""; }; F8DE79E4251594D700A6C2D5 /* APIClientService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClientService.swift; sourceTree = ""; }; F8E5C0E8251881C80083D2B1 /* AlertableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertableView.swift; sourceTree = ""; }; @@ -434,6 +444,7 @@ F89B022C250D446200B41293 /* DutchNewsTests */ = { isa = PBXGroup; children = ( + F8C83464251C1F0B0051A0FD /* Presistence */, F858998B2517906B00A6BA2A /* Repositories */, F858997E251772AF00A6BA2A /* ModelsTests */, F82C8EFB2516051D002B27B3 /* NetworkTests */, @@ -443,6 +454,40 @@ path = DutchNewsTests; sourceTree = ""; }; + F8C83457251C018F0051A0FD /* Persistence */ = { + isa = PBXGroup; + children = ( + F8C8345D251C04010051A0FD /* Abstracts */, + F8C8345A251C04010051A0FD /* Database */, + ); + path = Persistence; + sourceTree = ""; + }; + F8C8345A251C04010051A0FD /* Database */ = { + isa = PBXGroup; + children = ( + F8C8345B251C04010051A0FD /* CodableDataManager.swift */, + ); + path = Database; + sourceTree = ""; + }; + F8C8345D251C04010051A0FD /* Abstracts */ = { + isa = PBXGroup; + children = ( + F8C8345E251C04010051A0FD /* Storable.swift */, + F8C8345F251C04010051A0FD /* Storage.swift */, + ); + path = Abstracts; + sourceTree = ""; + }; + F8C83464251C1F0B0051A0FD /* Presistence */ = { + isa = PBXGroup; + children = ( + F8C83465251C1F480051A0FD /* CodableDataManagerTests.swift */, + ); + path = Presistence; + sourceTree = ""; + }; F8E5C0E7251881400083D2B1 /* AlertView */ = { isa = PBXGroup; children = ( @@ -482,6 +527,7 @@ F865F7512516998A001FD067 /* Repositories */, F865F74A251686C1001FD067 /* Authenticator */, F8F14C6E250D70F900C24FF5 /* Networking */, + F8C83457251C018F0051A0FD /* Persistence */, ); path = "Data Layers"; sourceTree = ""; @@ -555,6 +601,7 @@ F8E5C11D25191D5D0083D2B1 /* URL+ApplicationPath.swift */, F8154D782518207B00BFB42C /* String+HTML.swift */, F8E5C13225193A290083D2B1 /* String+Localization.swift */, + F8C83467251C22C80051A0FD /* DispatchQueue+Additonals.swift */, ); path = Extensions; sourceTree = ""; @@ -813,10 +860,12 @@ F85899882517894B00A6BA2A /* HeadlinesArticleRemoteRepository.swift in Sources */, F8E5C0F32518D8AC0083D2B1 /* ArticleViewModel.swift in Sources */, F8E5C0ED2518833B0083D2B1 /* ArticlesViewModel.swift in Sources */, + F8C83462251C04020051A0FD /* Storable.swift in Sources */, F8154D6F25180B0200BFB42C /* UIView+Nib.swift in Sources */, F85E319E251B8D61002753AC /* RxWKUIDelegateProxy.swift in Sources */, F858997B25176DC800A6BA2A /* Article.swift in Sources */, F85E319D251B8D61002753AC /* Rx+WebKit.swift in Sources */, + F8C83460251C04020051A0FD /* CodableDataManager.swift in Sources */, F865F7622516A6C1001FD067 /* APIServerResponseError.swift in Sources */, F8E5C11E25191D5E0083D2B1 /* Date+Convertor.swift in Sources */, F865F7602516A643001FD067 /* APIServerResponseStatus.swift in Sources */, @@ -824,6 +873,7 @@ F8E5C12125191D5E0083D2B1 /* Bundle+Extensions.swift in Sources */, F8154D522517EBC700BFB42C /* HeadlinesViewController+MagazineLayout.swift in Sources */, F8DE79E32515904400A6C2D5 /* NetworkService.swift in Sources */, + F8C83468251C22C80051A0FD /* DispatchQueue+Additonals.swift in Sources */, F8154D752518156000BFB42C /* HeadlinesViewController+DataSource.swift in Sources */, F85E319F251B8D61002753AC /* WKNavigationDelegateEvents+Rx.swift in Sources */, F85E31A1251B8D61002753AC /* RxWKUserContentController.swift in Sources */, @@ -853,6 +903,7 @@ F8154D792518207B00BFB42C /* String+HTML.swift in Sources */, F85E318E251B8484002753AC /* ArticleDetailViewController.swift in Sources */, F8E5C11F25191D5E0083D2B1 /* Collection+Additionals.swift in Sources */, + F8C83463251C04020051A0FD /* Storage.swift in Sources */, F8E5C0EB251882560083D2B1 /* UIViewController+AlertableView.swift in Sources */, F85E3192251B85E4002753AC /* ArticleDetailHeaderView.swift in Sources */, F8154D552517EC0D00BFB42C /* HeadlineLayoutConfiguration.swift in Sources */, @@ -880,6 +931,7 @@ files = ( F85899902517910700A6BA2A /* HeadlinesArticleRemoteRepositoryTests.swift in Sources */, F85899822517738C00A6BA2A /* ModelsDataFactory.swift in Sources */, + F8C83466251C1F480051A0FD /* CodableDataManagerTests.swift in Sources */, F865F7662516AB66001FD067 /* APIServerResponseTests.swift in Sources */, F88800692517A423008DCC54 /* RepositoryDependenciesFactory.swift in Sources */, F89B022E250D446200B41293 /* DutchNewsTests.swift in Sources */, diff --git a/DutchNews/Classes/Data Layers/Persistence/Abstracts/Storable.swift b/DutchNews/Classes/Data Layers/Persistence/Abstracts/Storable.swift new file mode 100644 index 0000000..04c9b86 --- /dev/null +++ b/DutchNews/Classes/Data Layers/Persistence/Abstracts/Storable.swift @@ -0,0 +1,17 @@ +// +// Storable.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/24/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation + +/// Abstract `Storable` +/// The purpose of this abstract is to store and retreive Entities in Presistence Layer. +protocol Storable: Codable { + + func primaryKeyValue() -> String + +} diff --git a/DutchNews/Classes/Data Layers/Persistence/Abstracts/Storage.swift b/DutchNews/Classes/Data Layers/Persistence/Abstracts/Storage.swift new file mode 100644 index 0000000..a36048d --- /dev/null +++ b/DutchNews/Classes/Data Layers/Persistence/Abstracts/Storage.swift @@ -0,0 +1,91 @@ +// +// Storage.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/24/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation + +/// CreatableStorage Abstract +protocol CreatableStorage { + + /// Create a new object with default values + /// + /// - Parameters: + /// - model: the type of object + /// - completion: a completion block that return an object that is conformed to the `Storable` protocol. + /// - Throws: throw an error object which describe more about occured error. + func create(_ model: T.Type, completion:@escaping (T) -> Void) throws +} + +/// SavableStorage Abstract +protocol SavableStorage { + + /// Save an object that is conformed to the `Storable` protocol + /// + /// - Parameter object: an object that is conformed to the `Storable`. + /// - Throws: throw an error when saving operation has been failed. + func save(object: T) throws +} + +/// UpdatableStorage Abstract +protocol UpdatableStorage { + + /// Update the object which is conformed to the `Storable` protocol. + /// + /// - Parameter object: the `Storable` conformed object. + /// - Throws: throw the proper error when update failed. + func update(object: T) throws +} + +/// DeletableStorage Abstract +protocol DeletableStorage { + + /// Delete an object that is conformed to the `Storable` protocol + /// + /// - Parameter object: <#object description#> + /// - Throws: throw an error when operation failed. + func delete(object: Storable) throws + + /// Delete an object that is conformed to the `Storable` protocol + /// + /// - Parameter object: the storable object + /// - Throws: throw an error when operation failed. + func delete(object: [Storable]) throws + + /// Delete all objects that are conformed to the `Storable` protocol + /// + /// - Parameter model: <#model description#> + /// - Throws: throw an error when operation failed. + func deleteAll (_ model: T.Type) throws +} + +/// Abstract `DeletableStorage` +extension DeletableStorage { + + func delete(object: [Storable]) throws { + // make delete object method to optional. + } +} + +/// Abstract `FetchableStorage` +protocol FetchableStorage { + + /// The `Sort` type contain `key` and `ascending` object + typealias Sort = (key: String, ascending: Bool) + + /// Fetch and return a list of objects that are conformed to the `Storable` protocol. + /// + /// - Parameters: + /// - type: <#type description#> + /// - predicate: The predicate object of 'NSPredicate' + /// - sort: An sort value which the result sorts base on. + /// - completion: The completion block return a list of objects that are conformed to the `Storable` protocol + /// - Throws: <#throws value description#> + func fetch (type: T.Type, predicate: NSPredicate?, sort: Sort?, completion:@escaping ([T]) -> Void) throws +} + +/// The `Storage` Type combine all abstracts in this file. +typealias Storage = CreatableStorage & SavableStorage & UpdatableStorage & DeletableStorage & FetchableStorage diff --git a/DutchNews/Classes/Data Layers/Persistence/Database/CodableDataManager.swift b/DutchNews/Classes/Data Layers/Persistence/Database/CodableDataManager.swift new file mode 100644 index 0000000..3785042 --- /dev/null +++ b/DutchNews/Classes/Data Layers/Persistence/Database/CodableDataManager.swift @@ -0,0 +1,158 @@ +// +// RealmDataManager.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/24/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import CryptoSwift + +/// The `CodableDataManager` class extended and implemented `Storage` abstract. +/// The Default DataManager of this application. +class CodableDataManager: Storage { + + /// Rhe default instace of `CodableDataManager` + static let `default` = CodableDataManager(fileProvider: .default) + + /// The Stored fileURL object. + private let fileURL: URL + + private let decoder = JSONDecoder() + private let encoder = JSONEncoder() + + /// Constructor + /// + /// - Parameter fileProvider: the file provider options + init(fileProvider: FileProvider) { + + self.fileURL = fileProvider.url + + decoder.dateDecodingStrategy = .iso8601 + decoder.dataDecodingStrategy = .base64 + + encoder.dateEncodingStrategy = .iso8601 + encoder.dataEncodingStrategy = .base64 + encoder.outputFormatting = [.sortedKeys, .prettyPrinted] + + } + + deinit { + + } + + // MARK: - Storage Implementation methods + + func create(_ model: T.Type, completion: @escaping (T) -> Void) throws where T: Storable { + fatalError("not implemented for demo") + } + + func save(object: T) throws { + + let data = try encoder.encode(object) + try data.write(to: makeFileURL(for: object),options: [.withoutOverwriting]) + } + + func update (object: T) throws { + let data = try encoder.encode(object) + try data.write(to: makeFileURL(for: object)) + } + + func delete(object: Storable) throws { + fatalError("not implemented for demo") + } + + func deleteAll(_ model: T.Type) throws where T: Storable { + fatalError("not implemented for demo") + } + + func fetch(type: T.Type, predicate: NSPredicate?, sort: Sort?, completion: @escaping ([T]) -> Void) throws where T: Storable { + + let url = self.fileURL + perform(onQueue: .global(qos: .utility), block: {[weak self] in + + let fileManager = FileManager.default + guard let list = fileManager.enumerator(at: url, + includingPropertiesForKeys: [], + options: [.skipsHiddenFiles], + errorHandler: { (url,error) -> Bool in + Logger.errorLog("Error \(error) for url \(url)", tag: "FileManager") + return true + }) else { + self?.perform { + completion([]) + } + return + } + + let result = list.compactMap({ $0 as? URL }) + + var objects = [T]() + for path in result { + + guard let data = try? Data(contentsOf: path), let objc = try? self?.decoder.decode(type, from: data) else { + continue + } + + objects.append(objc) + } + + let output = objects.filter({ predicate?.evaluate(with: $0) ?? true }) + + self?.perform { + completion(output) + } + }) + + } + + private func makeFileURL (for obj: T) -> URL { + + let fileName = "\(obj.primaryKeyValue().md5())-\(String(describing: T.self)).json" + let url = fileURL.appendingPathComponent(fileName) + return url + } + + private func perform(onQueue queue: DispatchQueue = .main, + block workItem:@escaping () -> Void) { + + guard DispatchQueue.current !== queue else { + workItem() + return + } + + queue.async(execute: workItem) + + } + +} + +extension CodableDataManager { + + enum FileProvider { + case `default` + case custom(url: URL) + + fileprivate var url: URL { + + let fileURL: URL + switch self { + case .default: + + guard let url = try? URL.applicationSupportDirectoryURL() else { + return URL(fileURLWithPath: "", isDirectory: true) + } + + fileURL = url + + case .custom(let url): + fileURL = url + } + + return fileURL + } + + } + +} diff --git a/DutchNews/Classes/Extensions/DispatchQueue+Additonals.swift b/DutchNews/Classes/Extensions/DispatchQueue+Additonals.swift new file mode 100644 index 0000000..7080205 --- /dev/null +++ b/DutchNews/Classes/Extensions/DispatchQueue+Additonals.swift @@ -0,0 +1,51 @@ +// +// DispatchQueue+Additonals.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/24/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation + +// MARK: private functionality + +extension DispatchQueue { + + private struct QueueReference { weak var queue: DispatchQueue? } + + private static let key: DispatchSpecificKey = { + let key = DispatchSpecificKey() + setupSystemQueuesDetection(key: key) + return key + }() + + private static func _registerDetection(of queues: [DispatchQueue], key: DispatchSpecificKey) { + queues.forEach { $0.setSpecific(key: key, value: QueueReference(queue: $0)) } + } + + private static func setupSystemQueuesDetection(key: DispatchSpecificKey) { + let queues: [DispatchQueue] = [ + .main, + .global(qos: .background), + .global(qos: .default), + .global(qos: .unspecified), + .global(qos: .userInitiated), + .global(qos: .userInteractive), + .global(qos: .utility) + ] + _registerDetection(of: queues, key: key) + } +} + +// MARK: public functionality + +extension DispatchQueue { + + static func registerDetection(of queue: DispatchQueue) { + _registerDetection(of: [queue], key: key) + } + + static var currentQueueLabel: String? { current?.label } + static var current: DispatchQueue? { getSpecific(key: key)?.queue } +} diff --git a/DutchNews/Classes/Models/Article.swift b/DutchNews/Classes/Models/Article.swift index 3100bfe..ca28e88 100644 --- a/DutchNews/Classes/Models/Article.swift +++ b/DutchNews/Classes/Models/Article.swift @@ -8,8 +8,8 @@ import Foundation -struct Article: Codable { - +struct Article: Storable { + let title: String let author: String? let description: String? @@ -41,6 +41,9 @@ struct Article: Codable { case publishedAt, content } + func primaryKeyValue() -> String { + return url.absoluteString + "\(publishedAt.timeIntervalSince1970)" + } } enum ArticleType: Int, Codable { @@ -55,6 +58,7 @@ extension Article: Hashable { hasher.combine(source) hasher.combine(url) hasher.combine(type) + hasher.combine(publishedAt) } } diff --git a/DutchNewsTests/ModelsTests/ModelsDataFactory.swift b/DutchNewsTests/ModelsTests/ModelsDataFactory.swift index 7799e63..f709c5d 100644 --- a/DutchNewsTests/ModelsTests/ModelsDataFactory.swift +++ b/DutchNewsTests/ModelsTests/ModelsDataFactory.swift @@ -8,8 +8,6 @@ import Foundation -@testable import DutchNewsTests - struct ModelsDataFactory { //swiftlint:disable all diff --git a/DutchNewsTests/Presistence/CodableDataManagerTests.swift b/DutchNewsTests/Presistence/CodableDataManagerTests.swift new file mode 100644 index 0000000..6dadeea --- /dev/null +++ b/DutchNewsTests/Presistence/CodableDataManagerTests.swift @@ -0,0 +1,134 @@ +// +// CodableDataManagerTests.swift +// DutchNewsTests +// +// Created by Farshad Mousalou on 9/24/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import XCTest +import Foundation + +@testable import DutchNews + +class CodableDataManagerTests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testSaveObject() { + do { + + let storage: Storage = CodableDataManager(fileProvider: .custom(url: try! URL.documentDirectoryURL())) + + try storage.save(object: Article.htmlArticle()) + try storage.save(object: Article.htmlArticle()) + + }catch { + XCTFail(error.localizedDescription) + } + } + + func testFetchAllElements() { + + do { + let expe = self.expectation(description: "testEEE") + expe.expectedFulfillmentCount = 1 + + let storage: Storage = CodableDataManager(fileProvider: .custom(url: try! URL.documentDirectoryURL())) + + try storage.save(object: Article.htmlArticle()) + try storage.save(object: Article.htmlArticle()) + try storage.save(object: Article.htmlArticle()) + try storage.save(object: Article.htmlArticle()) + + try storage.fetch(type: Article.self, predicate: nil, sort: nil) { (result) in + print(result) + expe.fulfill() + } + + self.waitForExpectations(timeout: 30.0) { (error) in + if let error = error { + XCTFail(error.localizedDescription) + } + } + }catch { + XCTFail(error.localizedDescription) + } + + } + + func testFetchElementsWithPrediction() { + + do { + let expe = self.expectation(description: "testEEE") + expe.expectedFulfillmentCount = 2 + + let storage: Storage = CodableDataManager(fileProvider: .custom(url: try! URL.documentDirectoryURL())) + + let people = [StubPerson(name: "John", age: 21, email: "m@mail.com"), + StubPerson(name: "Sam", age: 35, email: "sam@gmail.com"), + StubPerson(name: "Goorge", age: 40, email: "goorge@gmail.com")] + + for person in people { + try? storage.save(object: person) + } + + let commitPredicate = NSPredicate { (obj, _) -> Bool in + return ((obj as? StubPerson)?.name ?? "") == "John" + } + + try storage.fetch(type: StubPerson.self, predicate: commitPredicate, sort: nil) { (result) in + XCTAssertTrue(result.count > 0, "Not Found John") + expe.fulfill() + } + + let predict2 = NSPredicate { (obj, _) -> Bool in + return ((obj as? StubPerson)?.email ?? "").contains(".com") + } + + try storage.fetch(type: StubPerson.self, predicate: predict2, sort: nil) { (result) in + XCTAssertTrue(result.count > 0, "no names found in list which contains 'o'") + expe.fulfill() + } + + self.waitForExpectations(timeout: 30.0) { (error) in + if let error = error { + XCTFail(error.localizedDescription) + } + } + }catch { + XCTFail(error.localizedDescription) + } + + } + + func testPerformanceExample() { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} + +extension StubPerson: Storable { + + func primaryKeyValue() -> String { + return self.name + } + +} + +extension StubPerson: Equatable { + + static func ==(lhs: StubPerson, rhs: StubPerson) -> Bool { + return lhs.name == rhs.name && lhs.age == rhs.age && lhs.email == rhs.email + } + +} diff --git a/Podfile.lock b/Podfile.lock index fb8f898..64ea6e6 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -5,7 +5,6 @@ PODS: - CocoaLumberjack/Core - CryptoSwift (1.1.2) - Differentiator (4.0.1) - - JEKScrollableSectionCollectionViewLayout (1.3.0) - MagazineLayout (1.6.2) - MaterialComponents (109.8.0): - MaterialComponents/ActionSheet (= 109.8.0) @@ -652,11 +651,12 @@ PODS: - Nimble (8.1.2) - Pageboy (3.6.1) - PureLayout (3.1.6) - - Realm (5.1.0): - - Realm/Headers (= 5.1.0) - - Realm/Headers (5.1.0) - - RealmSwift (5.1.0): - - Realm (= 5.1.0) + - Realm (5.0.3): + - Realm/Headers (= 5.0.3) + - Realm/Headers (5.0.3) + - RealmSwift (5.0.3): + - Realm (= 5.0.3) + - RuntimeNew (2.1.5) - RxAlamofire (5.5.0): - RxAlamofire/Core (= 5.5.0) - RxAlamofire/Core (5.5.0): @@ -680,12 +680,14 @@ PODS: - SDWebImage/Core (= 5.9.1) - SDWebImage/Core (5.9.1) - SwiftLint (0.39.2) + - Unrealm (1.3.5): + - RealmSwift (= 5.0.3) + - RuntimeNew (= 2.1.5) DEPENDENCIES: - Alamofire - CocoaLumberjack/Swift - CryptoSwift (= 1.1.2) - - JEKScrollableSectionCollectionViewLayout (from `https://github.com/farshadmb/JEKScrollableSectionCollectionViewLayout.git`) - MagazineLayout - MaterialComponents - Mocker (~> 1.0.0) @@ -693,7 +695,6 @@ DEPENDENCIES: - Nimble - Pageboy - PureLayout - - RealmSwift - RxAlamofire - RxBlocking - RxCocoa @@ -702,6 +703,7 @@ DEPENDENCIES: - RxTest - SDWebImage - SwiftLint + - Unrealm SPEC REPOS: trunk: @@ -722,6 +724,7 @@ SPEC REPOS: - PureLayout - Realm - RealmSwift + - RuntimeNew - RxAlamofire - RxBlocking - RxCocoa @@ -731,22 +734,13 @@ SPEC REPOS: - RxTest - SDWebImage - SwiftLint - -EXTERNAL SOURCES: - JEKScrollableSectionCollectionViewLayout: - :git: https://github.com/farshadmb/JEKScrollableSectionCollectionViewLayout.git - -CHECKOUT OPTIONS: - JEKScrollableSectionCollectionViewLayout: - :commit: df6250fd4b6e3334a5598d74ae182b8d64774aa8 - :git: https://github.com/farshadmb/JEKScrollableSectionCollectionViewLayout.git + - Unrealm SPEC CHECKSUMS: Alamofire: e911732990610fe89af59ac0077f923d72dc3dfd CocoaLumberjack: b17ae15142558d08bbacf69775fa10c4abbebcc9 CryptoSwift: 31dacd1f13427439ddae5b5cbaae4c8dbc43047e Differentiator: 886080237d9f87f322641dedbc5be257061b0602 - JEKScrollableSectionCollectionViewLayout: 80def4834e535880029917c374324ef8b089c448 MagazineLayout: 8e995730bc2b1ff8f11f44cb7d7926ab9640892f MaterialComponents: 00df0652f52cd6968b02d531bd2e6956b0f907b8 MDFInternationalization: 010097556d6b09d2c4ea38e0820ea6d37be6a314 @@ -758,8 +752,9 @@ SPEC CHECKSUMS: Nimble: 3864815b4703c7ebffba875973c70e854489fbae Pageboy: 29a2d474ad99404b4d77f325e0ab6d705930a4fb PureLayout: bd3c4ec3a3819ad387c99ebb72c6b129c3ed4d2d - Realm: bdea546851e37b4cf0a2400cef115c7c26fda488 - RealmSwift: 70945aa168db93b215c460e9c8ef680261aa28af + Realm: bfca1699b61b0b17c3a69ae0e648314eae91fbdb + RealmSwift: 493c9f089cd3893b3959007973c0e4f640906ba0 + RuntimeNew: ef34cf1783be4c1cbe798970bc590924dab5df87 RxAlamofire: 22287c710761466d0123504c566a8381520d4d63 RxBlocking: 5f700a78cad61ce253ebd37c9a39b5ccc76477b4 RxCocoa: 32065309a38d29b5b0db858819b5bf9ef038b601 @@ -769,7 +764,8 @@ SPEC CHECKSUMS: RxTest: 711632d5644dffbeb62c936a521b5b008a1e1faa SDWebImage: a990c053fff71e388a10f3357edb0be17929c9c5 SwiftLint: 22ccbbe3b8008684be5955693bab135e0ed6a447 + Unrealm: ba1c168935344084ba28dd6bff5ded86f4837d7f -PODFILE CHECKSUM: ceb1a84305b4be47ad7dd206a4d7f422db78790b +PODFILE CHECKSUM: 00f21cd7eb8be2bb477174c3f27b62143f80f78c COCOAPODS: 1.9.3 From dc7c0e2cf62aee7270ffb3c7987f88f42ce3c218 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Thu, 24 Sep 2020 21:04:51 +0330 Subject: [PATCH 090/108] - Added some saving method to ArticleRepository. - Added and implemented HeadlinesArticleRemoteRepository. - wrote and run unit test for HeadlinesArticleRemoteRepository functionality. - Fixed some bugs. --- DutchNews.xcodeproj/project.pbxproj | 8 + .../Repositories/ArticleRepository.swift | 17 ++ .../HeadlinesArticleLocalRepository.swift | 79 ++++++++ .../HeadlinesArticleRemoteRepository.swift | 27 ++- DutchNews/Classes/Models/Article.swift | 19 +- ...HeadlinesArticleLocalRepositoryTests.swift | 179 ++++++++++++++++++ .../RepositoryDependenciesFactory.swift | 4 + 7 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 DutchNews/Classes/Data Layers/Repositories/HeadlinesArticleLocalRepository.swift create mode 100644 DutchNewsTests/Repositories/HeadLines/HeadlinesArticleLocalRepositoryTests.swift diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index 01eab1a..843866d 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -71,6 +71,8 @@ F8C83463251C04020051A0FD /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C8345F251C04010051A0FD /* Storage.swift */; }; F8C83466251C1F480051A0FD /* CodableDataManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C83465251C1F480051A0FD /* CodableDataManagerTests.swift */; }; F8C83468251C22C80051A0FD /* DispatchQueue+Additonals.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C83467251C22C80051A0FD /* DispatchQueue+Additonals.swift */; }; + F8D58B83251CFC1E00B426AC /* HeadlinesArticleLocalRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D58B82251CFC1E00B426AC /* HeadlinesArticleLocalRepository.swift */; }; + F8D58B85251D04F900B426AC /* HeadlinesArticleLocalRepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D58B84251D04F900B426AC /* HeadlinesArticleLocalRepositoryTests.swift */; }; F8DE79E32515904400A6C2D5 /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DE79E22515904400A6C2D5 /* NetworkService.swift */; }; F8DE79E5251594D700A6C2D5 /* APIClientService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DE79E4251594D700A6C2D5 /* APIClientService.swift */; }; F8E5C0E9251881C80083D2B1 /* AlertableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0E8251881C80083D2B1 /* AlertableView.swift */; }; @@ -189,6 +191,8 @@ F8C8345F251C04010051A0FD /* Storage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; F8C83465251C1F480051A0FD /* CodableDataManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableDataManagerTests.swift; sourceTree = ""; }; F8C83467251C22C80051A0FD /* DispatchQueue+Additonals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+Additonals.swift"; sourceTree = ""; }; + F8D58B82251CFC1E00B426AC /* HeadlinesArticleLocalRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlinesArticleLocalRepository.swift; sourceTree = ""; }; + F8D58B84251D04F900B426AC /* HeadlinesArticleLocalRepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlinesArticleLocalRepositoryTests.swift; sourceTree = ""; }; F8DE79E22515904400A6C2D5 /* NetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = ""; }; F8DE79E4251594D700A6C2D5 /* APIClientService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClientService.swift; sourceTree = ""; }; F8E5C0E8251881C80083D2B1 /* AlertableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertableView.swift; sourceTree = ""; }; @@ -354,6 +358,7 @@ F888006A2517A8EE008DCC54 /* HeadlineSuccessResponse.json */, F888006C2517AA1F008DCC54 /* HeadlineFailureResponse.json */, F858998F2517910700A6BA2A /* HeadlinesArticleRemoteRepositoryTests.swift */, + F8D58B84251D04F900B426AC /* HeadlinesArticleLocalRepositoryTests.swift */, ); path = HeadLines; sourceTree = ""; @@ -393,6 +398,7 @@ children = ( F8589985251784B200A6BA2A /* ArticleRepository.swift */, F85899872517894B00A6BA2A /* HeadlinesArticleRemoteRepository.swift */, + F8D58B82251CFC1E00B426AC /* HeadlinesArticleLocalRepository.swift */, ); path = Repositories; sourceTree = ""; @@ -875,6 +881,7 @@ F8DE79E32515904400A6C2D5 /* NetworkService.swift in Sources */, F8C83468251C22C80051A0FD /* DispatchQueue+Additonals.swift in Sources */, F8154D752518156000BFB42C /* HeadlinesViewController+DataSource.swift in Sources */, + F8D58B83251CFC1E00B426AC /* HeadlinesArticleLocalRepository.swift in Sources */, F85E319F251B8D61002753AC /* WKNavigationDelegateEvents+Rx.swift in Sources */, F85E31A1251B8D61002753AC /* RxWKUserContentController.swift in Sources */, F85E31A0251B8D61002753AC /* RxWKUIDelegateEvents+Rx.swift in Sources */, @@ -940,6 +947,7 @@ F865F74E251687E4001FD067 /* APIAuthenticatorTests.swift in Sources */, F8589980251772CC00A6BA2A /* ModelTests.swift in Sources */, F82C8EFD2516304D002B27B3 /* APIClientServiceTests.swift in Sources */, + F8D58B85251D04F900B426AC /* HeadlinesArticleLocalRepositoryTests.swift in Sources */, F858998D2517909A00A6BA2A /* MockArticleValidResponse.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/DutchNews/Classes/Data Layers/Repositories/ArticleRepository.swift b/DutchNews/Classes/Data Layers/Repositories/ArticleRepository.swift index 116df87..73b54cb 100644 --- a/DutchNews/Classes/Data Layers/Repositories/ArticleRepository.swift +++ b/DutchNews/Classes/Data Layers/Repositories/ArticleRepository.swift @@ -9,6 +9,7 @@ import Foundation import RxSwift +/// `ArticleRepository` Abstract. protocol ArticleRepository { typealias DataType = Article @@ -20,4 +21,20 @@ protocol ArticleRepository { /// - Parameter keyword: <#keyword description#> func search(keyword: String) -> Observable<[DataType]> + /// <#Description#> + /// - Parameter articleByIdentifier: <#articleByIdentifier description#> + func find(articleByIdentifier: T) -> Observable + + /// <#Description#> + /// - Parameter articleByIdentifier: <#articleByIdentifier description#> + func find(articleByIdentifier: T) -> DataType? + + /// <#Description#> + /// - Parameter article: <#article description#> + func save(article: DataType) throws + + /// <#Description#> + /// - Parameter articles: <#articles description#> + func save(articles: [DataType]) throws + } diff --git a/DutchNews/Classes/Data Layers/Repositories/HeadlinesArticleLocalRepository.swift b/DutchNews/Classes/Data Layers/Repositories/HeadlinesArticleLocalRepository.swift new file mode 100644 index 0000000..899f8f1 --- /dev/null +++ b/DutchNews/Classes/Data Layers/Repositories/HeadlinesArticleLocalRepository.swift @@ -0,0 +1,79 @@ +// +// HeadlinesArticleLocalRepository.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/24/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import RxSwift +import RxAlamofire +import Alamofire + +class HeadlinesArticleLocalRepository: ArticleRepository { + + typealias DataType = Article + + let storage: Storage + + init(storage: Storage) { + self.storage = storage + } + + func fetchArticles() -> Observable<[Article]> { + let storage = self.storage + return .create { obs in + + do { + try storage.fetch(type: DataType.self, + predicate: nil, + sort: nil, + completion: { (result) in + obs.on(.next(result)) + + }) + + }catch { + obs.on(.error(error)) + } + + return Disposables.create { + obs.onCompleted() + } + } + } + + func search(keyword: String) -> Observable<[Article]> { + return fetchArticles() + .map { + $0.filter({ $0.title.contains(keyword) || + $0.author?.contains(keyword) == true || + $0.content?.contains(keyword) == true + }) + } + } + + func save(article: DataType) throws { + try storage.save(object: article) + } + + func save(articles: [DataType]) throws { + for article in articles { + try self.save(article: article) + } + } + +} +// MARK: - Unneed Abstract methods implementation +extension HeadlinesArticleLocalRepository { + + func find(articleByIdentifier identifier: T) -> Observable where T: Hashable { + return .empty() + } + + func find(articleByIdentifier: T) -> DataType? where T: Hashable { + return nil + } + +} diff --git a/DutchNews/Classes/Data Layers/Repositories/HeadlinesArticleRemoteRepository.swift b/DutchNews/Classes/Data Layers/Repositories/HeadlinesArticleRemoteRepository.swift index d49dc86..37b0af5 100644 --- a/DutchNews/Classes/Data Layers/Repositories/HeadlinesArticleRemoteRepository.swift +++ b/DutchNews/Classes/Data Layers/Repositories/HeadlinesArticleRemoteRepository.swift @@ -12,7 +12,7 @@ import RxAlamofire import Alamofire class HeadlinesArticleRemoteRepository: ArticleRepository { - + typealias DataType = Article let networkService: NetworkServiceInterceptable @@ -46,9 +46,34 @@ class HeadlinesArticleRemoteRepository: ArticleRepository { .map(map(response:)) } + func get(identiferKey: K) -> [DataType] where K: Hashable { + fatalError() + } + private func map(response: ResponseResult) throws -> [Article] { let result = try response.get() return result.data ?? [] } } + +// MARK: - Unneed method implementation +extension HeadlinesArticleRemoteRepository { + + func find(articleByIdentifier: T) -> Observable where T: Hashable { + return .empty() + } + + func find(articleByIdentifier: T) -> DataType? where T: Hashable { + return nil + } + + func save(article: DataType) throws { + + } + + func save(articles: [DataType]) throws { + + } + +} diff --git a/DutchNews/Classes/Models/Article.swift b/DutchNews/Classes/Models/Article.swift index ca28e88..541ef0e 100644 --- a/DutchNews/Classes/Models/Article.swift +++ b/DutchNews/Classes/Models/Article.swift @@ -8,7 +8,7 @@ import Foundation -struct Article: Storable { +struct Article: Storable, Codable { let title: String let author: String? @@ -41,6 +41,23 @@ struct Article: Storable { case publishedAt, content } + init(title: String, author: String? = nil, description: String? = nil, + source: ArticleSource, + url: URL, imageUrl: URL? = nil , publishedAt: Date = .now, + content: String? = nil, type: ArticleType = .news) { + + self.title = title + self.author = author + self.description = description + self.url = url + self.imageUrl = imageUrl?.absoluteString + self.publishedAt = publishedAt + self.source = source + self.content = content + self.type = type + + } + func primaryKeyValue() -> String { return url.absoluteString + "\(publishedAt.timeIntervalSince1970)" } diff --git a/DutchNewsTests/Repositories/HeadLines/HeadlinesArticleLocalRepositoryTests.swift b/DutchNewsTests/Repositories/HeadLines/HeadlinesArticleLocalRepositoryTests.swift new file mode 100644 index 0000000..129baef --- /dev/null +++ b/DutchNewsTests/Repositories/HeadLines/HeadlinesArticleLocalRepositoryTests.swift @@ -0,0 +1,179 @@ +// +// HeadlinesArticleLocalRepositoryTests.swift +// DutchNewsTests +// +// Created by Farshad Mousalou on 9/24/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import RxSwift +import RxTest +import RxBlocking +import Alamofire +import Mocker +import XCTest + +@testable import DutchNews + +class HeadlinesArticleLocalRepositoryTests: XCTestCase { + + var articleRepository: ArticleRepository! + var disposeBag: DisposeBag! + + override func setUp() { + + articleRepository = HeadlinesArticleLocalRepository(storage: RepositoryDependenciesFactory.createStorage()) + disposeBag = DisposeBag() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + disposeBag = nil + articleRepository = nil + } + + func testSaveArticle() { + + let article = Article(title: "Mock 1", + author: "Unit Test", + description: "Mock object for unit test ", + source: ArticleSource(id: "", name: "DutchApps"), + url: URL(string: "https://example.com")!, + imageUrl: nil, + publishedAt: Date(), + content: "test content",type: .mock) + + do { + try articleRepository.save(article: article) + print("object saved successfully.") + }catch { + XCTFail("Error Occured in saving article with info: \(error.localizedDescription)") + } + } + + func testSaveArticles() { + + let articles = self.mockResponserFetcher(name: "HeadlineSuccessResponse") + + do { + try articleRepository.save(articles: articles) + print("articles saved successfully.") + }catch { + XCTFail("Error Occured in saving articles with info: \(error.localizedDescription)") + } + + } + + func testErrorOnSaveArticle() { + + let article = Article(title: "Mock 2", + author: "Unit Test", + description: "Mock object for unit test ", + source: ArticleSource(id: "", name: "DutchApps"), + url: URL(string: "https://example.com")!, + imageUrl: nil, + publishedAt: Date(), + content: "test content",type: .mock) + + do { + try articleRepository.save(article: article) + try articleRepository.save(article: article) + XCTFail("Expected that calling save method twise rise error.") + }catch { + XCTAssertNotNil(error, "An error must be occured.") + print("Error was happened. Fulfill expecations") + } + } + + func testRealFetchArticles() { + + let exptection = self.expectation(description: "testRealFetchArticles") + exptection.expectedFulfillmentCount = 1 + + articleRepository.fetchArticles() + .observeOn(MainScheduler.asyncInstance) + .subscribe { (event) in + switch event { + case .next(let element): + print("element ", element) + case .error(let error): + XCTFail("Error Occured with info: \(error.localizedDescription)") + exptection.fulfill() + case .completed: + print("Observer completed") + } + + exptection.fulfill() + + }.disposed(by: disposeBag) + + self.waitForExpectations(timeout: 30.0) { (error) in + XCTAssertTrue(error == nil, "Error Occured with info: \(error!.localizedDescription)") + } + + } + + func testSearchingArticleFromLocalRepository() { + + let exptection = self.expectation(description: "testRealFetchArticles") + exptection.expectedFulfillmentCount = 2 + + articleRepository.search(keyword: "Mock") + .observeOn(MainScheduler.asyncInstance) + .subscribe { (event) in + switch event { + case .next(let element): + print("element ", element) + case .error(let error): + XCTFail("Error Occured with info: \(error.localizedDescription)") + exptection.fulfill() + case .completed: + print("Observer completed") + } + + exptection.fulfill() + + }.disposed(by: disposeBag) + + articleRepository.search(keyword: "test").subscribe { (event) in + switch event { + case .next(let element): + print("element ", element) + case .error(let error): + XCTFail("Error Occured with info: \(error.localizedDescription)") + exptection.fulfill() + case .completed: + print("Observer completed") + } + + exptection.fulfill() + + }.disposed(by: disposeBag) + + self.waitForExpectations(timeout: 20.0) { (error) in + XCTAssertTrue(error == nil, "Error Occured with info: \(error!.localizedDescription)") + } + + } + + func mockResponserFetcher(name: String) -> [Article] { + let bundle = Bundle(for: type(of: self)) + + guard let data = try? Data(contentsOf: bundle.url(forResource: name, withExtension: "json")!) else { + return [] + } + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + + guard let objs = try? decoder.decode([Article].self, from: data) else { + return [] + } + + return objs + + } + +} diff --git a/DutchNewsTests/Repositories/RepositoryDependenciesFactory.swift b/DutchNewsTests/Repositories/RepositoryDependenciesFactory.swift index 76ab8f3..daeaef1 100644 --- a/DutchNewsTests/Repositories/RepositoryDependenciesFactory.swift +++ b/DutchNewsTests/Repositories/RepositoryDependenciesFactory.swift @@ -39,4 +39,8 @@ struct RepositoryDependenciesFactory { return MockArticleValidResponse() } + static func createStorage() -> Storage { + return CodableDataManager.default + } + } From 49aec554d4dbfe85b8855c436b4b0d6cf43f83cd Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Thu, 24 Sep 2020 21:11:24 +0330 Subject: [PATCH 091/108] - Removed unneeded devices. - Tweaked on building step. --- .github/workflows/unit-test.yml | 11 ++++------- fastlane/Fastfile | 4 +++- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 8e140a1..1bc7470 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -24,7 +24,7 @@ jobs: runs-on: macOS-latest strategy: matrix: - devices: ["iPhone 8 (11.4)","iPhone SE (12.4)","iPhone X (13.6)","iPhone 11","iPad Air (11.4)","iPad Air 2 (12.4)","iPad Pro (10.5-inch) (13.6)","iPad Pro (12.9-inch)"] + devices: ["iPhone 8 (11.4)","iPhone SE (12.4)","iPhone X (13.6)","iPad Air (11.4)","iPad Air 2 (12.4)","iPad Pro (10.5-inch) (13.6)"] steps: - name: Checkout Branch uses: actions/checkout@v1 @@ -43,8 +43,7 @@ jobs: sudo ln -s /Applications/Xcode_10.3.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 12.4.simruntime sudo ln -s /Applications/Xcode_11.6.app//Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 13.6.simruntime bundle exec xcversion simulators --install="iOS 11.4 Simulator" --verbose - xcrun simctl list runtimes - xcrun simctl list devicetypes + echo "Installed iOS Simulators" - name: Creating iOS Simulators run: | xcrun simctl create "iPhone 8" com.apple.CoreSimulator.SimDeviceType.iPhone-8 com.apple.CoreSimulator.SimRuntime.iOS-11-4 @@ -53,14 +52,12 @@ jobs: xcrun simctl create "iPad Air" com.apple.CoreSimulator.SimDeviceType.iPad-Air "com.apple.CoreSimulator.SimRuntime.iOS-11-4" xcrun simctl create "iPad Air 2" com.apple.CoreSimulator.SimDeviceType.iPad-Air-2 "com.apple.CoreSimulator.SimRuntime.iOS-12-4" xcrun simctl create "iPad Pro (10.5-inch)" com.apple.CoreSimulator.SimDeviceType.iPad-Pro--10-5-inch- "com.apple.CoreSimulator.SimRuntime.iOS-13-6" - xcrun simctl list devices 11.4 - xcrun simctl list devices 12.4 - xcrun simctl list devices 13.6 + echo "Created iOS Simulators" - name: Build and test on Device run: | echo "Destination => ${destination}" - bundle exec fastlane run_ci_tests device:"${destination}" --verbose + bundle exec fastlane run_ci_tests device:"${destination}" clean:true --verbose env: destination: ${{ matrix.devices }} diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 72ca337..10af4b3 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -43,8 +43,9 @@ platform :ios do cocoapods() end + clear_derived_data() #clear all derived_data + if lane != :run_ci_tests - clear_derived_data() #clear all derived_data enable_automatic_code_signing() #autosiging end @@ -97,6 +98,7 @@ platform :ios do rescue => ex UI.error "Failure on #{scheme} #{version} info : #{ex.to_s}" + raise Exception.new "Failure on #{scheme} #{version} info : #{ex.to_s}" end end From 3ddcd30755bf519d2e439ec4793cd4598f2a24c8 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Thu, 24 Sep 2020 23:05:15 +0330 Subject: [PATCH 092/108] - Moved HeadlinesUseCases to Abstract folder. - Modified HeadlinesFetchingUseCase to fetch cached articles. - Minor changes in DIContainer. --- DutchNews.xcodeproj/project.pbxproj | 14 ++++++++--- DutchNews/AppDIContainer.swift | 13 ++++++++-- .../Repositories/ArticleRepository.swift | 2 +- .../{ => Abstract}/HeadlinesUseCases.swift | 0 .../Domains/HeadlinesFetchingUseCase.swift | 25 ++++++++++++++++++- 5 files changed, 47 insertions(+), 7 deletions(-) rename DutchNews/Classes/Domains/{ => Abstract}/HeadlinesUseCases.swift (100%) diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index 843866d..291a727 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -59,6 +59,7 @@ F865F7662516AB66001FD067 /* APIServerResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F7652516AB66001FD067 /* APIServerResponseTests.swift */; }; F865F76B2516C08C001FD067 /* NetworkValidResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F76A2516C08C001FD067 /* NetworkValidResponse.swift */; }; F865F76D2516C826001FD067 /* DefaultAPIValidResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F76C2516C826001FD067 /* DefaultAPIValidResponse.swift */; }; + F884C0FB251D24000078E88B /* HeadlinesUseCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0F72518E7770083D2B1 /* HeadlinesUseCases.swift */; }; F88800692517A423008DCC54 /* RepositoryDependenciesFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88800682517A423008DCC54 /* RepositoryDependenciesFactory.swift */; }; F888006B2517A8EE008DCC54 /* HeadlineSuccessResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = F888006A2517A8EE008DCC54 /* HeadlineSuccessResponse.json */; }; F888006D2517AA1F008DCC54 /* HeadlineFailureResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = F888006C2517AA1F008DCC54 /* HeadlineFailureResponse.json */; }; @@ -82,7 +83,6 @@ F8E5C0F1251884A20083D2B1 /* HeadlineSearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0F0251884A20083D2B1 /* HeadlineSearchViewModel.swift */; }; F8E5C0F32518D8AC0083D2B1 /* ArticleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0F22518D8AC0083D2B1 /* ArticleViewModel.swift */; }; F8E5C0F52518DD3E0083D2B1 /* ViewModelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0F42518DD3E0083D2B1 /* ViewModelState.swift */; }; - F8E5C0F82518E7770083D2B1 /* HeadlinesUseCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0F72518E7770083D2B1 /* HeadlinesUseCases.swift */; }; F8E5C0FA2518E8100083D2B1 /* HeadlinesFetchingUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0F92518E8100083D2B1 /* HeadlinesFetchingUseCase.swift */; }; F8E5C0FC2518E9150083D2B1 /* HeadlinesSearchingUseCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0FB2518E9150083D2B1 /* HeadlinesSearchingUseCases.swift */; }; F8E5C0FE25190CDD0083D2B1 /* HeadlineCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0FD25190CDD0083D2B1 /* HeadlineCellViewModel.swift */; }; @@ -414,6 +414,14 @@ path = Response; sourceTree = ""; }; + F884C0FA251D1FCC0078E88B /* Abstract */ = { + isa = PBXGroup; + children = ( + F8E5C0F72518E7770083D2B1 /* HeadlinesUseCases.swift */, + ); + path = Abstract; + sourceTree = ""; + }; F89B0211250D446000B41293 = { isa = PBXGroup; children = ( @@ -505,7 +513,7 @@ F8E5C0F62518E7300083D2B1 /* Domains */ = { isa = PBXGroup; children = ( - F8E5C0F72518E7770083D2B1 /* HeadlinesUseCases.swift */, + F884C0FA251D1FCC0078E88B /* Abstract */, F8E5C0F92518E8100083D2B1 /* HeadlinesFetchingUseCase.swift */, F8E5C0FB2518E9150083D2B1 /* HeadlinesSearchingUseCases.swift */, ); @@ -876,6 +884,7 @@ F8E5C11E25191D5E0083D2B1 /* Date+Convertor.swift in Sources */, F865F7602516A643001FD067 /* APIServerResponseStatus.swift in Sources */, F8E5C0FA2518E8100083D2B1 /* HeadlinesFetchingUseCase.swift in Sources */, + F884C0FB251D24000078E88B /* HeadlinesUseCases.swift in Sources */, F8E5C12125191D5E0083D2B1 /* Bundle+Extensions.swift in Sources */, F8154D522517EBC700BFB42C /* HeadlinesViewController+MagazineLayout.swift in Sources */, F8DE79E32515904400A6C2D5 /* NetworkService.swift in Sources */, @@ -893,7 +902,6 @@ F8E5C0F1251884A20083D2B1 /* HeadlineSearchViewModel.swift in Sources */, F8E5C12D25191DA60083D2B1 /* UIViewController+StoryboardName.swift in Sources */, F8154D7125180C3900BFB42C /* ArticleWebContainerCollectionViewCell.swift in Sources */, - F8E5C0F82518E7770083D2B1 /* HeadlinesUseCases.swift in Sources */, F8E5C12A25191DA60083D2B1 /* UIImage+Additionals.swift in Sources */, F8E5C12B25191DA60083D2B1 /* UINavigationBar+Additionals.swift in Sources */, F865F75025168F05001FD067 /* AppConfig.swift in Sources */, diff --git a/DutchNews/AppDIContainer.swift b/DutchNews/AppDIContainer.swift index 7c02237..bce5475 100644 --- a/DutchNews/AppDIContainer.swift +++ b/DutchNews/AppDIContainer.swift @@ -14,7 +14,7 @@ struct AppDIContainer { //////////////////////////////////////////////////////////////// // MARK: - - // MARK: Authroization DI Container + // MARK: Data Layers DI Container // MARK: - //////////////////////////////////////////////////////////////// @@ -42,6 +42,10 @@ struct AppDIContainer { return decoder }() + static let storage: Storage = { + return CodableDataManager.default + }() + //////////////////////////////////////////////////////////////// // MARK: - // MARK: Repository DI Container @@ -54,6 +58,10 @@ struct AppDIContainer { validator: DefaultAPIValidResponse()) } + static var headlineLocalArticleRepository: ArticleRepository { + return HeadlinesArticleLocalRepository(storage: storage) + } + //////////////////////////////////////////////////////////////// // MARK: - // MARK: Use Cases DI Container @@ -61,7 +69,8 @@ struct AppDIContainer { //////////////////////////////////////////////////////////////// static var headlineFetchingUseCase: HeadlinesUseCases { - return HeadlinesFetchingUseCase(repository: headlineArticleRepository) + return HeadlinesFetchingUseCase(repository: headlineArticleRepository, + localRespository: headlineLocalArticleRepository) } } diff --git a/DutchNews/Classes/Data Layers/Repositories/ArticleRepository.swift b/DutchNews/Classes/Data Layers/Repositories/ArticleRepository.swift index 73b54cb..96cb54e 100644 --- a/DutchNews/Classes/Data Layers/Repositories/ArticleRepository.swift +++ b/DutchNews/Classes/Data Layers/Repositories/ArticleRepository.swift @@ -10,7 +10,7 @@ import Foundation import RxSwift /// `ArticleRepository` Abstract. -protocol ArticleRepository { +protocol ArticleRepository: class { typealias DataType = Article diff --git a/DutchNews/Classes/Domains/HeadlinesUseCases.swift b/DutchNews/Classes/Domains/Abstract/HeadlinesUseCases.swift similarity index 100% rename from DutchNews/Classes/Domains/HeadlinesUseCases.swift rename to DutchNews/Classes/Domains/Abstract/HeadlinesUseCases.swift diff --git a/DutchNews/Classes/Domains/HeadlinesFetchingUseCase.swift b/DutchNews/Classes/Domains/HeadlinesFetchingUseCase.swift index 80ef832..25ace39 100644 --- a/DutchNews/Classes/Domains/HeadlinesFetchingUseCase.swift +++ b/DutchNews/Classes/Domains/HeadlinesFetchingUseCase.swift @@ -12,13 +12,36 @@ import RxSwift class HeadlinesFetchingUseCase: HeadlinesUseCases { let repository: ArticleRepository + let local: ArticleRepository? init(repository: ArticleRepository) { self.repository = repository + self.local = nil + } + + init(repository: ArticleRepository, localRespository: ArticleRepository) { + self.repository = repository + self.local = localRespository } func fetchArticles() -> Observable<[T]> { - return repository.fetchArticles() + return loadLocalThenFetchFromAPI() + } + + private func loadLocalThenFetchFromAPI() -> Observable<[T]> { + + let remoteSource = repository.fetchArticles() + guard let localSource = local?.fetchArticles() else { + return remoteSource + } + + let source = Observable.merge(remoteSource, localSource) + + return source + .debug("#\(#file.replacingOccurrences(of: ".swift", with: "")).\(#function)") + .do(afterNext: {[weak local] in + try? local?.save(articles: $0) + }) } } From c5e1abdf68537a84379230fdde4bed490198e03a Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 25 Sep 2020 00:14:13 +0330 Subject: [PATCH 093/108] - Added ArticlePageUseCase. - Added ArticlePageViewModel - Moved view models abstraction. - Added and implemented ViewModels related to PageView and Detail View. --- DutchNews.xcodeproj/project.pbxproj | 32 ++++++- DutchNews/AppDIContainer.swift | 4 + .../Domains/Abstract/ArticlesUseCases.swift | 27 ++++++ .../Classes/Domains/ArticlesPageUseCase.swift | 24 +++++ .../{ => Abstract}/ArticleViewModel.swift | 0 .../Abstract/ArticlesPageViewModel.swift | 42 +++++++++ .../{ => Abstract}/ArticlesViewModel.swift | 4 +- .../ViewModels/ArticleDetailViewModel.swift | 74 +++++++++++++++ .../ViewModels/ArticleDetailsPageView.swift | 92 +++++++++++++++++++ .../ViewModels/HeadlinesViewModel.swift | 38 ++++++-- 10 files changed, 325 insertions(+), 12 deletions(-) create mode 100644 DutchNews/Classes/Domains/Abstract/ArticlesUseCases.swift create mode 100644 DutchNews/Classes/Domains/ArticlesPageUseCase.swift rename DutchNews/Classes/ViewModels/{ => Abstract}/ArticleViewModel.swift (100%) create mode 100644 DutchNews/Classes/ViewModels/Abstract/ArticlesPageViewModel.swift rename DutchNews/Classes/ViewModels/{ => Abstract}/ArticlesViewModel.swift (93%) create mode 100644 DutchNews/Classes/ViewModels/ArticleDetailViewModel.swift create mode 100644 DutchNews/Classes/ViewModels/ArticleDetailsPageView.swift diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index 291a727..2e84bed 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -26,6 +26,10 @@ F8154D7625181B1C00BFB42C /* ArticleWebContainerCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F8154D7225180F0E00BFB42C /* ArticleWebContainerCollectionViewCell.xib */; }; F8154D7725181D4100BFB42C /* HeadlineSuccessResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = F888006A2517A8EE008DCC54 /* HeadlineSuccessResponse.json */; }; F8154D792518207B00BFB42C /* String+HTML.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8154D782518207B00BFB42C /* String+HTML.swift */; }; + F815729D251D3077009DBFD7 /* ArticleDetailsPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F815729C251D3077009DBFD7 /* ArticleDetailsPageView.swift */; }; + F81572A0251D3254009DBFD7 /* ArticlesPageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F815729F251D3254009DBFD7 /* ArticlesPageViewModel.swift */; }; + F81572A2251D37F7009DBFD7 /* ArticleDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F81572A1251D37F7009DBFD7 /* ArticleDetailViewModel.swift */; }; + F81572A4251D3B39009DBFD7 /* ArticlesPageUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F81572A3251D3B39009DBFD7 /* ArticlesPageUseCase.swift */; }; F82C8EFD2516304D002B27B3 /* APIClientServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8EFC2516304D002B27B3 /* APIClientServiceTests.swift */; }; F82C8EFF25163073002B27B3 /* NetworkMocking.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8EFE25163073002B27B3 /* NetworkMocking.swift */; }; F82C8F0125163898002B27B3 /* NetworkMockingDataFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8F0025163898002B27B3 /* NetworkMockingDataFactory.swift */; }; @@ -60,6 +64,7 @@ F865F76B2516C08C001FD067 /* NetworkValidResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F76A2516C08C001FD067 /* NetworkValidResponse.swift */; }; F865F76D2516C826001FD067 /* DefaultAPIValidResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F76C2516C826001FD067 /* DefaultAPIValidResponse.swift */; }; F884C0FB251D24000078E88B /* HeadlinesUseCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0F72518E7770083D2B1 /* HeadlinesUseCases.swift */; }; + F884C0FD251D2CAE0078E88B /* ArticlesUseCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = F884C0FC251D2CAE0078E88B /* ArticlesUseCases.swift */; }; F88800692517A423008DCC54 /* RepositoryDependenciesFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88800682517A423008DCC54 /* RepositoryDependenciesFactory.swift */; }; F888006B2517A8EE008DCC54 /* HeadlineSuccessResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = F888006A2517A8EE008DCC54 /* HeadlineSuccessResponse.json */; }; F888006D2517AA1F008DCC54 /* HeadlineFailureResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = F888006C2517AA1F008DCC54 /* HeadlineFailureResponse.json */; }; @@ -142,6 +147,10 @@ F8154D7225180F0E00BFB42C /* ArticleWebContainerCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ArticleWebContainerCollectionViewCell.xib; sourceTree = ""; }; F8154D742518156000BFB42C /* HeadlinesViewController+DataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HeadlinesViewController+DataSource.swift"; sourceTree = ""; }; F8154D782518207B00BFB42C /* String+HTML.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+HTML.swift"; sourceTree = ""; }; + F815729C251D3077009DBFD7 /* ArticleDetailsPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleDetailsPageView.swift; sourceTree = ""; }; + F815729F251D3254009DBFD7 /* ArticlesPageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticlesPageViewModel.swift; sourceTree = ""; }; + F81572A1251D37F7009DBFD7 /* ArticleDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleDetailViewModel.swift; sourceTree = ""; }; + F81572A3251D3B39009DBFD7 /* ArticlesPageUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticlesPageUseCase.swift; sourceTree = ""; }; F82C8EFC2516304D002B27B3 /* APIClientServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClientServiceTests.swift; sourceTree = ""; }; F82C8EFE25163073002B27B3 /* NetworkMocking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMocking.swift; sourceTree = ""; }; F82C8F0025163898002B27B3 /* NetworkMockingDataFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMockingDataFactory.swift; sourceTree = ""; }; @@ -175,6 +184,7 @@ F865F7652516AB66001FD067 /* APIServerResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIServerResponseTests.swift; sourceTree = ""; }; F865F76A2516C08C001FD067 /* NetworkValidResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkValidResponse.swift; sourceTree = ""; }; F865F76C2516C826001FD067 /* DefaultAPIValidResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultAPIValidResponse.swift; sourceTree = ""; }; + F884C0FC251D2CAE0078E88B /* ArticlesUseCases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticlesUseCases.swift; sourceTree = ""; }; F88800682517A423008DCC54 /* RepositoryDependenciesFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryDependenciesFactory.swift; sourceTree = ""; }; F888006A2517A8EE008DCC54 /* HeadlineSuccessResponse.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = HeadlineSuccessResponse.json; sourceTree = ""; }; F888006C2517AA1F008DCC54 /* HeadlineFailureResponse.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = HeadlineFailureResponse.json; sourceTree = ""; }; @@ -294,6 +304,16 @@ path = UI; sourceTree = ""; }; + F815729E251D323D009DBFD7 /* Abstract */ = { + isa = PBXGroup; + children = ( + F8E5C0F22518D8AC0083D2B1 /* ArticleViewModel.swift */, + F8E5C0EC2518833B0083D2B1 /* ArticlesViewModel.swift */, + F815729F251D3254009DBFD7 /* ArticlesPageViewModel.swift */, + ); + path = Abstract; + sourceTree = ""; + }; F82C8EFB2516051D002B27B3 /* NetworkTests */ = { isa = PBXGroup; children = ( @@ -418,6 +438,7 @@ isa = PBXGroup; children = ( F8E5C0F72518E7770083D2B1 /* HeadlinesUseCases.swift */, + F884C0FC251D2CAE0078E88B /* ArticlesUseCases.swift */, ); path = Abstract; sourceTree = ""; @@ -516,6 +537,7 @@ F884C0FA251D1FCC0078E88B /* Abstract */, F8E5C0F92518E8100083D2B1 /* HeadlinesFetchingUseCase.swift */, F8E5C0FB2518E9150083D2B1 /* HeadlinesSearchingUseCases.swift */, + F81572A3251D3B39009DBFD7 /* ArticlesPageUseCase.swift */, ); path = Domains; sourceTree = ""; @@ -560,12 +582,13 @@ F8F14C6B250D70C700C24FF5 /* ViewModels */ = { isa = PBXGroup; children = ( + F815729E251D323D009DBFD7 /* Abstract */, F8E5C0F42518DD3E0083D2B1 /* ViewModelState.swift */, - F8E5C0F22518D8AC0083D2B1 /* ArticleViewModel.swift */, - F8E5C0EC2518833B0083D2B1 /* ArticlesViewModel.swift */, F8E5C0EE2518848D0083D2B1 /* HeadlinesViewModel.swift */, F8E5C0F0251884A20083D2B1 /* HeadlineSearchViewModel.swift */, F8E5C0FD25190CDD0083D2B1 /* HeadlineCellViewModel.swift */, + F815729C251D3077009DBFD7 /* ArticleDetailsPageView.swift */, + F81572A1251D37F7009DBFD7 /* ArticleDetailViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -881,6 +904,7 @@ F85E319D251B8D61002753AC /* Rx+WebKit.swift in Sources */, F8C83460251C04020051A0FD /* CodableDataManager.swift in Sources */, F865F7622516A6C1001FD067 /* APIServerResponseError.swift in Sources */, + F884C0FD251D2CAE0078E88B /* ArticlesUseCases.swift in Sources */, F8E5C11E25191D5E0083D2B1 /* Date+Convertor.swift in Sources */, F865F7602516A643001FD067 /* APIServerResponseStatus.swift in Sources */, F8E5C0FA2518E8100083D2B1 /* HeadlinesFetchingUseCase.swift in Sources */, @@ -918,8 +942,11 @@ F8154D792518207B00BFB42C /* String+HTML.swift in Sources */, F85E318E251B8484002753AC /* ArticleDetailViewController.swift in Sources */, F8E5C11F25191D5E0083D2B1 /* Collection+Additionals.swift in Sources */, + F81572A2251D37F7009DBFD7 /* ArticleDetailViewModel.swift in Sources */, F8C83463251C04020051A0FD /* Storage.swift in Sources */, F8E5C0EB251882560083D2B1 /* UIViewController+AlertableView.swift in Sources */, + F81572A0251D3254009DBFD7 /* ArticlesPageViewModel.swift in Sources */, + F81572A4251D3B39009DBFD7 /* ArticlesPageUseCase.swift in Sources */, F85E3192251B85E4002753AC /* ArticleDetailHeaderView.swift in Sources */, F8154D552517EC0D00BFB42C /* HeadlineLayoutConfiguration.swift in Sources */, F85E31A2251B8D61002753AC /* RxWKNavigationDelegateProxy.swift in Sources */, @@ -937,6 +964,7 @@ F8E5C12225191D5E0083D2B1 /* String+EmptyChecking.swift in Sources */, F89B021E250D446000B41293 /* AppDelegate.swift in Sources */, F8E5C12C25191DA60083D2B1 /* UILabel+Localization.swift in Sources */, + F815729D251D3077009DBFD7 /* ArticleDetailsPageView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/DutchNews/AppDIContainer.swift b/DutchNews/AppDIContainer.swift index bce5475..d8c152c 100644 --- a/DutchNews/AppDIContainer.swift +++ b/DutchNews/AppDIContainer.swift @@ -73,4 +73,8 @@ struct AppDIContainer { localRespository: headlineLocalArticleRepository) } + static var articlesPageUseCase: ArticlesUseCase { + return ArticlesPageUseCase(repository: headlineLocalArticleRepository) + } + } diff --git a/DutchNews/Classes/Domains/Abstract/ArticlesUseCases.swift b/DutchNews/Classes/Domains/Abstract/ArticlesUseCases.swift new file mode 100644 index 0000000..274d965 --- /dev/null +++ b/DutchNews/Classes/Domains/Abstract/ArticlesUseCases.swift @@ -0,0 +1,27 @@ +// +// ArticlesUseCase.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/24/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import RxSwift + +/// Abstract `ArticlesUseCase` +protocol ArticlesUseCase { + + typealias T = Article + + func fetchLocalArticles() -> Observable<[T]> + +} + +extension ArticlesUseCase { + + func fetchLocalArticles() -> Observable<[T]> { + return .empty() + } + +} diff --git a/DutchNews/Classes/Domains/ArticlesPageUseCase.swift b/DutchNews/Classes/Domains/ArticlesPageUseCase.swift new file mode 100644 index 0000000..b787cc4 --- /dev/null +++ b/DutchNews/Classes/Domains/ArticlesPageUseCase.swift @@ -0,0 +1,24 @@ +// +// ArticlesPageUseCase.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/25/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import RxSwift + +class ArticlesPageUseCase: ArticlesUseCase { + + let repository: ArticleRepository + + init(repository: ArticleRepository) { + self.repository = repository + } + + func fetchLocalArticles() -> Observable<[T]> { + return repository.fetchArticles() + } + +} diff --git a/DutchNews/Classes/ViewModels/ArticleViewModel.swift b/DutchNews/Classes/ViewModels/Abstract/ArticleViewModel.swift similarity index 100% rename from DutchNews/Classes/ViewModels/ArticleViewModel.swift rename to DutchNews/Classes/ViewModels/Abstract/ArticleViewModel.swift diff --git a/DutchNews/Classes/ViewModels/Abstract/ArticlesPageViewModel.swift b/DutchNews/Classes/ViewModels/Abstract/ArticlesPageViewModel.swift new file mode 100644 index 0000000..b67af11 --- /dev/null +++ b/DutchNews/Classes/ViewModels/Abstract/ArticlesPageViewModel.swift @@ -0,0 +1,42 @@ +// +// ArticlesPageViewModel.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/24/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import RxSwift +import RxCocoa +import RxDataSources + +/// ArticleViewModel Interface +protocol ArticlesPageViewModel: ArticlesViewModel { + + var currentLoadingProgress: BehaviorRelay { get } + + func viewModel(atIndex index: Int) -> T.Item? + + subscript(index: Int) -> T.Item? { get } +} + +// MARK: - Implemented optional methods +extension ArticlesPageViewModel { + + var output: Driver<[T]> { + return .empty() + } + + func refreshArticles() { + + } + + func didSelect(article: T.Item) { + + } + + func didSelect(articleAtIndex: IndexPath) { + + } +} diff --git a/DutchNews/Classes/ViewModels/ArticlesViewModel.swift b/DutchNews/Classes/ViewModels/Abstract/ArticlesViewModel.swift similarity index 93% rename from DutchNews/Classes/ViewModels/ArticlesViewModel.swift rename to DutchNews/Classes/ViewModels/Abstract/ArticlesViewModel.swift index dc2d2e0..4d0f80a 100644 --- a/DutchNews/Classes/ViewModels/ArticlesViewModel.swift +++ b/DutchNews/Classes/ViewModels/Abstract/ArticlesViewModel.swift @@ -20,12 +20,12 @@ protocol ArticlesViewModel: class { var output: Driver<[T]> { get } + var selectedIndex: BehaviorRelay { get set } + func fetchArticles() func refreshArticles() - func article(atIndex: IndexPath) -> T.Item? - func didSelect(article: T.Item) func didSelect(articleAtIndex: IndexPath) diff --git a/DutchNews/Classes/ViewModels/ArticleDetailViewModel.swift b/DutchNews/Classes/ViewModels/ArticleDetailViewModel.swift new file mode 100644 index 0000000..2618ec8 --- /dev/null +++ b/DutchNews/Classes/ViewModels/ArticleDetailViewModel.swift @@ -0,0 +1,74 @@ +// +// ArticleDetailViewModel.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/24/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import RxSwift +import RxCocoa + +final class ArticleDetailViewModel: ArticleViewModel { + + var state: Driver { + return .empty() + } + + var output: Driver { + return _output.asDriver() + } + + private var _output: BehaviorRelay + + var model: T { + didSet { + guard model != oldValue else { + return + } + _output.accept(Self.convert(model: model)) + } + } + + init(model: T) { + self.model = model + _output = BehaviorRelay(value: Self.convert(model: model)) + } + + private static func convert(model: T) -> ArticleRepresentable { + + let dateFormatter = DateFormatter.currentZoneFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .short + + return ArticleDetailViewModel(title: model.title, + author: model.author, + description: model.title, + source: model.source.name, + url: model.url, + urlToImage: model.urlToImage, + publishedAt: dateFormatter.string(from: model.publishedAt), + content: model.content, type: model.type) + } + +} + +private extension ArticleDetailViewModel { + + struct ArticleDetailViewModel: ArticleRepresentable { + + var title: String + var author: String? + var description: String? + var source: String? + var url: URL + var urlToImage: URL? + var publishedAt: String + var content: String? + + var type: ArticleType + + } + +} diff --git a/DutchNews/Classes/ViewModels/ArticleDetailsPageView.swift b/DutchNews/Classes/ViewModels/ArticleDetailsPageView.swift new file mode 100644 index 0000000..8810380 --- /dev/null +++ b/DutchNews/Classes/ViewModels/ArticleDetailsPageView.swift @@ -0,0 +1,92 @@ +// +// ArticleDetailsPageView.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/24/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import RxSwift +import RxCocoa + +final class ArticleDetailsPageView: ArticlesPageViewModel { + + private var statePublisher: BehaviorRelay + + var state: Driver { + return statePublisher.asDriver { + return .just(.error($0) ) + }.distinctUntilChanged() + } + + private var items: [T.Item] = [] { + didSet { + + } + } + + var count: Int { + return items.count + } + + var selectedIndex: BehaviorRelay + + var currentLoadingProgress: BehaviorRelay + + let useCase: ArticlesUseCase + + let disposeBag = DisposeBag() + + init(useCase: ArticlesUseCase) { + self.useCase = useCase + self.selectedIndex = .init(value: 0) + self.currentLoadingProgress = .init(value: 0.0) + self.statePublisher = BehaviorRelay(value: .idle) + } + + func viewModel(atIndex index: Int) -> T.Item? { + + guard let item = items[safe: index] else { + return nil + } + + return item + } + + subscript(index: Int) -> T.Item? { + return viewModel(atIndex: index) + } + + func fetchArticles() { + + statePublisher.accept(.loading(isRefreshing: false)) + + useCase.fetchLocalArticles() + .subscribe {[weak self] event in + + switch event { + case .next(let newItems): + guard let `self` = self else { + break + } + + let mapped = newItems.map { + ArticleDetailViewModel(model: $0) + } + + self.items = mapped + + self.statePublisher.accept(.loaded) + + case .error(let error): + self?.statePublisher.accept(.error(error)) + self?.statePublisher.accept(.idle) + case .completed: + self?.statePublisher.accept(.idle) + } + + }.disposed(by: disposeBag) + } + +} diff --git a/DutchNews/Classes/ViewModels/HeadlinesViewModel.swift b/DutchNews/Classes/ViewModels/HeadlinesViewModel.swift index 7063d52..e025e6a 100644 --- a/DutchNews/Classes/ViewModels/HeadlinesViewModel.swift +++ b/DutchNews/Classes/ViewModels/HeadlinesViewModel.swift @@ -12,6 +12,8 @@ import RxCocoa final class HeadlinesViewModel: ArticlesViewModel { + var selectedIndex: BehaviorRelay + private var statePublisher: BehaviorRelay var state: Driver { @@ -43,6 +45,8 @@ final class HeadlinesViewModel: ArticlesViewModel { self.useCase = useCase self.statePublisher = state self.outputPublisher = output + self.selectedIndex = BehaviorRelay(value: nil) + } required convenience init(useCase: HeadlinesUseCases) { @@ -65,17 +69,35 @@ final class HeadlinesViewModel: ArticlesViewModel { fetchArticlesFromRepository(isRefreshing: true) } - func article(atIndex index: IndexPath) -> T.Item? { - //TODO: Implement method - return nil - } - func didSelect(article: T.Item) { - //TODO: Implement method + let datas = items + guard var index = datas.firstIndex(where: { $0 == article }), + index != 3 else { + return + } + + if index > 3 { + index -= 1 + } + + self.selectedIndex.accept(index) } - func didSelect(articleAtIndex: IndexPath) { - //TODO: Implement method + func didSelect(articleAtIndex index: IndexPath) { + let datas = items + + // make mutable. + + var index = index + guard index.item != 3, datas[safe:index.item] != nil else { + return + } + + if index.item > 3 { + index.item -= 1 + } + + self.selectedIndex.accept(index.item) } //////////////////////////////////////////////////////////////// From 1218060e7a882b648f1767740be128fffcf1a6af Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 25 Sep 2020 01:00:33 +0330 Subject: [PATCH 094/108] - remove try catch at run_ci_tests methods in fastlane. --- fastlane/Fastfile | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 10af4b3..275de83 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -93,12 +93,8 @@ platform :ios do scan(clean: clean, # clean project folder before test execution device: device, # Devices for testing configuration: "Debug", - open_report: open_report, - result_bundle: true) - - rescue => ex - UI.error "Failure on #{scheme} #{version} info : #{ex.to_s}" - raise Exception.new "Failure on #{scheme} #{version} info : #{ex.to_s}" + open_report: open_report) + end end From c5745c58d491109142cb483e3c7bc81e0bc942ba Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 25 Sep 2020 02:12:39 +0330 Subject: [PATCH 095/108] - Added ViewControllerFactory Utilities. - Added UIStoryboard Extensions. --- DutchNews.xcodeproj/project.pbxproj | 32 +++++++-- DutchNews/AppDIContainer.swift | 14 ++++ .../UI/UIStoryboard+Additional.swift | 21 ++++++ .../ViewControllerFactory/Screen.swift | 14 ++++ .../ViewControllerFactory/ScreenEnum.swift | 29 ++++++++ .../ViewControllerFactory.swift | 67 +++++++++++++++++++ ...wift => ArticleDetailsPageViewModel.swift} | 2 +- 7 files changed, 174 insertions(+), 5 deletions(-) create mode 100644 DutchNews/Classes/Extensions/UI/UIStoryboard+Additional.swift create mode 100644 DutchNews/Classes/Utilites/ViewControllerFactory/Screen.swift create mode 100644 DutchNews/Classes/Utilites/ViewControllerFactory/ScreenEnum.swift create mode 100644 DutchNews/Classes/Utilites/ViewControllerFactory/ViewControllerFactory.swift rename DutchNews/Classes/ViewModels/{ArticleDetailsPageView.swift => ArticleDetailsPageViewModel.swift} (97%) diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index 2e84bed..ed775a7 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -26,10 +26,14 @@ F8154D7625181B1C00BFB42C /* ArticleWebContainerCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F8154D7225180F0E00BFB42C /* ArticleWebContainerCollectionViewCell.xib */; }; F8154D7725181D4100BFB42C /* HeadlineSuccessResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = F888006A2517A8EE008DCC54 /* HeadlineSuccessResponse.json */; }; F8154D792518207B00BFB42C /* String+HTML.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8154D782518207B00BFB42C /* String+HTML.swift */; }; - F815729D251D3077009DBFD7 /* ArticleDetailsPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F815729C251D3077009DBFD7 /* ArticleDetailsPageView.swift */; }; + F815729D251D3077009DBFD7 /* ArticleDetailsPageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F815729C251D3077009DBFD7 /* ArticleDetailsPageViewModel.swift */; }; F81572A0251D3254009DBFD7 /* ArticlesPageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F815729F251D3254009DBFD7 /* ArticlesPageViewModel.swift */; }; F81572A2251D37F7009DBFD7 /* ArticleDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F81572A1251D37F7009DBFD7 /* ArticleDetailViewModel.swift */; }; F81572A4251D3B39009DBFD7 /* ArticlesPageUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F81572A3251D3B39009DBFD7 /* ArticlesPageUseCase.swift */; }; + F81572BD251D4839009DBFD7 /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F81572BC251D4839009DBFD7 /* Screen.swift */; }; + F81572BF251D487E009DBFD7 /* ScreenEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = F81572BE251D487E009DBFD7 /* ScreenEnum.swift */; }; + F81572C3251D4AB2009DBFD7 /* ViewControllerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F81572C2251D4AB2009DBFD7 /* ViewControllerFactory.swift */; }; + F81572C5251D4BEB009DBFD7 /* UIStoryboard+Additional.swift in Sources */ = {isa = PBXBuildFile; fileRef = F81572C4251D4BEB009DBFD7 /* UIStoryboard+Additional.swift */; }; F82C8EFD2516304D002B27B3 /* APIClientServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8EFC2516304D002B27B3 /* APIClientServiceTests.swift */; }; F82C8EFF25163073002B27B3 /* NetworkMocking.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8EFE25163073002B27B3 /* NetworkMocking.swift */; }; F82C8F0125163898002B27B3 /* NetworkMockingDataFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F82C8F0025163898002B27B3 /* NetworkMockingDataFactory.swift */; }; @@ -147,10 +151,14 @@ F8154D7225180F0E00BFB42C /* ArticleWebContainerCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ArticleWebContainerCollectionViewCell.xib; sourceTree = ""; }; F8154D742518156000BFB42C /* HeadlinesViewController+DataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HeadlinesViewController+DataSource.swift"; sourceTree = ""; }; F8154D782518207B00BFB42C /* String+HTML.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+HTML.swift"; sourceTree = ""; }; - F815729C251D3077009DBFD7 /* ArticleDetailsPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleDetailsPageView.swift; sourceTree = ""; }; + F815729C251D3077009DBFD7 /* ArticleDetailsPageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleDetailsPageViewModel.swift; sourceTree = ""; }; F815729F251D3254009DBFD7 /* ArticlesPageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticlesPageViewModel.swift; sourceTree = ""; }; F81572A1251D37F7009DBFD7 /* ArticleDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleDetailViewModel.swift; sourceTree = ""; }; F81572A3251D3B39009DBFD7 /* ArticlesPageUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticlesPageUseCase.swift; sourceTree = ""; }; + F81572BC251D4839009DBFD7 /* Screen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = ""; }; + F81572BE251D487E009DBFD7 /* ScreenEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenEnum.swift; sourceTree = ""; }; + F81572C2251D4AB2009DBFD7 /* ViewControllerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerFactory.swift; sourceTree = ""; }; + F81572C4251D4BEB009DBFD7 /* UIStoryboard+Additional.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIStoryboard+Additional.swift"; sourceTree = ""; }; F82C8EFC2516304D002B27B3 /* APIClientServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClientServiceTests.swift; sourceTree = ""; }; F82C8EFE25163073002B27B3 /* NetworkMocking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMocking.swift; sourceTree = ""; }; F82C8F0025163898002B27B3 /* NetworkMockingDataFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMockingDataFactory.swift; sourceTree = ""; }; @@ -300,6 +308,7 @@ F8E5C12725191DA50083D2B1 /* UIViewController+StoryboardName.swift */, F8154D6E25180B0200BFB42C /* UIView+Nib.swift */, F8E5C0EA251882560083D2B1 /* UIViewController+AlertableView.swift */, + F81572C4251D4BEB009DBFD7 /* UIStoryboard+Additional.swift */, ); path = UI; sourceTree = ""; @@ -314,6 +323,16 @@ path = Abstract; sourceTree = ""; }; + F81572BB251D4819009DBFD7 /* ViewControllerFactory */ = { + isa = PBXGroup; + children = ( + F81572BC251D4839009DBFD7 /* Screen.swift */, + F81572BE251D487E009DBFD7 /* ScreenEnum.swift */, + F81572C2251D4AB2009DBFD7 /* ViewControllerFactory.swift */, + ); + path = ViewControllerFactory; + sourceTree = ""; + }; F82C8EFB2516051D002B27B3 /* NetworkTests */ = { isa = PBXGroup; children = ( @@ -587,7 +606,7 @@ F8E5C0EE2518848D0083D2B1 /* HeadlinesViewModel.swift */, F8E5C0F0251884A20083D2B1 /* HeadlineSearchViewModel.swift */, F8E5C0FD25190CDD0083D2B1 /* HeadlineCellViewModel.swift */, - F815729C251D3077009DBFD7 /* ArticleDetailsPageView.swift */, + F815729C251D3077009DBFD7 /* ArticleDetailsPageViewModel.swift */, F81572A1251D37F7009DBFD7 /* ArticleDetailViewModel.swift */, ); path = ViewModels; @@ -608,6 +627,7 @@ F8F14C6D250D70DC00C24FF5 /* Utilites */ = { isa = PBXGroup; children = ( + F81572BB251D4819009DBFD7 /* ViewControllerFactory */, F8154D532517EBE300BFB42C /* HeadlineLayoutConfiguration */, F8E5C1302519250E0083D2B1 /* Logger.swift */, ); @@ -898,6 +918,7 @@ F8E5C0F32518D8AC0083D2B1 /* ArticleViewModel.swift in Sources */, F8E5C0ED2518833B0083D2B1 /* ArticlesViewModel.swift in Sources */, F8C83462251C04020051A0FD /* Storable.swift in Sources */, + F81572C5251D4BEB009DBFD7 /* UIStoryboard+Additional.swift in Sources */, F8154D6F25180B0200BFB42C /* UIView+Nib.swift in Sources */, F85E319E251B8D61002753AC /* RxWKUIDelegateProxy.swift in Sources */, F858997B25176DC800A6BA2A /* Article.swift in Sources */, @@ -917,6 +938,7 @@ F8D58B83251CFC1E00B426AC /* HeadlinesArticleLocalRepository.swift in Sources */, F85E319F251B8D61002753AC /* WKNavigationDelegateEvents+Rx.swift in Sources */, F85E31A1251B8D61002753AC /* RxWKUserContentController.swift in Sources */, + F81572C3251D4AB2009DBFD7 /* ViewControllerFactory.swift in Sources */, F85E31A0251B8D61002753AC /* RxWKUIDelegateEvents+Rx.swift in Sources */, F8154D6C25180A9000BFB42C /* HeadlineBaseCollectionViewCell.swift in Sources */, F865F76B2516C08C001FD067 /* NetworkValidResponse.swift in Sources */, @@ -924,11 +946,13 @@ F8E5C12325191D5E0083D2B1 /* URL+ApplicationPath.swift in Sources */, F8154D612518011500BFB42C /* MainArticleCollectionViewCell.swift in Sources */, F8E5C0F1251884A20083D2B1 /* HeadlineSearchViewModel.swift in Sources */, + F81572BF251D487E009DBFD7 /* ScreenEnum.swift in Sources */, F8E5C12D25191DA60083D2B1 /* UIViewController+StoryboardName.swift in Sources */, F8154D7125180C3900BFB42C /* ArticleWebContainerCollectionViewCell.swift in Sources */, F8E5C12A25191DA60083D2B1 /* UIImage+Additionals.swift in Sources */, F8E5C12B25191DA60083D2B1 /* UINavigationBar+Additionals.swift in Sources */, F865F75025168F05001FD067 /* AppConfig.swift in Sources */, + F81572BD251D4839009DBFD7 /* Screen.swift in Sources */, F8E5C0FE25190CDD0083D2B1 /* HeadlineCellViewModel.swift in Sources */, F865F7642516A9D5001FD067 /* APIServerResponse.swift in Sources */, F8E5C13325193A290083D2B1 /* String+Localization.swift in Sources */, @@ -964,7 +988,7 @@ F8E5C12225191D5E0083D2B1 /* String+EmptyChecking.swift in Sources */, F89B021E250D446000B41293 /* AppDelegate.swift in Sources */, F8E5C12C25191DA60083D2B1 /* UILabel+Localization.swift in Sources */, - F815729D251D3077009DBFD7 /* ArticleDetailsPageView.swift in Sources */, + F815729D251D3077009DBFD7 /* ArticleDetailsPageViewModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/DutchNews/AppDIContainer.swift b/DutchNews/AppDIContainer.swift index d8c152c..2f40683 100644 --- a/DutchNews/AppDIContainer.swift +++ b/DutchNews/AppDIContainer.swift @@ -77,4 +77,18 @@ struct AppDIContainer { return ArticlesPageUseCase(repository: headlineLocalArticleRepository) } + //////////////////////////////////////////////////////////////// + // MARK: - + // MARK: ViewModels DI Container + // MARK: - + //////////////////////////////////////////////////////////////// + + static var headlinesViewModel: ArticlesViewModel { + return HeadlinesViewModel(useCase: headlineFetchingUseCase) + } + + static var articlePagesViewModel: ArticlesPageViewModel { + return ArticleDetailsPageViewModel(useCase: articlesPageUseCase) + } + } diff --git a/DutchNews/Classes/Extensions/UI/UIStoryboard+Additional.swift b/DutchNews/Classes/Extensions/UI/UIStoryboard+Additional.swift new file mode 100644 index 0000000..8377618 --- /dev/null +++ b/DutchNews/Classes/Extensions/UI/UIStoryboard+Additional.swift @@ -0,0 +1,21 @@ +// +// UIStoryboard+Additional.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/25/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import UIKit + +extension UIStoryboard { + + func instantiateViewController(identifier: String) -> T? { + return self.instantiateViewController(withIdentifier: identifier) as? T + } + + func instantiateViewController (withIdentifer: String, type: T.Type) -> T? { + return self.instantiateViewController(withIdentifier: withIdentifer) as? T + } +} diff --git a/DutchNews/Classes/Utilites/ViewControllerFactory/Screen.swift b/DutchNews/Classes/Utilites/ViewControllerFactory/Screen.swift new file mode 100644 index 0000000..bdd1b2c --- /dev/null +++ b/DutchNews/Classes/Utilites/ViewControllerFactory/Screen.swift @@ -0,0 +1,14 @@ +// +// Screen.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/25/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation + +protocol Screen { + + func screenIdentifier() -> String +} diff --git a/DutchNews/Classes/Utilites/ViewControllerFactory/ScreenEnum.swift b/DutchNews/Classes/Utilites/ViewControllerFactory/ScreenEnum.swift new file mode 100644 index 0000000..af91e06 --- /dev/null +++ b/DutchNews/Classes/Utilites/ViewControllerFactory/ScreenEnum.swift @@ -0,0 +1,29 @@ +// +// ScreenEnum.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/25/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import UIKit + +enum ScreenName: Screen { + + case headlines + case pages + case detail + + func screenIdentifier() -> String { + switch self { + case .headlines: + return HeadlinesViewController.className + case .pages: + return ArticlePageViewController.className + case .detail: + return ArticleDetailViewController.className + } + } + +} diff --git a/DutchNews/Classes/Utilites/ViewControllerFactory/ViewControllerFactory.swift b/DutchNews/Classes/Utilites/ViewControllerFactory/ViewControllerFactory.swift new file mode 100644 index 0000000..3cc00b1 --- /dev/null +++ b/DutchNews/Classes/Utilites/ViewControllerFactory/ViewControllerFactory.swift @@ -0,0 +1,67 @@ +// +// ViewControllerFactory.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/25/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import UIKit + +protocol ViewControllerFactory { + + func makeHeadlinesViewController() throws -> HeadlinesViewController + func makePageViewController(selected: Int) throws -> ArticlePageViewController + func makeArticleDetailViewController() throws -> ArticleDetailViewController + +} + +struct ViewModelViewControllerFactory: ViewControllerFactory { + + enum Error: Swift.Error { + case notFound + } + + let storyboard: UIStoryboard + + init(storyboard: UIStoryboard) { + self.storyboard = storyboard + } + + func makeHeadlinesViewController() throws -> HeadlinesViewController { + guard let vc: HeadlinesViewController = makeViewController(forScreen: ScreenName.headlines) else { + throw Error.notFound + } + + vc.viewModel = AppDIContainer.headlinesViewModel + + return vc + } + + func makePageViewController(selected: Int) throws -> ArticlePageViewController { + guard let vc: ArticlePageViewController = makeViewController(forScreen: ScreenName.pages) else { + throw Error.notFound + } + + return vc + } + + func makeArticleDetailViewController() throws -> ArticleDetailViewController { + guard let vc: ArticleDetailViewController = makeViewController(forScreen: ScreenName.detail) else { + throw Error.notFound + } + + return vc + } + + private func makeViewController(forScreen screen: Screen) -> T? { + + guard let vc = storyboard.instantiateViewController(identifier: screen.screenIdentifier()) as? T else { + return nil + } + + return vc + } + +} diff --git a/DutchNews/Classes/ViewModels/ArticleDetailsPageView.swift b/DutchNews/Classes/ViewModels/ArticleDetailsPageViewModel.swift similarity index 97% rename from DutchNews/Classes/ViewModels/ArticleDetailsPageView.swift rename to DutchNews/Classes/ViewModels/ArticleDetailsPageViewModel.swift index 8810380..59d5fd8 100644 --- a/DutchNews/Classes/ViewModels/ArticleDetailsPageView.swift +++ b/DutchNews/Classes/ViewModels/ArticleDetailsPageViewModel.swift @@ -10,7 +10,7 @@ import Foundation import RxSwift import RxCocoa -final class ArticleDetailsPageView: ArticlesPageViewModel { +final class ArticleDetailsPageViewModel: ArticlesPageViewModel { private var statePublisher: BehaviorRelay From 9511a95abdb8a67410493aaf5138d2b010d77756 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 25 Sep 2020 03:13:44 +0330 Subject: [PATCH 096/108] - Implemented and linked headline to article page controller. - Added concrete ViewModelViewControllerFactory that implemented ViewControllerFactory. - enhancement and adjustment. --- DutchNews.xcodeproj/project.pbxproj | 4 + DutchNews/AppDIContainer.swift | 5 ++ DutchNews/AppDelegate.swift | 29 ++++-- .../ViewControllerFactory.swift | 52 +---------- .../ViewModelViewControllerFactory.swift | 69 ++++++++++++++ .../ArticleDetailViewController.swift | 2 + .../ArticlesPageViewController.swift | 90 +++++++++++++++++-- .../HeadlinesViewController.swift | 26 +++++- .../Abstract/ArticlesPageViewModel.swift | 4 +- DutchNews/Info.plist | 2 - .../Base.lproj/LaunchScreen.storyboard | 16 +++- .../Storyboards/Base.lproj/Main.storyboard | 7 +- 12 files changed, 232 insertions(+), 74 deletions(-) create mode 100644 DutchNews/Classes/Utilites/ViewControllerFactory/ViewModelViewControllerFactory.swift diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index ed775a7..eeae094 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -72,6 +72,7 @@ F88800692517A423008DCC54 /* RepositoryDependenciesFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88800682517A423008DCC54 /* RepositoryDependenciesFactory.swift */; }; F888006B2517A8EE008DCC54 /* HeadlineSuccessResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = F888006A2517A8EE008DCC54 /* HeadlineSuccessResponse.json */; }; F888006D2517AA1F008DCC54 /* HeadlineFailureResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = F888006C2517AA1F008DCC54 /* HeadlineFailureResponse.json */; }; + F897BD3C251D5C7E003822EA /* ViewModelViewControllerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F897BD3B251D5C7E003822EA /* ViewModelViewControllerFactory.swift */; }; F89B021E250D446000B41293 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89B021D250D446000B41293 /* AppDelegate.swift */; }; F89B0220250D446200B41293 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F89B021F250D446200B41293 /* Assets.xcassets */; }; F89B0223250D446200B41293 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F89B0221250D446200B41293 /* LaunchScreen.storyboard */; }; @@ -196,6 +197,7 @@ F88800682517A423008DCC54 /* RepositoryDependenciesFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryDependenciesFactory.swift; sourceTree = ""; }; F888006A2517A8EE008DCC54 /* HeadlineSuccessResponse.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = HeadlineSuccessResponse.json; sourceTree = ""; }; F888006C2517AA1F008DCC54 /* HeadlineFailureResponse.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = HeadlineFailureResponse.json; sourceTree = ""; }; + F897BD3B251D5C7E003822EA /* ViewModelViewControllerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelViewControllerFactory.swift; sourceTree = ""; }; F89B021A250D446000B41293 /* DutchNews.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DutchNews.app; sourceTree = BUILT_PRODUCTS_DIR; }; F89B021D250D446000B41293 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F89B021F250D446200B41293 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -329,6 +331,7 @@ F81572BC251D4839009DBFD7 /* Screen.swift */, F81572BE251D487E009DBFD7 /* ScreenEnum.swift */, F81572C2251D4AB2009DBFD7 /* ViewControllerFactory.swift */, + F897BD3B251D5C7E003822EA /* ViewModelViewControllerFactory.swift */, ); path = ViewControllerFactory; sourceTree = ""; @@ -958,6 +961,7 @@ F8E5C13325193A290083D2B1 /* String+Localization.swift in Sources */, F841DD0C25195429006E7E90 /* LinearGradientView.swift in Sources */, F8E5C0F52518DD3E0083D2B1 /* ViewModelState.swift in Sources */, + F897BD3C251D5C7E003822EA /* ViewModelViewControllerFactory.swift in Sources */, F8E5C0FC2518E9150083D2B1 /* HeadlinesSearchingUseCases.swift in Sources */, F8154D572517F03600BFB42C /* ArticleHeadlineLayoutConfiguration.swift in Sources */, F8E5C0EF2518848D0083D2B1 /* HeadlinesViewModel.swift in Sources */, diff --git a/DutchNews/AppDIContainer.swift b/DutchNews/AppDIContainer.swift index 2f40683..f615b09 100644 --- a/DutchNews/AppDIContainer.swift +++ b/DutchNews/AppDIContainer.swift @@ -91,4 +91,9 @@ struct AppDIContainer { return ArticleDetailsPageViewModel(useCase: articlesPageUseCase) } + static let viewModelViewControllerFactory: ViewControllerFactory = { + let storyboard = UIStoryboard(name: "Main", bundle: Bundle(for: AppDelegate.self)) + return ViewModelViewControllerFactory(storyboard: storyboard) + }() + } diff --git a/DutchNews/AppDelegate.swift b/DutchNews/AppDelegate.swift index f81b12f..9898e97 100644 --- a/DutchNews/AppDelegate.swift +++ b/DutchNews/AppDelegate.swift @@ -10,30 +10,45 @@ import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - + @IBOutlet var window: UIWindow? - + + private var viewControllerFactory: ViewControllerFactory = AppDIContainer.viewModelViewControllerFactory + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. _ = Logger() + + window = UIWindow(forAutoLayout: ()) + do { + let vc = try viewControllerFactory.makeHeadlinesViewController() + let root = viewControllerFactory.makeRootViewController() + root?.setViewControllers([vc], animated: true) + window?.rootViewController = root + }catch { + window?.rootViewController = UIViewController() + } + + window?.makeKeyAndVisible() + return true } - + func applicationWillResignActive(_ application: UIApplication) { // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. } - + func applicationDidEnterBackground(_ application: UIApplication) { // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. } - + func applicationWillEnterForeground(_ application: UIApplication) { // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. } - + func applicationDidBecomeActive(_ application: UIApplication) { // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. } - + } diff --git a/DutchNews/Classes/Utilites/ViewControllerFactory/ViewControllerFactory.swift b/DutchNews/Classes/Utilites/ViewControllerFactory/ViewControllerFactory.swift index 3cc00b1..c259f5b 100644 --- a/DutchNews/Classes/Utilites/ViewControllerFactory/ViewControllerFactory.swift +++ b/DutchNews/Classes/Utilites/ViewControllerFactory/ViewControllerFactory.swift @@ -9,59 +9,15 @@ import Foundation import UIKit -protocol ViewControllerFactory { +protocol ViewControllerFactory: class { + func makeRootViewController() -> UINavigationController? func makeHeadlinesViewController() throws -> HeadlinesViewController func makePageViewController(selected: Int) throws -> ArticlePageViewController func makeArticleDetailViewController() throws -> ArticleDetailViewController } -struct ViewModelViewControllerFactory: ViewControllerFactory { - - enum Error: Swift.Error { - case notFound - } - - let storyboard: UIStoryboard - - init(storyboard: UIStoryboard) { - self.storyboard = storyboard - } - - func makeHeadlinesViewController() throws -> HeadlinesViewController { - guard let vc: HeadlinesViewController = makeViewController(forScreen: ScreenName.headlines) else { - throw Error.notFound - } - - vc.viewModel = AppDIContainer.headlinesViewModel - - return vc - } - - func makePageViewController(selected: Int) throws -> ArticlePageViewController { - guard let vc: ArticlePageViewController = makeViewController(forScreen: ScreenName.pages) else { - throw Error.notFound - } - - return vc - } - - func makeArticleDetailViewController() throws -> ArticleDetailViewController { - guard let vc: ArticleDetailViewController = makeViewController(forScreen: ScreenName.detail) else { - throw Error.notFound - } - - return vc - } - - private func makeViewController(forScreen screen: Screen) -> T? { - - guard let vc = storyboard.instantiateViewController(identifier: screen.screenIdentifier()) as? T else { - return nil - } - - return vc - } - +protocol ViewControllerFactoryable { + var controllerFactory: ViewControllerFactory? { get set } } diff --git a/DutchNews/Classes/Utilites/ViewControllerFactory/ViewModelViewControllerFactory.swift b/DutchNews/Classes/Utilites/ViewControllerFactory/ViewModelViewControllerFactory.swift new file mode 100644 index 0000000..a6d902f --- /dev/null +++ b/DutchNews/Classes/Utilites/ViewControllerFactory/ViewModelViewControllerFactory.swift @@ -0,0 +1,69 @@ +// +// ViewModelViewControllerFactory.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/25/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import UIKit + +final class ViewModelViewControllerFactory: ViewControllerFactory { + + enum Error: Swift.Error { + case notFound + } + + let storyboard: UIStoryboard + + init(storyboard: UIStoryboard) { + self.storyboard = storyboard + } + + func makeRootViewController() -> UINavigationController? { + return self.storyboard.instantiateInitialViewController() as? UINavigationController + } + + func makeHeadlinesViewController() throws -> HeadlinesViewController { + + guard let vc: HeadlinesViewController = makeViewController(forScreen: ScreenName.headlines) else { + throw Error.notFound + } + + vc.viewModel = AppDIContainer.headlinesViewModel + vc.controllerFactory = self + + return vc + } + + func makePageViewController(selected: Int) throws -> ArticlePageViewController { + guard let vc: ArticlePageViewController = makeViewController(forScreen: ScreenName.pages) else { + throw Error.notFound + } + + vc.viewModel = AppDIContainer.articlePagesViewModel + vc.viewModel?.selectedIndex.accept(selected) + vc.controllerFactory = self + + return vc + } + + func makeArticleDetailViewController() throws -> ArticleDetailViewController { + guard let vc: ArticleDetailViewController = makeViewController(forScreen: ScreenName.detail) else { + throw Error.notFound + } + + return vc + } + + private func makeViewController(forScreen screen: Screen) -> T? { + + guard let vc = storyboard.instantiateViewController(identifier: screen.screenIdentifier()) as? T else { + return nil + } + + return vc + } + +} diff --git a/DutchNews/Classes/ViewControllers/ArticleDetailViewController.swift b/DutchNews/Classes/ViewControllers/ArticleDetailViewController.swift index 5a5c61b..07df49c 100644 --- a/DutchNews/Classes/ViewControllers/ArticleDetailViewController.swift +++ b/DutchNews/Classes/ViewControllers/ArticleDetailViewController.swift @@ -25,6 +25,8 @@ class ArticleDetailViewController: UIViewController, AlertableView { return view }() + var viewModel: ArticleViewModel? + let disposeBag = DisposeBag() override func viewDidLoad() { diff --git a/DutchNews/Classes/ViewControllers/ArticlesPageViewController.swift b/DutchNews/Classes/ViewControllers/ArticlesPageViewController.swift index eb82316..8f748a2 100644 --- a/DutchNews/Classes/ViewControllers/ArticlesPageViewController.swift +++ b/DutchNews/Classes/ViewControllers/ArticlesPageViewController.swift @@ -8,14 +8,31 @@ import Foundation import Pageboy +import RxSwift +import RxCocoa -class ArticlePageViewController: PageboyViewController { +class ArticlePageViewController: PageboyViewController, AlertableView, ViewControllerFactoryable { + + var viewModel: ArticlesPageViewModel? + + private var selectedIndex = 0 + + let disposeBag = DisposeBag() + + var controllerFactory: ViewControllerFactory? + + deinit { + viewModel = nil + controllerFactory = nil + } override func viewDidLoad() { super.viewDidLoad() interPageSpacing = 8.0 self.dataSource = self + bindViewModel() + loadContentsIfNeeded() // Do any additional setup after loading the view. } @@ -30,26 +47,83 @@ class ArticlePageViewController: PageboyViewController { } */ + func updateLayouts(onState state: ViewModelState) { + switch state { + + case .loaded: + self.reloadData() + case .error(let error): + + let message: String + if let err = error as? URLError { + message = err.localizedDescription + }else { + message = error.localizedDescription + } + + presentAlert(message: message, + actionTitle: "retry".localized) {[weak self] in + self?.loadContentsIfNeeded() + } + default: + break + } + } + + //////////////////////////////////////////////////////////////// + // MARK: - + // MARK: ViewModel + // MARK: - + //////////////////////////////////////////////////////////////// + + func bindViewModel() { + + guard let viewModel = self.viewModel else { + return + } + + bind(viewModel: viewModel) + } + + func bind(viewModel: ArticlesPageViewModel) { + + viewModel.selectedIndex.asDriver() + .drive(onNext: {[weak self] in + self?.selectedIndex = $0 ?? 0 + }).disposed(by: disposeBag) + + viewModel.state.drive(onNext: {[weak self] (state) in + self?.updateLayouts(onState: state) + }).disposed(by: disposeBag) + + } + + func loadContentsIfNeeded() { + viewModel?.fetchArticles() + } + } extension ArticlePageViewController: PageboyViewControllerDataSource { func numberOfViewControllers(in pageboyViewController: PageboyViewController) -> Int { - 10 + return viewModel?.count ?? 0 } func viewController(for pageboyViewController: PageboyViewController, at index: PageboyViewController.PageIndex) -> UIViewController? { - let id = String(describing: ArticleDetailViewController.self) - if #available(iOS 13.0, *) { - return self.storyboard?.instantiateViewController(identifier: id) - } else { - return self.storyboard?.instantiateViewController(withIdentifier: id) + + guard let vc = try? controllerFactory?.makeArticleDetailViewController() else { + return nil } + + vc.viewModel = viewModel?[index] + + return vc } func defaultPage(for pageboyViewController: PageboyViewController) -> PageboyViewController.Page? { - return .first + return .at(index: selectedIndex) } } diff --git a/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift b/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift index ce2bea2..36d1916 100644 --- a/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift +++ b/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift @@ -60,6 +60,8 @@ class HeadlinesViewController: UIViewController { var viewModel: ArticlesViewModel? = HeadlinesViewModel(useCase: AppDIContainer.headlineFetchingUseCase) + var controllerFactory: ViewControllerFactory? + override func viewDidLoad() { super.viewDidLoad() @@ -177,6 +179,14 @@ class HeadlinesViewController: UIViewController { .bind { [weak self] _ in self?.loadContentsIfNeeded() }.disposed(by: disposeBag) + + viewModel.selectedIndex.observeOn(MainScheduler.instance) + .filter { $0 != nil }.map { $0! } + .bind {[weak self] in + + self?.navigateToPages(withIndex: $0) + + }.disposed(by: disposeBag) } func loadContentsIfNeeded() { @@ -188,6 +198,19 @@ class HeadlinesViewController: UIViewController { viewModel?.refreshArticles() } + func navigateToPages(withIndex index: Int) { + do { + guard let vc = try controllerFactory?.makePageViewController(selected: index) else { + return + } + + self.navigationController?.show(vc, sender: false) + + }catch { + presentAlert(message: error.localizedDescription, actionTitle: nil) { } + } + } + } extension HeadlinesViewController: UICollectionViewDelegate { @@ -198,8 +221,9 @@ extension HeadlinesViewController: UICollectionViewDelegate { let item = dataSource[indexPath] viewModel?.didSelect(article: item) - self.performSegue(withIdentifier: "showDetails", sender: item) + } } extension HeadlinesViewController: AlertableView {} +extension HeadlinesViewController: ViewControllerFactoryable {} diff --git a/DutchNews/Classes/ViewModels/Abstract/ArticlesPageViewModel.swift b/DutchNews/Classes/ViewModels/Abstract/ArticlesPageViewModel.swift index b67af11..001b9bd 100644 --- a/DutchNews/Classes/ViewModels/Abstract/ArticlesPageViewModel.swift +++ b/DutchNews/Classes/ViewModels/Abstract/ArticlesPageViewModel.swift @@ -13,7 +13,9 @@ import RxDataSources /// ArticleViewModel Interface protocol ArticlesPageViewModel: ArticlesViewModel { - + + var count: Int { get } + var currentLoadingProgress: BehaviorRelay { get } func viewModel(atIndex index: Int) -> T.Item? diff --git a/DutchNews/Info.plist b/DutchNews/Info.plist index 952074c..75404a2 100644 --- a/DutchNews/Info.plist +++ b/DutchNews/Info.plist @@ -22,8 +22,6 @@ UILaunchStoryboardName LaunchScreen - UIMainStoryboardFile - Main UIRequiredDeviceCapabilities armv7 diff --git a/DutchNews/Resources/Storyboards/Base.lproj/LaunchScreen.storyboard b/DutchNews/Resources/Storyboards/Base.lproj/LaunchScreen.storyboard index 5ca9c85..9ae7699 100644 --- a/DutchNews/Resources/Storyboards/Base.lproj/LaunchScreen.storyboard +++ b/DutchNews/Resources/Storyboards/Base.lproj/LaunchScreen.storyboard @@ -1,6 +1,6 @@ - + @@ -13,9 +13,21 @@ - + + + + + + + + diff --git a/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard b/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard index 29a9ea6..5ae0fa1 100644 --- a/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard +++ b/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard @@ -17,9 +17,6 @@ - - - @@ -34,7 +31,7 @@ - + @@ -73,7 +70,7 @@ - + From e4049d141e393d14f3a9a8447f862e11e18c3d6e Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 25 Sep 2020 04:44:36 +0330 Subject: [PATCH 097/108] - Article-Detail is Done. --- DutchNews.xcodeproj/project.pbxproj | 4 ++ .../Classes/Domains/ArticlesPageUseCase.swift | 5 +- .../Utilites/RxHeadlinesDataSource.swift | 27 +++++++++ .../ArticleDetailViewController.swift | 58 ++++++++++++++----- .../HeadlinesViewController+DataSource.swift | 4 +- .../HeadlinesViewController.swift | 8 ++- .../Headers/ArticleDetailHeaderView.swift | 2 +- .../Views/Headers/ArticleDetailHeaderView.xib | 16 ++--- fastlane/Fastfile | 4 +- 9 files changed, 98 insertions(+), 30 deletions(-) create mode 100644 DutchNews/Classes/Utilites/RxHeadlinesDataSource.swift diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index eeae094..8baa3dc 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -73,6 +73,7 @@ F888006B2517A8EE008DCC54 /* HeadlineSuccessResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = F888006A2517A8EE008DCC54 /* HeadlineSuccessResponse.json */; }; F888006D2517AA1F008DCC54 /* HeadlineFailureResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = F888006C2517AA1F008DCC54 /* HeadlineFailureResponse.json */; }; F897BD3C251D5C7E003822EA /* ViewModelViewControllerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F897BD3B251D5C7E003822EA /* ViewModelViewControllerFactory.swift */; }; + F897BD3E251D740C003822EA /* RxHeadlinesDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F897BD3D251D740C003822EA /* RxHeadlinesDataSource.swift */; }; F89B021E250D446000B41293 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89B021D250D446000B41293 /* AppDelegate.swift */; }; F89B0220250D446200B41293 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F89B021F250D446200B41293 /* Assets.xcassets */; }; F89B0223250D446200B41293 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F89B0221250D446200B41293 /* LaunchScreen.storyboard */; }; @@ -198,6 +199,7 @@ F888006A2517A8EE008DCC54 /* HeadlineSuccessResponse.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = HeadlineSuccessResponse.json; sourceTree = ""; }; F888006C2517AA1F008DCC54 /* HeadlineFailureResponse.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = HeadlineFailureResponse.json; sourceTree = ""; }; F897BD3B251D5C7E003822EA /* ViewModelViewControllerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelViewControllerFactory.swift; sourceTree = ""; }; + F897BD3D251D740C003822EA /* RxHeadlinesDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RxHeadlinesDataSource.swift; sourceTree = ""; }; F89B021A250D446000B41293 /* DutchNews.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DutchNews.app; sourceTree = BUILT_PRODUCTS_DIR; }; F89B021D250D446000B41293 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F89B021F250D446200B41293 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -633,6 +635,7 @@ F81572BB251D4819009DBFD7 /* ViewControllerFactory */, F8154D532517EBE300BFB42C /* HeadlineLayoutConfiguration */, F8E5C1302519250E0083D2B1 /* Logger.swift */, + F897BD3D251D740C003822EA /* RxHeadlinesDataSource.swift */, ); path = Utilites; sourceTree = ""; @@ -987,6 +990,7 @@ F8E5C12025191D5E0083D2B1 /* Date+TimeAgo.swift in Sources */, F865F76D2516C826001FD067 /* DefaultAPIValidResponse.swift in Sources */, F8E5C1312519250E0083D2B1 /* Logger.swift in Sources */, + F897BD3E251D740C003822EA /* RxHeadlinesDataSource.swift in Sources */, F858997D25176E8F00A6BA2A /* ArticleSource.swift in Sources */, F8E5C135251943CF0083D2B1 /* AppDIContainer.swift in Sources */, F8E5C12225191D5E0083D2B1 /* String+EmptyChecking.swift in Sources */, diff --git a/DutchNews/Classes/Domains/ArticlesPageUseCase.swift b/DutchNews/Classes/Domains/ArticlesPageUseCase.swift index b787cc4..2698695 100644 --- a/DutchNews/Classes/Domains/ArticlesPageUseCase.swift +++ b/DutchNews/Classes/Domains/ArticlesPageUseCase.swift @@ -18,7 +18,8 @@ class ArticlesPageUseCase: ArticlesUseCase { } func fetchLocalArticles() -> Observable<[T]> { - return repository.fetchArticles() + return repository.fetchArticles().map { + $0.sorted(by: { $0.publishedAt >= $1.publishedAt }) + } } - } diff --git a/DutchNews/Classes/Utilites/RxHeadlinesDataSource.swift b/DutchNews/Classes/Utilites/RxHeadlinesDataSource.swift new file mode 100644 index 0000000..0670112 --- /dev/null +++ b/DutchNews/Classes/Utilites/RxHeadlinesDataSource.swift @@ -0,0 +1,27 @@ +// +// RxHeadlinesDataSource.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/25/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import Foundation +import RxSwift +import RxDataSources +import RxCocoa + +/// Helper Class for fixing the issue with `MagazineLayout` +class RxHeadlinesDataSource: RxCollectionViewSectionedReloadDataSource where T: SectionModelType { + +// override func collectionView(_ collectionView: UICollectionView, observedEvent: Event) { +// Binder(self) { dataSource, element in +// +// dataSource.setSections(element) +// collectionView.reloadData() +// collectionView.collectionViewLayout.invalidateLayout() +// +// }.on(observedEvent) +// } + +} diff --git a/DutchNews/Classes/ViewControllers/ArticleDetailViewController.swift b/DutchNews/Classes/ViewControllers/ArticleDetailViewController.swift index 07df49c..6699041 100644 --- a/DutchNews/Classes/ViewControllers/ArticleDetailViewController.swift +++ b/DutchNews/Classes/ViewControllers/ArticleDetailViewController.swift @@ -29,15 +29,40 @@ class ArticleDetailViewController: UIViewController, AlertableView { let disposeBag = DisposeBag() + deinit { + viewModel = nil + } + override func viewDidLoad() { super.viewDidLoad() setupViews() - contentView.load(URLRequest(url: URL(string: "https://news.google.com/topstories?hl=en-US&gl=US&ceid=US:en")!)) + + viewModel?.output.asObservable().bind(onNext: {[weak self] in + self?.headerView.config(content: $0) + self?.updateHeaderSize() + }).disposed(by: disposeBag) + + viewModel?.buildURLContent() + .observeOn(MainScheduler.asyncInstance) + .bind(onNext: {[weak contentView] (request) in + contentView?.load(request) + }).disposed(by: disposeBag) + // Do any additional setup after loading the view. } + override func viewWillAppear(_ animated: Bool) { + super.viewDidAppear(animated) + contentView.reload() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + contentView.stopLoading() + } + /* // MARK: - Navigation @@ -53,9 +78,9 @@ class ArticleDetailViewController: UIViewController, AlertableView { // MARK: UI Methods // MARK: - //////////////////////////////////////////////////////////////// - + func setupViews() { - setupContentView() + setupHeaderView() } @@ -70,16 +95,23 @@ class ArticleDetailViewController: UIViewController, AlertableView { containerScrollView.parallaxHeader.height = height } - func setupContentView() { -// contentView.scrollView.isScrollEnabled = true -// contentView.scrollView.rx.observe(CGSize.self, #keyPath(UIScrollView.contentSize), -// options: [.initial,.new], retainSelf: false) -// .filter { $0 != nil }.map { $0! } -// .distinctUntilChanged() -// .bind {[weak self ] (size) in -// self?.contentView.autoSetDimension(.height, toSize: size.height) -// self?.view.layoutIfNeeded() -// } + func updateHeaderSize() { + + headerView.layoutIfNeeded() + + let size = headerView.systemLayoutSizeFitting(headerView.bounds.size, + withHorizontalFittingPriority: .required, + verticalFittingPriority: .fittingSizeLevel) + + if let minHeight = headerView.titleLabel.superview?.bounds.height { + containerScrollView.parallaxHeader.minimumHeight = minHeight + 16 + } + + let height = AVMakeRect(aspectRatio: CGSize(width: 16, height: 9), + insideRect: view.bounds).size.height + + containerScrollView.parallaxHeader.height = max(size.height, height) + view.layoutIfNeeded() } diff --git a/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift b/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift index e375851..3ab4f40 100644 --- a/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift +++ b/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift @@ -16,9 +16,9 @@ extension HeadlinesViewController { typealias SectionType = ArticlesViewModel.T - func buildDataSource() -> RxCollectionViewSectionedReloadDataSource { + func buildDataSource() -> RxHeadlinesDataSource { - return RxCollectionViewSectionedReloadDataSource(configureCell: {[weak self] (dataSource, collectionView, indexPath, _) -> UICollectionViewCell in + return RxHeadlinesDataSource(configureCell: {[weak self] (dataSource, collectionView, indexPath, _) -> UICollectionViewCell in guard let `self` = self else { return HeadlineBaseCollectionViewCell() diff --git a/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift b/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift index 36d1916..40b8da5 100644 --- a/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift +++ b/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift @@ -54,7 +54,7 @@ class HeadlinesViewController: UIViewController { let disposeBag = DisposeBag() - lazy var dataSource: RxCollectionViewSectionedReloadDataSource = { + lazy var dataSource: RxHeadlinesDataSource = { return self.buildDataSource() }() @@ -94,6 +94,12 @@ class HeadlinesViewController: UIViewController { setupColletionView() view.addSubview(loadingIndicator) loadingIndicator.autoCenterInSuperview() + if #available(iOS 13, *) { + loadingIndicator.tintColor = UIColor.label + }else { + loadingIndicator.tintColor = .red + } + loadingIndicator.hidesWhenStopped = true } diff --git a/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.swift b/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.swift index b76d309..a50b579 100644 --- a/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.swift +++ b/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.swift @@ -29,7 +29,7 @@ class ArticleDetailHeaderView: UIView { publishDateLabel.text = article.publishedAt backgroundImageView.setImage(url: article.urlToImage) - + layoutIfNeeded() } } diff --git a/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.xib b/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.xib index e349ffc..b975df8 100644 --- a/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.xib +++ b/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.xib @@ -15,22 +15,22 @@ - + - + - + - + - - + + + + + @@ -65,7 +69,7 @@ - + @@ -75,7 +79,11 @@ - + + + + + From 47bab51237c0235fc490b7daf913e2a633f98342 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 25 Sep 2020 15:39:01 +0330 Subject: [PATCH 099/108] - Fixed Image Rendering and autolayout in ArticleHeaderView.swift --- .../UI/UIImageView+SDWebImage.swift | 12 +++++++--- .../ArticleDetailViewController.swift | 1 - .../Headers/ArticleDetailHeaderView.swift | 2 +- .../Views/Headers/ArticleDetailHeaderView.xib | 23 +++++++++++-------- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/DutchNews/Classes/Extensions/UI/UIImageView+SDWebImage.swift b/DutchNews/Classes/Extensions/UI/UIImageView+SDWebImage.swift index ab770df..f1a4c7a 100644 --- a/DutchNews/Classes/Extensions/UI/UIImageView+SDWebImage.swift +++ b/DutchNews/Classes/Extensions/UI/UIImageView+SDWebImage.swift @@ -15,9 +15,15 @@ extension UIImageView { static let cacheSDWebImageOptions: SDWebImageOptions = [.lowPriority,.scaleDownLargeImages,.queryMemoryData,.refreshCached] func setImage(url: URL?,placeHolderImage: UIImage? = #imageLiteral(resourceName: "image-placeHolder"), - options: SDWebImageOptions = UIImageView.defaultSDWebImageOptions, - completed: SDExternalCompletionBlock? = nil) { - self.contentMode = .scaleAspectFill + contentMode: UIView.ContentMode? = .scaleAspectFill, + options: SDWebImageOptions = UIImageView.defaultSDWebImageOptions, + completed: SDExternalCompletionBlock? = nil) { + + if let contentMode = contentMode { + self.contentMode = contentMode + self.setNeedsDisplay() + } + self.sd_setImage(with: url, placeholderImage: placeHolderImage, options: options, completed: completed) } diff --git a/DutchNews/Classes/ViewControllers/ArticleDetailViewController.swift b/DutchNews/Classes/ViewControllers/ArticleDetailViewController.swift index 6699041..4fce0ec 100644 --- a/DutchNews/Classes/ViewControllers/ArticleDetailViewController.swift +++ b/DutchNews/Classes/ViewControllers/ArticleDetailViewController.swift @@ -111,7 +111,6 @@ class ArticleDetailViewController: UIViewController, AlertableView { insideRect: view.bounds).size.height containerScrollView.parallaxHeader.height = max(size.height, height) - view.layoutIfNeeded() } diff --git a/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.swift b/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.swift index a50b579..5dbd77a 100644 --- a/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.swift +++ b/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.swift @@ -28,7 +28,7 @@ class ArticleDetailHeaderView: UIView { sourceLabel.text = article.source publishDateLabel.text = article.publishedAt - backgroundImageView.setImage(url: article.urlToImage) + backgroundImageView.setImage(url: article.urlToImage, contentMode: .scaleAspectFill) layoutIfNeeded() } diff --git a/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.xib b/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.xib index b975df8..be74e61 100644 --- a/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.xib +++ b/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.xib @@ -15,22 +15,22 @@ - + - - + + - + - + - - + - - + - + + @@ -127,4 +127,7 @@ + + + From 8e8a9dfaacb4fe780335cfdec55f36a243264703 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Fri, 25 Sep 2020 22:30:38 +0330 Subject: [PATCH 100/108] Bugfix/headlines custom layouts (#19) * - Removed unnecessary log. * - Fixed DataSource for reloading and inserting a new items. * - Fixed Headlines CollectionView Custom Layout. --- .../Utilites/RxHeadlinesDataSource.swift | 36 ++++-- .../HeadlinesViewController+DataSource.swift | 1 - .../HeadlinesViewController.swift | 1 + .../Cells/ArticleRowCollectionViewCell.swift | 4 +- .../Cells/ArticleRowCollectionViewCell.xib | 71 ++++++----- .../ArticleWebContainerCollectionViewCell.xib | 2 +- .../HalfWidthArticleCollectionViewCell.swift | 4 +- .../HalfWidthArticleCollectionViewCell.xib | 64 +++++----- .../HeadlineBaseCollectionViewCell.swift | 48 ++++---- .../Cells/MainArticleCollectionViewCell.swift | 16 ++- .../Cells/MainArticleCollectionViewCell.xib | 112 ++++++++---------- Podfile | 3 +- Podfile.lock | 32 ++--- 13 files changed, 194 insertions(+), 200 deletions(-) diff --git a/DutchNews/Classes/Utilites/RxHeadlinesDataSource.swift b/DutchNews/Classes/Utilites/RxHeadlinesDataSource.swift index 0670112..46d9708 100644 --- a/DutchNews/Classes/Utilites/RxHeadlinesDataSource.swift +++ b/DutchNews/Classes/Utilites/RxHeadlinesDataSource.swift @@ -14,14 +14,32 @@ import RxCocoa /// Helper Class for fixing the issue with `MagazineLayout` class RxHeadlinesDataSource: RxCollectionViewSectionedReloadDataSource where T: SectionModelType { -// override func collectionView(_ collectionView: UICollectionView, observedEvent: Event) { -// Binder(self) { dataSource, element in -// -// dataSource.setSections(element) -// collectionView.reloadData() -// collectionView.collectionViewLayout.invalidateLayout() -// -// }.on(observedEvent) -// } + override func collectionView(_ collectionView: UICollectionView, observedEvent: Event) { + Binder(self) { dataSource, element in + + let indices = (0..= element.count }) + let reloadIndicies = indices.filter { $0 >= element.count } + collectionView.insertSections(insertIndicies, animationStyle: .fade) + collectionView.reloadSections(reloadIndicies, animationStyle: .none) + } + + dataSource.setSections(element) + }, completion: { (finish) in + guard finish else { + return + } + collectionView.collectionViewLayout.invalidateLayout() + }) + + }.on(observedEvent) + } } diff --git a/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift b/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift index 3ab4f40..6c22db9 100644 --- a/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift +++ b/DutchNews/Classes/ViewControllers/HeadlinesViewController+DataSource.swift @@ -66,7 +66,6 @@ extension HeadlinesViewController { } case (let cell as HeadlineBaseCollectionViewCell): article.output - .debug() .drive(onNext: {[weak cell] viewModel in cell?.config(viewModel: viewModel) }).disposed(by: cell.disposeBag) diff --git a/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift b/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift index de70c5f..3875271 100644 --- a/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift +++ b/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift @@ -176,6 +176,7 @@ class HeadlinesViewController: UIViewController { // RxSwift assigned another delelgate object after running the upper code // we have to make sure that current vc present as delegate collectionView.delegate = self + collectionView.reloadData() viewModel.state.drive(onNext: {[weak self] (state) in self?.updateLayoutsBase(onState: state) diff --git a/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.swift b/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.swift index eb8ac6a..3aa35b6 100644 --- a/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.swift +++ b/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.swift @@ -10,7 +10,6 @@ import UIKit class ArticleRowCollectionViewCell: HeadlineBaseCollectionViewCell { - @IBOutlet weak var cellContentView: UIView! @IBOutlet weak var imageView: UIImageView! @IBOutlet weak var titleLabel: UILabel! @IBOutlet weak var descriptionLabel: UILabel! @@ -31,7 +30,7 @@ class ArticleRowCollectionViewCell: HeadlineBaseCollectionViewCell { descriptionLabel.text = nil sourceLabel.text = nil dateLabel.text = nil - imageView.image = nil + imageView.image = #imageLiteral(resourceName: "image-placeHolder") imageView.cancelCurrentImageLoad() } @@ -41,6 +40,7 @@ class ArticleRowCollectionViewCell: HeadlineBaseCollectionViewCell { sourceLabel.text = article.source imageView.setImage(url: article.urlToImage) dateLabel.text = article.publishedAt + super.config(viewModel: article) } } diff --git a/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.xib b/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.xib index df886fa..05f5796 100644 --- a/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.xib +++ b/DutchNews/Classes/Views/Cells/ArticleRowCollectionViewCell.xib @@ -9,14 +9,14 @@ - + - + @@ -26,23 +26,23 @@ - - - - - + + + + - - + + - + diff --git a/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.xib b/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.xib index 92b727f..0a7e886 100644 --- a/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.xib +++ b/DutchNews/Classes/Views/Cells/ArticleWebContainerCollectionViewCell.xib @@ -15,7 +15,7 @@ - + - - - - + + + + - + diff --git a/Podfile b/Podfile index c6785b8..4e90a13 100644 --- a/Podfile +++ b/Podfile @@ -24,9 +24,8 @@ target 'DutchNews' do pod 'MXParallaxHeader' pod 'Pageboy' # pod 'JEKScrollableSectionCollectionViewLayout', :git => 'https://github.com/farshadmb/JEKScrollableSectionCollectionViewLayout.git' - pod 'MagazineLayout' + pod 'MagazineLayout', :git => 'https://github.com/farshadmb/MagazineLayout.git' - pod 'RealmSwift' pod 'MaterialComponents' pod 'SwiftLint' pod 'CryptoSwift', '1.1.2' diff --git a/Podfile.lock b/Podfile.lock index 64ea6e6..a9ccf64 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -651,12 +651,6 @@ PODS: - Nimble (8.1.2) - Pageboy (3.6.1) - PureLayout (3.1.6) - - Realm (5.0.3): - - Realm/Headers (= 5.0.3) - - Realm/Headers (5.0.3) - - RealmSwift (5.0.3): - - Realm (= 5.0.3) - - RuntimeNew (2.1.5) - RxAlamofire (5.5.0): - RxAlamofire/Core (= 5.5.0) - RxAlamofire/Core (5.5.0): @@ -680,15 +674,12 @@ PODS: - SDWebImage/Core (= 5.9.1) - SDWebImage/Core (5.9.1) - SwiftLint (0.39.2) - - Unrealm (1.3.5): - - RealmSwift (= 5.0.3) - - RuntimeNew (= 2.1.5) DEPENDENCIES: - Alamofire - CocoaLumberjack/Swift - CryptoSwift (= 1.1.2) - - MagazineLayout + - MagazineLayout (from `https://github.com/farshadmb/MagazineLayout.git`) - MaterialComponents - Mocker (~> 1.0.0) - MXParallaxHeader @@ -703,7 +694,6 @@ DEPENDENCIES: - RxTest - SDWebImage - SwiftLint - - Unrealm SPEC REPOS: trunk: @@ -711,7 +701,6 @@ SPEC REPOS: - CocoaLumberjack - CryptoSwift - Differentiator - - MagazineLayout - MaterialComponents - MDFInternationalization - MDFTextAccessibility @@ -722,9 +711,6 @@ SPEC REPOS: - Nimble - Pageboy - PureLayout - - Realm - - RealmSwift - - RuntimeNew - RxAlamofire - RxBlocking - RxCocoa @@ -734,7 +720,15 @@ SPEC REPOS: - RxTest - SDWebImage - SwiftLint - - Unrealm + +EXTERNAL SOURCES: + MagazineLayout: + :git: https://github.com/farshadmb/MagazineLayout.git + +CHECKOUT OPTIONS: + MagazineLayout: + :commit: 1e795ecae0bb8f944a1c4c5f3bd92620c0ce0c93 + :git: https://github.com/farshadmb/MagazineLayout.git SPEC CHECKSUMS: Alamofire: e911732990610fe89af59ac0077f923d72dc3dfd @@ -752,9 +746,6 @@ SPEC CHECKSUMS: Nimble: 3864815b4703c7ebffba875973c70e854489fbae Pageboy: 29a2d474ad99404b4d77f325e0ab6d705930a4fb PureLayout: bd3c4ec3a3819ad387c99ebb72c6b129c3ed4d2d - Realm: bfca1699b61b0b17c3a69ae0e648314eae91fbdb - RealmSwift: 493c9f089cd3893b3959007973c0e4f640906ba0 - RuntimeNew: ef34cf1783be4c1cbe798970bc590924dab5df87 RxAlamofire: 22287c710761466d0123504c566a8381520d4d63 RxBlocking: 5f700a78cad61ce253ebd37c9a39b5ccc76477b4 RxCocoa: 32065309a38d29b5b0db858819b5bf9ef038b601 @@ -764,8 +755,7 @@ SPEC CHECKSUMS: RxTest: 711632d5644dffbeb62c936a521b5b008a1e1faa SDWebImage: a990c053fff71e388a10f3357edb0be17929c9c5 SwiftLint: 22ccbbe3b8008684be5955693bab135e0ed6a447 - Unrealm: ba1c168935344084ba28dd6bff5ded86f4837d7f -PODFILE CHECKSUM: 00f21cd7eb8be2bb477174c3f27b62143f80f78c +PODFILE CHECKSUM: cde5b5350234211e6adef69591befc23defa638c COCOAPODS: 1.9.3 From 06f3f18325da2193cf7e6a03ab7e8418b719b3e3 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sat, 26 Sep 2020 01:15:31 +0330 Subject: [PATCH 101/108] - Fixed Logic. --- DutchNews/AppDIContainer.swift | 6 +++--- .../Data Layers/Networking/APIClientService.swift | 2 +- DutchNews/Classes/Domains/ArticlesPageUseCase.swift | 4 +--- .../Classes/Domains/HeadlinesFetchingUseCase.swift | 4 ++-- .../Classes/Views/Headers/ArticleDetailHeaderView.xib | 11 +---------- 5 files changed, 8 insertions(+), 19 deletions(-) diff --git a/DutchNews/AppDIContainer.swift b/DutchNews/AppDIContainer.swift index f615b09..2dafa24 100644 --- a/DutchNews/AppDIContainer.swift +++ b/DutchNews/AppDIContainer.swift @@ -69,12 +69,12 @@ struct AppDIContainer { //////////////////////////////////////////////////////////////// static var headlineFetchingUseCase: HeadlinesUseCases { - return HeadlinesFetchingUseCase(repository: headlineArticleRepository, - localRespository: headlineLocalArticleRepository) + return HeadlinesFetchingUseCase(repository: headlineArticleRepository) + } static var articlesPageUseCase: ArticlesUseCase { - return ArticlesPageUseCase(repository: headlineLocalArticleRepository) + return ArticlesPageUseCase(repository: headlineArticleRepository) } //////////////////////////////////////////////////////////////// diff --git a/DutchNews/Classes/Data Layers/Networking/APIClientService.swift b/DutchNews/Classes/Data Layers/Networking/APIClientService.swift index 5a1cc26..0040da5 100644 --- a/DutchNews/Classes/Data Layers/Networking/APIClientService.swift +++ b/DutchNews/Classes/Data Layers/Networking/APIClientService.swift @@ -230,7 +230,7 @@ final class APIClientService: NetworkServiceInterceptable { Logger.debugLog(result.debugDescription,tag: "Networking") } - return map(dataRequest: dataTask, decoder: decoder).debug() + return map(dataRequest: dataTask, decoder: decoder) }catch let error { return .just(.failure(error)) diff --git a/DutchNews/Classes/Domains/ArticlesPageUseCase.swift b/DutchNews/Classes/Domains/ArticlesPageUseCase.swift index 2698695..f83945c 100644 --- a/DutchNews/Classes/Domains/ArticlesPageUseCase.swift +++ b/DutchNews/Classes/Domains/ArticlesPageUseCase.swift @@ -18,8 +18,6 @@ class ArticlesPageUseCase: ArticlesUseCase { } func fetchLocalArticles() -> Observable<[T]> { - return repository.fetchArticles().map { - $0.sorted(by: { $0.publishedAt >= $1.publishedAt }) - } + return repository.fetchArticles() } } diff --git a/DutchNews/Classes/Domains/HeadlinesFetchingUseCase.swift b/DutchNews/Classes/Domains/HeadlinesFetchingUseCase.swift index 25ace39..e8fe45c 100644 --- a/DutchNews/Classes/Domains/HeadlinesFetchingUseCase.swift +++ b/DutchNews/Classes/Domains/HeadlinesFetchingUseCase.swift @@ -41,7 +41,7 @@ class HeadlinesFetchingUseCase: HeadlinesUseCases { .debug("#\(#file.replacingOccurrences(of: ".swift", with: "")).\(#function)") .do(afterNext: {[weak local] in try? local?.save(articles: $0) - }) + }) } - + } diff --git a/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.xib b/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.xib index be74e61..9ded1ad 100644 --- a/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.xib +++ b/DutchNews/Classes/Views/Headers/ArticleDetailHeaderView.xib @@ -39,17 +39,8 @@ - + + + + + + + + + + + + @@ -62,6 +74,7 @@ + @@ -129,19 +142,33 @@ + + + + + diff --git a/DutchNewsTests/ModelsTests/ModelTests.swift b/DutchNewsTests/ModelsTests/ModelTests.swift index 6b10a01..67e7b22 100644 --- a/DutchNewsTests/ModelsTests/ModelTests.swift +++ b/DutchNewsTests/ModelsTests/ModelTests.swift @@ -8,7 +8,6 @@ import Foundation import XCTest -import Foundation @testable import DutchNews From 945db13de6f34117392a7fa03442fc1029411741 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sat, 26 Sep 2020 04:02:05 +0330 Subject: [PATCH 103/108] - Implemented Searching feature logic and view controller. --- DutchNews.xcodeproj/project.pbxproj | 4 + DutchNews/AppDIContainer.swift | 8 + .../Response/APIServerResponse.swift | 2 - .../ViewControllerFactory/ScreenEnum.swift | 3 + .../ViewControllerFactory.swift | 1 + .../ViewModelViewControllerFactory.swift | 19 ++ .../HeadlineSearchViewController.swift | 205 ++++++++++++++++++ .../HeadlinesViewController.swift | 14 ++ .../Abstract/ArticlesViewModel.swift | 5 +- .../ViewModels/HeadlineSearchViewModel.swift | 127 +++++++++++ .../Storyboards/Base.lproj/Main.storyboard | 58 ++++- 11 files changed, 432 insertions(+), 14 deletions(-) create mode 100644 DutchNews/Classes/ViewControllers/HeadlineSearchViewController.swift diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index 9c245a6..a5fc0b2 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -70,6 +70,7 @@ F865F76D2516C826001FD067 /* DefaultAPIValidResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = F865F76C2516C826001FD067 /* DefaultAPIValidResponse.swift */; }; F884C0FB251D24000078E88B /* HeadlinesUseCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0F72518E7770083D2B1 /* HeadlinesUseCases.swift */; }; F884C0FD251D2CAE0078E88B /* ArticlesUseCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = F884C0FC251D2CAE0078E88B /* ArticlesUseCases.swift */; }; + F8866A3F251EABE8008AF310 /* HeadlineSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8866A3E251EABE8008AF310 /* HeadlineSearchViewController.swift */; }; F88800692517A423008DCC54 /* RepositoryDependenciesFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88800682517A423008DCC54 /* RepositoryDependenciesFactory.swift */; }; F888006B2517A8EE008DCC54 /* HeadlineSuccessResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = F888006A2517A8EE008DCC54 /* HeadlineSuccessResponse.json */; }; F888006D2517AA1F008DCC54 /* HeadlineFailureResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = F888006C2517AA1F008DCC54 /* HeadlineFailureResponse.json */; }; @@ -198,6 +199,7 @@ F865F76A2516C08C001FD067 /* NetworkValidResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkValidResponse.swift; sourceTree = ""; }; F865F76C2516C826001FD067 /* DefaultAPIValidResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultAPIValidResponse.swift; sourceTree = ""; }; F884C0FC251D2CAE0078E88B /* ArticlesUseCases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticlesUseCases.swift; sourceTree = ""; }; + F8866A3E251EABE8008AF310 /* HeadlineSearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlineSearchViewController.swift; sourceTree = ""; }; F88800682517A423008DCC54 /* RepositoryDependenciesFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryDependenciesFactory.swift; sourceTree = ""; }; F888006A2517A8EE008DCC54 /* HeadlineSuccessResponse.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = HeadlineSuccessResponse.json; sourceTree = ""; }; F888006C2517AA1F008DCC54 /* HeadlineFailureResponse.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = HeadlineFailureResponse.json; sourceTree = ""; }; @@ -634,6 +636,7 @@ F8154D4D2517D28700BFB42C /* HeadlinesViewController.swift */, F8154D742518156000BFB42C /* HeadlinesViewController+DataSource.swift */, F8154D512517EBC700BFB42C /* HeadlinesViewController+MagazineLayout.swift */, + F8866A3E251EABE8008AF310 /* HeadlineSearchViewController.swift */, F85E318B251B8462002753AC /* ArticlesPageViewController.swift */, F85E318D251B8484002753AC /* ArticleDetailViewController.swift */, ); @@ -944,6 +947,7 @@ F858997B25176DC800A6BA2A /* Article.swift in Sources */, F85E319D251B8D61002753AC /* Rx+WebKit.swift in Sources */, F8C83460251C04020051A0FD /* CodableDataManager.swift in Sources */, + F8866A3F251EABE8008AF310 /* HeadlineSearchViewController.swift in Sources */, F865F7622516A6C1001FD067 /* APIServerResponseError.swift in Sources */, F884C0FD251D2CAE0078E88B /* ArticlesUseCases.swift in Sources */, F8E5C11E25191D5E0083D2B1 /* Date+Convertor.swift in Sources */, diff --git a/DutchNews/AppDIContainer.swift b/DutchNews/AppDIContainer.swift index 2dafa24..a3594eb 100644 --- a/DutchNews/AppDIContainer.swift +++ b/DutchNews/AppDIContainer.swift @@ -77,6 +77,10 @@ struct AppDIContainer { return ArticlesPageUseCase(repository: headlineArticleRepository) } + static var headlineSearchUseCase: HeadlinesUseCases { + return HeadlinesSearchingUseCases(repository: headlineArticleRepository) + } + //////////////////////////////////////////////////////////////// // MARK: - // MARK: ViewModels DI Container @@ -91,6 +95,10 @@ struct AppDIContainer { return ArticleDetailsPageViewModel(useCase: articlesPageUseCase) } + static var headlineSearchViewModel: ArticlesSearchViewModel { + return HeadlineSearchViewModel(useCase: headlineSearchUseCase) + } + static let viewModelViewControllerFactory: ViewControllerFactory = { let storyboard = UIStoryboard(name: "Main", bundle: Bundle(for: AppDelegate.self)) return ViewModelViewControllerFactory(storyboard: storyboard) diff --git a/DutchNews/Classes/Data Layers/Networking/Response/APIServerResponse.swift b/DutchNews/Classes/Data Layers/Networking/Response/APIServerResponse.swift index 312926f..0a8a0b5 100644 --- a/DutchNews/Classes/Data Layers/Networking/Response/APIServerResponse.swift +++ b/DutchNews/Classes/Data Layers/Networking/Response/APIServerResponse.swift @@ -54,8 +54,6 @@ extension APIServerResponse: Decodable { throw APIServerResponseError.unknown } - self.data = nil - return } do { diff --git a/DutchNews/Classes/Utilites/ViewControllerFactory/ScreenEnum.swift b/DutchNews/Classes/Utilites/ViewControllerFactory/ScreenEnum.swift index af91e06..fab4b0f 100644 --- a/DutchNews/Classes/Utilites/ViewControllerFactory/ScreenEnum.swift +++ b/DutchNews/Classes/Utilites/ViewControllerFactory/ScreenEnum.swift @@ -14,6 +14,7 @@ enum ScreenName: Screen { case headlines case pages case detail + case search func screenIdentifier() -> String { switch self { @@ -23,6 +24,8 @@ enum ScreenName: Screen { return ArticlePageViewController.className case .detail: return ArticleDetailViewController.className + case .search: + return HeadlineSearchViewController.className } } diff --git a/DutchNews/Classes/Utilites/ViewControllerFactory/ViewControllerFactory.swift b/DutchNews/Classes/Utilites/ViewControllerFactory/ViewControllerFactory.swift index c259f5b..a1d9464 100644 --- a/DutchNews/Classes/Utilites/ViewControllerFactory/ViewControllerFactory.swift +++ b/DutchNews/Classes/Utilites/ViewControllerFactory/ViewControllerFactory.swift @@ -13,6 +13,7 @@ protocol ViewControllerFactory: class { func makeRootViewController() -> UINavigationController? func makeHeadlinesViewController() throws -> HeadlinesViewController + func makeHeadlinesSearchViewController() throws -> UISearchController func makePageViewController(selected: Int) throws -> ArticlePageViewController func makeArticleDetailViewController() throws -> ArticleDetailViewController diff --git a/DutchNews/Classes/Utilites/ViewControllerFactory/ViewModelViewControllerFactory.swift b/DutchNews/Classes/Utilites/ViewControllerFactory/ViewModelViewControllerFactory.swift index a6d902f..f986354 100644 --- a/DutchNews/Classes/Utilites/ViewControllerFactory/ViewModelViewControllerFactory.swift +++ b/DutchNews/Classes/Utilites/ViewControllerFactory/ViewModelViewControllerFactory.swift @@ -33,6 +33,7 @@ final class ViewModelViewControllerFactory: ViewControllerFactory { vc.viewModel = AppDIContainer.headlinesViewModel vc.controllerFactory = self + vc.searchController = try? makeHeadlinesSearchViewController() return vc } @@ -57,6 +58,24 @@ final class ViewModelViewControllerFactory: ViewControllerFactory { return vc } + func makeHeadlinesSearchViewController() throws -> UISearchController { + + guard let vc: HeadlineSearchViewController = makeViewController(forScreen: ScreenName.search) else { + throw Error.notFound + } + + vc.viewModel = AppDIContainer.headlineSearchViewModel + vc.controllerFactory = self + + let searchController = UISearchController(searchResultsController: vc) + vc.searchController = searchController + searchController.searchResultsUpdater = vc + searchController.obscuresBackgroundDuringPresentation = true + searchController.searchBar.placeholder = "search_headline_title".localized + + return searchController + } + private func makeViewController(forScreen screen: Screen) -> T? { guard let vc = storyboard.instantiateViewController(identifier: screen.screenIdentifier()) as? T else { diff --git a/DutchNews/Classes/ViewControllers/HeadlineSearchViewController.swift b/DutchNews/Classes/ViewControllers/HeadlineSearchViewController.swift new file mode 100644 index 0000000..b2f4f26 --- /dev/null +++ b/DutchNews/Classes/ViewControllers/HeadlineSearchViewController.swift @@ -0,0 +1,205 @@ +// +// HeadlineSearchViewController.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/26/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import UIKit +import RxCocoa +import RxSwift +import RxDataSources +import PureLayout +import MaterialComponents + +class HeadlineSearchViewController: UIViewController, AlertableView { + + @IBOutlet weak var tableView: UITableView! + + @IBOutlet weak var loadingIndicator: MDCProgressView! + + let disposeBag = DisposeBag() + + lazy var dataSource: RxTableViewSectionedReloadDataSource = { + return self.buildDataSource() + }() + + var viewModel: ArticlesSearchViewModel? + + weak var searchController: UISearchController? + + var controllerFactory: ViewControllerFactory? + + deinit { + viewModel = nil + searchController = nil + controllerFactory = nil + } + + override func viewDidLoad() { + super.viewDidLoad() + bindViewModel() + // Do any additional setup after loading the view. + } + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ + + //////////////////////////////////////////////////////////////// + // MARK: - + // MARK: UI Methods + // MARK: - + //////////////////////////////////////////////////////////////// + func setupLayouts() { + setupTableView() + + loadingIndicator.mode = .indeterminate + loadingIndicator.trackTintColor = .clear + loadingIndicator.progressTintColor = .black + loadingIndicator.stopAnimating() + loadingIndicator.isHidden = true + + } + + func setupTableView() { + tableView.estimatedRowHeight = UITableView.automaticDimension + // tableView.register(<#T##cellClass: AnyClass?##AnyClass?#>, forCellReuseIdentifier: <#T##String#>) + } + + func updateLayoutsBase(onState state: ViewModelState) { + switch state { + case .loading(isRefreshing: let isRefreshing) where isRefreshing == false : + loadingIndicator.startAnimating() + loadingIndicator.isHidden = false + + case .loaded, .idle: + + loadingIndicator.stopAnimating() + loadingIndicator.isHidden = true + + case .error(let error): + loadingIndicator.stopAnimating() + + let message: String + if let err = error as? URLError { + message = err.localizedDescription + }else { + message = error.localizedDescription + } + + presentAlertView(withMessage: message, + actionTitle: "retry".localized) {[weak self] in + self?.searchAgain() + } + default: + break + } + } + + ///////////////////////////////////////////////////////////// + // MARK: - + // MARK: View Model Methods + // MARK: - + //////////////////////////////////////////////////////////////// + + func bindViewModel() { + + guard let viewModel = self.viewModel else { + return + } + + bind(viewModel: viewModel) + } + + func bind(viewModel: ArticlesSearchViewModel) { + + viewModel.output.map { + return $0.map { SectionType(model: "search_result_title", items: $0.items) } + } .drive(tableView.rx.items(dataSource: dataSource)).disposed(by: disposeBag) + + tableView.rx.itemSelected.bind {[weak viewModel] (indexPath) in + viewModel?.didSelect(articleAtIndex: indexPath) + }.disposed(by: disposeBag) + + viewModel.state.drive(onNext: {[weak self] (state) in + self?.updateLayoutsBase(onState: state) + }).disposed(by: disposeBag) + + searchController?.searchBar.rx.text + .debounce(.milliseconds(500), scheduler: MainScheduler.instance) + .bind(onNext: {[weak viewModel] (keyword) in + viewModel?.searchArticles(keyword: keyword ?? "") + }).disposed(by: disposeBag) + + searchController?.searchBar.rx.cancelButtonClicked.bind(onNext: { [weak viewModel] in + viewModel?.searchArticles(keyword: "") + }).disposed(by: disposeBag) + + viewModel.selectedItem.observeOn(MainScheduler.instance) + .filter { $0 != nil }.map { $0! } + .bind {[weak self] in + self?.navigateToDetail(with: $0) + }.disposed(by: disposeBag) + } + +} + +extension HeadlineSearchViewController: UISearchControllerDelegate, UISearchResultsUpdating { + + func updateSearchResults(for searchController: UISearchController) { + + } + +} + +//////////////////////////////////////////////////////////////// +// MARK: - +// MARK: Helper Methods +// MARK: - +//////////////////////////////////////////////////////////////// + +extension HeadlineSearchViewController { + + typealias SectionType = ArticlesSearchViewModel.T + + func buildDataSource() -> RxTableViewSectionedReloadDataSource { + + return .init(configureCell: { (dataSource, tableView, indexPath, viewModel) -> UITableViewCell in + return UITableViewCell() + }, titleForHeaderInSection: { (dataSource, section) -> String? in + dataSource[section].model.localized + }) + } + + func search(keyword: String?) { + viewModel?.searchArticles(keyword: keyword ?? "") + } + + func searchAgain() { + self.search(keyword: searchController?.searchBar.text) + } + + func navigateToDetail(with viewModel: ArticleViewModel) { + do { + guard let vc = try controllerFactory?.makeArticleDetailViewController() else { + return + } + + vc.viewModel = viewModel + + self.navigationController?.show(vc, sender: false) + + }catch { + presentAlertView(withMessage: error.localizedDescription) + } + } + +} diff --git a/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift b/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift index 935acdd..d5a1a0f 100644 --- a/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift +++ b/DutchNews/Classes/ViewControllers/HeadlinesViewController.swift @@ -57,6 +57,14 @@ class HeadlinesViewController: UIViewController { var controllerFactory: ViewControllerFactory? + var searchController: UISearchController? + + deinit { + viewModel = nil + searchController = nil + controllerFactory = nil + } + override func viewDidLoad() { super.viewDidLoad() @@ -94,6 +102,7 @@ class HeadlinesViewController: UIViewController { loadingIndicator.stopAnimating() loadingIndicator.isHidden = true + setupSearchView() } func setupColletionView() { @@ -149,6 +158,11 @@ class HeadlinesViewController: UIViewController { } } + func setupSearchView() { + + navigationItem.searchController = searchController + definesPresentationContext = true + } //////////////////////////////////////////////////////////////// // MARK: - // MARK: View Model Methods diff --git a/DutchNews/Classes/ViewModels/Abstract/ArticlesViewModel.swift b/DutchNews/Classes/ViewModels/Abstract/ArticlesViewModel.swift index 4d0f80a..00337c7 100644 --- a/DutchNews/Classes/ViewModels/Abstract/ArticlesViewModel.swift +++ b/DutchNews/Classes/ViewModels/Abstract/ArticlesViewModel.swift @@ -34,5 +34,8 @@ protocol ArticlesViewModel: class { protocol ArticlesSearchViewModel: ArticlesViewModel { - func searchArticles(keyword: String) -> Observable<[T]> + var selectedItem: BehaviorRelay { get } + + func searchArticles(keyword: String) + } diff --git a/DutchNews/Classes/ViewModels/HeadlineSearchViewModel.swift b/DutchNews/Classes/ViewModels/HeadlineSearchViewModel.swift index 299c625..5e4e3b7 100644 --- a/DutchNews/Classes/ViewModels/HeadlineSearchViewModel.swift +++ b/DutchNews/Classes/ViewModels/HeadlineSearchViewModel.swift @@ -7,3 +7,130 @@ // import Foundation +import RxSwift +import RxCocoa + +final class HeadlineSearchViewModel: ArticlesSearchViewModel { + + var selectedIndex: BehaviorRelay + + private(set) var selectedItem: BehaviorRelay = BehaviorRelay(value: nil) + + private var statePublisher: BehaviorRelay + + var state: Driver { + return statePublisher.asDriver { + return .just(.error($0) ) + }.distinctUntilChanged() + } + + private var outputPublisher: BehaviorRelay<[T]> + + private var items: [T.Item] = [] { + didSet { + outputPublisher.accept([T(model: "Headlines", items: items)]) + } + } + + var output: Driver<[T]> { + outputPublisher.asDriver { _ in return .never() } + } + + let useCase: HeadlinesUseCases + + let disposeBag = DisposeBag() + + private init(useCase: HeadlinesUseCases, + state: BehaviorRelay, + output: BehaviorRelay<[T]>) { + + self.useCase = useCase + self.statePublisher = state + self.outputPublisher = output + self.selectedIndex = BehaviorRelay(value: nil) + + } + + required convenience init(useCase: HeadlinesUseCases) { + self.init(useCase: useCase, + state: .init(value: .idle), + output: .init(value: [])) + } + + //////////////////////////////////////////////////////////////// + // MARK: - + // MARK: Article View Model Implementation + // MARK: - + //////////////////////////////////////////////////////////////// + + func searchArticles(keyword: String) { + searchFromRepository(keyword: keyword) + } + + func fetchArticles() { + + } + + func refreshArticles() { + + } + + func didSelect(article: T.Item) { + let datas = items + + guard let index = datas.firstIndex(where: { $0 == article }), + index != 3 else { + return + } + + self.selectedIndex.accept(index) + self.selectedItem.accept(article) + } + + func didSelect(articleAtIndex index: IndexPath) { + let datas = items + guard datas[safe:index.item] != nil else { + return + } + self.selectedIndex.accept(index.item) + self.selectedItem.accept(datas[safe:index.item]) + } + + //////////////////////////////////////////////////////////////// + // MARK: - + // MARK: Private Methods + // MARK: - + //////////////////////////////////////////////////////////////// + + func searchFromRepository(keyword: String) { + + statePublisher.accept(.loading(isRefreshing: false)) + + useCase.searchInArticle(keyword: keyword) + .subscribe {[weak self] event in + + switch event { + case .next(let newItems): + guard let `self` = self else { + break + } + + let mapped = newItems.map { + HeadlineCellViewModel(model: $0) + } + + self.items = mapped + self.statePublisher.accept(.loaded) + + case .error(let error): + self?.statePublisher.accept(.error(error)) + self?.statePublisher.accept(.idle) + case .completed: + self?.statePublisher.accept(.idle) + } + + }.disposed(by: disposeBag) + + } + +} diff --git a/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard b/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard index 5d56001..1e899cd 100644 --- a/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard +++ b/DutchNews/Resources/Storyboards/Base.lproj/Main.storyboard @@ -5,7 +5,6 @@ - @@ -34,16 +33,7 @@ - - - - - - - - - - + @@ -175,5 +165,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From a054f02e0feb1cacece5abdb0553d328c3a6fe5d Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sat, 26 Sep 2020 04:41:32 +0330 Subject: [PATCH 104/108] - Added HeadlineSearchTableViewCell. - Implemented to fill searching result. --- DutchNews.xcodeproj/project.pbxproj | 8 ++ .../HeadlineSearchViewController.swift | 35 +++++- .../Cells/HeadlineSearchTableViewCell.swift | 63 +++++++++++ .../Cells/HeadlineSearchTableViewCell.xib | 106 ++++++++++++++++++ .../Localization/en.lproj/Localizable.strings | 3 +- .../nl-NL.lproj/Localizable.strings | 2 + 6 files changed, 210 insertions(+), 7 deletions(-) create mode 100644 DutchNews/Classes/Views/Cells/HeadlineSearchTableViewCell.swift create mode 100644 DutchNews/Classes/Views/Cells/HeadlineSearchTableViewCell.xib diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index a5fc0b2..9fddcd4 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -71,6 +71,8 @@ F884C0FB251D24000078E88B /* HeadlinesUseCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E5C0F72518E7770083D2B1 /* HeadlinesUseCases.swift */; }; F884C0FD251D2CAE0078E88B /* ArticlesUseCases.swift in Sources */ = {isa = PBXBuildFile; fileRef = F884C0FC251D2CAE0078E88B /* ArticlesUseCases.swift */; }; F8866A3F251EABE8008AF310 /* HeadlineSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8866A3E251EABE8008AF310 /* HeadlineSearchViewController.swift */; }; + F8866A42251EC3BC008AF310 /* HeadlineSearchTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8866A40251EC3BC008AF310 /* HeadlineSearchTableViewCell.swift */; }; + F8866A43251EC3BC008AF310 /* HeadlineSearchTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F8866A41251EC3BC008AF310 /* HeadlineSearchTableViewCell.xib */; }; F88800692517A423008DCC54 /* RepositoryDependenciesFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88800682517A423008DCC54 /* RepositoryDependenciesFactory.swift */; }; F888006B2517A8EE008DCC54 /* HeadlineSuccessResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = F888006A2517A8EE008DCC54 /* HeadlineSuccessResponse.json */; }; F888006D2517AA1F008DCC54 /* HeadlineFailureResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = F888006C2517AA1F008DCC54 /* HeadlineFailureResponse.json */; }; @@ -200,6 +202,8 @@ F865F76C2516C826001FD067 /* DefaultAPIValidResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultAPIValidResponse.swift; sourceTree = ""; }; F884C0FC251D2CAE0078E88B /* ArticlesUseCases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticlesUseCases.swift; sourceTree = ""; }; F8866A3E251EABE8008AF310 /* HeadlineSearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlineSearchViewController.swift; sourceTree = ""; }; + F8866A40251EC3BC008AF310 /* HeadlineSearchTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlineSearchTableViewCell.swift; sourceTree = ""; }; + F8866A41251EC3BC008AF310 /* HeadlineSearchTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = HeadlineSearchTableViewCell.xib; sourceTree = ""; }; F88800682517A423008DCC54 /* RepositoryDependenciesFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryDependenciesFactory.swift; sourceTree = ""; }; F888006A2517A8EE008DCC54 /* HeadlineSuccessResponse.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = HeadlineSuccessResponse.json; sourceTree = ""; }; F888006C2517AA1F008DCC54 /* HeadlineFailureResponse.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = HeadlineFailureResponse.json; sourceTree = ""; }; @@ -302,6 +306,8 @@ F8154D682518016800BFB42C /* HalfWidthArticleCollectionViewCell.xib */, F8154D7025180C3900BFB42C /* ArticleWebContainerCollectionViewCell.swift */, F8154D7225180F0E00BFB42C /* ArticleWebContainerCollectionViewCell.xib */, + F8866A40251EC3BC008AF310 /* HeadlineSearchTableViewCell.swift */, + F8866A41251EC3BC008AF310 /* HeadlineSearchTableViewCell.xib */, ); path = Cells; sourceTree = ""; @@ -812,6 +818,7 @@ F85E3195251B8665002753AC /* ArticleDetailHeaderView.xib in Resources */, F89B0220250D446200B41293 /* Assets.xcassets in Resources */, F850575A251E066F00257884 /* Localizable.strings in Resources */, + F8866A43251EC3BC008AF310 /* HeadlineSearchTableViewCell.xib in Resources */, F8154D622518011500BFB42C /* MainArticleCollectionViewCell.xib in Resources */, F8154D662518012F00BFB42C /* ArticleRowCollectionViewCell.xib in Resources */, F8154D7725181D4100BFB42C /* HeadlineSuccessResponse.json in Resources */, @@ -974,6 +981,7 @@ F8E5C12D25191DA60083D2B1 /* UIViewController+StoryboardName.swift in Sources */, F8154D7125180C3900BFB42C /* ArticleWebContainerCollectionViewCell.swift in Sources */, F8E5C12A25191DA60083D2B1 /* UIImage+Additionals.swift in Sources */, + F8866A42251EC3BC008AF310 /* HeadlineSearchTableViewCell.swift in Sources */, F8E5C12B25191DA60083D2B1 /* UINavigationBar+Additionals.swift in Sources */, F865F75025168F05001FD067 /* AppConfig.swift in Sources */, F81572BD251D4839009DBFD7 /* Screen.swift in Sources */, diff --git a/DutchNews/Classes/ViewControllers/HeadlineSearchViewController.swift b/DutchNews/Classes/ViewControllers/HeadlineSearchViewController.swift index b2f4f26..3452543 100644 --- a/DutchNews/Classes/ViewControllers/HeadlineSearchViewController.swift +++ b/DutchNews/Classes/ViewControllers/HeadlineSearchViewController.swift @@ -39,6 +39,7 @@ class HeadlineSearchViewController: UIViewController, AlertableView { override func viewDidLoad() { super.viewDidLoad() + setupLayouts() bindViewModel() // Do any additional setup after loading the view. } @@ -69,9 +70,13 @@ class HeadlineSearchViewController: UIViewController, AlertableView { } + fileprivate static let cellId = "ResultCellId" + func setupTableView() { + tableView.register(HeadlineSearchTableViewCell.nib(), forCellReuseIdentifier: Self.cellId) tableView.estimatedRowHeight = UITableView.automaticDimension - // tableView.register(<#T##cellClass: AnyClass?##AnyClass?#>, forCellReuseIdentifier: <#T##String#>) + tableView.separatorStyle = .none + tableView.separatorColor = nil } func updateLayoutsBase(onState state: ViewModelState) { @@ -172,13 +177,31 @@ extension HeadlineSearchViewController { func buildDataSource() -> RxTableViewSectionedReloadDataSource { - return .init(configureCell: { (dataSource, tableView, indexPath, viewModel) -> UITableViewCell in - return UITableViewCell() - }, titleForHeaderInSection: { (dataSource, section) -> String? in - dataSource[section].model.localized + return .init(configureCell: {[weak self] (_, tableView, indexPath, viewModel) -> UITableViewCell in + + guard let cell = tableView.dequeueReusableCell(withIdentifier: Self.cellId, + for: indexPath) as? HeadlineSearchTableViewCell else { + return UITableViewCell() + } + + self?.config(cell: cell, viewModel: viewModel) + return cell + }, + titleForHeaderInSection: { (dataSource, section) -> String? in + dataSource[section].model.localized }) } + private func config(cell: HeadlineSearchTableViewCell, + viewModel: ArticleViewModel) { + + viewModel.output + .drive(onNext: {[weak cell] viewModel in + cell?.config(viewModel: viewModel) + }).disposed(by: cell.disposeBag) + + } + func search(keyword: String?) { viewModel?.searchArticles(keyword: keyword ?? "") } @@ -195,7 +218,7 @@ extension HeadlineSearchViewController { vc.viewModel = viewModel - self.navigationController?.show(vc, sender: false) + self.presentingViewController?.navigationController?.show(vc, sender: false) }catch { presentAlertView(withMessage: error.localizedDescription) diff --git a/DutchNews/Classes/Views/Cells/HeadlineSearchTableViewCell.swift b/DutchNews/Classes/Views/Cells/HeadlineSearchTableViewCell.swift new file mode 100644 index 0000000..6af9bef --- /dev/null +++ b/DutchNews/Classes/Views/Cells/HeadlineSearchTableViewCell.swift @@ -0,0 +1,63 @@ +// +// HeadlineSearchTableViewCell.swift +// DutchNews +// +// Created by Farshad Mousalou on 9/26/20. +// Copyright © 2020 Farshad Mousalou. All rights reserved. +// + +import UIKit +import Foundation +import MaterialComponents +import RxSwift + +class HeadlineSearchTableViewCell: UITableViewCell { + + @IBOutlet weak var backgroundCard: MDCCardCollectionCell! + @IBOutlet weak var articleimageView: UIImageView! + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var descriptionLabel: UILabel! + @IBOutlet weak var sourceLabel: UILabel! + @IBOutlet weak var dateLabel: UILabel! + + var disposeBag: DisposeBag! = DisposeBag() + + deinit { + disposeBag = nil + backgroundCard = nil + } + + override func awakeFromNib() { + super.awakeFromNib() + articleimageView.clipsToBounds = true + articleimageView.layer.cornerRadius = 5.0 + + backgroundCard?.isInteractable = false + backgroundCard?.isSelectable = true + backgroundCard?.setShadowElevation(.cardResting, for: .normal) + + // Initialization code + } + + override func prepareForReuse() { + super.prepareForReuse() + disposeBag = nil + disposeBag = DisposeBag() + titleLabel.text = nil + descriptionLabel.text = nil + sourceLabel.text = nil + dateLabel.text = nil + articleimageView.image = #imageLiteral(resourceName: "image-placeHolder") + articleimageView.cancelCurrentImageLoad() + } + + func config(viewModel article: ArticleRepresentable) { + titleLabel.text = article.title + descriptionLabel.text = article.description + sourceLabel.text = article.source + articleimageView.setImage(url: article.urlToImage) + dateLabel.text = article.publishedAt + + } + +} diff --git a/DutchNews/Classes/Views/Cells/HeadlineSearchTableViewCell.xib b/DutchNews/Classes/Views/Cells/HeadlineSearchTableViewCell.xib new file mode 100644 index 0000000..3a906ba --- /dev/null +++ b/DutchNews/Classes/Views/Cells/HeadlineSearchTableViewCell.xib @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DutchNews/Resources/Localization/en.lproj/Localizable.strings b/DutchNews/Resources/Localization/en.lproj/Localizable.strings index 2214df8..522341c 100644 --- a/DutchNews/Resources/Localization/en.lproj/Localizable.strings +++ b/DutchNews/Resources/Localization/en.lproj/Localizable.strings @@ -5,4 +5,5 @@ "retry" = "Retry"; "headlines_title" = "Headlines"; "article_title" = "Article"; - +"search_headline_title" = "Search In Headline"; +"search_result_title" = "Result"; diff --git a/DutchNews/Resources/Localization/nl-NL.lproj/Localizable.strings b/DutchNews/Resources/Localization/nl-NL.lproj/Localizable.strings index 9f00c5d..745b9fc 100644 --- a/DutchNews/Resources/Localization/nl-NL.lproj/Localizable.strings +++ b/DutchNews/Resources/Localization/nl-NL.lproj/Localizable.strings @@ -5,3 +5,5 @@ "retry" = "Ppnieuw proberen"; "headlines_title" = "Krantenkoppen"; "article_title" = "Artikel"; +"search_headline_title" = "Zoek in Headlines"; +"search_result_title" = "Resultaat"; From d19107d76bc93340f2a9b29ec2ca40001c6cedf6 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sat, 26 Sep 2020 04:52:13 +0330 Subject: [PATCH 105/108] - Search Feature done. --- DutchNews.xcodeproj/project.pbxproj | 2 ++ .../HeadlineSearchViewController.swift | 14 +++++++++++++- .../Localization/en.lproj/Localizable.strings | 1 + .../Localization/nl-NL.lproj/Localizable.strings | 1 + 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/DutchNews.xcodeproj/project.pbxproj b/DutchNews.xcodeproj/project.pbxproj index 9fddcd4..2834089 100644 --- a/DutchNews.xcodeproj/project.pbxproj +++ b/DutchNews.xcodeproj/project.pbxproj @@ -1089,6 +1089,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -1149,6 +1150,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; diff --git a/DutchNews/Classes/ViewControllers/HeadlineSearchViewController.swift b/DutchNews/Classes/ViewControllers/HeadlineSearchViewController.swift index 3452543..fa796d3 100644 --- a/DutchNews/Classes/ViewControllers/HeadlineSearchViewController.swift +++ b/DutchNews/Classes/ViewControllers/HeadlineSearchViewController.swift @@ -31,6 +31,11 @@ class HeadlineSearchViewController: UIViewController, AlertableView { var controllerFactory: ViewControllerFactory? + var inSearching: Bool { + (searchController?.isActive ?? false) && + !(searchController?.searchBar.text.isEmpty ?? true) + } + deinit { viewModel = nil searchController = nil @@ -85,7 +90,14 @@ class HeadlineSearchViewController: UIViewController, AlertableView { loadingIndicator.startAnimating() loadingIndicator.isHidden = false - case .loaded, .idle: + case .loaded: + + if dataSource.sectionModels.flatMap({ $0.items }).count == 0, inSearching { + presentAlertView(withMessage: "no_result_found".localized) + } + + fallthrough + case .idle: loadingIndicator.stopAnimating() loadingIndicator.isHidden = true diff --git a/DutchNews/Resources/Localization/en.lproj/Localizable.strings b/DutchNews/Resources/Localization/en.lproj/Localizable.strings index 522341c..0ae5d35 100644 --- a/DutchNews/Resources/Localization/en.lproj/Localizable.strings +++ b/DutchNews/Resources/Localization/en.lproj/Localizable.strings @@ -7,3 +7,4 @@ "article_title" = "Article"; "search_headline_title" = "Search In Headline"; "search_result_title" = "Result"; +"no_result_found" = "No Result Found! "; diff --git a/DutchNews/Resources/Localization/nl-NL.lproj/Localizable.strings b/DutchNews/Resources/Localization/nl-NL.lproj/Localizable.strings index 745b9fc..d7a83d4 100644 --- a/DutchNews/Resources/Localization/nl-NL.lproj/Localizable.strings +++ b/DutchNews/Resources/Localization/nl-NL.lproj/Localizable.strings @@ -7,3 +7,4 @@ "article_title" = "Artikel"; "search_headline_title" = "Zoek in Headlines"; "search_result_title" = "Resultaat"; +"no_result_found" = "Geen resultaat gevonden!"; From 3c8eb51366cb8c316a26e33e93fbc7d28526e111 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sat, 26 Sep 2020 06:43:44 +0330 Subject: [PATCH 106/108] - Added Readme.md --- README.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..b8687bb --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# DutchNewsApp + +The Application was written in a Swift 5.1. The App represent Headlines API and Authentication. Application contains the three layers which are the `Data Layer`, `Domain Layer` or `Business Layer` and `Presentation Layer` in MVVM. + + +## Requirements + +- Xcode 11.2 or laster. +- Swift 5.1 or later. +- iOS 11.0 or later. + +## Installation + +- Step 1: + Download or clone the project github repository + + ```Bash + $ git clone https://github.com/farshadmb/DutchNewsApp.git + ``` +- Step 2: + Install Bundler via Running the below command + ```Bash + gem install bundler + ``` + Install Cocoapods via + ```Bash + gem install cocoapods --user-install + ``` + + then run ``` $ pods install ``` command. + +- Step 3: + Open ```DutchNews.xcworspace``` file. + +- Step 4: + Run and enjoy the app. + +## Technology use in this project +* Reactive Functional Programming +* RxSwift +* Clean Code +* Clean Arch [Reference](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) +* Modern MVVM + +* CI Testing +* CollectionView Custom Layout + +## - Gitwokflow + +[See PRs](https://github.com/farshadmb/DutchNewsApp/pulls?q=is%3Apr+is%3Aclosed) Closed PRs + From ad68e3a6306952784f6acf5da331f24c19f2aff6 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sat, 26 Sep 2020 06:44:58 +0330 Subject: [PATCH 107/108] - Updated Readme. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b8687bb..13ca289 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ The Application was written in a Swift 5.1. The App represent Headlines API and * Clean Code * Clean Arch [Reference](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) * Modern MVVM - +* Loclization * CI Testing * CollectionView Custom Layout From 9317f82fb2d4ef48deecc22852b09370c7f42fe9 Mon Sep 17 00:00:00 2001 From: Farshad Mousalou Date: Sat, 26 Sep 2020 07:12:13 +0330 Subject: [PATCH 108/108] -Fixed Padding Bug in iOS 12 and later. --- .../Views/Cells/HeadlineSearchTableViewCell.swift | 2 +- .../Views/Cells/HeadlineSearchTableViewCell.xib | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DutchNews/Classes/Views/Cells/HeadlineSearchTableViewCell.swift b/DutchNews/Classes/Views/Cells/HeadlineSearchTableViewCell.swift index 6af9bef..d6574e6 100644 --- a/DutchNews/Classes/Views/Cells/HeadlineSearchTableViewCell.swift +++ b/DutchNews/Classes/Views/Cells/HeadlineSearchTableViewCell.swift @@ -35,7 +35,7 @@ class HeadlineSearchTableViewCell: UITableViewCell { backgroundCard?.isInteractable = false backgroundCard?.isSelectable = true backgroundCard?.setShadowElevation(.cardResting, for: .normal) - + backgroundCard?.autoPinEdgesToSuperviewMargins() // Initialization code } diff --git a/DutchNews/Classes/Views/Cells/HeadlineSearchTableViewCell.xib b/DutchNews/Classes/Views/Cells/HeadlineSearchTableViewCell.xib index 3a906ba..e9d5052 100644 --- a/DutchNews/Classes/Views/Cells/HeadlineSearchTableViewCell.xib +++ b/DutchNews/Classes/Views/Cells/HeadlineSearchTableViewCell.xib @@ -17,7 +17,7 @@ - + @@ -85,10 +85,10 @@ - - - - + + + +