From 14459235529bc717589ee22187b3a2c7fc420b4a Mon Sep 17 00:00:00 2001 From: Behnam Date: Tue, 8 Nov 2022 08:47:59 +0330 Subject: [PATCH 1/7] Modify some settings for successful builds --- Podfile | 6 +- mvvm-ios.xcodeproj/project.pbxproj | 96 ++++++++++++++++-------------- mvvm-ios/AppDelegate.swift | 26 +------- 3 files changed, 55 insertions(+), 73 deletions(-) diff --git a/Podfile b/Podfile index 4e6443a..a6280fa 100644 --- a/Podfile +++ b/Podfile @@ -1,12 +1,12 @@ # 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 'Alamofire' pod 'SwiftyJSON' pod 'RxSwift' pod 'RxCocoa' @@ -18,7 +18,7 @@ target 'mvvm-ios-tests' do use_frameworks! # Pods for mvvm-ios - pod 'Alamofire', '~> 4.5' + pod 'Alamofire' pod 'SwiftyJSON' pod 'RxSwift' pod 'RxCocoa' diff --git a/mvvm-ios.xcodeproj/project.pbxproj b/mvvm-ios.xcodeproj/project.pbxproj index 3302cac..b7df45e 100644 --- a/mvvm-ios.xcodeproj/project.pbxproj +++ b/mvvm-ios.xcodeproj/project.pbxproj @@ -200,7 +200,6 @@ CE4A727F1F7F5163007EE558 /* Frameworks */, CE4A72801F7F5163007EE558 /* Resources */, D309DF0561E6841FB64E5DB2 /* [CP] Embed Pods Frameworks */, - F9198169E067605E75E57AB8 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -221,7 +220,6 @@ CEF9FE9D1F72B09400D629AB /* Frameworks */, CEF9FE9E1F72B09400D629AB /* Resources */, 931AAEFE2A5101323B86FFE0 /* [CP] Embed Pods Frameworks */, - 4BCEF05899632F547C9D1DA4 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -250,7 +248,6 @@ }; CEF9FE9F1F72B09400D629AB = { CreatedOnToolsVersion = 8.3.1; - DevelopmentTeam = 4V6HDFWSR4; ProvisioningStyle = Automatic; }; }; @@ -260,6 +257,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, Base, ); @@ -299,34 +297,34 @@ /* 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}/Alamofire/Alamofire.framework", + "${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", + "${BUILT_PRODUCTS_DIR}/SwiftyJSON/SwiftyJSON.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework", + "${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", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyJSON.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 +333,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 +351,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 +369,28 @@ files = ( ); inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-mvvm-ios-tests/Pods-mvvm-ios-tests-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework", + "${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", + "${BUILT_PRODUCTS_DIR}/SwiftyJSON/SwiftyJSON.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework", + "${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", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyJSON.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 */ @@ -457,7 +461,7 @@ 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; @@ -472,7 +476,7 @@ 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 +528,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = ""; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -567,6 +572,7 @@ MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = ""; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -577,13 +583,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 +598,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:. - } - - } From 7d19fb60a023c47a3b354e19fc877b80e0912266 Mon Sep 17 00:00:00 2001 From: Behnam Date: Tue, 8 Nov 2022 09:23:10 +0330 Subject: [PATCH 2/7] fix deprecated problems - replace behaviorRelay from RXCocoa instead of Variable - change disposable function - add some models --- mvvm-ios.xcodeproj/project.pbxproj | 36 ++++++++++++++++++++++++--- mvvm-ios/Repo.swift | 30 ++++++++++++++++++++++ mvvm-ios/Repository.swift | 19 ++++++++++++++ mvvm-ios/data/Repo.swift | 23 ----------------- mvvm-ios/ui/ReposViewController.swift | 4 +-- mvvm-ios/ui/ReposViewModel.swift | 14 ++++++----- 6 files changed, 91 insertions(+), 35 deletions(-) create mode 100644 mvvm-ios/Repo.swift create mode 100644 mvvm-ios/Repository.swift delete mode 100644 mvvm-ios/data/Repo.swift diff --git a/mvvm-ios.xcodeproj/project.pbxproj b/mvvm-ios.xcodeproj/project.pbxproj index b7df45e..d2890b3 100644 --- a/mvvm-ios.xcodeproj/project.pbxproj +++ b/mvvm-ios.xcodeproj/project.pbxproj @@ -7,13 +7,14 @@ 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 */; }; 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 */; }; @@ -40,6 +41,8 @@ /* 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 = ""; }; 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; }; @@ -51,7 +54,6 @@ 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 = ""; }; @@ -90,6 +92,30 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 030F6F03291A23260025AA44 /* Infrastructure */ = { + isa = PBXGroup; + children = ( + 030F6F04291A23390025AA44 /* Network */, + ); + name = Infrastructure; + sourceTree = ""; + }; + 030F6F04291A23390025AA44 /* Network */ = { + isa = PBXGroup; + children = ( + ); + name = Network; + sourceTree = ""; + }; + 030F6F05291A253B0025AA44 /* Models */ = { + isa = PBXGroup; + children = ( + 030F6F06291A25550025AA44 /* Repo.swift */, + 030F6F07291A25550025AA44 /* Repository.swift */, + ); + name = Models; + sourceTree = ""; + }; B1A41D0AB6E470B8B83C2E4E /* Frameworks */ = { isa = PBXGroup; children = ( @@ -125,7 +151,6 @@ CE4A72911F7F53F5007EE558 /* GithubAPIClient.swift */, CE4A72921F7F53F5007EE558 /* JSONParser.swift */, CE4A72931F7F53F5007EE558 /* RealmHelper.swift */, - CE4A72941F7F53F5007EE558 /* Repo.swift */, ); name = data; sourceTree = ""; @@ -167,6 +192,8 @@ CEF9FEA21F72B09400D629AB /* mvvm-ios */ = { isa = PBXGroup; children = ( + 030F6F05291A253B0025AA44 /* Models */, + 030F6F03291A23260025AA44 /* Infrastructure */, CEF3129C1F73805700CC7206 /* data */, CEF3129F1F7380EB00CC7206 /* ui */, CEF9FEA31F72B09400D629AB /* AppDelegate.swift */, @@ -409,13 +436,14 @@ 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 */, CE4A729C1F7F541D007EE558 /* ReposViewController.swift in Sources */, + 030F6F09291A25550025AA44 /* Repository.swift in Sources */, CE4A729D1F7F541D007EE558 /* ReposViewModel.swift in Sources */, CEF9FEB61F72B10E00D629AB /* README.md in Sources */, + 030F6F08291A25550025AA44 /* Repo.swift in Sources */, CE4A72951F7F53F5007EE558 /* DataManager.swift in Sources */, CEF9FEA41F72B09400D629AB /* AppDelegate.swift in Sources */, ); diff --git a/mvvm-ios/Repo.swift b/mvvm-ios/Repo.swift new file mode 100644 index 0000000..535efb7 --- /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 Repo: Object { + @objc dynamic var id: Int32 = -1 + @objc 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 + } + + 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..d6b23d8 --- /dev/null +++ b/mvvm-ios/Repository.swift @@ -0,0 +1,19 @@ +// +// 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 +} 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..461a891 100644 --- a/mvvm-ios/ui/ReposViewController.swift +++ b/mvvm-ios/ui/ReposViewController.swift @@ -44,7 +44,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 +63,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() { diff --git a/mvvm-ios/ui/ReposViewModel.swift b/mvvm-ios/ui/ReposViewModel.swift index 29f06ad..352f2ad 100644 --- a/mvvm-ios/ui/ReposViewModel.swift +++ b/mvvm-ios/ui/ReposViewModel.swift @@ -8,11 +8,12 @@ import Foundation import RxSwift +import RxCocoa class ReposViewModel { let dataManager = DataManager.shared - var requestCount = Variable(0) - var repos = Variable<[Repo]>([]) + var requestCount = BehaviorRelay(value: 0) + var repos = BehaviorRelay<[Repo]>(value: []) var cachedRepos: [Repo] = [] init() { // Load local data @@ -20,20 +21,21 @@ class ReposViewModel { } func loadTrendingRepos(online: Bool) { - requestCount.value += 1 + requestCount.accept(requestCount.value + 1) self.dataManager.loadRepos(online: online, completion: { result in - self.repos.value = result + self.repos.accept(result) self.cachedRepos = result }) } 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) } } } From 0b208876a47486fd90eb8cfdb470d6d01e1e1c91 Mon Sep 17 00:00:00 2001 From: Behnam Date: Tue, 8 Nov 2022 09:32:07 +0330 Subject: [PATCH 3/7] Implement Network Layer --- mvvm-ios.xcodeproj/project.pbxproj | 20 +++++++++++++ mvvm-ios/Endpoint.swift | 34 ++++++++++++++++++++++ mvvm-ios/Method.swift | 18 ++++++++++++ mvvm-ios/RequestProtocol.swift | 45 ++++++++++++++++++++++++++++++ mvvm-ios/Resource.swift | 13 +++++++++ mvvm-ios/WebService.swift | 24 ++++++++++++++++ 6 files changed, 154 insertions(+) create mode 100644 mvvm-ios/Endpoint.swift create mode 100644 mvvm-ios/Method.swift create mode 100644 mvvm-ios/RequestProtocol.swift create mode 100644 mvvm-ios/Resource.swift create mode 100644 mvvm-ios/WebService.swift diff --git a/mvvm-ios.xcodeproj/project.pbxproj b/mvvm-ios.xcodeproj/project.pbxproj index d2890b3..3f15916 100644 --- a/mvvm-ios.xcodeproj/project.pbxproj +++ b/mvvm-ios.xcodeproj/project.pbxproj @@ -9,6 +9,11 @@ /* 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 */; }; @@ -43,6 +48,11 @@ /* 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; }; @@ -103,6 +113,11 @@ 030F6F04291A23390025AA44 /* Network */ = { isa = PBXGroup; children = ( + 030F6F0A291A26680025AA44 /* Resource.swift */, + 030F6F12291A268A0025AA44 /* WebService.swift */, + 030F6F0E291A267C0025AA44 /* Endpoint.swift */, + 030F6F0F291A267C0025AA44 /* RequestProtocol.swift */, + 030F6F0C291A266F0025AA44 /* Method.swift */, ); name = Network; sourceTree = ""; @@ -439,13 +454,18 @@ 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; }; 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/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() + } + +} From f982a09818a8c7028c0006465acf41288227f84d Mon Sep 17 00:00:00 2001 From: Behnam Date: Tue, 8 Nov 2022 09:53:28 +0330 Subject: [PATCH 4/7] fix and improvement and optimize - delete JsonParser - convert callback of DataManager to reactive - implement Api Client - do some change for ViewModel and View layer --- mvvm-ios.xcodeproj/project.pbxproj | 4 --- mvvm-ios/data/DataManager.swift | 34 ++++++------------ mvvm-ios/data/GithubAPIClient.swift | 50 ++++++--------------------- mvvm-ios/data/JSONParser.swift | 45 ------------------------ mvvm-ios/ui/ReposViewController.swift | 2 +- mvvm-ios/ui/ReposViewModel.swift | 21 ++++++----- 6 files changed, 34 insertions(+), 122 deletions(-) delete mode 100644 mvvm-ios/data/JSONParser.swift diff --git a/mvvm-ios.xcodeproj/project.pbxproj b/mvvm-ios.xcodeproj/project.pbxproj index 3f15916..b6835d5 100644 --- a/mvvm-ios.xcodeproj/project.pbxproj +++ b/mvvm-ios.xcodeproj/project.pbxproj @@ -18,7 +18,6 @@ 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 */; }; CE4A729C1F7F541D007EE558 /* ReposViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE4A729A1F7F541D007EE558 /* ReposViewController.swift */; }; CE4A729D1F7F541D007EE558 /* ReposViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE4A729B1F7F541D007EE558 /* ReposViewModel.swift */; }; @@ -62,7 +61,6 @@ 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 = ""; }; 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 = ""; }; @@ -164,7 +162,6 @@ children = ( CE4A72901F7F53F5007EE558 /* DataManager.swift */, CE4A72911F7F53F5007EE558 /* GithubAPIClient.swift */, - CE4A72921F7F53F5007EE558 /* JSONParser.swift */, CE4A72931F7F53F5007EE558 /* RealmHelper.swift */, ); name = data; @@ -453,7 +450,6 @@ files = ( 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 */, diff --git a/mvvm-ios/data/DataManager.swift b/mvvm-ios/data/DataManager.swift index 0bce460..a19c781 100644 --- a/mvvm-ios/data/DataManager.swift +++ b/mvvm-ios/data/DataManager.swift @@ -7,7 +7,7 @@ // import Foundation - +import RxSwift /// MARK: - A central place to manipulate data from both remote and local sources. class DataManager { private let apiClient = GithubAPIClient.shared @@ -17,35 +17,21 @@ class DataManager { public static let shared = DataManager() - 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() + } } - } } 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/ui/ReposViewController.swift b/mvvm-ios/ui/ReposViewController.swift index 461a891..b731c48 100644 --- a/mvvm-ios/ui/ReposViewController.swift +++ b/mvvm-ios/ui/ReposViewController.swift @@ -75,7 +75,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 352f2ad..d0d37af 100644 --- a/mvvm-ios/ui/ReposViewModel.swift +++ b/mvvm-ios/ui/ReposViewModel.swift @@ -13,8 +13,11 @@ import RxCocoa class ReposViewModel { let dataManager = DataManager.shared var requestCount = BehaviorRelay(value: 0) - var repos = BehaviorRelay<[Repo]>(value: []) - var cachedRepos: [Repo] = [] + var repos = BehaviorRelay<[Repository]>(value: []) + var cachedRepos: [Repository] = [] + + private let disposeBag = DisposeBag() + init() { // Load local data loadTrendingRepos(online: false) @@ -22,19 +25,19 @@ class ReposViewModel { func loadTrendingRepos(online: Bool) { requestCount.accept(requestCount.value + 1) - - self.dataManager.loadRepos(online: online, completion: { - result in - self.repos.accept(result) - self.cachedRepos = result - }) + dataManager.loadRepos(online: online) + .subscribe(onNext: { repositories in + guard let repositories = repositories else { return } + self.repos.accept(repositories) + self.cachedRepos = repositories + }).disposed(by: disposeBag) } func filter(text: String) { if (text.count == 0) { repos.accept(cachedRepos) } else { - let filteredRepos = cachedRepos.filter{$0.name.lowercased().contains(text.lowercased())} + let filteredRepos = cachedRepos.filter{ $0.name.lowercased().contains(text.lowercased()) } repos.accept(filteredRepos) } } From f3d7aedf41b6baf47eb8823c02a9add185091ccc Mon Sep 17 00:00:00 2001 From: Behnam Date: Tue, 8 Nov 2022 10:04:26 +0330 Subject: [PATCH 5/7] Remove singleton class and inject api service to viewModel --- mvvm-ios/data/DataManager.swift | 11 ++++++----- mvvm-ios/data/RealmHelper.swift | 7 +++---- mvvm-ios/ui/ReposViewController.swift | 9 ++++++--- mvvm-ios/ui/ReposViewModel.swift | 5 +++-- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/mvvm-ios/data/DataManager.swift b/mvvm-ios/data/DataManager.swift index a19c781..5f038c6 100644 --- a/mvvm-ios/data/DataManager.swift +++ b/mvvm-ios/data/DataManager.swift @@ -10,12 +10,13 @@ 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 let apiClient: GithubAPIClient + private let realmHelper: RealmHelper - private init() {} - - public static let shared = DataManager() + init(apiClient: GithubAPIClient, realmHelper: RealmHelper) { + self.apiClient = apiClient + self.realmHelper = realmHelper + } public func loadRepos(online: Bool) -> Observable<[Repository]?> { if online { diff --git a/mvvm-ios/data/RealmHelper.swift b/mvvm-ios/data/RealmHelper.swift index dd51316..02f01fd 100644 --- a/mvvm-ios/data/RealmHelper.swift +++ b/mvvm-ios/data/RealmHelper.swift @@ -9,12 +9,11 @@ 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)) diff --git a/mvvm-ios/ui/ReposViewController.swift b/mvvm-ios/ui/ReposViewController.swift index b731c48..e955053 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: [Repo] = [] + private var disposeBag = DisposeBag() + override func viewDidLoad() { super.viewDidLoad() + viewModel = ReposViewModel(dataManager: DataManager(apiClient: .init(api: WebService()), + realmHelper: .shared)) setupDataBinding() } diff --git a/mvvm-ios/ui/ReposViewModel.swift b/mvvm-ios/ui/ReposViewModel.swift index d0d37af..b1bab93 100644 --- a/mvvm-ios/ui/ReposViewModel.swift +++ b/mvvm-ios/ui/ReposViewModel.swift @@ -11,15 +11,16 @@ import RxSwift import RxCocoa class ReposViewModel { - let dataManager = DataManager.shared + let dataManager: DataManager var requestCount = BehaviorRelay(value: 0) var repos = BehaviorRelay<[Repository]>(value: []) var cachedRepos: [Repository] = [] private let disposeBag = DisposeBag() - init() { + init(dataManager: DataManager) { // Load local data + self.dataManager = dataManager loadTrendingRepos(online: false) } From 49319733b509227c906ea2b4269fc87a1931c3fa Mon Sep 17 00:00:00 2001 From: Behnam Date: Tue, 8 Nov 2022 10:11:48 +0330 Subject: [PATCH 6/7] remove tests that doesn't need anymore - Remove Alamofire and SwiftyJson which are no longer needed --- Podfile | 4 --- mvvm-ios-tests/JSONParserTests.swift | 36 ------------------------ mvvm-ios-tests/TestDataUtils.swift | 41 ---------------------------- mvvm-ios.xcodeproj/project.pbxproj | 23 +++------------- 4 files changed, 4 insertions(+), 100 deletions(-) delete mode 100644 mvvm-ios-tests/JSONParserTests.swift delete mode 100644 mvvm-ios-tests/TestDataUtils.swift diff --git a/Podfile b/Podfile index a6280fa..61c3a92 100644 --- a/Podfile +++ b/Podfile @@ -6,8 +6,6 @@ target 'mvvm-ios' do use_frameworks! # Pods for mvvm-ios - pod 'Alamofire' - 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' - 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 b6835d5..61a48f7 100644 --- a/mvvm-ios.xcodeproj/project.pbxproj +++ b/mvvm-ios.xcodeproj/project.pbxproj @@ -21,9 +21,7 @@ CE4A72981F7F53F5007EE558 /* RealmHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE4A72931F7F53F5007EE558 /* RealmHelper.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 */; }; @@ -64,9 +62,7 @@ CE4A72931F7F53F5007EE558 /* RealmHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RealmHelper.swift; path = data/RealmHelper.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; }; @@ -142,8 +138,6 @@ isa = PBXGroup; children = ( CE4A72861F7F5163007EE558 /* Info.plist */, - CE4A729E1F7F57A3007EE558 /* JSONParserTests.swift */, - CE4A72A31F7F5CFE007EE558 /* TestDataUtils.swift */, ); path = "mvvm-ios-tests"; sourceTree = ""; @@ -281,7 +275,6 @@ TargetAttributes = { CE4A72811F7F5163007EE558 = { CreatedOnToolsVersion = 8.3.1; - DevelopmentTeam = U6G6XZT3DJ; ProvisioningStyle = Automatic; TestTargetID = CEF9FE9F1F72B09400D629AB; }; @@ -343,23 +336,19 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-mvvm-ios/Pods-mvvm-ios-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework", "${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", - "${BUILT_PRODUCTS_DIR}/SwiftyJSON/SwiftyJSON.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework", "${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", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyJSON.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -409,23 +398,19 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-mvvm-ios-tests/Pods-mvvm-ios-tests-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework", "${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", - "${BUILT_PRODUCTS_DIR}/SwiftyJSON/SwiftyJSON.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework", "${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", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyJSON.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -439,8 +424,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - CE4A72A41F7F5CFE007EE558 /* TestDataUtils.swift in Sources */, - CE4A729F1F7F57A3007EE558 /* JSONParserTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -500,8 +483,9 @@ 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)"; @@ -515,8 +499,9 @@ 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)"; From cb1a32117beca72542b079f437be6f71b1bda4b6 Mon Sep 17 00:00:00 2001 From: Behnam Date: Tue, 8 Nov 2022 11:48:34 +0330 Subject: [PATCH 7/7] import repositories to local database for offline state --- mvvm-ios.xcodeproj/project.pbxproj | 8 ++++---- mvvm-ios/Repo.swift | 6 +++--- mvvm-ios/Repository.swift | 7 +++++++ mvvm-ios/data/DataManager.swift | 7 +++++++ mvvm-ios/data/RealmHelper.swift | 8 ++++---- mvvm-ios/ui/ReposViewController.swift | 2 +- mvvm-ios/ui/ReposViewModel.swift | 2 ++ 7 files changed, 28 insertions(+), 12 deletions(-) diff --git a/mvvm-ios.xcodeproj/project.pbxproj b/mvvm-ios.xcodeproj/project.pbxproj index 61a48f7..1c56077 100644 --- a/mvvm-ios.xcodeproj/project.pbxproj +++ b/mvvm-ios.xcodeproj/project.pbxproj @@ -161,14 +161,14 @@ 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 = { @@ -198,10 +198,10 @@ CEF9FEA21F72B09400D629AB /* mvvm-ios */ = { isa = PBXGroup; children = ( + CEF3129F1F7380EB00CC7206 /* Scenes */, + CEF3129C1F73805700CC7206 /* data */, 030F6F05291A253B0025AA44 /* Models */, 030F6F03291A23260025AA44 /* Infrastructure */, - CEF3129C1F73805700CC7206 /* data */, - CEF3129F1F7380EB00CC7206 /* ui */, CEF9FEA31F72B09400D629AB /* AppDelegate.swift */, CEF9FEAA1F72B09400D629AB /* Assets.xcassets */, CEF9FEAC1F72B09400D629AB /* LaunchScreen.storyboard */, diff --git a/mvvm-ios/Repo.swift b/mvvm-ios/Repo.swift index 535efb7..0aed5a2 100644 --- a/mvvm-ios/Repo.swift +++ b/mvvm-ios/Repo.swift @@ -9,15 +9,15 @@ import Foundation import RealmSwift -class Repo: Object { +class RepositoryDBModel: Object { @objc dynamic var id: Int32 = -1 @objc dynamic var name: String = "" override func isEqual(_ object: Any?) -> Bool { - guard object is Repo else { + guard object is RepositoryDBModel else { return false } - let repo = object as! Repo + let repo = object as! RepositoryDBModel return repo.id == self.id && repo.name == self.name } diff --git a/mvvm-ios/Repository.swift b/mvvm-ios/Repository.swift index d6b23d8..61b528c 100644 --- a/mvvm-ios/Repository.swift +++ b/mvvm-ios/Repository.swift @@ -16,4 +16,11 @@ struct Repositories: Codable { 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/data/DataManager.swift b/mvvm-ios/data/DataManager.swift index 5f038c6..c920923 100644 --- a/mvvm-ios/data/DataManager.swift +++ b/mvvm-ios/data/DataManager.swift @@ -33,6 +33,13 @@ class DataManager { 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/RealmHelper.swift b/mvvm-ios/data/RealmHelper.swift index 02f01fd..d5505c1 100644 --- a/mvvm-ios/data/RealmHelper.swift +++ b/mvvm-ios/data/RealmHelper.swift @@ -15,11 +15,11 @@ class RealmHelper { 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) } @@ -27,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/ui/ReposViewController.swift b/mvvm-ios/ui/ReposViewController.swift index e955053..0dcc221 100644 --- a/mvvm-ios/ui/ReposViewController.swift +++ b/mvvm-ios/ui/ReposViewController.swift @@ -17,7 +17,7 @@ class ReposViewController: UIViewController { @IBOutlet weak var searchBar: UISearchBar! private var viewModel: ReposViewModel! - private var repos: [Repo] = [] + private var repos: [RepositoryDBModel] = [] private var disposeBag = DisposeBag() override func viewDidLoad() { diff --git a/mvvm-ios/ui/ReposViewModel.swift b/mvvm-ios/ui/ReposViewModel.swift index b1bab93..a5fe52f 100644 --- a/mvvm-ios/ui/ReposViewModel.swift +++ b/mvvm-ios/ui/ReposViewModel.swift @@ -27,8 +27,10 @@ class ReposViewModel { func loadTrendingRepos(online: Bool) { 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)