diff --git a/Cars Map.xcodeproj/xcuserdata/mamadfarrahi.xcuserdatad/xcschemes/xcschememanagement.plist b/Cars Map.xcodeproj/xcuserdata/mamadfarrahi.xcuserdatad/xcschemes/xcschememanagement.plist deleted file mode 100644 index 0bb5b23..0000000 --- a/Cars Map.xcodeproj/xcuserdata/mamadfarrahi.xcuserdatad/xcschemes/xcschememanagement.plist +++ /dev/null @@ -1,14 +0,0 @@ - - - - - SchemeUserState - - Cars Map.xcscheme_^#shared#^_ - - orderHint - 0 - - - - diff --git a/Cars Map/Scenes/AppCoordinator.swift b/Cars Map/Scenes/AppCoordinator.swift deleted file mode 100644 index 60c9b18..0000000 --- a/Cars Map/Scenes/AppCoordinator.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// AppCoordinator.swift -// Cat Facts -// -// Created by iMamad on 4/8/22. -// - -import UIKit - -// TODO: Add child coordinators as well -final class AppCoordinator: Coordinator { // TabCoordinator - - // MARK: Properties - let window: UIWindow? - - var rootTabBarController = UITabBarController() - - var apiClient: Network = { - let configuration = URLSessionConfiguration.default - configuration.httpAdditionalHeaders = ["Content-Type": "application/json; charset=utf-8"] - let apiClient = ApiClient(configuration: configuration) - return apiClient - }() - - // MARK: Child Coordinators - weak var mapCarsCoordinator: Coordinator? - weak var listCarsCoordinator: Coordinator? - - init(window: UIWindow?) { - self.window = window - } - - override func start() { - guard let window = window else { return } - - window.rootViewController = rootTabBarController - window.makeKeyAndVisible() - - startWaitingVC() - } -} - -// MARK: Starts -extension AppCoordinator { - private func startTabBarControllers(with cars: [Car]) { - // first tab - let mapCarsCoordinator = MapCarsCoordinator(rootTabBarController: rootTabBarController) - self.addChildCoordinator(mapCarsCoordinator) - mapCarsCoordinator.start() - - // second tab - let listCarsCoordinator = ListCarsCoordinator(rootTabBarController: rootTabBarController) - self.addChildCoordinator(listCarsCoordinator) - listCarsCoordinator.start() - } - - - private func startWaitingVC() { - let waitingStoryBoard = UIStoryboard.init(name: "Waiting", bundle: nil) - let waitingVC = waitingStoryBoard.instantiateViewController(withIdentifier: "WaitingVC") as! WaitingVC - let waitingVM = WaitingViewModel(apiClient: apiClient) - waitingVC.viewModel = waitingVM - window?.rootViewController = waitingVC - } -} - diff --git a/Cars Map/Scenes/Car.swift b/Cars Map/Scenes/Car.swift deleted file mode 100644 index 85bfa09..0000000 --- a/Cars Map/Scenes/Car.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// Car.swift -// Cars Map -// -// Created by iMamad on 4/11/22. -// - - -struct Car { - let name: String -} diff --git a/Cars Map/Scenes/ListCars/ListCarsCoordinator.swift b/Cars Map/Scenes/ListCars/ListCarsCoordinator.swift deleted file mode 100644 index fc235bd..0000000 --- a/Cars Map/Scenes/ListCars/ListCarsCoordinator.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// ListCarsCoordinator.swift -// Cars Map -// -// Created by iMamad on 4/11/22. -// - -import UIKit - -class ListCarsCoordinator: Coordinator { - - // MARK: Properties - private weak var rootTabBarController: UITabBarController! // write a test for it - Must be injected - private var listCarsNavigationContrller = UINavigationController() - - private let listCarsStoryboard = UIStoryboard(name: "ListCars", bundle: nil) - -// private let apiClient: Network - - // MARK: VM - - - // MARK: Coordinator - - init(rootTabBarController: UITabBarController) { - // set MapCarsVC to root VC - - let listCarsVC = listCarsStoryboard.instantiateViewController(withIdentifier: "ListCarsVC") as! ListCarsVC - listCarsVC.tabBarItem = UITabBarItem(tabBarSystemItem: .bookmarks, tag: 1) // write a test to check if the tab sequences are correct - - listCarsNavigationContrller.setViewControllers([listCarsVC], animated: true) - rootTabBarController.viewControllers?.append(listCarsNavigationContrller) - } - - override func start() { - print("I'm in ListCarsCoordinator") - } -} diff --git a/Cars Map/Scenes/ListCars/View/ListCars.storyboard b/Cars Map/Scenes/ListCars/View/ListCars.storyboard deleted file mode 100644 index 9ea8087..0000000 --- a/Cars Map/Scenes/ListCars/View/ListCars.storyboard +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Cars Map/Scenes/ListCars/ViewController/ListCarsVC.swift b/Cars Map/Scenes/ListCars/ViewController/ListCarsVC.swift deleted file mode 100644 index 5844b86..0000000 --- a/Cars Map/Scenes/ListCars/ViewController/ListCarsVC.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// ListCarsVC.swift -// Cars Map -// -// Created by iMamad on 4/11/22. -// - -import UIKit - -class ListCarsVC: UIViewController {} diff --git a/Cars Map/Scenes/MapCars/MapCarsCoordinator.swift b/Cars Map/Scenes/MapCars/MapCarsCoordinator.swift deleted file mode 100644 index 3ace089..0000000 --- a/Cars Map/Scenes/MapCars/MapCarsCoordinator.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// MapCarsCoordinator.swift -// Cars Map -// -// Created by iMamad on 4/11/22. -// - -import UIKit - -class MapCarsCoordinator: Coordinator { - - // MARK: Properties - private weak var rootTabBarController: UITabBarController! // write a test for it - Must be injected - private var mapCarsNavigationContrller = UINavigationController() - - private let mapCarsStoryboard = UIStoryboard(name: "MapCars", bundle: nil) - -// private let apiClient: Network - - // MARK: VM - - - // MARK: Coordinator - // inject VM and API - init(rootTabBarController: UITabBarController) { - // set MapCarsVC to root VC - - let mapCarsVC = mapCarsStoryboard.instantiateViewController(withIdentifier: "MapCarsVC") as! MapCarsVC - mapCarsVC.tabBarItem = UITabBarItem(tabBarSystemItem: .downloads, tag: 0) - - mapCarsNavigationContrller.setViewControllers([mapCarsVC], animated: true) - rootTabBarController.setViewControllers([mapCarsNavigationContrller], animated: true) - } - - override func start() { - print("I'm in MapCarsCoordinator") - } -} diff --git a/Cars Map/Scenes/MapCars/View/MapCars.storyboard b/Cars Map/Scenes/MapCars/View/MapCars.storyboard deleted file mode 100644 index 380a72d..0000000 --- a/Cars Map/Scenes/MapCars/View/MapCars.storyboard +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Cars Map/Scenes/MapCars/ViewController/MapCarsVC.swift b/Cars Map/Scenes/MapCars/ViewController/MapCarsVC.swift deleted file mode 100644 index d3b8928..0000000 --- a/Cars Map/Scenes/MapCars/ViewController/MapCarsVC.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// MapCarsVC.swift -// Cars Map -// -// Created by iMamad on 4/11/22. -// - -import UIKit - -class MapCarsVC: UIViewController {} diff --git a/Cars Map/Scenes/MapCars/ViewModel/CatsViewModel.swift b/Cars Map/Scenes/MapCars/ViewModel/CatsViewModel.swift deleted file mode 100644 index f3f9ef8..0000000 --- a/Cars Map/Scenes/MapCars/ViewModel/CatsViewModel.swift +++ /dev/null @@ -1,161 +0,0 @@ -//// -//// CatsViewModel.swift -//// Cat Facts -//// -//// Created by iMamad on 4/8/22. -//// -// -//import UIKit -//import CoreData -// -//class CatsViewModel { -// -// // MARK: Delegates -// var coordinatorDelegate: CatsCoordinator? -// var viewDelegate: CatsViewModelViewDelegate? -// -// // MARK: Properties -// private let service: CatsServices // API Call & CoreData -// -// var cats: [Cat] = [] -// var catsNSManagedObjects: [NSManagedObject]? -// -// // MARK: Init -// init(service: CatsServices) { self.service = service } -// -// func start() { -// service.fetchSavedCats { -// [weak self] -// (catsNSObjectArray, error) in -// guard let sSelf = self, -// let catsNSObjectArray = catsNSObjectArray as? [NSManagedObject] else { -// return -// } -// sSelf.catsNSManagedObjects = catsNSObjectArray -// let cats = CatCoreDataConvertor().giveMeCats(from: catsNSObjectArray) -// sSelf.cats = cats -// } -// } -// -//} -// -//// MARK: - Network -//extension CatsViewModel { -// private func getNewCat() { -// DispatchQueue.main.async { -// self.viewDelegate?.hud(show: true) -// } -// service.fetchCat { -// [weak self] -// (cat, error) in -// guard let sSelf = self else { return } -// -// if let error = error { -// DispatchQueue.main.async { -// sSelf.viewDelegate?.showError(errorMessage: error.localizedDescription) -// } -// return -// } -// -// if let cat = cat as? Cat { -// DispatchQueue.main.async { -// sSelf.saveNewCat(cat: cat) -// sSelf.viewDelegate?.updateScreen() -// sSelf.viewDelegate?.hud(show: false) -// } -// return -// } -// sSelf.viewDelegate?.showError(errorMessage: "Some Errors when communicating with the server occured!") -// -// } -// } -//} -// -//// MARK: - Core Data -//extension CatsViewModel { -// -// func saveNewCat(cat: Cat) { -// service.save(cat: cat) { -// [weak self] -// (error) in -// guard let sSelf = self else { return } -// -// if let error = error { -// DispatchQueue.main.async { -// sSelf.viewDelegate?.showError(errorMessage: error.localizedDescription) -// } -// return -// } -// -// sSelf.refreshView() -// } -// } -// -// func remove(at index: Int?) { -// -// guard let row = index, -// let catNSManagedObj = catsNSManagedObjects?[row] else { return } -// -// service.delete(cat: catNSManagedObj) { -// [weak self] -// (deleted, error) in -// guard let sSelf = self else { return } -// if let error = error { -// DispatchQueue.main.async { -// sSelf.viewDelegate?.showError(errorMessage: error.localizedDescription) -// } -// return -// } -// -// if deleted { -// sSelf.refreshView() -// } -// } -// -// } -//} -// -//// MARK: - ViewModelType -//extension CatsViewModel: CatsViewModelType { -// -// func numberOfItems() -> Int { -// return cats.count -// } -// -// func itemFor(row: Int) -> UITableViewCell { -// let cell = UITableViewCell(style: .value1, reuseIdentifier: "catID") -// let catViewData = CatViewData(cat: cats[row]) -// cell.textLabel?.text = catViewData._id -// cell.detailTextLabel?.text = catViewData.createdAt -// return cell -// } -// -// func add() { -// getNewCat() -// viewDelegate?.updateScreen() -// } -// -// func delete() { -// let row = self.viewDelegate?.selectedCatRow() -// self.remove(at: row) -// } -// -// func didSelectRow(_ row: Int, from controller: UIViewController) { -// print("Cat in \(row) selected") -// didSelect(cat: cats[row], from: controller) -// } -// -// func refreshView() { -// start() // to refresh arrays -// viewDelegate?.updateScreen() -// } -//} -// -//// MARK: - ViewModelCoordinator -//extension CatsViewModel: CatsViewModelCoordinatorDelegate { -// func didSelect(cat: Cat, from controller: UIViewController) { -// coordinatorDelegate?.didSelect(cat: cat, -// from: controller) -// // It'll open CatDetail VC -// } -//} diff --git a/Cars Map/Scenes/MapCars/ViewModel/ViewModelAbstractions.swift b/Cars Map/Scenes/MapCars/ViewModel/ViewModelAbstractions.swift deleted file mode 100644 index 8df54bd..0000000 --- a/Cars Map/Scenes/MapCars/ViewModel/ViewModelAbstractions.swift +++ /dev/null @@ -1,41 +0,0 @@ -//// -//// ViewModelAbstractions.swift -//// Cat Facts -//// -//// Created by iMamad on 4/9/22. -//// -// -//import UIKit -// -//// MARK: - ViewModelType -//protocol CatsViewModelType { -// -// var viewDelegate: CatsViewModelViewDelegate? { get set } -// -// // Data Source -// func numberOfItems() -> Int -// -// func itemFor(row: Int) -> UITableViewCell -// -// // Events -// func add() -// -// func delete() -// -// func didSelectRow(_ row: Int, from controller: UIViewController) -// -// func refreshView() -//} -// -//// MARK: - ViewModelCoordinator(delegate) -//protocol CatsViewModelCoordinatorDelegate: class { -// func didSelect(cat: Cat, from controller: UIViewController) -//} -// -//// MARK: - ViewModelViewDelegate -//protocol CatsViewModelViewDelegate: class { -// func updateScreen() -// func hud(show: Bool) -// func showError(errorMessage: String) -// func selectedCatRow() -> Int -//} diff --git a/Cars Map/Scenes/Waiting/WaitingVC.swift b/Cars Map/Scenes/Waiting/WaitingVC.swift deleted file mode 100644 index 88bb3f7..0000000 --- a/Cars Map/Scenes/Waiting/WaitingVC.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// WaitingVC.swift -// Cars Map -// -// Created by iMamad on 4/11/22. -// - -import UIKit - -class WaitingVC: UIViewController { - - // MARK: Outlets - - // MARK: Properties - var viewModel: WaitingViewModel! { - didSet { - viewModel.viewDelegate = self - } - } - - // MARK: UIViewController - override func viewDidLoad() { - print("WaitingVC loaded!") - } - - // MARK: Setup - - // MARK: Actions -} - -// MARK: - ViewModel Delegate -extension WaitingVC: WaitingViewModelViewDelegate { - func updateLabelWith(text: String) { - print("Update error label to" + text) - } - - func animate(_ flag: Bool) { - print("you should \(flag)") - } - - -} diff --git a/Cars Map/Scenes/Waiting/WaitingViewModel.swift b/Cars Map/Scenes/Waiting/WaitingViewModel.swift deleted file mode 100644 index feb5109..0000000 --- a/Cars Map/Scenes/Waiting/WaitingViewModel.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// WaitingViewModel.swift -// Cars Map -// -// Created by iMamad on 4/11/22. -// - -import Foundation - - -class WaitingViewModel: WaitingViewModelType { - - // MARK: Properties - private let apiClient: Network - // delegates - var appCoordinatorDelegate: AppCoordinatorDelegate? - var viewDelegate: WaitingViewModelViewDelegate? - - init(apiClient: Network) { - self.apiClient = apiClient - print("WaitingVM is running!") - } -} - -extension WaitingViewModel { - func fetch() { - // 1. request - // 2. handle errors and show on label -> viewDelegate. - // 3. give back data -> appCoordinatorDelegate - } - - func retry() { - // it should resend the request - fetch() - // update views - } -} - -// MARK: - ViewModelType -protocol WaitingViewModelType { - func fetch() - func retry() -} - -// MARK: - ViewModelCoordinator(delegate) -protocol AppCoordinatorDelegate { - func dataReceived(cars: [Car]) -} - -// MARK: - ViewModelViewDelegate -protocol WaitingViewModelViewDelegate { - func updateLabelWith(text: String) - func animate(_:Bool) -} diff --git a/architecture_diagram.drawio b/architecture_diagram.drawio new file mode 100644 index 0000000..1645958 --- /dev/null +++ b/architecture_diagram.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/architecture_diagram.jpg b/architecture_diagram.jpg new file mode 100644 index 0000000..7531187 Binary files /dev/null and b/architecture_diagram.jpg differ diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..42071d2 --- /dev/null +++ b/readme.md @@ -0,0 +1,47 @@ +## What is it? πŸ™‹πŸ» + +It's a pet project which has been developed as a code challenge. It's written purely in Swift without using 3rd party frameworks. + + +## What do you want to show by this project❓ + +How DO I **respect** the topics below while developing a software: + +* Reusability of the code πŸ” +* Clean Code Principles 🧼 +* SOLID Principles πŸ₯° +* Design Patterns πŸ–Œ +* Loose coupling πŸ™‡πŸ»β€β™‚οΈ +* Abstraction ☁️ +* Modularity 🧱 +* Testability πŸ§ͺ +* and Clean Architecture for sure! 😁 + +## Architecture (heart of the app❀️) +#### MVVM-C + Services + + +J + + + + +## Main Components +* MapKit πŸ“ +* Tab Bar Controller +* Navigation Controller 🧭 +* Table View + +## Video πŸŽ₯ + + +https://user-images.githubusercontent.com/28094207/166106625-19af8899-16fc-4b25-8a1c-7912c759da64.mov + + + + + + + +### Diagram +You can find the diagram file in the repo and open it in [here](draw.io) diff --git a/Cars Map.xcodeproj/project.pbxproj b/src/Cars Map.xcodeproj/project.pbxproj similarity index 65% rename from Cars Map.xcodeproj/project.pbxproj rename to src/Cars Map.xcodeproj/project.pbxproj index 964a7ab..9c95f91 100644 --- a/Cars Map.xcodeproj/project.pbxproj +++ b/src/Cars Map.xcodeproj/project.pbxproj @@ -7,32 +7,60 @@ objects = { /* Begin PBXBuildFile section */ + 87296F2D280722390094D59B /* InternetConnectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87296F2C280722390094D59B /* InternetConnectionManager.swift */; }; + 87296F3A280741FE0094D59B /* Cars_MapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87296F39280741FE0094D59B /* Cars_MapTests.swift */; }; + 873847A7280969BF005D79E1 /* AppServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873847A6280969BF005D79E1 /* AppServices.swift */; }; 879EFF272804AAA000ED0CD0 /* MapCars.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 879EFF262804AAA000ED0CD0 /* MapCars.storyboard */; }; 879EFF2A2804B17900ED0CD0 /* Waiting.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 879EFF292804B17900ED0CD0 /* Waiting.storyboard */; }; 879EFF2E2804B6F900ED0CD0 /* WaitingVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 879EFF2D2804B6F900ED0CD0 /* WaitingVC.swift */; }; - 879EFF332804B85500ED0CD0 /* WaitingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 879EFF322804B85500ED0CD0 /* WaitingViewModel.swift */; }; + 879EFF332804B85500ED0CD0 /* WaitingVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 879EFF322804B85500ED0CD0 /* WaitingVM.swift */; }; 879EFF362804B95900ED0CD0 /* Car.swift in Sources */ = {isa = PBXBuildFile; fileRef = 879EFF352804B95900ED0CD0 /* Car.swift */; }; + 87A8C5D62805B22000BF5775 /* CarAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87A8C5D52805B22000BF5775 /* CarAnnotationView.swift */; }; + 87A8C5E02805B2FD00BF5775 /* CarAnnotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87A8C5DF2805B2FD00BF5775 /* CarAnnotation.swift */; }; + 87A8C5F62805B86A00BF5775 /* ListCarsVMAbstractions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87A8C5F42805B86A00BF5775 /* ListCarsVMAbstractions.swift */; }; + 87A8C5F72805B86A00BF5775 /* ListCarsVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87A8C5F52805B86A00BF5775 /* ListCarsVM.swift */; }; 87B6E70728049BE60059A1E3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B6E70628049BE60059A1E3 /* AppDelegate.swift */; }; 87B6E71028049BE70059A1E3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 87B6E70F28049BE70059A1E3 /* Assets.xcassets */; }; 87B6E71328049BE70059A1E3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 87B6E71128049BE70059A1E3 /* LaunchScreen.storyboard */; }; 87B6E73228049E110059A1E3 /* MapCarsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B6E73128049E110059A1E3 /* MapCarsVC.swift */; }; 87B6E73C28049EF10059A1E3 /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B6E73A28049EF10059A1E3 /* Coordinator.swift */; }; 87B6E73D28049EF10059A1E3 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B6E73B28049EF10059A1E3 /* AppCoordinator.swift */; }; - 87B6E7412804A1100059A1E3 /* CatsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B6E73F2804A1100059A1E3 /* CatsViewModel.swift */; }; - 87B6E7422804A1100059A1E3 /* ViewModelAbstractions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B6E7402804A1100059A1E3 /* ViewModelAbstractions.swift */; }; + 87B6E7412804A1100059A1E3 /* MapCarsVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B6E73F2804A1100059A1E3 /* MapCarsVM.swift */; }; + 87B6E7422804A1100059A1E3 /* MapCarsVMAbstractions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B6E7402804A1100059A1E3 /* MapCarsVMAbstractions.swift */; }; 87B6E7492804A1310059A1E3 /* ListCars.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 87B6E7482804A1310059A1E3 /* ListCars.storyboard */; }; 87B6E74C2804A1370059A1E3 /* ListCarsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B6E74B2804A1370059A1E3 /* ListCarsVC.swift */; }; 87B6E7502804A4700059A1E3 /* MapCarsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B6E74F2804A4700059A1E3 /* MapCarsCoordinator.swift */; }; 87B6E7532804A4C50059A1E3 /* ListCarsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B6E7522804A4C50059A1E3 /* ListCarsCoordinator.swift */; }; 87B6E7572804A5480059A1E3 /* ApiClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B6E7562804A5480059A1E3 /* ApiClient.swift */; }; + 87DF2AC02805CFDF00AC89BB /* CarInfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87DF2ABF2805CFDF00AC89BB /* CarInfoVC.swift */; }; + 87DF2AC42805CFED00AC89BB /* CarInfo.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 87DF2AC32805CFED00AC89BB /* CarInfo.storyboard */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 87296F3C280741FE0094D59B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 87B6E6FB28049BE60059A1E3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 87B6E70228049BE60059A1E3; + remoteInfo = "Cars Map"; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ + 87296F2C280722390094D59B /* InternetConnectionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InternetConnectionManager.swift; sourceTree = ""; }; + 87296F37280741FE0094D59B /* Cars MapTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Cars MapTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 87296F39280741FE0094D59B /* Cars_MapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cars_MapTests.swift; sourceTree = ""; }; + 87296F3B280741FE0094D59B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 873847A6280969BF005D79E1 /* AppServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppServices.swift; path = "Cars Map/Scenes/Service/AppServices.swift"; sourceTree = SOURCE_ROOT; }; 879EFF262804AAA000ED0CD0 /* MapCars.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = MapCars.storyboard; sourceTree = ""; }; 879EFF292804B17900ED0CD0 /* Waiting.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Waiting.storyboard; sourceTree = ""; }; 879EFF2D2804B6F900ED0CD0 /* WaitingVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitingVC.swift; sourceTree = ""; }; - 879EFF322804B85500ED0CD0 /* WaitingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitingViewModel.swift; sourceTree = ""; }; + 879EFF322804B85500ED0CD0 /* WaitingVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitingVM.swift; sourceTree = ""; }; 879EFF352804B95900ED0CD0 /* Car.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Car.swift; sourceTree = ""; }; + 87A8C5D52805B22000BF5775 /* CarAnnotationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarAnnotationView.swift; sourceTree = ""; }; + 87A8C5DF2805B2FD00BF5775 /* CarAnnotation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CarAnnotation.swift; sourceTree = ""; }; + 87A8C5F42805B86A00BF5775 /* ListCarsVMAbstractions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListCarsVMAbstractions.swift; sourceTree = ""; }; + 87A8C5F52805B86A00BF5775 /* ListCarsVM.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListCarsVM.swift; sourceTree = ""; }; 87B6E70328049BE60059A1E3 /* Cars Map.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Cars Map.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 87B6E70628049BE60059A1E3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 87B6E70F28049BE70059A1E3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -41,16 +69,25 @@ 87B6E73128049E110059A1E3 /* MapCarsVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapCarsVC.swift; sourceTree = ""; }; 87B6E73A28049EF10059A1E3 /* Coordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; }; 87B6E73B28049EF10059A1E3 /* AppCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; - 87B6E73F2804A1100059A1E3 /* CatsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CatsViewModel.swift; sourceTree = ""; }; - 87B6E7402804A1100059A1E3 /* ViewModelAbstractions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewModelAbstractions.swift; sourceTree = ""; }; + 87B6E73F2804A1100059A1E3 /* MapCarsVM.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapCarsVM.swift; sourceTree = ""; }; + 87B6E7402804A1100059A1E3 /* MapCarsVMAbstractions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapCarsVMAbstractions.swift; sourceTree = ""; }; 87B6E7482804A1310059A1E3 /* ListCars.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = ListCars.storyboard; sourceTree = ""; }; 87B6E74B2804A1370059A1E3 /* ListCarsVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListCarsVC.swift; sourceTree = ""; }; 87B6E74F2804A4700059A1E3 /* MapCarsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapCarsCoordinator.swift; sourceTree = ""; }; 87B6E7522804A4C50059A1E3 /* ListCarsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListCarsCoordinator.swift; sourceTree = ""; }; 87B6E7562804A5480059A1E3 /* ApiClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiClient.swift; sourceTree = ""; }; + 87DF2ABF2805CFDF00AC89BB /* CarInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarInfoVC.swift; sourceTree = ""; }; + 87DF2AC32805CFED00AC89BB /* CarInfo.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = CarInfo.storyboard; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 87296F34280741FE0094D59B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 87B6E70028049BE60059A1E3 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -61,20 +98,47 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 87296F38280741FE0094D59B /* Cars MapTests */ = { + isa = PBXGroup; + children = ( + 87296F39280741FE0094D59B /* Cars_MapTests.swift */, + 87296F3B280741FE0094D59B /* Info.plist */, + ); + path = "Cars MapTests"; + sourceTree = ""; + }; + 8754360D2809B14300BB28C4 /* Service */ = { + isa = PBXGroup; + children = ( + 873847A6280969BF005D79E1 /* AppServices.swift */, + 87B6E7562804A5480059A1E3 /* ApiClient.swift */, + ); + path = Service; + sourceTree = ""; + }; 879EFF312804B7FE00ED0CD0 /* Waiting */ = { isa = PBXGroup; children = ( 879EFF292804B17900ED0CD0 /* Waiting.storyboard */, 879EFF2D2804B6F900ED0CD0 /* WaitingVC.swift */, - 879EFF322804B85500ED0CD0 /* WaitingViewModel.swift */, + 879EFF322804B85500ED0CD0 /* WaitingVM.swift */, ); path = Waiting; sourceTree = ""; }; + 87A8C5D82805B23300BF5775 /* Model */ = { + isa = PBXGroup; + children = ( + 87A8C5DF2805B2FD00BF5775 /* CarAnnotation.swift */, + ); + path = Model; + sourceTree = ""; + }; 87B6E6FA28049BE60059A1E3 = { isa = PBXGroup; children = ( 87B6E70528049BE60059A1E3 /* Cars Map */, + 87296F38280741FE0094D59B /* Cars MapTests */, 87B6E70428049BE60059A1E3 /* Products */, ); sourceTree = ""; @@ -83,6 +147,7 @@ isa = PBXGroup; children = ( 87B6E70328049BE60059A1E3 /* Cars Map.app */, + 87296F37280741FE0094D59B /* Cars MapTests.xctest */, ); name = Products; sourceTree = ""; @@ -100,13 +165,14 @@ 87B6E71D28049CD20059A1E3 /* Scenes */ = { isa = PBXGroup; children = ( - 87B6E7562804A5480059A1E3 /* ApiClient.swift */, 87B6E73A28049EF10059A1E3 /* Coordinator.swift */, 87B6E73B28049EF10059A1E3 /* AppCoordinator.swift */, 879EFF352804B95900ED0CD0 /* Car.swift */, + 8754360D2809B14300BB28C4 /* Service */, 879EFF312804B7FE00ED0CD0 /* Waiting */, 87B6E72428049D760059A1E3 /* MapCars */, 87B6E72528049D800059A1E3 /* ListCars */, + 87DF2ABE2805CFAB00AC89BB /* CarInfoModal */, ); path = Scenes; sourceTree = ""; @@ -114,6 +180,7 @@ 87B6E71F28049CEC0059A1E3 /* Resources */ = { isa = PBXGroup; children = ( + 87296F2C280722390094D59B /* InternetConnectionManager.swift */, 87B6E70F28049BE70059A1E3 /* Assets.xcassets */, 87B6E71128049BE70059A1E3 /* LaunchScreen.storyboard */, 87B6E71428049BE70059A1E3 /* Info.plist */, @@ -125,6 +192,7 @@ isa = PBXGroup; children = ( 87B6E74F2804A4700059A1E3 /* MapCarsCoordinator.swift */, + 87A8C5D82805B23300BF5775 /* Model */, 87B6E72A28049DD00059A1E3 /* ViewModel */, 87B6E72B28049DD80059A1E3 /* View */, 87B6E72D28049DE40059A1E3 /* ViewController */, @@ -162,6 +230,8 @@ 87B6E72928049DB30059A1E3 /* ViewModel */ = { isa = PBXGroup; children = ( + 87A8C5F42805B86A00BF5775 /* ListCarsVMAbstractions.swift */, + 87A8C5F52805B86A00BF5775 /* ListCarsVM.swift */, ); path = ViewModel; sourceTree = ""; @@ -169,8 +239,8 @@ 87B6E72A28049DD00059A1E3 /* ViewModel */ = { isa = PBXGroup; children = ( - 87B6E73F2804A1100059A1E3 /* CatsViewModel.swift */, - 87B6E7402804A1100059A1E3 /* ViewModelAbstractions.swift */, + 87B6E7402804A1100059A1E3 /* MapCarsVMAbstractions.swift */, + 87B6E73F2804A1100059A1E3 /* MapCarsVM.swift */, ); path = ViewModel; sourceTree = ""; @@ -179,6 +249,7 @@ isa = PBXGroup; children = ( 879EFF262804AAA000ED0CD0 /* MapCars.storyboard */, + 87A8C5D52805B22000BF5775 /* CarAnnotationView.swift */, ); path = View; sourceTree = ""; @@ -191,9 +262,36 @@ path = ViewController; sourceTree = ""; }; + 87DF2ABE2805CFAB00AC89BB /* CarInfoModal */ = { + isa = PBXGroup; + children = ( + 87DF2AC32805CFED00AC89BB /* CarInfo.storyboard */, + 87DF2ABF2805CFDF00AC89BB /* CarInfoVC.swift */, + ); + path = CarInfoModal; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 87296F36280741FE0094D59B /* Cars MapTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 87296F40280741FE0094D59B /* Build configuration list for PBXNativeTarget "Cars MapTests" */; + buildPhases = ( + 87296F33280741FE0094D59B /* Sources */, + 87296F34280741FE0094D59B /* Frameworks */, + 87296F35280741FE0094D59B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 87296F3D280741FE0094D59B /* PBXTargetDependency */, + ); + name = "Cars MapTests"; + productName = "Cars MapTests"; + productReference = 87296F37280741FE0094D59B /* Cars MapTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 87B6E70228049BE60059A1E3 /* Cars Map */ = { isa = PBXNativeTarget; buildConfigurationList = 87B6E71728049BE70059A1E3 /* Build configuration list for PBXNativeTarget "Cars Map" */; @@ -220,6 +318,10 @@ LastSwiftUpdateCheck = 1220; LastUpgradeCheck = 1220; TargetAttributes = { + 87296F36280741FE0094D59B = { + CreatedOnToolsVersion = 12.2; + TestTargetID = 87B6E70228049BE60059A1E3; + }; 87B6E70228049BE60059A1E3 = { CreatedOnToolsVersion = 12.2; }; @@ -239,11 +341,19 @@ projectRoot = ""; targets = ( 87B6E70228049BE60059A1E3 /* Cars Map */, + 87296F36280741FE0094D59B /* Cars MapTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 87296F35280741FE0094D59B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 87B6E70128049BE60059A1E3 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -253,34 +363,58 @@ 87B6E71328049BE70059A1E3 /* LaunchScreen.storyboard in Resources */, 87B6E71028049BE70059A1E3 /* Assets.xcassets in Resources */, 87B6E7492804A1310059A1E3 /* ListCars.storyboard in Resources */, + 87DF2AC42805CFED00AC89BB /* CarInfo.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 87296F33280741FE0094D59B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 87296F3A280741FE0094D59B /* Cars_MapTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 87B6E6FF28049BE60059A1E3 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 879EFF332804B85500ED0CD0 /* WaitingViewModel.swift in Sources */, + 87A8C5E02805B2FD00BF5775 /* CarAnnotation.swift in Sources */, + 879EFF332804B85500ED0CD0 /* WaitingVM.swift in Sources */, + 87A8C5D62805B22000BF5775 /* CarAnnotationView.swift in Sources */, 87B6E73D28049EF10059A1E3 /* AppCoordinator.swift in Sources */, - 87B6E7422804A1100059A1E3 /* ViewModelAbstractions.swift in Sources */, + 87296F2D280722390094D59B /* InternetConnectionManager.swift in Sources */, + 87B6E7422804A1100059A1E3 /* MapCarsVMAbstractions.swift in Sources */, 879EFF2E2804B6F900ED0CD0 /* WaitingVC.swift in Sources */, 879EFF362804B95900ED0CD0 /* Car.swift in Sources */, + 87A8C5F72805B86A00BF5775 /* ListCarsVM.swift in Sources */, 87B6E73228049E110059A1E3 /* MapCarsVC.swift in Sources */, 87B6E74C2804A1370059A1E3 /* ListCarsVC.swift in Sources */, 87B6E70728049BE60059A1E3 /* AppDelegate.swift in Sources */, - 87B6E7412804A1100059A1E3 /* CatsViewModel.swift in Sources */, + 87B6E7412804A1100059A1E3 /* MapCarsVM.swift in Sources */, 87B6E7502804A4700059A1E3 /* MapCarsCoordinator.swift in Sources */, 87B6E7532804A4C50059A1E3 /* ListCarsCoordinator.swift in Sources */, 87B6E73C28049EF10059A1E3 /* Coordinator.swift in Sources */, 87B6E7572804A5480059A1E3 /* ApiClient.swift in Sources */, + 873847A7280969BF005D79E1 /* AppServices.swift in Sources */, + 87A8C5F62805B86A00BF5775 /* ListCarsVMAbstractions.swift in Sources */, + 87DF2AC02805CFDF00AC89BB /* CarInfoVC.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 87296F3D280741FE0094D59B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 87B6E70228049BE60059A1E3 /* Cars Map */; + targetProxy = 87296F3C280741FE0094D59B /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 87B6E71128049BE70059A1E3 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; @@ -293,6 +427,46 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 87296F3E280741FE0094D59B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 5GB4GGS54M; + INFOPLIST_FILE = "Cars MapTests/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.kabok.mohammadfarrahi.Cars-MapTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Cars Map.app/Cars Map"; + }; + name = Debug; + }; + 87296F3F280741FE0094D59B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 5GB4GGS54M; + INFOPLIST_FILE = "Cars MapTests/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.kabok.mohammadfarrahi.Cars-MapTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Cars Map.app/Cars Map"; + }; + name = Release; + }; 87B6E71528049BE70059A1E3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -452,6 +626,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 87296F40280741FE0094D59B /* Build configuration list for PBXNativeTarget "Cars MapTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 87296F3E280741FE0094D59B /* Debug */, + 87296F3F280741FE0094D59B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 87B6E6FE28049BE60059A1E3 /* Build configuration list for PBXProject "Cars Map" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Cars Map.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/src/Cars Map.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Cars Map.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to src/Cars Map.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/Cars Map.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/src/Cars Map.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from Cars Map.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to src/Cars Map.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/src/Cars Map.xcodeproj/xcshareddata/xcschemes/Cars Map.xcscheme b/src/Cars Map.xcodeproj/xcshareddata/xcschemes/Cars Map.xcscheme new file mode 100644 index 0000000..4e8dc39 --- /dev/null +++ b/src/Cars Map.xcodeproj/xcshareddata/xcschemes/Cars Map.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Cars Map/AppDelegate.swift b/src/Cars Map/AppDelegate.swift similarity index 100% rename from Cars Map/AppDelegate.swift rename to src/Cars Map/AppDelegate.swift diff --git a/Cars Map/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/src/Cars Map/Resources/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Cars Map/Resources/Assets.xcassets/AccentColor.colorset/Contents.json rename to src/Cars Map/Resources/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/Cars Map/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/src/Cars Map/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Cars Map/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json rename to src/Cars Map/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Cars Map/Resources/Assets.xcassets/Contents.json b/src/Cars Map/Resources/Assets.xcassets/Contents.json similarity index 100% rename from Cars Map/Resources/Assets.xcassets/Contents.json rename to src/Cars Map/Resources/Assets.xcassets/Contents.json diff --git a/src/Cars Map/Resources/Assets.xcassets/car.imageset/Contents.json b/src/Cars Map/Resources/Assets.xcassets/car.imageset/Contents.json new file mode 100644 index 0000000..d9ccb6e --- /dev/null +++ b/src/Cars Map/Resources/Assets.xcassets/car.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "car@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "car@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "car@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/Cars Map/Resources/Assets.xcassets/car.imageset/car@1x.png b/src/Cars Map/Resources/Assets.xcassets/car.imageset/car@1x.png new file mode 100644 index 0000000..8c59692 Binary files /dev/null and b/src/Cars Map/Resources/Assets.xcassets/car.imageset/car@1x.png differ diff --git a/src/Cars Map/Resources/Assets.xcassets/car.imageset/car@2x.png b/src/Cars Map/Resources/Assets.xcassets/car.imageset/car@2x.png new file mode 100644 index 0000000..1521176 Binary files /dev/null and b/src/Cars Map/Resources/Assets.xcassets/car.imageset/car@2x.png differ diff --git a/src/Cars Map/Resources/Assets.xcassets/car.imageset/car@3x.png b/src/Cars Map/Resources/Assets.xcassets/car.imageset/car@3x.png new file mode 100644 index 0000000..99a1677 Binary files /dev/null and b/src/Cars Map/Resources/Assets.xcassets/car.imageset/car@3x.png differ diff --git a/src/Cars Map/Resources/Assets.xcassets/gas.imageset/Contents.json b/src/Cars Map/Resources/Assets.xcassets/gas.imageset/Contents.json new file mode 100644 index 0000000..f89a6e6 --- /dev/null +++ b/src/Cars Map/Resources/Assets.xcassets/gas.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "gas@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "gas@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "gas@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/Cars Map/Resources/Assets.xcassets/gas.imageset/gas@1x.png b/src/Cars Map/Resources/Assets.xcassets/gas.imageset/gas@1x.png new file mode 100644 index 0000000..22ea394 Binary files /dev/null and b/src/Cars Map/Resources/Assets.xcassets/gas.imageset/gas@1x.png differ diff --git a/src/Cars Map/Resources/Assets.xcassets/gas.imageset/gas@2x.png b/src/Cars Map/Resources/Assets.xcassets/gas.imageset/gas@2x.png new file mode 100644 index 0000000..158bcd0 Binary files /dev/null and b/src/Cars Map/Resources/Assets.xcassets/gas.imageset/gas@2x.png differ diff --git a/src/Cars Map/Resources/Assets.xcassets/gas.imageset/gas@3x.png b/src/Cars Map/Resources/Assets.xcassets/gas.imageset/gas@3x.png new file mode 100644 index 0000000..1c3d588 Binary files /dev/null and b/src/Cars Map/Resources/Assets.xcassets/gas.imageset/gas@3x.png differ diff --git a/src/Cars Map/Resources/Assets.xcassets/list.imageset/Contents.json b/src/Cars Map/Resources/Assets.xcassets/list.imageset/Contents.json new file mode 100644 index 0000000..b4e579f --- /dev/null +++ b/src/Cars Map/Resources/Assets.xcassets/list.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "list.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "list-1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "list-2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/Cars Map/Resources/Assets.xcassets/list.imageset/list-1.png b/src/Cars Map/Resources/Assets.xcassets/list.imageset/list-1.png new file mode 100644 index 0000000..6387061 Binary files /dev/null and b/src/Cars Map/Resources/Assets.xcassets/list.imageset/list-1.png differ diff --git a/src/Cars Map/Resources/Assets.xcassets/list.imageset/list-2.png b/src/Cars Map/Resources/Assets.xcassets/list.imageset/list-2.png new file mode 100644 index 0000000..c689b4e Binary files /dev/null and b/src/Cars Map/Resources/Assets.xcassets/list.imageset/list-2.png differ diff --git a/src/Cars Map/Resources/Assets.xcassets/list.imageset/list.png b/src/Cars Map/Resources/Assets.xcassets/list.imageset/list.png new file mode 100644 index 0000000..5eeac63 Binary files /dev/null and b/src/Cars Map/Resources/Assets.xcassets/list.imageset/list.png differ diff --git a/src/Cars Map/Resources/Assets.xcassets/map.imageset/Contents.json b/src/Cars Map/Resources/Assets.xcassets/map.imageset/Contents.json new file mode 100644 index 0000000..2667fd7 --- /dev/null +++ b/src/Cars Map/Resources/Assets.xcassets/map.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "map.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "map@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "map@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/Cars Map/Resources/Assets.xcassets/map.imageset/map.png b/src/Cars Map/Resources/Assets.xcassets/map.imageset/map.png new file mode 100644 index 0000000..7546ede Binary files /dev/null and b/src/Cars Map/Resources/Assets.xcassets/map.imageset/map.png differ diff --git a/src/Cars Map/Resources/Assets.xcassets/map.imageset/map@2x.png b/src/Cars Map/Resources/Assets.xcassets/map.imageset/map@2x.png new file mode 100644 index 0000000..3b14f79 Binary files /dev/null and b/src/Cars Map/Resources/Assets.xcassets/map.imageset/map@2x.png differ diff --git a/src/Cars Map/Resources/Assets.xcassets/map.imageset/map@3x.png b/src/Cars Map/Resources/Assets.xcassets/map.imageset/map@3x.png new file mode 100644 index 0000000..729a00f Binary files /dev/null and b/src/Cars Map/Resources/Assets.xcassets/map.imageset/map@3x.png differ diff --git a/src/Cars Map/Resources/Assets.xcassets/sixt_black.colorset/Contents.json b/src/Cars Map/Resources/Assets.xcassets/sixt_black.colorset/Contents.json new file mode 100644 index 0000000..e86505e --- /dev/null +++ b/src/Cars Map/Resources/Assets.xcassets/sixt_black.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.094", + "green" : "0.102", + "red" : "0.102" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/Cars Map/Resources/Assets.xcassets/sixt_orange.colorset/Contents.json b/src/Cars Map/Resources/Assets.xcassets/sixt_orange.colorset/Contents.json new file mode 100644 index 0000000..4ccfe2f --- /dev/null +++ b/src/Cars Map/Resources/Assets.xcassets/sixt_orange.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.000", + "green" : "0.373", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Cars Map/Resources/Base.lproj/LaunchScreen.storyboard b/src/Cars Map/Resources/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from Cars Map/Resources/Base.lproj/LaunchScreen.storyboard rename to src/Cars Map/Resources/Base.lproj/LaunchScreen.storyboard diff --git a/Cars Map/Resources/Info.plist b/src/Cars Map/Resources/Info.plist similarity index 100% rename from Cars Map/Resources/Info.plist rename to src/Cars Map/Resources/Info.plist diff --git a/src/Cars Map/Resources/InternetConnectionManager.swift b/src/Cars Map/Resources/InternetConnectionManager.swift new file mode 100644 index 0000000..6859e1d --- /dev/null +++ b/src/Cars Map/Resources/InternetConnectionManager.swift @@ -0,0 +1,42 @@ +// +// ConnectionManager.swift +// Cat Facts +// +// Created by iMamad on 4/10/22. +// + +import Foundation +import UIKit +import SystemConfiguration + +public class InternetConnectionManager { + + static let shared = InternetConnectionManager() // Singleton + + func isConnectedToNetwork() -> Bool { + + var zeroAddress = sockaddr_in() + zeroAddress.sin_len = UInt8(MemoryLayout.size(ofValue: zeroAddress)) + zeroAddress.sin_family = sa_family_t(AF_INET) + guard let defaultRouteReachability = withUnsafePointer(to: &zeroAddress, { + + $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { + + SCNetworkReachabilityCreateWithAddress(nil, $0) + + } + + }) else { + + return false + } + var flags = SCNetworkReachabilityFlags() + if !SCNetworkReachabilityGetFlags(defaultRouteReachability, &flags) { + return false + } + let isReachable = (flags.rawValue & UInt32(kSCNetworkFlagsReachable)) != 0 + let needsConnection = (flags.rawValue & UInt32(kSCNetworkFlagsConnectionRequired)) != 0 + return (isReachable && !needsConnection) + } + +} diff --git a/src/Cars Map/Scenes/AppCoordinator.swift b/src/Cars Map/Scenes/AppCoordinator.swift new file mode 100644 index 0000000..039a520 --- /dev/null +++ b/src/Cars Map/Scenes/AppCoordinator.swift @@ -0,0 +1,97 @@ +// +// AppCoordinator.swift +// Cat Facts +// +// Created by iMamad on 4/8/22. +// + +import UIKit + +final class AppCoordinator: Coordinator { // TabCoordinator + + // MARK: Properties + private let window: UIWindow? + + private let rootTabBarController = UITabBarController() + + lazy private var apiClient: Network = { + let configuration = URLSessionConfiguration.default + configuration.httpAdditionalHeaders = ["Content-Type": "application/json; charset=utf-8"] + let apiClient = ApiClient(configuration: configuration) + return apiClient + }() + + lazy private var services: Serviceable = { + let services = AppServices(apiClient: apiClient) + return services + }() + + // MARK: Init + init(window: UIWindow?) { self.window = window } + + override func start() { + guard let window = window else { return } + + window.rootViewController = rootTabBarController + window.makeKeyAndVisible() + + startWaitingVC() + } +} + +// MARK: Starts +extension AppCoordinator { + private func startTabBarControllers(with carViewDatas: [CarViewData]) { + + configAppAppearance() + // first tab + let mapCarsCoordinator = MapCarsCoordinator(rootTabBarController: rootTabBarController, + carViewDatas: carViewDatas) + self.addChildCoordinator(mapCarsCoordinator) + mapCarsCoordinator.start() + + // second tab + let listCarsCoordinator = ListCarsCoordinator(rootTabBarController: rootTabBarController, + carViewDatas: carViewDatas) + self.addChildCoordinator(listCarsCoordinator) + listCarsCoordinator.start() + } + + private func startWaitingVC() { + let waitingVM = WaitingVM(service: services) + let waitingVC = WaitingVC.`init`(waitingVM: waitingVM) + waitingVM.appCoordinatorDelegate = self + window?.rootViewController = waitingVC + } +} + +//MARK: View Configs +extension AppCoordinator { + + private func configAppAppearance() { + configTabBarAppearance() + configNavigationBarAppearance() + } + + private func configTabBarAppearance() { + rootTabBarController.tabBar.barTintColor = UIColor(named: "sixt_black") + rootTabBarController.tabBar.tintColor = UIColor(named: "sixt_orange") + UINavigationBar.appearance().barTintColor = UIColor(named: "sixt_black") + } + + private func configNavigationBarAppearance() { + let textAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white] + UINavigationBar.appearance().titleTextAttributes = textAttributes + } +} + +// MARK: AppCoordinator Delegate +extension AppCoordinator: AppCoordinatorDelegate{ + func dataReceived(carViewDatas: [CarViewData]) { + print("\nI'm in AppCoordinator and received \(carViewDatas.count) cars.") + // close WaitingVC + self.window?.rootViewController = rootTabBarController + // start tabbars + startTabBarControllers(with: carViewDatas) + } +} diff --git a/src/Cars Map/Scenes/Car.swift b/src/Cars Map/Scenes/Car.swift new file mode 100644 index 0000000..d617c68 --- /dev/null +++ b/src/Cars Map/Scenes/Car.swift @@ -0,0 +1,88 @@ +// +// Car.swift +// Cars Map +// +// Created by iMamad on 4/11/22. +// + +import MapKit +import CoreLocation + +protocol CarViewDataType { + var id: String { get } + var modelName: String { get } + var name: String { get } + var make: String { get } + var color: String { get } + var fuelLevel: Float { get } + var latitude: Float { get } + var longitude: Float { get } + var carImageUrl: String { get } +} + +struct Car: Decodable { + let id: String + let modelName: String + let name: String + let make: String + let color: String + let fuelLevel: Float + let latitude: Float + let longitude: Float + let carImageUrl: String +} + + + +class CarViewData: CarViewDataType { + // MARK: custom properties + var coordinate: CLLocationCoordinate2D { + let coordinate = CLLocationCoordinate2D(latitude: CLLocationDegrees(latitude), + longitude: CLLocationDegrees(longitude)) + return coordinate + } + + var uiImage = UIImage(named: "car")! + + // MARK: properties + var id: String { return car.id } + + var modelName: String{ return car.modelName } + + var name: String{ return car.name } + + var make: String{ return car.make } + + var color: String{ return car.color } + + var fuelLevel: Float{ return car.fuelLevel } + + var latitude: Float{ return car.latitude } + + var longitude: Float{ return car.longitude } + + var carImageUrl: String { return car.carImageUrl } + + // MARK: Init + private let car: Car + + init(car: Car) { + self.car = car + if let url = URL(string: carImageUrl) { downloadImage(from: url) } + } + + + //MARK: Image Downloader + private func getData(from url: URL, completion: @escaping (Data?, URLResponse?, Error?) -> ()) { + URLSession.shared.dataTask(with: url, completionHandler: completion).resume() + } + + private func downloadImage(from url: URL) { + getData(from: url) { data, response, error in + guard let data = data, error == nil else { return } + DispatchQueue.main.async() { + self.uiImage = UIImage(data: data) ?? self.uiImage + } + } + } +} diff --git a/src/Cars Map/Scenes/CarInfoModal/CarInfo.storyboard b/src/Cars Map/Scenes/CarInfoModal/CarInfo.storyboard new file mode 100644 index 0000000..b782780 --- /dev/null +++ b/src/Cars Map/Scenes/CarInfoModal/CarInfo.storyboard @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Cars Map/Scenes/CarInfoModal/CarInfoVC.swift b/src/Cars Map/Scenes/CarInfoModal/CarInfoVC.swift new file mode 100644 index 0000000..e471f3d --- /dev/null +++ b/src/Cars Map/Scenes/CarInfoModal/CarInfoVC.swift @@ -0,0 +1,83 @@ +// +// CarInfoVC.swift +// Cars Map +// +// Created by iMamad on 4/12/22. +// + +import UIKit + +// TODO: good idea: calculate distance of the car from the user's +// current location and show it here to the user! + +class CarInfoVC: UIViewController { + + //MARK: Properties + var carViewData: CarViewData? = nil + + //MARK: Outlets + @IBOutlet private weak var gasImageView: UIImageView! + @IBOutlet private weak var carImageView: UIImageView! + @IBOutlet private weak var fuelPercentage: UILabel! + @IBOutlet private weak var infoText: UITextView! + + // MARK: UIViewController + override func viewDidLoad() { + super.viewDidLoad() + setup() + } + + + // MARK: setup + private func setup() { + guard let carViewData = carViewData else { return } + // Fuel module + self.gasImageView.image = gasImageView.image?.fill(with: .red, + percentage: CGFloat(carViewData.fuelLevel)) + fuelPercentage.text = "\(Int(carViewData.fuelLevel * 100))%" + // TODO: it's a good idea to make a module from + // percentage label and the gas icon and re-use it everywhere + + carImageView.image = carViewData.uiImage + + infoText.text = makeText(carViewData: carViewData) + } + + private func makeText(carViewData: CarViewData) -> String { + let words = [ + carViewData.name, + carViewData.make, + carViewData.modelName + ] + var str = "" + for word in words { + str += "\n\(word)" + } + return str + } +} + +extension UIImage { + func fill(with color: UIColor, percentage: CGFloat) -> UIImage { + let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) + + + UIGraphicsBeginImageContextWithOptions(size, false, scale) + draw(in: rect) + + let context = UIGraphicsGetCurrentContext()! + context.setBlendMode(CGBlendMode.sourceIn) + + context.setFillColor(color.cgColor) + + let start = (rect.height)-( size.height*percentage) + let rectToFill = CGRect(x: 0, y: start, width: size.width, height: size.height*percentage) + + context.fill(rectToFill) + + let newImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return newImage! + } +} diff --git a/Cars Map/Scenes/Coordinator.swift b/src/Cars Map/Scenes/Coordinator.swift similarity index 100% rename from Cars Map/Scenes/Coordinator.swift rename to src/Cars Map/Scenes/Coordinator.swift diff --git a/src/Cars Map/Scenes/ListCars/ListCarsCoordinator.swift b/src/Cars Map/Scenes/ListCars/ListCarsCoordinator.swift new file mode 100644 index 0000000..80b4b5d --- /dev/null +++ b/src/Cars Map/Scenes/ListCars/ListCarsCoordinator.swift @@ -0,0 +1,57 @@ +// +// ListCarsCoordinator.swift +// Cars Map +// +// Created by iMamad on 4/11/22. +// + +import UIKit + +class ListCarsCoordinator: Coordinator { + + // MARK: Properties + private weak var rootTabBarController: UITabBarController! + private let listCarsNavigationContrller = UINavigationController() + + // MARK: VM + private let carViewDatas: [CarViewData] + private var listCarsVM: ListCarsVM { + let listCarsVM = ListCarsVM(carViewDatas: carViewDatas) + listCarsVM.listCarsCoordinatorDelegate = self + return listCarsVM + } + + // MARK: Coordinator + init(rootTabBarController: UITabBarController, carViewDatas: [CarViewData]) { + self.rootTabBarController = rootTabBarController + self.carViewDatas = carViewDatas + } + + override func start() { + + let listCarsVC = ListCarsVC.`init`(listCarsVM: listCarsVM) + let tabBar = UITabBarItem(title: "List", image: UIImage(named: "list"), tag: 0) + listCarsVC.tabBarItem = tabBar + + listCarsNavigationContrller.setViewControllers([listCarsVC], animated: true) + rootTabBarController.viewControllers?.append(listCarsNavigationContrller) + // why append? it appends to the previously set VC(tab) - in this case - MapCarsVC + } +} + +// MARK: - ViewModel Callbacks +extension ListCarsCoordinator: ListCarsViewModelCoordinatorDelegate { + func didSelect(carViewData: CarViewData, from controller: UIViewController) { + showCarInfo(of: carViewData) + } +} + +//MARK: Navigation +extension ListCarsCoordinator { + private func showCarInfo(of carViewData: CarViewData) { // it shows a modal page + let carInfoSB = UIStoryboard.init(name: "CarInfo", bundle: nil) + let carInfoVC = carInfoSB.instantiateViewController(withIdentifier: "CarInfoVC") as! CarInfoVC + carInfoVC.carViewData = carViewData + listCarsNavigationContrller.present(carInfoVC, animated: true, completion: nil) + } +} diff --git a/src/Cars Map/Scenes/ListCars/View/ListCars.storyboard b/src/Cars Map/Scenes/ListCars/View/ListCars.storyboard new file mode 100644 index 0000000..1bbd766 --- /dev/null +++ b/src/Cars Map/Scenes/ListCars/View/ListCars.storyboard @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Cars Map/Scenes/ListCars/ViewController/ListCarsVC.swift b/src/Cars Map/Scenes/ListCars/ViewController/ListCarsVC.swift new file mode 100644 index 0000000..8ed6b7f --- /dev/null +++ b/src/Cars Map/Scenes/ListCars/ViewController/ListCarsVC.swift @@ -0,0 +1,54 @@ +// +// ListCarsVC.swift +// Cars Map +// +// Created by iMamad on 4/11/22. +// + +import UIKit + +class ListCarsVC: UIViewController { + + // MARK: Factory + class func `init`(listCarsVM: ListCarsVM) -> ListCarsVC { + let storyboard = UIStoryboard(name: "ListCars", bundle: nil) + let vc = storyboard.instantiateViewController(withIdentifier: "ListCarsVC") as! ListCarsVC + // take care of force unwrap above + vc.viewModel = listCarsVM + return vc + } + + // MARK: Properties + private var viewModel: ListCarsVM! + private let cellIndentifier = "CarCell" + + // MARK: Outlets + @IBOutlet weak var tableViewCars: UITableView! + + // MARK: UIViewController + + // MARK: Setup +} + +// MARK: - TableView Delegate & DataSource +extension ListCarsVC: UITableViewDelegate, UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + viewModel.numberOfItems() + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let carViewDataAny = viewModel.itemFor(row: indexPath.row) + + guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIndentifier), + let carViewData = carViewDataAny as? CarViewData else { + return UITableViewCell() + } + cell.textLabel?.text = carViewData.modelName + cell.accessoryType = .disclosureIndicator + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + viewModel.didSelectRow(indexPath.row, from: self) + } +} diff --git a/src/Cars Map/Scenes/ListCars/ViewModel/ListCarsVM.swift b/src/Cars Map/Scenes/ListCars/ViewModel/ListCarsVM.swift new file mode 100644 index 0000000..4a761e5 --- /dev/null +++ b/src/Cars Map/Scenes/ListCars/ViewModel/ListCarsVM.swift @@ -0,0 +1,48 @@ +// +// CatsViewModel.swift +// Cat Facts +// +// Created by iMamad on 4/8/22. +// + +import UIKit +import MapKit + +// TIP: It's very thin VM +// so you can delete this VM +// and move the codes to VC + +class ListCarsVM { + + // MARK: Delegates + var listCarsCoordinatorDelegate: ListCarsViewModelCoordinatorDelegate? + + // MARK: Properties + private let carViewDatas: [CarViewData]! + // check if it must be weak or not + + // MARK: Init + init(carViewDatas: [CarViewData]) { self.carViewDatas = carViewDatas } +} + +// MARK: - ViewModelType +extension ListCarsVM: ListCarsVMType { + func numberOfItems() -> Int { + carViewDatas.count + } + + func itemFor(row: Int) -> Any { + return carViewDatas[row] + } + + func didSelectRow(_ row: Int, from controller: UIViewController) { + didSelect(carViewData: carViewDatas[row], from: controller) + } +} + +// MARK: - ViewModelCoordinator +extension ListCarsVM: ListCarsViewModelCoordinatorDelegate { + func didSelect(carViewData: CarViewData, from controller: UIViewController) { + listCarsCoordinatorDelegate?.didSelect(carViewData: carViewData, from: controller) + } +} diff --git a/src/Cars Map/Scenes/ListCars/ViewModel/ListCarsVMAbstractions.swift b/src/Cars Map/Scenes/ListCars/ViewModel/ListCarsVMAbstractions.swift new file mode 100644 index 0000000..02bd960 --- /dev/null +++ b/src/Cars Map/Scenes/ListCars/ViewModel/ListCarsVMAbstractions.swift @@ -0,0 +1,32 @@ +// +// ViewModelAbstractions.swift +// Cat Facts +// +// Created by iMamad on 4/9/22. +// + +import UIKit +import MapKit + +// MARK: - ViewModelType +protocol ListCarsVMType { + // Data Source + func numberOfItems() -> Int + + func itemFor(row: Int) -> Any + + // Events + func didSelectRow(_ row: Int, from controller: UIViewController) +} + +// MARK: - ViewModelCoordinator(delegate) +protocol ListCarsViewModelCoordinatorDelegate: class { + func didSelect(carViewData: CarViewData, from controller: UIViewController) +} + + +// MARK: - ViewModelViewDelegate +//protocol ListCarsViewModelViewDelegate: class { +// func refreshScreen(with annotaions: [Car]) +//} +// Just to show the code is sOlid ( open to extension ) diff --git a/src/Cars Map/Scenes/MapCars/MapCarsCoordinator.swift b/src/Cars Map/Scenes/MapCars/MapCarsCoordinator.swift new file mode 100644 index 0000000..1ae4cc2 --- /dev/null +++ b/src/Cars Map/Scenes/MapCars/MapCarsCoordinator.swift @@ -0,0 +1,62 @@ +// +// MapCarsCoordinator.swift +// Cars Map +// +// Created by iMamad on 4/11/22. +// + +import UIKit +import MapKit + +class MapCarsCoordinator: Coordinator { + + // MARK: Properties + private weak var rootTabBarController: UITabBarController! + private let mapCarsNavigationContrller = UINavigationController() + + // MARK: VM + private let carViewDatas: [CarViewData] + private var mapCarsVM: MapCarsVM { + let mapCarsVM = MapCarsVM(carViewDatas: self.carViewDatas) + mapCarsVM.mapCarsCoordinatorDelegate = self + return mapCarsVM + } + + + // MARK: Coordinator + init(rootTabBarController: UITabBarController, carViewDatas: [CarViewData]) { + self.carViewDatas = carViewDatas + self.rootTabBarController = rootTabBarController + } + + override func start() { + + let mapCarsVC = MapCarsVC.`init`(mapCarsVM: mapCarsVM) + let tabBar = UITabBarItem(title: "Map", image: UIImage(named: "map"), tag: 0) + tabBar.badgeValue = "\(carViewDatas.count)" + mapCarsVC.tabBarItem = tabBar + + mapCarsNavigationContrller.setViewControllers([mapCarsVC], animated: true) + rootTabBarController.setViewControllers([mapCarsNavigationContrller], animated: true) + } +} + +// MARK: - ViewModel Callbacks +extension MapCarsCoordinator: MapCarsViewModelCoordinatorDelegate { + func didSelect(_ annotationView: MKAnnotationView, from mapView: MKMapView) { + guard let carAnnotationView = annotationView as? CarAnnotationView else { return } + let carViewData = carAnnotationView.carViewData + showCarInfo(of: carViewData) + } +} + +//MARK: Navigation +extension MapCarsCoordinator { + private func showCarInfo(of carViewData: CarViewData) { // it shows a modal page + let carInfoSB = UIStoryboard.init(name: "CarInfo", bundle: nil) // make a factory pattern for this class as well + let carInfoVC = carInfoSB.instantiateViewController(withIdentifier: "CarInfoVC") as! CarInfoVC + carInfoVC.carViewData = carViewData + mapCarsNavigationContrller.present(carInfoVC, + animated: true, completion: nil) + } +} diff --git a/src/Cars Map/Scenes/MapCars/Model/CarAnnotation.swift b/src/Cars Map/Scenes/MapCars/Model/CarAnnotation.swift new file mode 100644 index 0000000..60a5b5a --- /dev/null +++ b/src/Cars Map/Scenes/MapCars/Model/CarAnnotation.swift @@ -0,0 +1,25 @@ +// +// CarAnnotation.swift +// Cars Map +// +// Created by iMamad on 4/12/22. +// + +import MapKit + +class CarAnnotation: NSObject, MKAnnotation { + // default + var title: String? + var subtitle: String? + let coordinate: CLLocationCoordinate2D + + // customs + let carViewData : CarViewData! + + init(carViewData: CarViewData, coordinate: CLLocationCoordinate2D) { + self.coordinate = coordinate + self.title = carViewData.modelName + self.subtitle = carViewData.name + self.carViewData = carViewData + } +} diff --git a/src/Cars Map/Scenes/MapCars/View/CarAnnotationView.swift b/src/Cars Map/Scenes/MapCars/View/CarAnnotationView.swift new file mode 100644 index 0000000..97a7c98 --- /dev/null +++ b/src/Cars Map/Scenes/MapCars/View/CarAnnotationView.swift @@ -0,0 +1,38 @@ +// +// CarAnnotation.swift +// Cars Map +// +// Created by iMamad on 4/12/22. +// + +import MapKit + +class CarAnnotationView: MKAnnotationView { + + // MARK: Properties + // default + override var image: UIImage? { + get { return self.imageView.image } + + set { self.imageView.image = newValue } + } + // customs + var imageView: UIImageView! + let carViewData: CarViewData + + + // MARK: Init + init(carViewData: CarViewData, annotation: MKAnnotation?, reuseIdentifier: String?, desiredWidth: Int) { + self.carViewData = carViewData + super.init(annotation: annotation, reuseIdentifier: reuseIdentifier) + + self.frame = CGRect(x: 0, y: 0, width: desiredWidth, height: desiredWidth) + self.imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: desiredWidth, height: desiredWidth)) + self.imageView.contentMode = .scaleAspectFit + self.addSubview(self.imageView) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/src/Cars Map/Scenes/MapCars/View/MapCars.storyboard b/src/Cars Map/Scenes/MapCars/View/MapCars.storyboard new file mode 100644 index 0000000..b6c07ac --- /dev/null +++ b/src/Cars Map/Scenes/MapCars/View/MapCars.storyboard @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Cars Map/Scenes/MapCars/ViewController/MapCarsVC.swift b/src/Cars Map/Scenes/MapCars/ViewController/MapCarsVC.swift new file mode 100644 index 0000000..1df4b34 --- /dev/null +++ b/src/Cars Map/Scenes/MapCars/ViewController/MapCarsVC.swift @@ -0,0 +1,70 @@ +// +// MapCarsVC.swift +// Cars Map +// +// Created by iMamad on 4/11/22. +// + +import UIKit +import MapKit + + +class MapCarsVC: UIViewController { + + // MARK: Factory + class func `init`(mapCarsVM: MapCarsVM) -> MapCarsVC { + let storyboard = UIStoryboard(name: "MapCars", bundle: nil) + let vc = storyboard.instantiateViewController(withIdentifier: "MapCarsVC") as! MapCarsVC + // take care of force unwrapping above + vc.viewModel = mapCarsVM + return vc + } + + // MARK: Outlets + @IBOutlet private weak var mapView: MKMapView! + + // MARK: Properties + private var viewModel: MapCarsVM! { + didSet { viewModel.viewDelegate = self } + } + private let annotationViewID = "carAnnotationView" + private let annotationWidth = 70 + + // MARK: UIViewController + override func viewDidLoad() { + super.viewDidLoad() + mapView.delegate = self + viewModel.start() + } +} + +// MARK: - ViewModel Delegate +extension MapCarsVC: MapCarsViewModelViewDelegate { + func refreshScreen(with annotations: [MKAnnotation]) { + mapView.showAnnotations(annotations, animated: true) + } +} + +//MARK: Map Delegate +extension MapCarsVC: MKMapViewDelegate { + // annotation view + func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { + guard annotation is CarAnnotation else { return nil } + + let carViewData = viewModel.viewDataFor(annotation: annotation) + let carAnnotationView = CarAnnotationView(carViewData: carViewData, + annotation: annotation, + reuseIdentifier: annotationViewID, + desiredWidth: annotationWidth) +// newCarAnnotationView.imageView.downloaded(from: carData.carImageUrl) + carAnnotationView.image = carViewData.uiImage + return carAnnotationView + // TODO: use dequeue reuse annotation view for smoothing moves on map + } + + // didSelect Event + func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { + mapView.deselectAnnotation(view.annotation, animated: true) + viewModel.didSelectAnnotation(view: view, from: mapView) + } +} diff --git a/src/Cars Map/Scenes/MapCars/ViewModel/MapCarsVM.swift b/src/Cars Map/Scenes/MapCars/ViewModel/MapCarsVM.swift new file mode 100644 index 0000000..e0b2792 --- /dev/null +++ b/src/Cars Map/Scenes/MapCars/ViewModel/MapCarsVM.swift @@ -0,0 +1,68 @@ +// +// CatsViewModel.swift +// Cat Facts +// +// Created by iMamad on 4/8/22. +// + +import UIKit +import MapKit + +class MapCarsVM { + + // MARK: Delegates + var mapCarsCoordinatorDelegate: MapCarsViewModelCoordinatorDelegate? + var viewDelegate: MapCarsViewModelViewDelegate? + + // MARK: Properties + private let carViewDatas: [CarViewData] + + // MARK: Init + init(carViewDatas: [CarViewData]) { self.carViewDatas = carViewDatas } + + func start() { + // convert cars to annotations + let carAnnotations = convertCarViewDatasToannotations(from: carViewDatas) + // call VC to refresh + viewDelegate?.refreshScreen(with: carAnnotations) + } + + private func convertCarViewDatasToannotations(from carViewDatas: [CarViewData]) -> [CarAnnotation] { + var carAnnotations: [CarAnnotation] = [] + for carViewData in carViewDatas { + let carAnnotation = CarAnnotation(carViewData: carViewData, + coordinate: carViewData.coordinate) + carAnnotations.append(carAnnotation) + } + return carAnnotations + } +} + +// MARK: - ViewModelType +extension MapCarsVM: MapCarsVMType { + + // DataSource + func viewDataFor(annotation: MKAnnotation) -> CarViewData { + let carAnnotation = annotation as! CarAnnotation + // force unwrapp because we're sure it's type of CarAnnotation + return carAnnotation.carViewData + } + // TODO: + // make an abstraction for all annotations + // do switch on annotations and pass back data to VC + // on VC assign datas to views + + // Events + func didSelectAnnotation(view: MKAnnotationView, from: MKMapView) { + // go to show modal page + // view = CarAnnotation + didSelect(view, from: from) + } +} + +// MARK: - ViewModelCoordinator +extension MapCarsVM: MapCarsViewModelCoordinatorDelegate { + func didSelect(_ annotationView: MKAnnotationView, from mapView: MKMapView) { + mapCarsCoordinatorDelegate?.didSelect(annotationView, from: mapView) + } +} diff --git a/src/Cars Map/Scenes/MapCars/ViewModel/MapCarsVMAbstractions.swift b/src/Cars Map/Scenes/MapCars/ViewModel/MapCarsVMAbstractions.swift new file mode 100644 index 0000000..934b8b2 --- /dev/null +++ b/src/Cars Map/Scenes/MapCars/ViewModel/MapCarsVMAbstractions.swift @@ -0,0 +1,31 @@ +// +// ViewModelAbstractions.swift +// Cat Facts +// +// Created by iMamad on 4/9/22. +// + +import UIKit +import MapKit + +// MARK: - ViewModelType +protocol MapCarsVMType { + + var viewDelegate: MapCarsViewModelViewDelegate? { get set } + + // Data Source + func viewDataFor(annotation: MKAnnotation) -> CarViewData + + // Events + func didSelectAnnotation(view: MKAnnotationView, from: MKMapView) +} + +// MARK: - ViewModelCoordinator(delegate) +protocol MapCarsViewModelCoordinatorDelegate { + func didSelect(_ annotationView: MKAnnotationView, from mapView: MKMapView) +} + +// MARK: - ViewModelViewDelegate +protocol MapCarsViewModelViewDelegate { + func refreshScreen(with annotaions: [MKAnnotation]) +} diff --git a/Cars Map/Scenes/ApiClient.swift b/src/Cars Map/Scenes/Service/ApiClient.swift similarity index 52% rename from Cars Map/Scenes/ApiClient.swift rename to src/Cars Map/Scenes/Service/ApiClient.swift index 6905823..edf5b8f 100644 --- a/Cars Map/Scenes/ApiClient.swift +++ b/src/Cars Map/Scenes/Service/ApiClient.swift @@ -6,13 +6,13 @@ // import Foundation - +import UIKit protocol Network { func fetch(completionHandler: @escaping (Result) -> ()) } -class ApiClient: Network { +struct ApiClient: Network { private let configuration: URLSessionConfiguration init(configuration: URLSessionConfiguration) { @@ -21,11 +21,13 @@ class ApiClient: Network { func fetch(completionHandler: @escaping (Result) -> ()) { -// if !connected() { completionHandler(.failure(CatAPIError.disconnected)) } + if !connected() { completionHandler(.failure(CarsAPIError.disconnected)) } + - let url = URL(string: "https://cat-fact.herokuapp.com/" + "facts/" + "random")! + let url = URL(string: "https://cdn.sixt.io/" + "codingtask/" + "cars")! let session = URLSession(configuration: configuration) - let task = session.dataTask(with: url, completionHandler: { (data, response, error) in + let task = session.dataTask(with: url, completionHandler: { + (data, response, error) in guard let httpResponse = response as? HTTPURLResponse else { return } let clientError = (400...499).contains(httpResponse.statusCode) @@ -38,16 +40,16 @@ class ApiClient: Network { } else if serverError { completionHandler(.failure(CarsAPIError.serverError)) } else if let data = data { -// let cat = try? JSONDecoder().decode(Cat.self, from: data) -// completionHandler(.success(cat)) + let cat = try? JSONDecoder().decode([Car].self, from: data) + completionHandler(.success(cat)) } }) task.resume() } -// private func connected() -> Bool { // to the internet -// InternetConnectionManager.shared.isConnectedToNetwork() -// } + private func connected() -> Bool { // to the internet + InternetConnectionManager.shared.isConnectedToNetwork() + } } struct CarsAPIError: Error { @@ -56,3 +58,33 @@ struct CarsAPIError: Error { static let serverError = NSError(domain: "A HTTPS server error occured.", code: 03, userInfo: nil) static let disconnected = NSError(domain: " You're not connected to the internet. So, you can't add a new cat.\n Whatever you see are offline.", code: 04, userInfo: nil) } + + +// MARK: UIImageView Image Download Extension +extension UIImageView { + func downloaded(from url: URL, contentMode mode: ContentMode = .scaleAspectFit) { + contentMode = mode + URLSession.shared.dataTask(with: url) { + data, response, error in + + if let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200, + let mimeType = response?.mimeType, mimeType.hasPrefix("image"), + let data = data, error == nil, + let image = UIImage(data: data){ + DispatchQueue.main.async() { + self.image = image + } + } + else { + DispatchQueue.main.async { + self.image = UIImage(named: "car") + } + } + + }.resume() + } + func downloaded(from link: String, contentMode mode: ContentMode = .scaleAspectFit) { + guard let url = URL(string: link) else { return } + downloaded(from: url, contentMode: mode) + } +} diff --git a/src/Cars Map/Scenes/Service/AppServices.swift b/src/Cars Map/Scenes/Service/AppServices.swift new file mode 100644 index 0000000..a349609 --- /dev/null +++ b/src/Cars Map/Scenes/Service/AppServices.swift @@ -0,0 +1,43 @@ +// +// AppServices.swift +// Cars Map +// +// Created by iMamad on 4/15/22. +// + +import Foundation + + +protocol Serviceable: class { + func fetchCars(completionHandler: @escaping (Any?, Error?) -> ()) +} + +class AppServices: Serviceable { + + private var apiClient: Network? + + // MARK: Init + init(apiClient: Network) { self.apiClient = apiClient } +} + +// MARK:- API Call +extension AppServices { + func fetchCars(completionHandler: @escaping (Any?, Error?) -> ()){ + apiClient?.fetch { (result) in + switch result { + case .success(let cat): + completionHandler(cat, nil) + case .failure(let error): + completionHandler(nil, error) + } + } + } +} + +// -- Service Layer -- +// AppServices is a general class to use all over the app +// however, the architecture is potential +// to use same service design pattern per each scene + +// I know it's much for this pet-project, but I added services layer and the statemet above +// to show how do I care about scalability & testability & loose coupling etc. of the app. diff --git a/Cars Map/Scenes/Waiting/Waiting.storyboard b/src/Cars Map/Scenes/Waiting/Waiting.storyboard similarity index 73% rename from Cars Map/Scenes/Waiting/Waiting.storyboard rename to src/Cars Map/Scenes/Waiting/Waiting.storyboard index 8b2ceb1..f1d5645 100644 --- a/Cars Map/Scenes/Waiting/Waiting.storyboard +++ b/src/Cars Map/Scenes/Waiting/Waiting.storyboard @@ -18,13 +18,13 @@ - + - + + - + @@ -45,6 +52,11 @@ + + + + + diff --git a/src/Cars Map/Scenes/Waiting/WaitingVC.swift b/src/Cars Map/Scenes/Waiting/WaitingVC.swift new file mode 100644 index 0000000..1d6c047 --- /dev/null +++ b/src/Cars Map/Scenes/Waiting/WaitingVC.swift @@ -0,0 +1,58 @@ +// +// WaitingVC.swift +// Cars Map +// +// Created by iMamad on 4/11/22. +// + +import UIKit + + +class WaitingVC: UIViewController { + + //MARK: Factory + class func `init`(waitingVM: WaitingVM) -> WaitingVC { + let storyboard = UIStoryboard(name: "Waiting", bundle: nil) + let vc = storyboard.instantiateViewController(withIdentifier: "WaitingVC") as! WaitingVC + // take care of force unwrapping above + vc.viewModel = waitingVM + return vc + } + + // MARK: Outlets + @IBOutlet private weak var acitivityIndicator: UIActivityIndicatorView! + @IBOutlet private weak var infoLabel: UILabel! + @IBOutlet private weak var retryBtn: UIButton! + + // MARK: Properties + private var viewModel: WaitingVM! { + didSet { viewModel.viewDelegate = self } + } + + // MARK: UIViewController + override func viewDidLoad() { + super.viewDidLoad() + viewModel.start() + } + + // MARK: Actions + @IBAction private func retry(_ sender: Any) { + hideError() + viewModel.retry() + } +} + +// MARK: - ViewModel Delegate +extension WaitingVC: WaitingViewModelViewDelegate { + func showError(text: String) { + infoLabel.text = text + " ☹️" + acitivityIndicator.isHidden = true + retryBtn.isHidden = false + } + + func hideError() { + infoLabel.text = "I'm calling the server, please give me a sec!\n\nπŸ“žπŸŒβ˜ΊοΈ" + acitivityIndicator.isHidden = false + retryBtn.isHidden = true + } +} diff --git a/src/Cars Map/Scenes/Waiting/WaitingVM.swift b/src/Cars Map/Scenes/Waiting/WaitingVM.swift new file mode 100644 index 0000000..2105d33 --- /dev/null +++ b/src/Cars Map/Scenes/Waiting/WaitingVM.swift @@ -0,0 +1,82 @@ +// +// WaitingViewModel.swift +// Cars Map +// +// Created by iMamad on 4/11/22. +// + +import Foundation + + +class WaitingVM { + + // MARK: Properties + private weak var appServices: Serviceable? + // delegates + var appCoordinatorDelegate: AppCoordinatorDelegate? + var viewDelegate: WaitingViewModelViewDelegate? + + private var carViewDatas : [CarViewData]? { + didSet { + DispatchQueue.main.async { + self.appCoordinatorDelegate?.dataReceived(carViewDatas: self.carViewDatas!) + } + } + } + + //MARK: Waiting VM + init(service: Serviceable) { self.appServices = service } + + func start() { fetch() } +} + +// MARK: Network +extension WaitingVM: WaitingViewModelType { + func fetch() { + appServices?.fetchCars { + [weak self] + (cars, error) in + guard let sSelf = self else { return } + + // failure + if let error = error { + let errorMessage = error.localizedDescription + sSelf.showError(with: errorMessage) + return + } + + if let cars = cars as? [Car] { + // success + sSelf.carViewDatas = cars.compactMap { CarViewData(car: $0) } + }else { + // failure + sSelf.showError(with: CarsAPIError.noData.localizedDescription) + } + } + } + + private func showError(with errorMessage: String) { + DispatchQueue.main.async { + self.viewDelegate?.showError(text: errorMessage) + } + } + + func retry() { start() } +} + +// MARK: - ViewModelType +protocol WaitingViewModelType { + func fetch() + func retry() +} + +// MARK: - ViewModelCoordinator(delegate) +protocol AppCoordinatorDelegate { + func dataReceived(carViewDatas: [CarViewData]) +} + +// MARK: - ViewModelViewDelegate +protocol WaitingViewModelViewDelegate { + func showError(text: String) + func hideError() +} diff --git a/src/Cars MapTests/Cars_MapTests.swift b/src/Cars MapTests/Cars_MapTests.swift new file mode 100644 index 0000000..92fe8c0 --- /dev/null +++ b/src/Cars MapTests/Cars_MapTests.swift @@ -0,0 +1,165 @@ +// +// Cars_MapTests.swift +// Cars MapTests +// +// Created by iMamad on 4/13/22. +// + +import XCTest +@testable import Cars_Map + +class Cars_MapTests: XCTestCase { + +} + +// MARK: TabController tests +extension Cars_MapTests { + + // TODO: test below violates SRP, cause it tests more than 1 thing (badges and titles etc.) + // it'd be great to save rootTabBar and cars in test class + // and then separate function test below to several functinos + func testTabBarController() { + + // -- start tab tests -- + // - given - + let rootTabBarController = UITabBarController() + let carViewDatas = makeCarViewDatas() + + // first tab + let mapCarsCoordinator = MapCarsCoordinator(rootTabBarController: rootTabBarController, + carViewDatas: carViewDatas) + mapCarsCoordinator.start() + // second tab + let listCarsCoordinator = ListCarsCoordinator(rootTabBarController: rootTabBarController, + carViewDatas: carViewDatas) + listCarsCoordinator.start() + + // - when - 1st tab + let mapNav = rootTabBarController.viewControllers?[0] as? UINavigationController + let mapVC = mapNav?.viewControllers.first as? MapCarsVC + let mapTitle = mapVC?.title + // - then - 1st tab + XCTAssertEqual(mapTitle, "Map") + + // - when - 2nd tab + let listNav = rootTabBarController.viewControllers?[1] as? UINavigationController + let listVC = listNav?.viewControllers.first as? ListCarsVC + let listTitle = listVC?.title + // - then - 2nd tab + XCTAssertEqual(listTitle, "List") + // -- end tab tests -- + + + // - when - badge + let badgeValue = mapVC!.tabBarItem.badgeValue! + let carsCount = "\(carViewDatas.count)" + // - then - badge + XCTAssertEqual(badgeValue, carsCount) + + // - when - tab count + let tabs = rootTabBarController.tabBar.items + let tabsCount = tabs?.count + let allTabs = 2 + // - then - tab count + XCTAssertEqual(tabsCount, allTabs) + } +} + +// MARK: ListCars tests +extension Cars_MapTests { + func testListCarsTableView() { + + // - given - + let cars = makeCarViewDatas() + let listCarsVM = ListCarsVM(carViewDatas: cars) + let listCarsVC = ListCarsVC.`init`(listCarsVM: listCarsVM) + let _ = listCarsVC.view + + // - when - cells + let actualFirstCell = listCarsVC.tableView(listCarsVC.tableViewCars, cellForRowAt: IndexPath(row: 0, section: 0)) + let cellText = actualFirstCell.textLabel?.text + let firstCar = cars[0] + + // - then - cells + XCTAssertNotNil(actualFirstCell) + // text + XCTAssertEqual(cellText, firstCar.modelName) + // accessor type + XCTAssertEqual(actualFirstCell.accessoryType, .disclosureIndicator) + + + // - when - table view rows + let carsCount = cars.count + let tblViewRows = listCarsVC.tableViewCars.numberOfRows(inSection: 0) + // - then - table view rows + XCTAssertEqual(tblViewRows, carsCount) + } + + func testListCarsVM() { + + // - given - + let carViewDatas = makeCarViewDatas() + let listCarsVM = ListCarsVM(carViewDatas: carViewDatas) + let listCarsVC = ListCarsVC.`init`(listCarsVM: listCarsVM) + let _ = listCarsVC.view + + // - when - cells + let actual_Cell = listCarsVC.tableView(listCarsVC.tableViewCars, cellForRowAt: IndexPath(row: 0, section: 0)) + let vmViewData = listCarsVM.itemFor(row: 0) as! CarViewData + // - then - cells + XCTAssertNotNil(actual_Cell) + XCTAssertNotNil(vmViewData) + + + // - when - cell text + let vcCellText = actual_Cell.textLabel?.text + let vmCellText = vmViewData.modelName + // - then - cell text + XCTAssertEqual(vcCellText, vmCellText) + + // - when - accessory type + let vcCellAT = actual_Cell.accessoryType + // - then - accessory type + XCTAssertEqual(vcCellAT, .disclosureIndicator) + } +} + +// MARK: helpers +extension Cars_MapTests { + func makeCarViewDatas() -> [CarViewData] { + let car2 = Car(id: "WBAUE51070P352494", + modelName: "BMW 1er", + name: "Lasse", + make: "BMW", + color: "saphirschwarz", + fuelLevel: 0.92, + latitude: 48.170041, + longitude: 11.576643, + carImageUrl: "https://cdn.sixt.io/codingtask/images/bmw_1er.png") + + let car1 = Car(id: "WMWSW31030T222518", + modelName: "MINI", + name: "Vanessa", + make: "BMW", + color: "midnight_black", + fuelLevel: 0.7, + latitude: 48.134557, + longitude: 11.576921, + carImageUrl: "https://cdn.sixt.io/codingtask/images/mini.png") + return [car1, car2].compactMap { CarViewData(car: $0)} + } + + func makeMapCarsVC(mapCarsVM: MapCarsVM) -> MapCarsVC { + let mapCarsVC = MapCarsVC.`init`(mapCarsVM: mapCarsVM) + // Trigger view load and viewDidLoad() + _ = mapCarsVC.view + return mapCarsVC + } +} + + +// TODO: good to write tests for +// - checking fall-back images of cars +// - mapkit testing +// - modal page testing +// - waiting page and AppServices diff --git a/src/Cars MapTests/Info.plist b/src/Cars MapTests/Info.plist new file mode 100644 index 0000000..64d65ca --- /dev/null +++ b/src/Cars MapTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + +