diff --git a/Podfile b/Podfile index 4e6443a..61c3a92 100644 --- a/Podfile +++ b/Podfile @@ -1,13 +1,11 @@ # Uncomment the next line to define a global platform for your project -platform :ios, '8.0' +platform :ios, '13.0' target 'mvvm-ios' do # Comment the next line if you're not using Swift and don't want to use dynamic frameworks use_frameworks! # Pods for mvvm-ios - pod 'Alamofire', '~> 4.5' - pod 'SwiftyJSON' pod 'RxSwift' pod 'RxCocoa' pod 'RealmSwift' @@ -18,8 +16,6 @@ target 'mvvm-ios-tests' do use_frameworks! # Pods for mvvm-ios - pod 'Alamofire', '~> 4.5' - pod 'SwiftyJSON' pod 'RxSwift' pod 'RxCocoa' pod 'RealmSwift' diff --git a/mvvm-ios-tests/JSONParserTests.swift b/mvvm-ios-tests/JSONParserTests.swift deleted file mode 100644 index c104dd1..0000000 --- a/mvvm-ios-tests/JSONParserTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// JSONParserTests.swift -// mvvm-ios -// -// Created by Quang Nguyen on 9/30/17. -// Copyright © 2017 Aromajoin. All rights reserved. -// - -import Foundation -import XCTest -@testable import mvvm_ios - -class JSONParserTests: XCTestCase{ - - var parser: JSONParser! - override func setUp() { - super.setUp() - parser = JSONParser() - } - - override func tearDown() { - super.tearDown() - parser = nil - } - - func testThatJSONParserShouldReturnReposListFromCorrectJSON() { - // Given - let data = TestDataUtils.getRawJSONData() - - // When - let repos = parser.decode(from: data) - - // Then - XCTAssertEqual(repos, TestDataUtils.getSimpleMockRepos()) - } -} diff --git a/mvvm-ios-tests/TestDataUtils.swift b/mvvm-ios-tests/TestDataUtils.swift deleted file mode 100644 index cf0f4f7..0000000 --- a/mvvm-ios-tests/TestDataUtils.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// TestDataUtils.swift -// mvvm-ios -// -// Created by Quang Nguyen on 9/30/17. -// Copyright © 2017 Aromajoin. All rights reserved. -// - -import Foundation -@testable import mvvm_ios - -// MARK: - A utility class to provide testing data -class TestDataUtils { - private static let SIMPLE_MOCK_JSON_STRING:String = "{\"total_count\":3,\"incomplete_results\":false,\"items\":[{\"id\":22458259,\"name\":\"Alamofire\",\"full_name\":\"Alamofire/Alamofire\"},{\"id\":21700699,\"name\":\"awesome-ios\",\"full_name\":\"vsouza/awesome-ios\"},{\"id\":3606624,\"name\":\"ReactiveCocoa\",\"full_name\":\"ReactiveCocoa/ReactiveCocoa\"}]}" - - static func getRawJSONData() -> Data { - return SIMPLE_MOCK_JSON_STRING.data(using: .utf8)! - } - - static func getSimpleMockRepos() -> [Repo]{ - var repos: [Repo] = [] - - let first = Repo() - first.id = 22458259 - first.name = "Alamofire/Alamofire" - - let second = Repo() - second.id = 21700699 - second.name = "vsouza/awesome-ios" - - let third = Repo() - third.id = 3606624 - third.name = "ReactiveCocoa/ReactiveCocoa" - - repos.append(first) - repos.append(second) - repos.append(third) - - return repos - } -} diff --git a/mvvm-ios.xcodeproj/project.pbxproj b/mvvm-ios.xcodeproj/project.pbxproj index 3302cac..1c56077 100644 --- a/mvvm-ios.xcodeproj/project.pbxproj +++ b/mvvm-ios.xcodeproj/project.pbxproj @@ -7,18 +7,21 @@ objects = { /* Begin PBXBuildFile section */ + 030F6F08291A25550025AA44 /* Repo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030F6F06291A25550025AA44 /* Repo.swift */; }; + 030F6F09291A25550025AA44 /* Repository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030F6F07291A25550025AA44 /* Repository.swift */; }; + 030F6F0B291A26680025AA44 /* Resource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030F6F0A291A26680025AA44 /* Resource.swift */; }; + 030F6F0D291A266F0025AA44 /* Method.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030F6F0C291A266F0025AA44 /* Method.swift */; }; + 030F6F10291A267C0025AA44 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030F6F0E291A267C0025AA44 /* Endpoint.swift */; }; + 030F6F11291A267C0025AA44 /* RequestProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030F6F0F291A267C0025AA44 /* RequestProtocol.swift */; }; + 030F6F13291A268A0025AA44 /* WebService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030F6F12291A268A0025AA44 /* WebService.swift */; }; 0EFA6864BA45F7A4601F765F /* Pods_mvvm_ios.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2F638219E7A6EE4AFD08CD46 /* Pods_mvvm_ios.framework */; }; CE3B9BF01F78DE3300D83FCB /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = CE3B9BEF1F78DE3300D83FCB /* LICENSE */; }; CE4A72951F7F53F5007EE558 /* DataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE4A72901F7F53F5007EE558 /* DataManager.swift */; }; CE4A72961F7F53F5007EE558 /* GithubAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE4A72911F7F53F5007EE558 /* GithubAPIClient.swift */; }; - CE4A72971F7F53F5007EE558 /* JSONParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE4A72921F7F53F5007EE558 /* JSONParser.swift */; }; CE4A72981F7F53F5007EE558 /* RealmHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE4A72931F7F53F5007EE558 /* RealmHelper.swift */; }; - CE4A72991F7F53F5007EE558 /* Repo.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE4A72941F7F53F5007EE558 /* Repo.swift */; }; CE4A729C1F7F541D007EE558 /* ReposViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE4A729A1F7F541D007EE558 /* ReposViewController.swift */; }; CE4A729D1F7F541D007EE558 /* ReposViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE4A729B1F7F541D007EE558 /* ReposViewModel.swift */; }; - CE4A729F1F7F57A3007EE558 /* JSONParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE4A729E1F7F57A3007EE558 /* JSONParserTests.swift */; }; CE4A72A21F7F5C32007EE558 /* MOCK_REPOS.json in Resources */ = {isa = PBXBuildFile; fileRef = CE4A72A11F7F5C32007EE558 /* MOCK_REPOS.json */; }; - CE4A72A41F7F5CFE007EE558 /* TestDataUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE4A72A31F7F5CFE007EE558 /* TestDataUtils.swift */; }; CE4A72A61F7F621C007EE558 /* SIMPLE_MOCK_REPO.json in Resources */ = {isa = PBXBuildFile; fileRef = CE4A72A51F7F621C007EE558 /* SIMPLE_MOCK_REPO.json */; }; CE6F5FF41F74FC6E00784E15 /* TODO in Resources */ = {isa = PBXBuildFile; fileRef = CE6F5FF31F74FC6E00784E15 /* TODO */; }; CEF9FEA41F72B09400D629AB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF9FEA31F72B09400D629AB /* AppDelegate.swift */; }; @@ -40,6 +43,13 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 030F6F06291A25550025AA44 /* Repo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Repo.swift; sourceTree = ""; }; + 030F6F07291A25550025AA44 /* Repository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Repository.swift; sourceTree = ""; }; + 030F6F0A291A26680025AA44 /* Resource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Resource.swift; sourceTree = ""; }; + 030F6F0C291A266F0025AA44 /* Method.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Method.swift; sourceTree = ""; }; + 030F6F0E291A267C0025AA44 /* Endpoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = ""; }; + 030F6F0F291A267C0025AA44 /* RequestProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestProtocol.swift; sourceTree = ""; }; + 030F6F12291A268A0025AA44 /* WebService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebService.swift; sourceTree = ""; }; 2F638219E7A6EE4AFD08CD46 /* Pods_mvvm_ios.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_mvvm_ios.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B21965FD05CBA2FBCA03DD6 /* Pods-mvvm-ios-tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-mvvm-ios-tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-mvvm-ios-tests/Pods-mvvm-ios-tests.debug.xcconfig"; sourceTree = ""; }; 77F07C73D020D8BDF3F40A6B /* Pods_mvvm_ios_tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_mvvm_ios_tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -49,14 +59,10 @@ CE4A72861F7F5163007EE558 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; CE4A72901F7F53F5007EE558 /* DataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DataManager.swift; path = data/DataManager.swift; sourceTree = ""; }; CE4A72911F7F53F5007EE558 /* GithubAPIClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GithubAPIClient.swift; path = data/GithubAPIClient.swift; sourceTree = ""; }; - CE4A72921F7F53F5007EE558 /* JSONParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = JSONParser.swift; path = data/JSONParser.swift; sourceTree = ""; }; CE4A72931F7F53F5007EE558 /* RealmHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RealmHelper.swift; path = data/RealmHelper.swift; sourceTree = ""; }; - CE4A72941F7F53F5007EE558 /* Repo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Repo.swift; path = data/Repo.swift; sourceTree = ""; }; CE4A729A1F7F541D007EE558 /* ReposViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ReposViewController.swift; path = ui/ReposViewController.swift; sourceTree = ""; }; CE4A729B1F7F541D007EE558 /* ReposViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ReposViewModel.swift; path = ui/ReposViewModel.swift; sourceTree = ""; }; - CE4A729E1F7F57A3007EE558 /* JSONParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONParserTests.swift; sourceTree = ""; }; CE4A72A11F7F5C32007EE558 /* MOCK_REPOS.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = MOCK_REPOS.json; path = mock/MOCK_REPOS.json; sourceTree = ""; }; - CE4A72A31F7F5CFE007EE558 /* TestDataUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestDataUtils.swift; sourceTree = ""; }; CE4A72A51F7F621C007EE558 /* SIMPLE_MOCK_REPO.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = SIMPLE_MOCK_REPO.json; path = mock/SIMPLE_MOCK_REPO.json; sourceTree = ""; }; CE6F5FF31F74FC6E00784E15 /* TODO */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = TODO; sourceTree = ""; }; CEF9FEA01F72B09400D629AB /* mvvm-ios.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "mvvm-ios.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -90,6 +96,35 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 030F6F03291A23260025AA44 /* Infrastructure */ = { + isa = PBXGroup; + children = ( + 030F6F04291A23390025AA44 /* Network */, + ); + name = Infrastructure; + sourceTree = ""; + }; + 030F6F04291A23390025AA44 /* Network */ = { + isa = PBXGroup; + children = ( + 030F6F0A291A26680025AA44 /* Resource.swift */, + 030F6F12291A268A0025AA44 /* WebService.swift */, + 030F6F0E291A267C0025AA44 /* Endpoint.swift */, + 030F6F0F291A267C0025AA44 /* RequestProtocol.swift */, + 030F6F0C291A266F0025AA44 /* Method.swift */, + ); + name = Network; + sourceTree = ""; + }; + 030F6F05291A253B0025AA44 /* Models */ = { + isa = PBXGroup; + children = ( + 030F6F06291A25550025AA44 /* Repo.swift */, + 030F6F07291A25550025AA44 /* Repository.swift */, + ); + name = Models; + sourceTree = ""; + }; B1A41D0AB6E470B8B83C2E4E /* Frameworks */ = { isa = PBXGroup; children = ( @@ -103,8 +138,6 @@ isa = PBXGroup; children = ( CE4A72861F7F5163007EE558 /* Info.plist */, - CE4A729E1F7F57A3007EE558 /* JSONParserTests.swift */, - CE4A72A31F7F5CFE007EE558 /* TestDataUtils.swift */, ); path = "mvvm-ios-tests"; sourceTree = ""; @@ -123,21 +156,19 @@ children = ( CE4A72901F7F53F5007EE558 /* DataManager.swift */, CE4A72911F7F53F5007EE558 /* GithubAPIClient.swift */, - CE4A72921F7F53F5007EE558 /* JSONParser.swift */, CE4A72931F7F53F5007EE558 /* RealmHelper.swift */, - CE4A72941F7F53F5007EE558 /* Repo.swift */, ); name = data; sourceTree = ""; }; - CEF3129F1F7380EB00CC7206 /* ui */ = { + CEF3129F1F7380EB00CC7206 /* Scenes */ = { isa = PBXGroup; children = ( CE4A729A1F7F541D007EE558 /* ReposViewController.swift */, CE4A729B1F7F541D007EE558 /* ReposViewModel.swift */, CEF9FEA71F72B09400D629AB /* Main.storyboard */, ); - name = ui; + name = Scenes; sourceTree = ""; }; CEF9FE971F72B09400D629AB = { @@ -167,8 +198,10 @@ CEF9FEA21F72B09400D629AB /* mvvm-ios */ = { isa = PBXGroup; children = ( + CEF3129F1F7380EB00CC7206 /* Scenes */, CEF3129C1F73805700CC7206 /* data */, - CEF3129F1F7380EB00CC7206 /* ui */, + 030F6F05291A253B0025AA44 /* Models */, + 030F6F03291A23260025AA44 /* Infrastructure */, CEF9FEA31F72B09400D629AB /* AppDelegate.swift */, CEF9FEAA1F72B09400D629AB /* Assets.xcassets */, CEF9FEAC1F72B09400D629AB /* LaunchScreen.storyboard */, @@ -200,7 +233,6 @@ CE4A727F1F7F5163007EE558 /* Frameworks */, CE4A72801F7F5163007EE558 /* Resources */, D309DF0561E6841FB64E5DB2 /* [CP] Embed Pods Frameworks */, - F9198169E067605E75E57AB8 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -221,7 +253,6 @@ CEF9FE9D1F72B09400D629AB /* Frameworks */, CEF9FE9E1F72B09400D629AB /* Resources */, 931AAEFE2A5101323B86FFE0 /* [CP] Embed Pods Frameworks */, - 4BCEF05899632F547C9D1DA4 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -244,13 +275,11 @@ TargetAttributes = { CE4A72811F7F5163007EE558 = { CreatedOnToolsVersion = 8.3.1; - DevelopmentTeam = U6G6XZT3DJ; ProvisioningStyle = Automatic; TestTargetID = CEF9FE9F1F72B09400D629AB; }; CEF9FE9F1F72B09400D629AB = { CreatedOnToolsVersion = 8.3.1; - DevelopmentTeam = 4V6HDFWSR4; ProvisioningStyle = Automatic; }; }; @@ -260,6 +289,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, Base, ); @@ -299,34 +329,30 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 4BCEF05899632F547C9D1DA4 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-mvvm-ios/Pods-mvvm-ios-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; 931AAEFE2A5101323B86FFE0 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-mvvm-ios/Pods-mvvm-ios-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/Realm/Realm.framework", + "${BUILT_PRODUCTS_DIR}/RealmSwift/RealmSwift.framework", + "${BUILT_PRODUCTS_DIR}/RxCocoa/RxCocoa.framework", + "${BUILT_PRODUCTS_DIR}/RxRelay/RxRelay.framework", + "${BUILT_PRODUCTS_DIR}/RxSwift/RxSwift.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Realm.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RealmSwift.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxCocoa.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxRelay.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxSwift.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-mvvm-ios/Pods-mvvm-ios-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-mvvm-ios/Pods-mvvm-ios-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; A0CFF5C5C479F12D6822F449 /* [CP] Check Pods Manifest.lock */ = { @@ -335,13 +361,16 @@ files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-mvvm-ios-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"; + 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; }; B3B0DA69EA9D1E80D366B13A /* [CP] Check Pods Manifest.lock */ = { @@ -350,13 +379,16 @@ files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-mvvm-ios-tests-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"; + 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; }; D309DF0561E6841FB64E5DB2 /* [CP] Embed Pods Frameworks */ = { @@ -365,28 +397,24 @@ files = ( ); inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-mvvm-ios-tests/Pods-mvvm-ios-tests-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/Realm/Realm.framework", + "${BUILT_PRODUCTS_DIR}/RealmSwift/RealmSwift.framework", + "${BUILT_PRODUCTS_DIR}/RxCocoa/RxCocoa.framework", + "${BUILT_PRODUCTS_DIR}/RxRelay/RxRelay.framework", + "${BUILT_PRODUCTS_DIR}/RxSwift/RxSwift.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Realm.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RealmSwift.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxCocoa.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxRelay.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxSwift.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-mvvm-ios-tests/Pods-mvvm-ios-tests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - F9198169E067605E75E57AB8 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-mvvm-ios-tests/Pods-mvvm-ios-tests-resources.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-mvvm-ios-tests/Pods-mvvm-ios-tests-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -396,8 +424,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - CE4A72A41F7F5CFE007EE558 /* TestDataUtils.swift in Sources */, - CE4A729F1F7F57A3007EE558 /* JSONParserTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -405,15 +431,20 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - CE4A72991F7F53F5007EE558 /* Repo.swift in Sources */, CE4A72961F7F53F5007EE558 /* GithubAPIClient.swift in Sources */, CE4A72981F7F53F5007EE558 /* RealmHelper.swift in Sources */, - CE4A72971F7F53F5007EE558 /* JSONParser.swift in Sources */, + 030F6F10291A267C0025AA44 /* Endpoint.swift in Sources */, + 030F6F0B291A26680025AA44 /* Resource.swift in Sources */, CE4A729C1F7F541D007EE558 /* ReposViewController.swift in Sources */, + 030F6F09291A25550025AA44 /* Repository.swift in Sources */, CE4A729D1F7F541D007EE558 /* ReposViewModel.swift in Sources */, + 030F6F13291A268A0025AA44 /* WebService.swift in Sources */, CEF9FEB61F72B10E00D629AB /* README.md in Sources */, + 030F6F0D291A266F0025AA44 /* Method.swift in Sources */, + 030F6F08291A25550025AA44 /* Repo.swift in Sources */, CE4A72951F7F53F5007EE558 /* DataManager.swift in Sources */, CEF9FEA41F72B09400D629AB /* AppDelegate.swift in Sources */, + 030F6F11291A267C0025AA44 /* RequestProtocol.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -452,12 +483,13 @@ baseConfigurationReference = 3B21965FD05CBA2FBCA03DD6 /* Pods-mvvm-ios-tests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; - DEVELOPMENT_TEAM = U6G6XZT3DJ; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "mvvm-ios-tests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.quangnguyen.mvvm-ios-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/mvvm-ios.app/mvvm-ios"; }; name = Debug; @@ -467,12 +499,13 @@ baseConfigurationReference = AE9C0AA7B57274FDBA07B68B /* Pods-mvvm-ios-tests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; - DEVELOPMENT_TEAM = U6G6XZT3DJ; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "mvvm-ios-tests/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.quangnguyen.mvvm-ios-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/mvvm-ios.app/mvvm-ios"; }; name = Release; @@ -524,6 +557,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = ""; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -567,6 +601,7 @@ MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = ""; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -577,13 +612,13 @@ baseConfigurationReference = D122B3539CCD73E606DD7A63 /* Pods-mvvm-ios.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = 4V6HDFWSR4; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "mvvm-ios/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 8.1; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.aromajoin.mvv-ios"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -592,13 +627,13 @@ baseConfigurationReference = F7FC9DB488917E205C4099EA /* Pods-mvvm-ios.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = 4V6HDFWSR4; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "mvvm-ios/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 8.1; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.aromajoin.mvvm-ios"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 5.0; }; name = Release; }; diff --git a/mvvm-ios/AppDelegate.swift b/mvvm-ios/AppDelegate.swift index c4b574f..90da7b1 100644 --- a/mvvm-ios/AppDelegate.swift +++ b/mvvm-ios/AppDelegate.swift @@ -13,34 +13,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. 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. - // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. - } - - 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. - } - - func applicationWillTerminate(_ application: UIApplication) { - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. - } - - } diff --git a/mvvm-ios/Endpoint.swift b/mvvm-ios/Endpoint.swift new file mode 100644 index 0000000..0bca18a --- /dev/null +++ b/mvvm-ios/Endpoint.swift @@ -0,0 +1,34 @@ +// +// Endpoint.swift +// mvvm-ios +// +// Created by Behnam on 11/7/22. +// Copyright © 2022 Behnam. All rights reserved. +// + +import Foundation + +class Constants { + static let baseUrl: String = "https://api.github.com/" +} + +enum Endpoint: RequestProtocol { + + case searchFromRepos + + var baseUrl: String { Constants.baseUrl } + var method: Method { .get } + func getBody() -> Data? { nil } + + var path: String { + return "search/repositories" + } + + var queryParameters: [URLQueryItem]? { + [.init(name: "q", value: "language:swift"), + .init(name: "sort", value: "star"), + .init(name: "order", value: "desc"), + .init(name: "per_page", value: "20")] + } + +} diff --git a/mvvm-ios/Method.swift b/mvvm-ios/Method.swift new file mode 100644 index 0000000..e91ea76 --- /dev/null +++ b/mvvm-ios/Method.swift @@ -0,0 +1,18 @@ +// +// Method.swift +// mvvm-ios +// +// Created by Behnam on 11/7/22. +// Copyright © 2022 Behnam. All rights reserved. +// + +import Foundation + +enum Method: String { + + case get = "GET" + case post = "POST" + case put = "PUT" + case delete = "DELETE" + +} diff --git a/mvvm-ios/Repo.swift b/mvvm-ios/Repo.swift new file mode 100644 index 0000000..0aed5a2 --- /dev/null +++ b/mvvm-ios/Repo.swift @@ -0,0 +1,30 @@ +// +// Repo.swift +// mvvm-ios +// +// Created by Quang Nguyen on 9/21/17. +// Copyright © 2017 Aromajoin. All rights reserved. +// + +import Foundation +import RealmSwift + +class RepositoryDBModel: Object { + @objc dynamic var id: Int32 = -1 + @objc dynamic var name: String = "" + + override func isEqual(_ object: Any?) -> Bool { + guard object is RepositoryDBModel else { + return false + } + let repo = object as! RepositoryDBModel + + return repo.id == self.id && repo.name == self.name + } + + func toRepository() -> Repository { + .init(id: self.id, name: self.name) + } +} + + diff --git a/mvvm-ios/Repository.swift b/mvvm-ios/Repository.swift new file mode 100644 index 0000000..61b528c --- /dev/null +++ b/mvvm-ios/Repository.swift @@ -0,0 +1,26 @@ +// +// Repository.swift +// mvvm-ios +// +// Created by Behnam on 11/7/22. +// Copyright © 2022 Behnam. All rights reserved. +// + +import Foundation + + +struct Repositories: Codable { + let items: [Repository] +} + +struct Repository: Codable { + let id: Int32 + let name: String + + func toRepositoryDBModel() -> RepositoryDBModel { + let model = RepositoryDBModel() + model.id = self.id + model.name = self.name + return model + } +} diff --git a/mvvm-ios/RequestProtocol.swift b/mvvm-ios/RequestProtocol.swift new file mode 100644 index 0000000..fd4cd1a --- /dev/null +++ b/mvvm-ios/RequestProtocol.swift @@ -0,0 +1,45 @@ +// +// RequestProtocol.swift +// mvvm-ios +// +// Created by Behnam on 11/7/22. +// Copyright © 2022 Behnam. All rights reserved. +// + +import Foundation + +typealias Header = [String: String] + +protocol RequestProtocol { + var baseUrl: String { get } + var path: String { get } + var method: Method { get } + var header: Header? { get } + var queryParameters: [URLQueryItem]? { get } + + func getBody() -> Data? + func getRequest() -> URLRequest +} + +extension RequestProtocol { + func getRequest() -> URLRequest { + var request = URLRequest(url: getURL()!) + + request.httpMethod = method.rawValue + request.allHTTPHeaderFields = header + request.httpBody = getBody() + + return request + } + + func getURL() -> URL? { + var urlComponent = URLComponents(string: baseUrl) + urlComponent?.path += path + urlComponent?.queryItems = queryParameters + return urlComponent?.url + } + + var header: Header? { + return nil + } +} diff --git a/mvvm-ios/Resource.swift b/mvvm-ios/Resource.swift new file mode 100644 index 0000000..be66012 --- /dev/null +++ b/mvvm-ios/Resource.swift @@ -0,0 +1,13 @@ +// +// Resource.swift +// mvvm-ios +// +// Created by Behnam on 11/7/22. +// Copyright © 2022 Aromajoin. All rights reserved. +// + +import Foundation + +struct Resource { + let endpoint: Endpoint +} diff --git a/mvvm-ios/WebService.swift b/mvvm-ios/WebService.swift new file mode 100644 index 0000000..5484675 --- /dev/null +++ b/mvvm-ios/WebService.swift @@ -0,0 +1,24 @@ +// +// WebService.swift +// mvvm-ios +// +// Created by Behnam on 11/7/22. +// Copyright © 2022 Behnam. All rights reserved. +// + +import Foundation +import RxSwift + +class WebService { + + func request(resource: Resource) -> Observable { + Observable.just(resource.endpoint.getURL()!) + .flatMap { url -> Observable<(response: HTTPURLResponse, data: Data)> in + let request = resource.endpoint.getRequest() + return URLSession.shared.rx.response(request: request) + }.map({ (response: HTTPURLResponse, data: Data) -> T? in + return try? JSONDecoder().decode(T.self, from: data) + }).asObservable() + } + +} diff --git a/mvvm-ios/data/DataManager.swift b/mvvm-ios/data/DataManager.swift index 0bce460..c920923 100644 --- a/mvvm-ios/data/DataManager.swift +++ b/mvvm-ios/data/DataManager.swift @@ -7,45 +7,39 @@ // import Foundation - +import RxSwift /// MARK: - A central place to manipulate data from both remote and local sources. class DataManager { - private let apiClient = GithubAPIClient.shared - private let realmHelper = RealmHelper.shared - - private init() {} + private let apiClient: GithubAPIClient + private let realmHelper: RealmHelper - public static let shared = DataManager() + init(apiClient: GithubAPIClient, realmHelper: RealmHelper) { + self.apiClient = apiClient + self.realmHelper = realmHelper + } - public func loadRepos(online: Bool, completion callback: @escaping ((_ repos: [Repo]) -> Void)) { + public func loadRepos(online: Bool) -> Observable<[Repository]?> { if online { - // Force to load remote data source. - apiClient.fetchTrendingRepos(completion: { - repos, error in - guard error == nil else { - print("Get error when fetching repos: \(String(describing: error))") - return - } - - // Clear old data and save new data into realm database. - self.realmHelper.clearAllRepos() - for repo in repos { - self.realmHelper.addNewRepo(repo: repo) - } - - callback(repos) - }) + return apiClient.fetchTrendingRepos() + .map({ $0?.items }) + .asObservable() } else { // Load local data - let repos = realmHelper.loadRepos() if repos.count == 0 { - // No local data available. Need to load from remote source. - loadRepos(online: true, completion: callback) - } else { - callback(realmHelper.loadRepos()) + return Observable.just([]).asObservable() + } + return Observable + .just(repos.map({ $0.toRepository() })) + .asObservable() } + } + + public func saveRepos(repositories: [Repository]) { + repositories.forEach { repo in + realmHelper.addNewRepo(repo: repo.toRepositoryDBModel()) + } + } - } } diff --git a/mvvm-ios/data/GithubAPIClient.swift b/mvvm-ios/data/GithubAPIClient.swift index 6749716..c8f4c5f 100644 --- a/mvvm-ios/data/GithubAPIClient.swift +++ b/mvvm-ios/data/GithubAPIClient.swift @@ -7,49 +7,21 @@ // import Foundation -import Alamofire +import RxSwift class GithubAPIClient { - let API_HOST = "https://api.github.com/" - let SEARCH_REPO_ENDPOINT = "search/repositories" - let parser = JSONParser() - - private init(){} - - public static let shared = GithubAPIClient() - - /// Fetches all trending repositories written in Swift - public func fetchTrendingRepos(completion callback: @escaping ((_ repos: [Repo], _ error: String?) -> Void)) { - var error: String? - var repos: [Repo] = [] + public static let shared = GithubAPIClient(api: WebService()) + private let disposeBag = DisposeBag() + private let api: WebService - let urlStr = API_HOST + SEARCH_REPO_ENDPOINT - - let parameters: Parameters = [ - "q":"language:swift", - "sort":"star", - "order":"desc" - ] + init(api: WebService){ + self.api = api + } - Alamofire.request(urlStr, method: .get, parameters: parameters, encoding: URLEncoding.default, headers: nil) - .responseJSON(completionHandler: { - response in - // Guarantee that we get response with the HTTP status code 200. - guard response.response?.statusCode == 200 else { - error = "Failed to fetch data: \(String(describing: response.response?.statusCode))" - callback(repos, error) - return - } - - error = response.error?.localizedDescription - - if let data = response.data { - repos = self.parser.decode(from: data) - } - - callback(repos, error) - - }) + /// Fetches all trending repositories written in Swift + public func fetchTrendingRepos() -> Observable { + let resource = Resource(endpoint: .searchFromRepos) + return api.request(resource: resource).asObservable() } } diff --git a/mvvm-ios/data/JSONParser.swift b/mvvm-ios/data/JSONParser.swift deleted file mode 100644 index 2e75dda..0000000 --- a/mvvm-ios/data/JSONParser.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// JSONParser.swift -// mvvm-ios -// -// Created by Quang Nguyen on 9/30/17. -// Copyright © 2017 Aromajoin. All rights reserved. -// - -import Foundation -import SwiftyJSON - -// MARK: - A utility class which converts JSON to Swift object (Repo). -class JSONParser { - - /// Decodes JSON file into the list of repositories - /// - /// - Parameter rawValue: raw data in JSON format - /// - Returns: the list of repositories - func decode(from rawValue: Data) -> [Repo] { - let json = JSON(rawValue) - var repos: [Repo] = [] - - guard let items = json["items"].array else { - print("Invalid JSON array") - return repos - } - - for item in items { - guard let id = item["id"].int32 else{ - print("Failed to parse JSON") - return repos - } - guard let name = item["full_name"].string else { - print("Failed to parse JSON") - return repos - } - - let repo = Repo() - repo.id = id - repo.name = name - repos.append(repo) - } - return repos - } -} diff --git a/mvvm-ios/data/RealmHelper.swift b/mvvm-ios/data/RealmHelper.swift index dd51316..d5505c1 100644 --- a/mvvm-ios/data/RealmHelper.swift +++ b/mvvm-ios/data/RealmHelper.swift @@ -9,18 +9,17 @@ import Foundation import RealmSwift -class RealmHelper{ +class RealmHelper { + let uiRealm = try! Realm() - - private init() {} - public static let shared = RealmHelper() + private init() {} - public func loadRepos() -> [Repo]{ - return Array(uiRealm.objects(Repo.self)) + public func loadRepos() -> [RepositoryDBModel]{ + return Array(uiRealm.objects(RepositoryDBModel.self)) } - public func addNewRepo(repo: Repo) { + public func addNewRepo(repo: RepositoryDBModel) { try! uiRealm.write { uiRealm.add(repo) } @@ -28,7 +27,7 @@ class RealmHelper{ public func clearAllRepos() { try! uiRealm.write { - uiRealm.delete(uiRealm.objects(Repo.self)) + uiRealm.delete(uiRealm.objects(RepositoryDBModel.self)) } } } diff --git a/mvvm-ios/data/Repo.swift b/mvvm-ios/data/Repo.swift deleted file mode 100644 index ae83df8..0000000 --- a/mvvm-ios/data/Repo.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// Repo.swift -// mvvm-ios -// -// Created by Quang Nguyen on 9/21/17. -// Copyright © 2017 Aromajoin. All rights reserved. -// - -import Foundation -import RealmSwift -class Repo: Object{ - dynamic var id: Int32 = -1 - dynamic var name: String = "" - - override func isEqual(_ object: Any?) -> Bool { - guard object is Repo else { - return false - } - let repo = object as! Repo - - return repo.id == self.id && repo.name == self.name - } -} diff --git a/mvvm-ios/ui/ReposViewController.swift b/mvvm-ios/ui/ReposViewController.swift index 5ecc802..0dcc221 100644 --- a/mvvm-ios/ui/ReposViewController.swift +++ b/mvvm-ios/ui/ReposViewController.swift @@ -16,11 +16,14 @@ class ReposViewController: UIViewController { @IBOutlet weak var tableView: UITableView! @IBOutlet weak var searchBar: UISearchBar! - let viewModel = ReposViewModel() - var repos: [Repo] = [] - var disposeBag = DisposeBag() + private var viewModel: ReposViewModel! + private var repos: [RepositoryDBModel] = [] + private var disposeBag = DisposeBag() + override func viewDidLoad() { super.viewDidLoad() + viewModel = ReposViewModel(dataManager: DataManager(apiClient: .init(api: WebService()), + realmHelper: .shared)) setupDataBinding() } @@ -44,7 +47,7 @@ class ReposViewController: UIViewController { .asObservable() .map{"Data load time: \($0)"} .bind(to: requestCountLabel.rx.text) - .addDisposableTo(disposeBag) + .disposed(by: disposeBag) } private func bindRefreshButton() { @@ -63,7 +66,7 @@ class ReposViewController: UIViewController { private func bindTableView() { viewModel.repos.asObservable() .bind(to: tableView.rx.items(cellIdentifier: "repoViewCell"))(setupCell) - .addDisposableTo(disposeBag) + .disposed(by: disposeBag) } private func bindSearchBar() { @@ -75,7 +78,7 @@ class ReposViewController: UIViewController { }).disposed(by: disposeBag) } - private func setupCell(row: Int, element: Repo, cell: UITableViewCell){ + private func setupCell(row: Int, element: Repository, cell: UITableViewCell){ cell.textLabel?.text = element.name } } diff --git a/mvvm-ios/ui/ReposViewModel.swift b/mvvm-ios/ui/ReposViewModel.swift index 29f06ad..a5fe52f 100644 --- a/mvvm-ios/ui/ReposViewModel.swift +++ b/mvvm-ios/ui/ReposViewModel.swift @@ -8,32 +8,40 @@ import Foundation import RxSwift +import RxCocoa class ReposViewModel { - let dataManager = DataManager.shared - var requestCount = Variable(0) - var repos = Variable<[Repo]>([]) - var cachedRepos: [Repo] = [] - init() { + let dataManager: DataManager + var requestCount = BehaviorRelay(value: 0) + var repos = BehaviorRelay<[Repository]>(value: []) + var cachedRepos: [Repository] = [] + + private let disposeBag = DisposeBag() + + init(dataManager: DataManager) { // Load local data + self.dataManager = dataManager loadTrendingRepos(online: false) } func loadTrendingRepos(online: Bool) { - requestCount.value += 1 - - self.dataManager.loadRepos(online: online, completion: { - result in - self.repos.value = result - self.cachedRepos = result - }) + requestCount.accept(requestCount.value + 1) + dataManager.loadRepos(online: online) + .observe(on: MainScheduler.instance) + .subscribe(onNext: { repositories in + guard let repositories = repositories else { return } + self.dataManager.saveRepos(repositories: repositories) + self.repos.accept(repositories) + self.cachedRepos = repositories + }).disposed(by: disposeBag) } func filter(text: String) { if (text.count == 0) { - repos.value = cachedRepos + repos.accept(cachedRepos) } else { - repos.value = cachedRepos.filter{$0.name.lowercased().contains(text.lowercased())} + let filteredRepos = cachedRepos.filter{ $0.name.lowercased().contains(text.lowercased()) } + repos.accept(filteredRepos) } } }