├── home.png ├── MVVM-Clean ├── Assets │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── ItunesArtwork@2x.png │ │ │ ├── Icon-App-20x20@2x-1.png │ │ │ ├── Icon-App-29x29@2x-1.png │ │ │ ├── Icon-App-40x40@2x-1.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ │ └── user-profile.imageset │ │ │ ├── user-profile-1.png │ │ │ ├── user-profile-2.png │ │ │ ├── user-profile.png │ │ │ └── Contents.json │ └── Colors.xcassets │ │ ├── Contents.json │ │ ├── Primary.colorset │ │ └── Contents.json │ │ ├── PrimaryText.colorset │ │ └── Contents.json │ │ ├── Secondary.colorset │ │ └── Contents.json │ │ ├── PrimaryButton.colorset │ │ └── Contents.json │ │ ├── SecondaryText.colorset │ │ └── Contents.json │ │ └── TextfieldBackground.colorset │ │ └── Contents.json ├── Data │ ├── Models │ │ ├── User.swift │ │ ├── Country.swift │ │ ├── Summary.swift │ │ └── CustomError.swift │ ├── Network │ │ ├── Providers │ │ │ ├── MainProviders │ │ │ │ ├── APIConfiguration.swift │ │ │ │ ├── NetworkConstant.swift │ │ │ │ └── NetworkRequestPerfomer.swift │ │ │ ├── Summary │ │ │ │ ├── SummaryNetworkRequest.swift │ │ │ │ ├── SummaryNetworkProtocolRequest.swift │ │ │ │ ├── SummaryRouterSwift.swift │ │ │ │ └── SummaryRouter.swift │ │ │ └── Country │ │ │ │ ├── CountryNetworkRequest.swift │ │ │ │ ├── CountryNetworkProtocolRequest.swift │ │ │ │ └── CountryRouter.swift │ │ ├── DTO │ │ │ ├── SummaryDTO.swift │ │ │ ├── GlobalDTO.swift │ │ │ └── CountryDTO.swift │ │ └── Mappers │ │ │ ├── Country │ │ │ └── CountryDTOMapper.swift │ │ │ └── Summary │ │ │ └── SummaryDTOMapper.swift │ ├── UseCases │ │ ├── Summary │ │ │ ├── SummaryUseCaseDelegate.swift │ │ │ ├── SummaryUseCase.swift │ │ │ └── SummaryUseCaseResponseDelegate.swift │ │ ├── Profile │ │ │ ├── ProfileUseCaseDelegate.swift │ │ │ ├── LogoutUseCase.swift │ │ │ ├── ProfileUseCaseResponseDelegate.swift │ │ │ └── ProfileUseCase.swift │ │ ├── Country │ │ │ ├── CountryUseCaseResponseDelegate.swift │ │ │ ├── CountryUseCaseDelegate.swift │ │ │ └── CountryUseCase.swift │ │ └── Login │ │ │ ├── LoginUseCaseDelegate.swift │ │ │ ├── LoginUseCaseResponseDelegate.swift │ │ │ └── LoginUseCase.swift │ ├── Persistence │ │ ├── Summary │ │ │ ├── SummaryPersistenceProtocolRequest.swift │ │ │ └── SummaryPersistenceRequest.swift │ │ ├── Country │ │ │ ├── CountryPersistenceProtocolRequest.swift │ │ │ └── CountryPersistenceRequest.swift │ │ └── Profile │ │ │ ├── ProfilePersistenceProtocolData.swift │ │ │ ├── ProfileLocalRequest.swift │ │ │ └── ProfilePersistenceData.swift │ └── Repositories │ │ ├── Login │ │ ├── LoginRepositoryDelegate.swift │ │ └── LoginRepository.swift │ │ ├── Summary │ │ ├── SummaryRepositoryDelegate.swift │ │ └── SummaryRepository.swift │ │ ├── Country │ │ ├── CountryRepository.swift │ │ └── CountryRepositoryDelegate.swift │ │ └── Profile │ │ ├── ProfileRepository.swift │ │ └── ProfileRepositoryDelegate.swift ├── Presentations │ ├── UI │ │ ├── Splash │ │ │ ├── ViewModel │ │ │ │ ├── SplashScreenViewModelInputDelegate.swift │ │ │ │ ├── SplashScreenViewModelOutputDelegate.swift │ │ │ │ └── SplashScreenViewModel.swift │ │ │ ├── Router │ │ │ │ ├── SplashConfigurator.swift │ │ │ │ └── SplashRouter.swift │ │ │ └── ViewControllers │ │ │ │ └── SplashViewController.swift │ │ ├── BaseViewController.swift │ │ ├── Authentication │ │ │ ├── ViewModel │ │ │ │ ├── LoginViewModelOutputDelegate.swift │ │ │ │ ├── LoginViewModelInputDelegate.swift │ │ │ │ └── LoginViewModel.swift │ │ │ ├── Router │ │ │ │ ├── AuthNavigationConfigurator.swift │ │ │ │ └── AuthNavigationRouter.swift │ │ │ └── ViewControllers │ │ │ │ └── LoginViewController.swift │ │ └── Main │ │ │ ├── Summary │ │ │ ├── ViewModel │ │ │ │ ├── SummaryCovidViewModelInputDelegate.swift │ │ │ │ ├── SummaryCovidViewModelOutputDelegate.swift │ │ │ │ └── SummaryCovidViewModel.swift │ │ │ ├── Router │ │ │ │ ├── SummaryNavigatorConfigurator.swift │ │ │ │ └── SummaryNavigationRouter.swift │ │ │ ├── Cell │ │ │ │ └── SummaryTableViewCell.swift │ │ │ └── ViewControllers │ │ │ │ └── SummaryViewController.swift │ │ │ ├── Country │ │ │ ├── ViewModel │ │ │ │ ├── CountryCovidViewModelInputDelegate.swift │ │ │ │ ├── CountryCovidViewModelOutputDelegate.swift │ │ │ │ └── CountryCovidViewModel.swift │ │ │ ├── Router │ │ │ │ ├── CountryNavigationRouter.swift │ │ │ │ └── CountryNavigationConfigurator.swift │ │ │ ├── Cell │ │ │ │ └── CountryTableViewCell.swift │ │ │ └── ViewControllers │ │ │ │ └── CountryListViewController.swift │ │ │ └── Profile │ │ │ ├── ViewModel │ │ │ ├── ProfileViewModelOutputDelegate.swift │ │ │ ├── ProfileViewModelInputDelegate.swift │ │ │ └── ProfileViewModel.swift │ │ │ ├── Router │ │ │ ├── ProfileNavigationConfigurator.swift │ │ │ └── ProfileNavigationRouter.swift │ │ │ └── ViewContoller │ │ │ └── ProfileViewController.swift │ └── Storyboard │ │ └── Splash │ │ └── Splash.storyboard ├── Application │ ├── Configurations │ │ ├── Config.xcconfig │ │ └── Configurations.swift │ └── DI │ │ ├── Extensions │ │ └── Assembler+Extensions.swift │ │ ├── PersistenceAssembly.swift │ │ ├── NetworkAssembly.swift │ │ ├── UseCaseAssembly.swift │ │ ├── ViewModelAssembly.swift │ │ ├── RepositoryAssembly.swift │ │ └── ViewControllerAssembly.swift ├── Extensions │ ├── UIStoryboad+Extensions.swift │ ├── String+Extensions.swift │ ├── UIView+Extensions.swift │ ├── UIViewController+Extensions.swift │ └── UIColor+Extension.swift ├── Utility │ ├── Loadable │ │ ├── LoadingView.swift │ │ ├── LoadingView.xib │ │ └── Loadable.swift │ ├── Date │ │ └── CustomDateUtils.swift │ └── Log │ │ └── CustomConsoleLog.swift ├── Localization │ ├── Localized.swift │ ├── en.lproj │ │ └── Localizable.strings │ └── it.lproj │ │ └── Localizable.strings ├── AppDelegate │ ├── AppDelegate.swift │ └── SceneDelegate.swift ├── Infrastructure │ └── Network │ │ ├── HTTPMethodSwift.swift │ │ ├── AsyncNetworkPerformer.swift │ │ └── APIConfigurationSwift.swift ├── Base.lproj │ └── LaunchScreen.storyboard └── Info.plist ├── Podfile.lock ├── Podfile ├── MVVM-CleanTests ├── SummaryTest │ ├── SummaryTest.swift │ ├── SummaryDataRequestTest.swift │ └── SummaryIntegrationTest.swift └── Info.plist ├── MVVM-CleanUITests ├── Info.plist └── MVVM_CleanUITests.swift ├── CHANGELOG.md ├── .gitignore ├── README.md └── MVVM-Clean.xcodeproj └── xcshareddata └── xcschemes └── MVVM-Clean.xcscheme /home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alemarcon/MVVM-iOS-Clean/HEAD/home.png -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Colors.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alemarcon/MVVM-iOS-Clean/HEAD/MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alemarcon/MVVM-iOS-Clean/HEAD/MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alemarcon/MVVM-iOS-Clean/HEAD/MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alemarcon/MVVM-iOS-Clean/HEAD/MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alemarcon/MVVM-iOS-Clean/HEAD/MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alemarcon/MVVM-iOS-Clean/HEAD/MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alemarcon/MVVM-iOS-Clean/HEAD/MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alemarcon/MVVM-iOS-Clean/HEAD/MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alemarcon/MVVM-iOS-Clean/HEAD/MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alemarcon/MVVM-iOS-Clean/HEAD/MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alemarcon/MVVM-iOS-Clean/HEAD/MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alemarcon/MVVM-iOS-Clean/HEAD/MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alemarcon/MVVM-iOS-Clean/HEAD/MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alemarcon/MVVM-iOS-Clean/HEAD/MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Assets.xcassets/user-profile.imageset/user-profile-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alemarcon/MVVM-iOS-Clean/HEAD/MVVM-Clean/Assets/Assets.xcassets/user-profile.imageset/user-profile-1.png -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Assets.xcassets/user-profile.imageset/user-profile-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alemarcon/MVVM-iOS-Clean/HEAD/MVVM-Clean/Assets/Assets.xcassets/user-profile.imageset/user-profile-2.png -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Assets.xcassets/user-profile.imageset/user-profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alemarcon/MVVM-iOS-Clean/HEAD/MVVM-Clean/Assets/Assets.xcassets/user-profile.imageset/user-profile.png -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alemarcon/MVVM-iOS-Clean/HEAD/MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alemarcon/MVVM-iOS-Clean/HEAD/MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alemarcon/MVVM-iOS-Clean/HEAD/MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alemarcon/MVVM-iOS-Clean/HEAD/MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Swinject (2.7.1) 3 | 4 | DEPENDENCIES: 5 | - Swinject 6 | 7 | SPEC REPOS: 8 | trunk: 9 | - Swinject 10 | 11 | SPEC CHECKSUMS: 12 | Swinject: ddf78b8486dd9b71a667b852cad919ab4484478e 13 | 14 | PODFILE CHECKSUM: 504dccf8418412ce4e076ce9a839cb0ca551b853 15 | 16 | COCOAPODS: 1.11.2 17 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Models/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct User { 12 | var username: String 13 | 14 | init() { 15 | username = "" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Splash/ViewModel/SplashScreenViewModelInputDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplashScreenViewModelInputDelegate.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol SplashScreenViewModelInputDelegate { 12 | func checkUserState() 13 | } 14 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/BaseViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseViewController.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class BaseViewController: UIViewController, Loadable { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /MVVM-Clean/Application/Configurations/Config.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Config.xcconfig 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 30/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | // Configuration settings file format documentation can be found at: 10 | // https://help.apple.com/xcode/#/dev745c5c974 11 | 12 | API_BASE_URL = https:/$()/api.covid19api.com 13 | NETWORK_REQUEST_HOURS_DELTA = 12 14 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | platform :ios, '13.4' 3 | 4 | target 'MVVM-Clean' do 5 | # Comment the next line if you don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for MVVM-Clean 9 | pod 'Swinject' 10 | # pod 'Alamofire', '~> 5.0.0-rc.3' 11 | 12 | target 'MVVM-CleanTests' do 13 | inherit! :search_paths 14 | # Pods for testing 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Authentication/ViewModel/LoginViewModelOutputDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginViewModelOutputDelegate.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | protocol LoginViewModelOutputDelegate { 13 | var status: CurrentValueSubject { get set } 14 | var error: CustomError? { get } 15 | } 16 | -------------------------------------------------------------------------------- /MVVM-CleanTests/SummaryTest/SummaryTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SummaryTest.swift 3 | // MVVM-CleanTests 4 | // 5 | // Created by Alessandro Marcon on 20/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import MVVM_Clean 11 | 12 | class SummaryTest: XCTestCase { 13 | 14 | func testSummary() throws { 15 | let summaryTest = SummaryIntegrationTest(xcTestCase: self) 16 | summaryTest.runSummaryDataTest() 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Authentication/ViewModel/LoginViewModelInputDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginViewModelInputDelegate.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol LoginViewModelInputDelegate { 12 | 13 | var loginUseCase: LoginUseCaseDelegate? { get } 14 | 15 | /// Execute login action 16 | func executeLogin(username: String, password: String) 17 | } 18 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Main/Summary/ViewModel/SummaryCovidViewModelInputDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogoutViewModelInputDelegate.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol SummaryCovidViewModelInputDelegate { 12 | 13 | var summaryUseCaseAsync: SummaryUseCaseAsyncDelegate? { get } 14 | 15 | /// Get COVID 19 summary data 16 | func summaryDataAsync() 17 | 18 | } 19 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Splash/ViewModel/SplashScreenViewModelOutputDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplashScreenViewModelOutputDelegate.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | protocol SplashScreenViewModelOutputDelegate { 13 | var status: CurrentValueSubject { get set } 14 | var profileUseCase: ProfileUseCaseDelegate? { get } 15 | } 16 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Main/Country/ViewModel/CountryCovidViewModelInputDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountryCovidViewModelInputDelegate.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 06/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol CountryCovidViewModelInputDelegate { 12 | 13 | var countryAsyncUseCase: CountryUseCaseAsyncDelegate? { get } 14 | 15 | /// Get COVID 19 country list data 16 | func countryList() 17 | 18 | } 19 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Main/Profile/ViewModel/ProfileViewModelOutputDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileViewModelOutputDelegate.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 17/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | protocol ProfileViewModelOutputDelegate { 13 | var status: CurrentValueSubject { get set } 14 | var error: CustomError? { get set } 15 | var currentUser: User? { get set } 16 | } 17 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Main/Summary/ViewModel/SummaryCovidViewModelOutputDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogoutViewModelOutputDelegate.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | protocol SummaryCovidViewModelOutputDelegate { 13 | var status: CurrentValueSubject { get set } 14 | var error: CustomError? { get } 15 | var summary: Summary? { get set } 16 | } 17 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Main/Country/ViewModel/CountryCovidViewModelOutputDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountryCovidViewModelOutputDelegate.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 06/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | protocol CountryCovidViewModelOutputDelegate { 13 | var status: CurrentValueSubject { get set } 14 | var error: CustomError? { get } 15 | var countries: [Country]? { get } 16 | } 17 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Main/Profile/ViewModel/ProfileViewModelInputDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileViewModelInputDelegate.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 17/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol ProfileViewModelInputDelegate { 12 | 13 | var profileUseCase: ProfileUseCaseDelegate? { get } 14 | 15 | /// Get current user data 16 | func getUserData() 17 | 18 | /// Logout user from app 19 | func logoutUser() 20 | 21 | } 22 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Network/Providers/MainProviders/APIConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIConfiguration.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 30/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | //import Foundation 10 | //import Alamofire 11 | ///** 12 | // This is the base protocol used to configure all API router object 13 | // */ 14 | //protocol APIConfiguration: URLRequestConvertible { 15 | // var method: HTTPMethod { get } 16 | // var path: String { get } 17 | // var parameters: Parameters? { get } 18 | //} 19 | -------------------------------------------------------------------------------- /MVVM-Clean/Extensions/UIStoryboad+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIStoryboad+Extensions.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIStoryboard { 12 | 13 | enum Storyboard: String { 14 | case Splash 15 | case Auth 16 | case Main 17 | } 18 | 19 | convenience init(_ storyboard: Storyboard, bundle: Bundle? = nil) { 20 | self.init(name: storyboard.rawValue, bundle: bundle) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/UseCases/Summary/SummaryUseCaseDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Covid19UseCaseDelegate.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 03/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol SummaryUseCaseAsyncDelegate { 12 | 13 | //MARK: - Protocol properties 14 | var summaryRepository: SummaryRepositoryAsyncDelegate? { get set } 15 | 16 | /// Get summary data 17 | /// - Returns: Return Summary object 18 | func getAsyncSummaryData() async throws -> Summary 19 | } 20 | -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Assets.xcassets/user-profile.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "user-profile.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "user-profile-1.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "user-profile-2.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/UseCases/Profile/ProfileUseCaseDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogoutUseCaseDelegate.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol ProfileUseCaseDelegate { 12 | 13 | var responseDelegate: ProfileUseCaseResponseDelegate? { get set } 14 | var profileRepository: ProfileRepositoryDelegate? { get set } 15 | 16 | /// Execute logout 17 | func logout() 18 | 19 | /// Get current user data from local repository 20 | func getCurrentUserData() 21 | } 22 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Network/Providers/Summary/SummaryNetworkRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Covid19NetworkRequest.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 03/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class SummaryNetworkAsyncReqeust: SummaryNetworkProtocolAsyncRequest { 12 | 13 | func getSummaryData() async throws -> SummaryDTO { 14 | let covidAsyncRouter = SummaryRouterSwift.getSummaryData 15 | return try await AsyncNetworkPerformer.sendRequest(route: covidAsyncRouter, responseDTO: SummaryDTO.self) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/UseCases/Summary/SummaryUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Covid19UseCase.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 03/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class SummaryAsyncUseCase: SummaryUseCaseAsyncDelegate { 12 | 13 | var summaryRepository: SummaryRepositoryAsyncDelegate? 14 | 15 | func getAsyncSummaryData() async throws -> Summary { 16 | guard let summary = try await summaryRepository?.getSummaryData() else { 17 | throw CustomError.nilData 18 | } 19 | return summary 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Main/Country/Router/CountryNavigationRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountryNavigationRouter.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 08/04/22. 6 | // Copyright © 2022 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | //MARK: - Protocol 12 | protocol CountryNavigationRouterInput { 13 | var vc: CountryListViewController! { get } 14 | } 15 | 16 | //MARK: - Implementation 17 | class CountryNavigationRouter: CountryNavigationRouterInput { 18 | weak var vc: CountryListViewController! 19 | 20 | init(vc: CountryListViewController) { 21 | self.vc = vc 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/UseCases/Summary/SummaryUseCaseResponseDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Covid19UseCaseResponseDelegate.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 03/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol SummaryUseCaseResponseDelegate: AnyObject { 12 | 13 | /// Called on success received summary data 14 | /// - Parameter summary: Summary data model 15 | func onSummaryDataReceived(summary: Summary) 16 | 17 | /// Called on summary data failure event 18 | /// - Parameter error: CustomError object 19 | func onSummaryDataFailure(error: CustomError) 20 | } 21 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/UseCases/Country/CountryUseCaseResponseDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountryUseCaseResponseDelegate.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 06/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol CountryUseCaseResponseDelegate: AnyObject { 12 | 13 | /// Called on success received summary data 14 | /// - Parameter summary: Summary data model 15 | func onCountryDataReceived(countries: [Country]) 16 | 17 | /// Called on summary data failure event 18 | /// - Parameter error: CustomError object 19 | func onCountryDataFailure(error: CustomError) 20 | } 21 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/UseCases/Login/LoginUseCaseDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginUseCaseDelegate.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol LoginUseCaseDelegate { 12 | 13 | var responseDelegate: LoginUseCaseResponseDelegate? { get set } 14 | var loginRepository: LoginRepositoryDelegate? { get set } 15 | 16 | /// Method that execute code for start login 17 | /// - Parameters: 18 | /// - username: Username string 19 | /// - password: Password string 20 | func startLogin(username: String, password: String) 21 | } 22 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Persistence/Summary/SummaryPersistenceProtocolRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SummaryPersistenceProtocolRequest.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 03/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Protocol 12 | protocol SummaryPersistenceProtocolRequest { 13 | 14 | /// Save summary data in local storage. 15 | /// - Parameter data: Summary dto data to save locally 16 | func saveLocalSummaryDTO(data: SummaryDTO) 17 | 18 | /// Get Summary data from locale storage. If there is no data, nil will be returned 19 | func getLocalSummaryDataDTO() -> SummaryDTO? 20 | } 21 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/UseCases/Login/LoginUseCaseResponseDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginUseCaseResponseDelegate.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol LoginUseCaseResponseDelegate: AnyObject { 12 | 13 | /// Event fired on login process successed 14 | /// - Parameter user: The UserModel object corresponding to current user logged in 15 | func onLoginSuccess(user: User) 16 | 17 | /// Event fired in case of login failed. 18 | /// - Parameter error: The error object 19 | func onLoginFailure(error: CustomError) 20 | 21 | } 22 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Persistence/Country/CountryPersistenceProtocolRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountryPersistenceProtocolRequest.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Protocol 12 | protocol CountryPersistenceProtocolRequest { 13 | 14 | /// Save CountryModel data array locally. 15 | /// - Parameter data: The Country dto array to save locally 16 | func saveLocalCountryDataDTO(data: [CountryDTO]) 17 | 18 | /// Get country data saved locally. If there is no data, nil will be returned. 19 | func getLocalCountryDataDTO() -> [CountryDTO]? 20 | } 21 | -------------------------------------------------------------------------------- /MVVM-Clean/Extensions/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extensions.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | import UIKit 12 | 13 | extension String { 14 | 15 | 16 | /// Check if password is valid 17 | /// - Parameter minSizePassword: Min number of char for password lenght 18 | /// - Returns: TRUE is password is valid FALSE otherwise 19 | func isValidPassword(minSizePassword: Int) -> Bool { 20 | if( self.count >= minSizePassword ) { 21 | return true 22 | } else { 23 | return false 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Splash/Router/SplashConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplashConfigurator.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 07/04/22. 6 | // Copyright © 2022 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Swinject 11 | 12 | protocol SplashConfiguratorInput { 13 | func configure() -> SplashViewController 14 | } 15 | 16 | class SplashConfigurator: SplashConfiguratorInput { 17 | 18 | func configure() -> SplashViewController { 19 | guard let controller = Assembler.sharedAssembler.resolver.resolve(SplashViewController.self) else { 20 | return SplashViewController() 21 | } 22 | 23 | return controller 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Network/Providers/Country/CountryNetworkRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountryNetworkRequest.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 06/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class CountryNetworkAsyncRequest: CountryNetworkProtocolAsyncRequest { 12 | 13 | func getByCountryByStatus(countrySlug: String, status: Covid19Status, from: String, to: String) async throws -> [CountryDTO] { 14 | let countryRouter = CountryRouterSwift.getByCountryByStatus(countrySlug: countrySlug, status: status, from: from, to: to) 15 | return try await AsyncNetworkPerformer.sendRequest(route: countryRouter, responseDTO: [CountryDTO].self) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Models/Country.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Country.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Country { 12 | 13 | var countryName: String 14 | var newConfirmed: String 15 | var totalConfirmed: String 16 | var newDeaths: String 17 | var totalDeaths: String 18 | var newRecovered: String 19 | var totalRecovered: String 20 | 21 | init() { 22 | countryName = "" 23 | newConfirmed = "" 24 | totalConfirmed = "" 25 | newDeaths = "" 26 | totalDeaths = "" 27 | newRecovered = "" 28 | totalRecovered = "" 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Persistence/Profile/ProfilePersistenceProtocolData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfilePersistenceProtocolData.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 17/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Protocol 12 | protocol ProfilePersistenceProtocolData { 13 | 14 | /// Get local user data. If there is no data, nil will be returned. 15 | func getLocalUserData() -> User? 16 | 17 | /// Save current user data locally. 18 | /// - Parameter currentUser: The current UserModel data to save 19 | func saveLocalUserData(currentUser: User) 20 | 21 | /// Delete local user data 22 | func deleteLocalUserData() 23 | 24 | 25 | } 26 | -------------------------------------------------------------------------------- /MVVM-Clean/Utility/Loadable/LoadingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingView.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class LoadingView: UIView { 12 | 13 | static let NIB_NAME = "LoadingView" 14 | 15 | @IBOutlet var activityIndicator: UIActivityIndicatorView! 16 | 17 | weak var delegate: LoadingViewDelegate? 18 | 19 | override func layoutSubviews() { 20 | super.layoutSubviews() 21 | } 22 | 23 | func onBackButtonAction() { 24 | delegate?.onLoadingViewBackButton() 25 | } 26 | } 27 | 28 | 29 | protocol LoadingViewDelegate: AnyObject { 30 | func onLoadingViewBackButton() 31 | } 32 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Repositories/Login/LoginRepositoryDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginRepository.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol LoginRepositoryDelegate { 12 | 13 | var profileProtocol: ProfilePersistenceProtocolData? { get set } 14 | 15 | /// Execute login request and get 16 | /// - Parameters: 17 | /// - username: The username string 18 | /// - password: The password string 19 | /// - success: success event of login 20 | /// - failure: failure event of login 21 | func login(username: String, password: String, success: @escaping (User)->Void, failure: @escaping (CustomError)->Void) 22 | } 23 | -------------------------------------------------------------------------------- /MVVM-Clean/Localization/Localized.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Localized.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 29/03/22. 6 | // Copyright © 2022 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum Localized: String, CaseIterable { 12 | case error_404 13 | case nil_data_msg 14 | case generic_localized_net_error_message 15 | case generic_error_message 16 | case no_content_found 17 | case unauthorized_message 18 | case badRequest_message 19 | case not_found_message 20 | case forbidden_message 21 | case user_not_found_message 22 | case logout_error_message 23 | case login_error_message 24 | case no_connection_error_message 25 | case url_request_nil 26 | case no_local_data_found 27 | case json_decode_error 28 | } 29 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/UseCases/Profile/LogoutUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogoutUseCase.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class SessionUseCase: NSObject, SessionUseCaseDelegate { 12 | 13 | var responseDelegate: LogoutUseCaseResponseDelegate? 14 | var logoutRepository: LogoutRepositoryDelegate? 15 | 16 | /// Implementation of logout function 17 | func logout() { 18 | print("Execute logout") 19 | logoutRepository?.logout(success: { (success) in 20 | self.responseDelegate?.onLogoutSuccess() 21 | }, failure: { (error) in 22 | self.responseDelegate?.onLogoutFailure(error: error) 23 | }) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Authentication/Router/AuthNavigationConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthNavigationConfigurator.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 07/04/22. 6 | // Copyright © 2022 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Swinject 11 | 12 | protocol AuthNavigationConfiguratorInput { 13 | func configure() -> LoginViewController 14 | } 15 | 16 | class AuthNavigationConfigurator: AuthNavigationConfiguratorInput { 17 | 18 | func configure() -> LoginViewController { 19 | guard let controller = Assembler.sharedAssembler.resolver.resolve(LoginViewController.self) else { 20 | LOGE("LoginViewController not resolved!") 21 | return LoginViewController() 22 | } 23 | return controller 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MVVM-CleanTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /MVVM-CleanUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/UseCases/Login/LoginUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginUseCase.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class LoginUseCase: NSObject, LoginUseCaseDelegate { 12 | 13 | var loginRepository: LoginRepositoryDelegate? 14 | var responseDelegate: LoginUseCaseResponseDelegate? 15 | 16 | func startLogin(username: String, password: String) { 17 | print("Start login") 18 | loginRepository?.login(username: username, password: password, success: { (userModel) in 19 | self.responseDelegate?.onLoginSuccess(user: userModel) 20 | }, failure: { (error) in 21 | self.responseDelegate?.onLoginFailure(error: error) 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Main/Summary/Router/SummaryNavigatorConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SummaryNavigatorConfigurator.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 07/04/22. 6 | // Copyright © 2022 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Swinject 11 | 12 | protocol SummaryNavigationConfiguratorInput { 13 | func configure() -> SummaryViewController 14 | } 15 | 16 | class SummaryNavigatorConfigurator: SummaryNavigationConfiguratorInput { 17 | 18 | func configure() -> SummaryViewController { 19 | guard let controller = Assembler.sharedAssembler.resolver.resolve(SummaryViewController.self) else { 20 | LOGE("SummaryViewController not resolved!") 21 | return SummaryViewController() 22 | } 23 | return controller 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Main/Profile/Router/ProfileNavigationConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileNavigationConfigurator.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 07/04/22. 6 | // Copyright © 2022 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Swinject 11 | 12 | protocol ProfileNavigationConfiguratorInput { 13 | func configure() -> ProfileViewController 14 | } 15 | 16 | class ProfileNavigationConfigurator: ProfileNavigationConfiguratorInput { 17 | 18 | func configure() -> ProfileViewController { 19 | guard let controller = Assembler.sharedAssembler.resolver.resolve(ProfileViewController.self) else { 20 | LOGE("ProfileViewController not resolved!") 21 | return ProfileViewController() 22 | } 23 | return controller 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Main/Country/Router/CountryNavigationConfigurator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountryNavigationConfigurator.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 08/04/22. 6 | // Copyright © 2022 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Swinject 11 | 12 | protocol CountryNavigationConfiguratorInput { 13 | func configure() -> CountryListViewController 14 | } 15 | 16 | class CountryNavigationConfigurator: CountryNavigationConfiguratorInput { 17 | 18 | func configure() -> CountryListViewController { 19 | guard let controller = Assembler.sharedAssembler.resolver.resolve(CountryListViewController.self) else { 20 | LOGE("CountryListViewController not resolved!") 21 | return CountryListViewController() 22 | } 23 | return controller 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Colors.xcassets/Primary.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x56", 9 | "green" : "0x2A", 10 | "red" : "0x19" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x56", 27 | "green" : "0x2A", 28 | "red" : "0x19" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Colors.xcassets/PrimaryText.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xE1", 9 | "green" : "0xDD", 10 | "red" : "0xDC" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0xE1", 27 | "green" : "0xDD", 28 | "red" : "0xDC" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Colors.xcassets/Secondary.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x75", 9 | "green" : "0x3C", 10 | "red" : "0x27" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x75", 27 | "green" : "0x3C", 28 | "red" : "0x27" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Colors.xcassets/PrimaryButton.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xE6", 9 | "green" : "0x97", 10 | "red" : "0x00" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0xE6", 27 | "green" : "0x97", 28 | "red" : "0x00" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Colors.xcassets/SecondaryText.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x2C", 9 | "green" : "0xB1", 10 | "red" : "0xE1" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x2C", 27 | "green" : "0xB1", 28 | "red" : "0xE1" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Colors.xcassets/TextfieldBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xE1", 9 | "green" : "0xDD", 10 | "red" : "0xDC" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0xE1", 27 | "green" : "0xDD", 28 | "red" : "0xDC" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Repositories/Summary/SummaryRepositoryDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Covid19RepositoryDelegate.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 03/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol SummaryRepositoryAsyncDelegate { 12 | 13 | //MARK: - Data object 14 | var covidNetwork: SummaryNetworkProtocolAsyncRequest? { get } 15 | var summaryLocal: SummaryPersistenceProtocolRequest? { get } 16 | var countryLocal: CountryPersistenceProtocolRequest? { get } 17 | 18 | //MARK: - Test variable 19 | var isRunningFromTest: Bool? { get set } 20 | 21 | //MARK: - Methods 22 | /// Get COVID19 summary data. Implementation could get data or from HTTP network call, or if present, from local data. 23 | /// - Returns: <#description#> 24 | func getSummaryData() async throws -> Summary 25 | } 26 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/UseCases/Country/CountryUseCaseDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountryUseCaseDelegate.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 06/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol CountryUseCaseAsyncDelegate { 12 | 13 | //MARK: - Protocol properties 14 | var countryRepository: CountryRepositoryAsyncDelegate? { get } 15 | 16 | /// Get country data based on various parameters 17 | /// - Parameters: 18 | /// - countrySlug: Country slug 19 | /// - covidStatus: Covid status 20 | /// - dateFrom: Date from 21 | /// - dateTo: Date to 22 | func getCountryData(by countrySlug: String, covidStatus: Covid19Status, dateFrom: String, dateTo: String) async throws -> [Country] 23 | 24 | /// Get all countries data 25 | func getCountryList() async throws -> [Country] 26 | 27 | } 28 | -------------------------------------------------------------------------------- /MVVM-Clean/Application/DI/Extensions/Assembler+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Assembler+Extensions.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Swinject 11 | 12 | extension Assembler { 13 | 14 | static var type: AssemblerType = .Default 15 | 16 | static let sharedAssembler: Assembler = { 17 | let container = Container() 18 | 19 | let assembler = Assembler([ 20 | ViewControllerAssembly(), 21 | ViewModelAssembly(), 22 | RepositoryAssembly(), 23 | UseCaseAssembly(), 24 | NetworkAssembly(), 25 | PersistenceAssembly() 26 | ], container: container) 27 | 28 | return assembler 29 | }() 30 | } 31 | 32 | enum AssemblerType { 33 | case Default 34 | case SummaryTest 35 | } 36 | 37 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Network/Providers/Summary/SummaryNetworkProtocolRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Covid19NetworkProtocolRequest.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 03/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | //import Alamofire 11 | // 12 | ///// Protocol 13 | //protocol SummaryNetworkProtocolRequest { 14 | // 15 | // /// Collect summary data from COVID19 API 16 | // /// - Parameters: 17 | // /// - success: <#success description#> 18 | // /// - failure: <#failure description#> 19 | // func getSummaryData(success: @escaping (T) -> Void, failure: @escaping ((CustomError) -> Void)) 20 | //} 21 | 22 | //MARK: - Async version 23 | protocol SummaryNetworkProtocolAsyncRequest { 24 | 25 | /// Collect summary data from COVID19 API with async request 26 | /// - Returns: <#description#> 27 | func getSummaryData() async throws -> SummaryDTO 28 | } 29 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/UseCases/Country/CountryUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountryUseCase.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 06/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class CountryAsyncUseCase: CountryUseCaseAsyncDelegate { 12 | var countryRepository: CountryRepositoryAsyncDelegate? 13 | 14 | func getCountryData(by countrySlug: String, covidStatus: Covid19Status, dateFrom: String, dateTo: String) async throws -> [Country] { 15 | guard let country = try await countryRepository?.getCountryAsyncData(by: countrySlug, status: covidStatus, from: dateFrom, to: dateTo) else { 16 | throw CustomError.nilData 17 | } 18 | return country 19 | } 20 | 21 | func getCountryList() async throws -> [Country] { 22 | guard let country = try await countryRepository?.getCountriesAsyncData() else { 23 | throw CustomError.nilData 24 | } 25 | return country 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Repositories/Login/LoginRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginRepository.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class LoginRepository: LoginRepositoryDelegate { 12 | 13 | var profileProtocol: ProfilePersistenceProtocolData? 14 | 15 | private let USERNAME = "admin" 16 | private let PASSWORD = "pass123!" 17 | 18 | func login(username: String, password: String, success: @escaping (User) -> Void, failure: @escaping (CustomError) -> Void) { 19 | // Move here your network request instead of this code 20 | if( username == USERNAME && password == PASSWORD ) { 21 | var userModel = User() 22 | userModel.username = USERNAME 23 | profileProtocol?.saveLocalUserData(currentUser: userModel) 24 | 25 | success(userModel) 26 | } else { 27 | failure(CustomError.loginError) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Network/Providers/Summary/SummaryRouterSwift.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SummaryRouterSwift.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 31/03/22. 6 | // Copyright © 2022 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum SummaryRouterSwift: APIConfigurationSwift { 12 | 13 | case getSummaryData 14 | 15 | var path: String { 16 | switch self { 17 | case .getSummaryData: 18 | return "/summary" 19 | } 20 | } 21 | 22 | var method: HTTPMethodSwift { 23 | switch self { 24 | case .getSummaryData: 25 | return .get 26 | } 27 | } 28 | 29 | var header: [String : String]? { 30 | switch self { 31 | case .getSummaryData: 32 | return [HTTPHeaderField.acceptType.rawValue: ContentType.json.rawValue] 33 | } 34 | } 35 | 36 | var parameters: ParameterSwift? { 37 | switch self { 38 | case .getSummaryData: 39 | return nil 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /MVVM-Clean/Application/DI/PersistenceAssembly.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PersistenceAssembly.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 03/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Swinject 11 | 12 | class PersistenceAssembly: Assembly { 13 | 14 | func assemble(container: Container) { 15 | 16 | container.register(SummaryPersistenceProtocolRequest.self) { resolver in 17 | let request = SummaryPersistenceRequest() 18 | return request 19 | }.inObjectScope(.transient) 20 | 21 | container.register(CountryPersistenceProtocolRequest.self) { resolver in 22 | let request = CountryPersistenceRequest() 23 | return request 24 | }.inObjectScope(.transient) 25 | 26 | container.register(ProfilePersistenceProtocolData.self) { resolver in 27 | let profile = ProfilePersistenceData() 28 | return profile 29 | }.inObjectScope(.transient) 30 | 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /MVVM-Clean/Application/DI/NetworkAssembly.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkAssembly.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 03/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Swinject 11 | 12 | class NetworkAssembly: Assembly { 13 | 14 | func assemble(container: Container) { 15 | 16 | //MARK: - Async 17 | container.register(SummaryNetworkProtocolAsyncRequest.self) { resolver in 18 | 19 | if( Assembler.type == .SummaryTest ) { 20 | let summaryDataReuqest = SummaryDataRequestTest() 21 | return summaryDataReuqest 22 | } else { 23 | return SummaryNetworkAsyncReqeust() 24 | } 25 | 26 | }.inObjectScope(.transient) 27 | 28 | container.register(CountryNetworkProtocolAsyncRequest.self) { resolver in 29 | let request = CountryNetworkAsyncRequest() 30 | return request 31 | }.inObjectScope(.transient) 32 | 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Network/Providers/Country/CountryNetworkProtocolRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountryNetworkProtocolRequest.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 06/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Covid19 Status accepted by country request 12 | enum Covid19Status { 13 | case confirmed 14 | case recovered 15 | case deaths 16 | } 17 | 18 | /// Protocol for country request 19 | protocol CountryNetworkProtocolAsyncRequest { 20 | 21 | /// Get country data by status and date with async API 22 | /// - Parameters: 23 | /// - countrySlug: String rapresent country slug 24 | /// - status: Covid19 status choose from Covid19Status enum 25 | /// - from: Date to start collect data in format yyyy-mm-ddThh:MM:ssZ 26 | /// - to: Date to end collect data in format yyyy-mm-ddThh:MM:ssZ 27 | /// - Returns: CountryDTO object 28 | func getByCountryByStatus(countrySlug: String, status: Covid19Status, from: String, to: String) async throws -> [CountryDTO] 29 | } 30 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Persistence/Profile/ProfileLocalRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileLocalRequest.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 17/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class ProfileLocalData: ProfileLocalProtocolData { 12 | 13 | func getLocalUserData() -> UserModel? { 14 | LOGD("Getting local user data") 15 | let currentUser = UserModel() 16 | 17 | if let currentUsername = UserDefaults.standard.string(forKey: "current_user_username") { 18 | currentUser.username = currentUsername 19 | } 20 | 21 | return currentUser 22 | } 23 | 24 | func saveLocalUserData(currentUser: UserModel) { 25 | LOGD("Saving local user data") 26 | UserDefaults.standard.set(currentUser, forKey: "current_user_username") 27 | } 28 | 29 | func deleteLocalUserData() { 30 | LOGD("Deleting local user data") 31 | UserDefaults.standard.removeObject(forKey: "current_user_username") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Authentication/Router/AuthNavigationRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthNavigationRouter.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 07/04/22. 6 | // Copyright © 2022 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | //MARK: - Protocol 13 | protocol AuthNavigationRouterInput { 14 | var vc: LoginViewController! { get } 15 | 16 | func navigateToHomeView() 17 | } 18 | 19 | //MARK: - Implementation 20 | class AuthNavigationRouter: AuthNavigationRouterInput { 21 | weak var vc: LoginViewController! 22 | 23 | init(vc: LoginViewController) { 24 | self.vc = vc 25 | } 26 | 27 | func navigateToHomeView() { 28 | let mainViewController = SummaryNavigatorConfigurator().configure() 29 | let nvc: UINavigationController = UINavigationController(rootViewController: mainViewController) 30 | nvc.setNavigationBarHidden(false, animated: false) 31 | UIApplication.shared.windows.first?.rootViewController = nvc 32 | UIApplication.shared.windows.first?.makeKeyAndVisible() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Network/DTO/SummaryDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SummaryDTO.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 03/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | Summary DTO model retrieved from API JSON data. 13 | */ 14 | struct SummaryDTO: Codable { 15 | let message: String? 16 | let global: GlobalDTO? 17 | let countries: [CountryDTO]? 18 | let date: String? 19 | 20 | enum CodingKeys: String, CodingKey { 21 | case message = "Message" 22 | case global = "Global" 23 | case countries = "Countries" 24 | case date = "Date" 25 | } 26 | 27 | init(from decoder: Decoder) throws { 28 | let values = try decoder.container(keyedBy: CodingKeys.self) 29 | message = try values.decodeIfPresent(String.self, forKey: .message) 30 | global = try values.decodeIfPresent(GlobalDTO.self, forKey: .global) 31 | countries = try values.decodeIfPresent([CountryDTO].self, forKey: .countries) 32 | date = try values.decodeIfPresent(String.self, forKey: .date) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Main/Summary/Router/SummaryNavigationRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SummaryNavigationRouter.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 07/04/22. 6 | // Copyright © 2022 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | //MARK: - Protocol 12 | protocol SummaryNavigationRouterInput { 13 | var vc: SummaryViewController! { get } 14 | 15 | func navigateToProfileView() 16 | func navigateToCountryView() 17 | } 18 | 19 | //MARK: - Implementation 20 | class SummaryNavigationRouter: SummaryNavigationRouterInput { 21 | weak var vc: SummaryViewController! 22 | 23 | init(vc: SummaryViewController) { 24 | self.vc = vc 25 | } 26 | 27 | func navigateToProfileView() { 28 | let profileVC = ProfileNavigationConfigurator().configure() 29 | vc.navigationController?.pushViewController(profileVC, animated: true) 30 | } 31 | 32 | func navigateToCountryView() { 33 | let countryVC = CountryNavigationConfigurator().configure() 34 | vc.navigationController?.pushViewController(countryVC, animated: true) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Main/Profile/Router/ProfileNavigationRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileNavigationRouter.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 07/04/22. 6 | // Copyright © 2022 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | //MARK: - Protocol 13 | protocol ProfileNavigationRouterInput { 14 | var vc: ProfileViewController! { get } 15 | 16 | func navigateToLoginView() 17 | } 18 | 19 | //MARK: - Implementation 20 | class ProfileNavigationRouter: ProfileNavigationRouterInput { 21 | weak var vc: ProfileViewController! 22 | 23 | init(vc: ProfileViewController) { 24 | self.vc = vc 25 | } 26 | 27 | func navigateToLoginView() { 28 | let authViewController = AuthNavigationConfigurator().configure() 29 | let nvc: UINavigationController = UINavigationController(rootViewController: authViewController) 30 | nvc.setNavigationBarHidden(false, animated: false) 31 | UIApplication.shared.windows.first?.rootViewController = nvc 32 | UIApplication.shared.windows.first?.makeKeyAndVisible() 33 | } 34 | 35 | 36 | } 37 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Splash/ViewModel/SplashScreenViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplashScreenViewModel.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | enum SplashScreenViewModelStatus { 13 | case none 14 | case loggedIn 15 | case notLoggedIn 16 | } 17 | 18 | protocol SplashScreenViewModelDelegate: SplashScreenViewModelInputDelegate, SplashScreenViewModelOutputDelegate { } 19 | 20 | class SplashScreenViewModel: SplashScreenViewModelDelegate { 21 | 22 | var profileUseCase: ProfileUseCaseDelegate? 23 | var status: CurrentValueSubject = .init(.none) 24 | 25 | func checkUserState() { 26 | profileUseCase?.getCurrentUserData() 27 | } 28 | } 29 | 30 | extension SplashScreenViewModel: ProfileUseCaseResponseDelegate { 31 | 32 | func gettingUserDataSuccess(currentUser: User) { 33 | status.send(.loggedIn) 34 | } 35 | 36 | func gettingUserDataFailure(error: CustomError) { 37 | status.send(.notLoggedIn) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /MVVM-CleanUITests/MVVM_CleanUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MVVM_CleanUITests.swift 3 | // MVVM-CleanUITests 4 | // 5 | // Created by Alessandro Marcon on 24/11/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class MVVM_CleanUITests: XCTestCase { 12 | 13 | var app: XCUIApplication! 14 | 15 | override func setUp() { 16 | // Since UI tests are more expensive to run, it's usually a good idea 17 | // to exit if a failure was encountered 18 | continueAfterFailure = false 19 | 20 | app = XCUIApplication() 21 | 22 | // We send a command line argument to our app, 23 | // to enable it to reset its state 24 | app.launchArguments.append("--uitesting") 25 | } 26 | 27 | func testLoginViewController() throws { 28 | app.launch() 29 | // Make sure we're displaying login view controller 30 | XCTAssertTrue(app.isDisplayingLoginViewController) 31 | } 32 | } 33 | 34 | 35 | extension XCUIApplication { 36 | 37 | var isDisplayingLoginViewController: Bool { 38 | return otherElements["loginViewController"].exists 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /MVVM-Clean/AppDelegate/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | 19 | if CommandLine.arguments.contains("--uitesting") { 20 | resetState() 21 | } 22 | 23 | let navigationBarAppearace = UINavigationBar.appearance() 24 | navigationBarAppearace.tintColor = UIColor.Custom.primaryText 25 | navigationBarAppearace.barTintColor = UIColor.Custom.primary 26 | 27 | navigationBarAppearace.titleTextAttributes = [NSAttributedString.Key.foregroundColor : UIColor.Custom.primaryText] 28 | 29 | return true 30 | } 31 | 32 | func resetState() { 33 | // Clean test data 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /MVVM-Clean/Extensions/UIView+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Extensions.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIView { 12 | 13 | func getSubviews(type: T.Type) -> [T] { 14 | var result: [T] = [] 15 | for subview in subviews { 16 | result += subview.getSubviews(type: type) as [T] 17 | if let subview = subview as? T { 18 | result.append(subview) 19 | } 20 | } 21 | return result 22 | } 23 | 24 | func setVerticalGradient(colorTop: CGColor, colorBottom: CGColor) { 25 | let gradientLayer = CAGradientLayer() 26 | gradientLayer.colors = [colorTop, colorBottom] 27 | gradientLayer.locations = [0.0, 1.0] 28 | gradientLayer.frame = self.bounds 29 | self.layer.insertSublayer(gradientLayer, at:0) 30 | } 31 | 32 | @discardableResult static func fromNib() -> T? { 33 | guard let view = Bundle.main.loadNibNamed(String(describing: T.self), owner: self, options: nil)?.first as? T else { return nil } 34 | return view 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Network/Providers/Country/CountryRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountryRouter.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 06/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | //MARK: - Async 12 | enum CountryRouterSwift: APIConfigurationSwift { 13 | 14 | case getByCountryByStatus(countrySlug: String, status: Covid19Status, from: String, to: String) 15 | 16 | var path: String { 17 | switch self { 18 | case .getByCountryByStatus(let countrySlug, let status, _, _): 19 | return "/country/\(countrySlug)/status\(status)" 20 | } 21 | } 22 | 23 | var method: HTTPMethodSwift { 24 | switch self { 25 | case .getByCountryByStatus: 26 | return .get 27 | } 28 | } 29 | 30 | var header: [String : String]? { 31 | switch self { 32 | case .getByCountryByStatus: 33 | return [HTTPHeaderField.acceptType.rawValue: ContentType.json.rawValue] 34 | } 35 | } 36 | 37 | var parameters: ParameterSwift? { 38 | switch self { 39 | case .getByCountryByStatus(_, _, let from, let to): 40 | return ["from":from, "to":to] 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /MVVM-Clean/Infrastructure/Network/HTTPMethodSwift.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPMethodSwift.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 31/03/22. 6 | // Copyright © 2022 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct HTTPMethodSwift: RawRepresentable, Equatable, Hashable { 12 | /// `CONNECT` method. 13 | public static let connect = HTTPMethodSwift(rawValue: "CONNECT") 14 | /// `DELETE` method. 15 | public static let delete = HTTPMethodSwift(rawValue: "DELETE") 16 | /// `GET` method. 17 | public static let get = HTTPMethodSwift(rawValue: "GET") 18 | /// `HEAD` method. 19 | public static let head = HTTPMethodSwift(rawValue: "HEAD") 20 | /// `OPTIONS` method. 21 | public static let options = HTTPMethodSwift(rawValue: "OPTIONS") 22 | /// `PATCH` method. 23 | public static let patch = HTTPMethodSwift(rawValue: "PATCH") 24 | /// `POST` method. 25 | public static let post = HTTPMethodSwift(rawValue: "POST") 26 | /// `PUT` method. 27 | public static let put = HTTPMethodSwift(rawValue: "PUT") 28 | /// `TRACE` method. 29 | public static let trace = HTTPMethodSwift(rawValue: "TRACE") 30 | 31 | public let rawValue: String 32 | 33 | public init(rawValue: String) { 34 | self.rawValue = rawValue 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Repositories/Country/CountryRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountryRepository.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class CountryAsyncRepository: CountryRepositoryAsyncDelegate { 12 | 13 | var countryLocal: CountryPersistenceProtocolRequest? 14 | var countryAsyncNetwork: CountryNetworkProtocolAsyncRequest? 15 | 16 | func getCountriesAsyncData() async throws -> [Country] { 17 | if let localCountriesData = countryLocal?.getLocalCountryDataDTO() { 18 | let localCountries = CountryDTOMapper.map(countries: localCountriesData) 19 | return localCountries 20 | } else { 21 | LOGE("CountryLocalProtocolRequest returned nil object") 22 | throw CustomError.nilData 23 | } 24 | } 25 | 26 | func getCountryAsyncData(by countrySlug: String, status: Covid19Status, from: String, to: String) async throws -> [Country] { 27 | guard let data = try await countryAsyncNetwork?.getByCountryByStatus(countrySlug: countrySlug, status: status, from: from, to: to) else { 28 | throw CustomError.nilData 29 | } 30 | let countries = CountryDTOMapper.map(countries: data) 31 | return countries 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Persistence/Profile/ProfilePersistenceData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfilePersistenceData.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 17/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | This is the implementation of ProfileLocalProtocolData. For simplicity, we use UserDefaults instead of a real datatabase. 13 | Here, we are going to implement the methods that will be used to locally save and retrieve user data. 14 | */ 15 | class ProfilePersistenceData: ProfilePersistenceProtocolData { 16 | 17 | func getLocalUserData() -> User? { 18 | LOGD("Getting local user data") 19 | var currentUser: User? 20 | 21 | if let currentUsername = UserDefaults.standard.string(forKey: "current_user_username") { 22 | currentUser = User() 23 | currentUser!.username = currentUsername 24 | } 25 | 26 | return currentUser 27 | } 28 | 29 | func saveLocalUserData(currentUser: User) { 30 | LOGD("Saving local user data") 31 | UserDefaults.standard.set(currentUser.username, forKey: "current_user_username") 32 | } 33 | 34 | func deleteLocalUserData() { 35 | LOGD("Deleting local user data") 36 | UserDefaults.standard.removeObject(forKey: "current_user_username") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /MVVM-CleanTests/SummaryTest/SummaryDataRequestTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SummaryDataRequestTest.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 20/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | //class SummaryDataRequestTest: SummaryNetworkProtocolRequest { 12 | // 13 | // func getSummaryData(success: @escaping (T) -> Void, failure: @escaping ((CustomError) -> Void)) where T : Decodable { 14 | // 15 | // let filePath = Bundle.main.path(forResource: "summary_data", ofType: "json")! 16 | // let data = try! Data(contentsOf: URL(fileURLWithPath: filePath)) 17 | // 18 | // let decoder: JSONDecoder = JSONDecoder.init() 19 | // let summary: SummaryDTO = try! decoder.decode(SummaryDTO.self, from: data) 20 | // success(summary as! T) 21 | // } 22 | // 23 | //} 24 | 25 | class SummaryDataRequestTest: SummaryNetworkProtocolAsyncRequest { 26 | 27 | func getSummaryData() async throws -> SummaryDTO { 28 | let filePath = Bundle.main.path(forResource: "summary_data", ofType: "json")! 29 | let data = try! Data(contentsOf: URL(fileURLWithPath: filePath)) 30 | 31 | let decoder: JSONDecoder = JSONDecoder.init() 32 | let summary: SummaryDTO = try! decoder.decode(SummaryDTO.self, from: data) 33 | return summary 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Network/Providers/MainProviders/NetworkConstant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkConstant.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 30/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum HTTPHeaderField: String { 12 | case authentication = "Authorization" 13 | case contentType = "Content-Type" 14 | case acceptType = "Accept" 15 | case acceptEncoding = "Accept-Encoding" 16 | case acceptLanguage = "Accept-Language" 17 | case subscriptionKey = "Ocp-Apim-Subscription-Key" 18 | } 19 | 20 | 21 | enum ContentType: String { 22 | case all = "*/*" 23 | case json = "application/json" 24 | case xForm = "application/x-www-form-urlencoded" 25 | } 26 | 27 | 28 | enum NetworkStatusCode: Int { 29 | // Undefined error 30 | case undefined = 0 31 | // 2xx Success 32 | case success = 200 33 | case noContent = 204 34 | // 4xx Client errors 35 | case badRequest = 400 36 | case unauthorized = 401 37 | case forbidden = 403 38 | case notFound = 404 39 | case methodNotAllowed = 405 40 | //5xx Server errors 41 | case internalServerError = 500 42 | case notImplemented = 501 43 | case badGateway = 502 44 | case serviceUnavailable = 503 45 | } 46 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Authentication/ViewModel/LoginViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginViewModel.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | enum LoginViewModelStatus { 13 | case none 14 | case loginInProgress 15 | case loginSuccess 16 | case loginError 17 | } 18 | 19 | protocol LoginViewModelDelegate: LoginViewModelInputDelegate, LoginViewModelOutputDelegate { } 20 | 21 | class LoginViewModel: LoginViewModelDelegate { 22 | 23 | var error: CustomError? 24 | var loginUseCase: LoginUseCaseDelegate? 25 | var status: CurrentValueSubject = .init(.none) 26 | 27 | func executeLogin(username: String, password: String) { 28 | print("Execute login") 29 | status.send(.loginInProgress) 30 | error = nil 31 | loginUseCase?.responseDelegate = self 32 | loginUseCase?.startLogin(username: username, password: password) 33 | } 34 | 35 | } 36 | 37 | // MARK: - Login Use Case Response Delegate 38 | extension LoginViewModel: LoginUseCaseResponseDelegate { 39 | 40 | func onLoginSuccess(user: User) { 41 | status.send(.loginSuccess) 42 | } 43 | 44 | func onLoginFailure(error: CustomError) { 45 | self.error = error 46 | status.send(.loginError) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/UseCases/Profile/ProfileUseCaseResponseDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogoutUseCaseResponseDelegate.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol ProfileUseCaseResponseDelegate: AnyObject { 12 | 13 | //MARK: - Logout response delegate 14 | 15 | /// Event called on successful logout operation 16 | func onLogoutSuccess() 17 | 18 | /// Event called on error logout operation 19 | /// - Parameter error: The error occurred 20 | func onLogoutFailure(error: CustomError) 21 | 22 | //MARK: - User data response delegate 23 | 24 | /// Event called on success during getting user data operation 25 | func gettingUserDataSuccess(currentUser: User) 26 | 27 | /// Event called on error during getting user data operation 28 | /// - Parameter error: The error occurred 29 | func gettingUserDataFailure(error: CustomError) 30 | 31 | } 32 | 33 | /** 34 | This is the default implementation of ProfileUseCaseResponseDelegate. It avoids having to implement all methods even when not needed. 35 | */ 36 | extension ProfileUseCaseResponseDelegate { 37 | func onLogoutSuccess() {} 38 | func onLogoutFailure(error: CustomError) {} 39 | func gettingUserDataSuccess(currentUser: User) {} 40 | func gettingUserDataFailure(error: CustomError) {} 41 | } 42 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Splash/Router/SplashRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplashRouter.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 07/04/22. 6 | // Copyright © 2022 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | //MARK: - Protocol 13 | protocol SplashRouterInput { 14 | func navigateToHomeView() 15 | func navigateToLoginView() 16 | } 17 | 18 | //MARK: - Implementation 19 | class SplashRouter: SplashRouterInput { 20 | 21 | func navigateToHomeView() { 22 | LOGP("Navigate to home") 23 | let mainViewController = SummaryNavigatorConfigurator().configure() 24 | let nvc: UINavigationController = UINavigationController(rootViewController: mainViewController) 25 | nvc.setNavigationBarHidden(false, animated: false) 26 | UIApplication.shared.windows.first?.rootViewController = nvc 27 | UIApplication.shared.windows.first?.makeKeyAndVisible() 28 | } 29 | 30 | func navigateToLoginView() { 31 | LOGP("Navigate to login") 32 | let loginViewController = AuthNavigationConfigurator().configure() 33 | let nvc: UINavigationController = UINavigationController(rootViewController: loginViewController) 34 | nvc.setNavigationBarHidden(false, animated: false) 35 | UIApplication.shared.windows.first?.rootViewController = nvc 36 | UIApplication.shared.windows.first?.makeKeyAndVisible() 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /MVVM-CleanTests/SummaryTest/SummaryIntegrationTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SummaryIntegrationTest.swift 3 | // MVVM-CleanTests 4 | // 5 | // Created by Alessandro Marcon on 20/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | import Swinject 12 | @testable import MVVM_Clean 13 | 14 | class SummaryIntegrationTest { 15 | 16 | private var expectation: XCTestExpectation 17 | private var xcTestCase: XCTestCase 18 | private var summaryUseCase: SummaryUseCaseAsyncDelegate? 19 | 20 | init(xcTestCase: XCTestCase) { 21 | 22 | // Initialize properties 23 | self.xcTestCase = xcTestCase 24 | expectation = XCTestExpectation(description: "Summary data: get correctly summary data") 25 | 26 | } 27 | 28 | /// Get summary data and check if it will be correctly decoded 29 | func runSummaryDataTest() { 30 | Assembler.type = .SummaryTest 31 | 32 | summaryUseCase = Assembler.sharedAssembler.resolver.resolve(SummaryUseCaseAsyncDelegate.self) 33 | summaryUseCase?.summaryRepository?.isRunningFromTest = true 34 | Task { 35 | do { 36 | let _ = try await summaryUseCase?.getAsyncSummaryData() 37 | expectation.fulfill() 38 | } catch(_) { 39 | expectation.fulfill() 40 | XCTFail() 41 | } 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [2.2.0] - 2022-04-12 9 | 10 | ### Changed 11 | 12 | - Refactor network layer, changed from Alamofire to swift Async Await function. 13 | 14 | ## [2.1.0] - 2022-04-08 15 | 16 | ### Added 17 | 18 | - Router navigation pattern with router configurator. 19 | 20 | ## [2.0.0] - 2022-02-08 21 | 22 | ### Added 23 | 24 | - App icon added. 25 | - Added colors asset and color extension. 26 | 27 | ### Changed 28 | 29 | - Changed app ui and colors. 30 | 31 | ## [1.1.0] - 2021-11-30 32 | 33 | ### Changed 34 | 35 | - Replaced Bond framework with Apple Combine, used for view model/view controller binding. 36 | 37 | ## [1.0.2] - 2020-10-23 38 | 39 | ### Changed 40 | 41 | - Refactored mapper name and switched from class to struct. Deleted mapper protocol. 42 | - DTO and models name refactor. 43 | - Deleted unused models. 44 | 45 | ## [1.0.1] - 2020-10-22 46 | 47 | ### Changed 48 | 49 | - Renamed local files and folder data to Persistence 50 | 51 | ## [1.0] - 2020-10-20 52 | 53 | ### Added 54 | 55 | - Start using "changelog" file. 56 | 57 | ### Changed 58 | 59 | - ViewModel are now struct 60 | - Saved locally DTO instead model 61 | - Now method to check if new http call is neede, check date of Summary DTO model -------------------------------------------------------------------------------- /MVVM-Clean/Data/UseCases/Profile/ProfileUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogoutUseCase.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class ProfileUseCase: NSObject, ProfileUseCaseDelegate { 12 | 13 | var responseDelegate: ProfileUseCaseResponseDelegate? 14 | var profileRepository: ProfileRepositoryDelegate? 15 | 16 | /// Implementation of logout function 17 | func logout() { 18 | print("Execute logout") 19 | 20 | profileRepository?.deleteCurrentUserData(success: { (success) in 21 | if( success ) { 22 | self.responseDelegate?.onLogoutSuccess() 23 | } else { 24 | self.responseDelegate?.onLogoutFailure(error: CustomError.logoutError) 25 | } 26 | }, failure: { (error) in 27 | self.responseDelegate?.onLogoutFailure(error: error) 28 | }) 29 | } 30 | 31 | /// Implementation of get user data function 32 | func getCurrentUserData() { 33 | profileRepository?.getCurrentUserData(success: { (user) in 34 | LOGP("User data found") 35 | self.responseDelegate?.gettingUserDataSuccess(currentUser: user) 36 | }, failure: { (error) in 37 | LOGP("User data not found") 38 | self.responseDelegate?.gettingUserDataFailure(error: error) 39 | }) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Network/DTO/GlobalDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlobalDTO.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 03/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | Global DTO model mapped from API JSON data 13 | */ 14 | struct GlobalDTO: Codable { 15 | let newConfirmed: Int32? 16 | let totalConfirmed: Int32? 17 | let newDeaths: Int32? 18 | let totalDeaths: Int32? 19 | let newRecovered: Int32? 20 | let totalRecovered: Int32? 21 | 22 | enum CodingKeys: String, CodingKey { 23 | case newConfirmed = "NewConfirmed" 24 | case totalConfirmed = "TotalConfirmed" 25 | case newDeaths = "NewDeaths" 26 | case totalDeaths = "TotalDeaths" 27 | case newRecovered = "NewRecovered" 28 | case totalRecovered = "TotalRecovered" 29 | } 30 | 31 | init(from decoder: Decoder) throws { 32 | let values = try decoder.container(keyedBy: CodingKeys.self) 33 | 34 | newConfirmed = try values.decodeIfPresent(Int32.self, forKey: .newConfirmed) 35 | totalConfirmed = try values.decodeIfPresent(Int32.self, forKey: .totalConfirmed) 36 | newDeaths = try values.decodeIfPresent(Int32.self, forKey: .newDeaths) 37 | totalDeaths = try values.decodeIfPresent(Int32.self, forKey: .totalDeaths) 38 | newRecovered = try values.decodeIfPresent(Int32.self, forKey: .newRecovered) 39 | totalRecovered = try values.decodeIfPresent(Int32.self, forKey: .totalRecovered) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Repositories/Profile/ProfileRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionRepository.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class ProfileRepository: ProfileRepositoryDelegate { 12 | 13 | var profileLocalData: ProfilePersistenceProtocolData? 14 | 15 | func saveCurrentUserData(user: User, success: @escaping (Bool) -> Void, failure: @escaping (CustomError) -> Void) { 16 | LOGD("Saving current user") 17 | profileLocalData?.saveLocalUserData(currentUser: user) 18 | success(true) 19 | } 20 | 21 | func deleteCurrentUserData(success: @escaping (Bool) -> Void, failure: @escaping (CustomError) -> Void) { 22 | LOGD("Delete current user") 23 | profileLocalData?.deleteLocalUserData() 24 | success(true) 25 | } 26 | 27 | func getCurrentUserData(success: @escaping (User) -> Void, failure: @escaping (CustomError) -> Void) { 28 | LOGD("Get current user") 29 | if let currentUser = profileLocalData?.getLocalUserData() { 30 | success(currentUser) 31 | } else { 32 | failure(CustomError.userNotFound) 33 | } 34 | } 35 | 36 | func isUserSignedIn(success: @escaping (Bool) -> Void, failure: @escaping (CustomError) -> Void) { 37 | if let _ = profileLocalData?.getLocalUserData() { 38 | success(true) 39 | } else { 40 | success(false) 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Repositories/Profile/ProfileRepositoryDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionRepositoryDelegate.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol ProfileRepositoryDelegate { 12 | 13 | var profileLocalData: ProfilePersistenceProtocolData? { get set } 14 | 15 | /// Save user data 16 | /// - Parameters: 17 | /// - user: Current user model object 18 | /// - success: Event fired in success case 19 | /// - failure: Event fired in failure case 20 | func saveCurrentUserData(user: User, success: @escaping (Bool) -> Void, failure: @escaping (CustomError) -> Void) 21 | 22 | /// Delete data of current user 23 | /// - Parameters: 24 | /// - success: Event fired in success case 25 | /// - failure: Event fired in failure case 26 | func deleteCurrentUserData(success: @escaping (Bool) -> Void, failure: @escaping (CustomError) -> Void) 27 | 28 | /// Retrive current user data 29 | /// - Parameters: 30 | /// - success: Event fired in success case 31 | /// - failure: Event fired in failure case 32 | func getCurrentUserData(success: @escaping (User) -> Void, failure: @escaping (CustomError) -> Void) 33 | 34 | /// Check if users is signed in 35 | /// - Parameters: 36 | /// - success: Event fired in success case 37 | /// - failure: Event fired in failure case 38 | func isUserSignedIn(success: @escaping (Bool) -> Void, failure: @escaping (CustomError) -> Void) 39 | 40 | } 41 | -------------------------------------------------------------------------------- /MVVM-Clean/Infrastructure/Network/AsyncNetworkPerformer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncNetworkPerformer.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 31/03/22. 6 | // Copyright © 2022 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class AsyncNetworkPerformer { 12 | 13 | /// Send HTTP request using asyncronous function 14 | /// - Returns: Decodable generics object 15 | public static func sendRequest(route: APIConfigurationSwift, responseDTO: T.Type) async throws -> T { 16 | do { 17 | let (data, response) = try await URLSession.shared.data(for: route.asURLRequest()) 18 | guard let response = response as? HTTPURLResponse else { 19 | throw CustomError.nilData 20 | } 21 | 22 | if( response.statusCode >= 200 || response.statusCode <= 299 ) { 23 | guard let decodedResponse = try? JSONDecoder().decode(responseDTO, from: data) else { 24 | throw CustomError.jsonDecodeError 25 | } 26 | return decodedResponse 27 | } else { 28 | throw CustomError(errorCode: response.statusCode) 29 | } 30 | } catch let error { 31 | let code = URLError.Code(rawValue: (error as NSError).code) 32 | switch code { 33 | case .notConnectedToInternet: 34 | throw CustomError.noConnection 35 | default: 36 | throw CustomError.unknow(code: "\(error.localizedDescription)") 37 | } 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Main/Country/ViewModel/CountryCovidViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountryCovidViewModel.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 06/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | //MARK: - CountryCovidViewModel Status 13 | enum CountryCovidViewModelStatus { 14 | case none 15 | case gettingCountryData 16 | case countriesDataSuccess 17 | case countriesDataError 18 | } 19 | 20 | protocol CountryCovidViewModelDelegate: CountryCovidViewModelInputDelegate, CountryCovidViewModelOutputDelegate { } 21 | 22 | class CountryCovidViewModel: CountryCovidViewModelDelegate { 23 | 24 | var countryAsyncUseCase: CountryUseCaseAsyncDelegate? 25 | var status: CurrentValueSubject = .init(.none) 26 | var error: CustomError? 27 | var countries: [Country]? 28 | 29 | func countryList() { 30 | status.value = .gettingCountryData 31 | Task { 32 | do { 33 | guard let countries = try await countryAsyncUseCase?.getCountryList() else { 34 | self.error = CustomError.nilData 35 | status.send(.countriesDataError) 36 | return 37 | } 38 | self.countries = countries 39 | status.send(.countriesDataSuccess) 40 | } catch(let error) { 41 | self.error = error as? CustomError 42 | status.send(.countriesDataError) 43 | } 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Main/Summary/ViewModel/SummaryCovidViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogoutViewModel.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | //MARK: - SummaryCovidViewModel Status 13 | enum SummaryCovidViewModelStatus { 14 | case none 15 | case gettingSummaryData 16 | case summaryDataSuccess 17 | case summaryDataError 18 | } 19 | 20 | protocol SummaryCovidViewModelDelegate: SummaryCovidViewModelInputDelegate, SummaryCovidViewModelOutputDelegate { } 21 | 22 | class SummaryCovidViewModel: SummaryCovidViewModelDelegate { 23 | 24 | internal var summaryUseCaseAsync: SummaryUseCaseAsyncDelegate? 25 | var status: CurrentValueSubject = .init(.none) 26 | var summary: Summary? 27 | var error: CustomError? 28 | 29 | func summaryDataAsync() { 30 | LOGI("Begin recover summary data async") 31 | status.send(.gettingSummaryData) 32 | Task { 33 | do { 34 | guard let data = try await summaryUseCaseAsync?.getAsyncSummaryData() else { 35 | status.send(.summaryDataError) 36 | return 37 | } 38 | 39 | self.summary = data 40 | status.send(.summaryDataSuccess) 41 | 42 | } catch(let error) { 43 | self.error = error as? CustomError 44 | status.send(.summaryDataError) 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Models/Summary.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Summary.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 03/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | struct Summary { 13 | var summaryConfirmed: SummaryConfirmed 14 | var summaryDeath: SummaryDeath 15 | var summaryRecovered: SummaryRecovered 16 | var lastUpdate: Date 17 | 18 | init() { 19 | summaryConfirmed = SummaryConfirmed() 20 | summaryDeath = SummaryDeath() 21 | summaryRecovered = SummaryRecovered() 22 | lastUpdate = CustomDateUtils.currentDate() 23 | } 24 | } 25 | 26 | protocol SummaryData { 27 | var title: String { get set } 28 | } 29 | 30 | struct SummaryConfirmed: SummaryData { 31 | var title: String 32 | var newConfirmedCases: String 33 | var totalConfirmedCases: String 34 | 35 | init() { 36 | title = NSLocalizedString("new_confirmed_cases", comment: "") 37 | newConfirmedCases = "0" 38 | totalConfirmedCases = "0" 39 | } 40 | } 41 | 42 | struct SummaryDeath: SummaryData { 43 | var title: String 44 | var newDeath: String 45 | var totalDeaths: String 46 | 47 | init() { 48 | title = NSLocalizedString("new_deaths_cases", comment: "") 49 | newDeath = "0" 50 | totalDeaths = "0" 51 | } 52 | } 53 | 54 | struct SummaryRecovered: SummaryData { 55 | var title: String 56 | var newRecoveredCases: String 57 | var totalRecovered: String 58 | 59 | init() { 60 | title = NSLocalizedString("new_recovered", comment: "") 61 | newRecoveredCases = "0" 62 | totalRecovered = "0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Persistence/Country/CountryPersistenceRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountryPersistenceRequest.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | This is the implementation of CountryLocalProtocolRequest. For simplicity, we use UserDefaults instead of a real datatabase. 13 | Here, we are going to implement the methods that will be used to locally save and retrieve country data. 14 | */ 15 | class CountryPersistenceRequest: CountryPersistenceProtocolRequest { 16 | 17 | private let COUNTRY_DTO = "country_codable_object_dto" 18 | 19 | func saveLocalCountryDataDTO(data: [CountryDTO]) { 20 | LOGFSTART() 21 | let userDefaults = UserDefaults.standard 22 | 23 | do { 24 | let encoder = JSONEncoder() 25 | let encoded = try encoder.encode(data) 26 | userDefaults.set(encoded, forKey: COUNTRY_DTO) 27 | userDefaults.synchronize() 28 | } catch (let error){ 29 | LOGE(error.localizedDescription) 30 | } 31 | } 32 | 33 | func getLocalCountryDataDTO() -> [CountryDTO]? { 34 | LOGFSTART() 35 | let userDefaults = UserDefaults.standard 36 | do { 37 | if let countryDTO = userDefaults.object(forKey: COUNTRY_DTO) as? Data { 38 | let decoder = JSONDecoder() 39 | let loadedCountries = try decoder.decode([CountryDTO].self, from: countryDTO) 40 | return loadedCountries 41 | } 42 | } catch (let error) { 43 | LOGE(error.localizedDescription) 44 | } 45 | return nil 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | # System Files 9 | .DS_Store 10 | Thumbs.db 11 | 12 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 13 | *.xcscmblueprint 14 | *.xccheckout 15 | *.moved-aside 16 | *.xcuserstate 17 | 18 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 19 | build/ 20 | DerivedData/ 21 | *.moved-aside 22 | *.pbxuser 23 | !default.pbxuser 24 | *.mode1v3 25 | !default.mode1v3 26 | *.mode2v3 27 | !default.mode2v3 28 | *.perspectivev3 29 | !default.perspectivev3 30 | 31 | ## Obj-C/Swift specific 32 | *.hmap 33 | *.ipa 34 | 35 | ## App packaging 36 | *.ipa 37 | *.dSYM.zip 38 | *.dSYM 39 | 40 | ## Playgrounds 41 | timeline.xctimeline 42 | playground.xcworkspace 43 | 44 | # Swift Package Manager 45 | # 46 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 47 | # Packages/ 48 | # Package.pins 49 | # Package.resolved 50 | # *.xcodeproj 51 | # 52 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 53 | # hence it is not needed unless you have added a package configuration file to your project 54 | # .swiftpm 55 | 56 | .build/ 57 | DerivedData 58 | 59 | # CocoaPods 60 | # 61 | # We recommend against adding the Pods directory to your .gitignore. However 62 | # you should judge for yourself, the pros and cons are mentioned at: 63 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 64 | # 65 | Pods/ 66 | # 67 | # Add this line if you want to avoid checking in source code from the Xcode workspace 68 | *.xcworkspace -------------------------------------------------------------------------------- /MVVM-Clean/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /MVVM-Clean/Extensions/UIViewController+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Extensions.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | extension UIViewController { 13 | 14 | /// Hide keyboard when tapped on a UIVIewController if this is open 15 | func hideKeyboardWhenTappedAround() { 16 | let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(UIViewController.closeKeyboard)) 17 | tap.cancelsTouchesInView = false 18 | view.addGestureRecognizer(tap) 19 | } 20 | 21 | /// Selector method called to hide keyboard when open 22 | /// 23 | /// - Parameter sender: il sender della gesture 24 | @objc private func closeKeyboard() { 25 | self.view.endEditing(true) 26 | } 27 | 28 | /// Show an alert with custom title and message with one cancel button that close the alert 29 | /// 30 | /// - Parameters: 31 | /// - title: Alert title 32 | /// - message: Alert message 33 | /// - actionButtonTitle: Button title. Optional value if not present the default button title will be Ok 34 | func showCancelAlert(title: String, message: String, actionButtonTitle: String = "OK") { 35 | let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) 36 | let okAction = UIAlertAction(title: actionButtonTitle, style: .cancel) { (done) in 37 | print("[Alert closed] - \(message)") 38 | } 39 | alert.addAction(okAction) 40 | DispatchQueue.main.async { 41 | self.present(alert, animated: true, completion: nil) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /MVVM-Clean/Application/Configurations/Configurations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Configurations.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 30/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum API { 12 | /// Get base url value 13 | static var baseURL: URL { 14 | var baseEndpoint = "" 15 | do { 16 | baseEndpoint = try Configuration.value(for: "API_BASE_URL") as String 17 | } catch(let error) { 18 | print("Error :\(error)") 19 | baseEndpoint = "https://" 20 | } 21 | return URL(string: baseEndpoint)! 22 | } 23 | 24 | /// Value indicating the time that must pass between one HTTP call and the next 25 | static var networkHoursDelta: Int { 26 | var hours = 0 27 | do { 28 | hours = try Configuration.value(for: "NETWORK_REQUEST_HOURS_DELTA") as Int 29 | } catch(let error) { 30 | print("Error :\(error)") 31 | hours = 0 32 | } 33 | return hours 34 | } 35 | 36 | } 37 | 38 | 39 | enum Configuration { 40 | enum Error: Swift.Error { 41 | case missingKey, invalidValue 42 | } 43 | 44 | static func value(for key: String) throws -> T where T: LosslessStringConvertible { 45 | guard let object = Bundle.main.object(forInfoDictionaryKey:key) else { 46 | throw Error.missingKey 47 | } 48 | 49 | switch object { 50 | case let value as T: 51 | return value 52 | case let string as String: 53 | guard let value = T(string) else { fallthrough } 54 | return value 55 | default: 56 | throw Error.invalidValue 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Network/Providers/Summary/SummaryRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Covid19Router.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 03/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | //import Foundation 10 | //import Alamofire 11 | // 12 | //enum SummaryRouter: APIConfiguration { 13 | // 14 | // case getSummaryData 15 | // 16 | // var method: HTTPMethod { 17 | // switch self { 18 | // case .getSummaryData: 19 | // return .get 20 | // } 21 | // } 22 | // 23 | // var path: String { 24 | // switch self { 25 | // case .getSummaryData: 26 | // return "/summary" 27 | // } 28 | // } 29 | // 30 | // var parameters: Parameters? { 31 | // switch self { 32 | // case .getSummaryData: 33 | // return nil 34 | // } 35 | // } 36 | // 37 | // func asURLRequest() throws -> URLRequest { 38 | // let url = API.baseURL 39 | // 40 | // let pathWithEncoding = path.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! 41 | // var urlRequest = URLRequest(url: URL(string: "\(url)\(pathWithEncoding)")!) 42 | // 43 | // // HTTP Method 44 | // urlRequest.httpMethod = method.rawValue 45 | // 46 | // // Common Headers 47 | // urlRequest.setValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.acceptType.rawValue) 48 | // 49 | // // Parameters 50 | // if let parameters = parameters { 51 | // do { 52 | // urlRequest.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: []) 53 | // } catch { 54 | // throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error)) 55 | // } 56 | // } 57 | // 58 | // return urlRequest 59 | // } 60 | // 61 | //} 62 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Persistence/Summary/SummaryPersistenceRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SummaryPersistenceRequest.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 03/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | /** 11 | This is the implementation of SummaryLocalProtocolRequest. For simplicity, we use UserDefaults instead of a real datatabase. 12 | Here, we are going to implement the methods that will be used to locally save and retrieve summary data. 13 | */ 14 | class SummaryPersistenceRequest: SummaryPersistenceProtocolRequest { 15 | 16 | private let SUMMARY_DTO = "summary_codable_object_sto" 17 | 18 | /// Save DTO Summary object to UserDefaults 19 | /// - Parameter data: Summary data to save locally 20 | /// - Throws: 21 | func saveLocalSummaryDTO(data: SummaryDTO) { 22 | LOGFSTART() 23 | let userDefaults = UserDefaults.standard 24 | 25 | do { 26 | let encoder = JSONEncoder() 27 | let encoded = try encoder.encode(data) 28 | userDefaults.set(encoded, forKey: SUMMARY_DTO) 29 | userDefaults.synchronize() 30 | } catch (let error){ 31 | LOGE(error.localizedDescription) 32 | } 33 | } 34 | 35 | /// Get local summary DTO model from UserDefaults. If no data is stored, nil will be returned 36 | /// - Throws: 37 | /// - Returns: Summary object if is present, nil otherwise 38 | func getLocalSummaryDataDTO() -> SummaryDTO? { 39 | LOGFSTART() 40 | let userDefaults = UserDefaults.standard 41 | do { 42 | if let summaryDTO = userDefaults.object(forKey: SUMMARY_DTO) as? Data { 43 | let decoder = JSONDecoder() 44 | let loadedSummary = try decoder.decode(SummaryDTO.self, from: summaryDTO) 45 | return loadedSummary 46 | } 47 | } catch (let error) { 48 | LOGE(error.localizedDescription) 49 | } 50 | return nil 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Main/Profile/ViewModel/ProfileViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileViewModel.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 17/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | //MARK: - ProfileViewModel Status 13 | enum ProfileViewModelStatus { 14 | case none 15 | // Logout state 16 | case loggeoutProcessBegin 17 | case logoutProcessSuccess 18 | case logoutProcessFailure 19 | // Getting user data state 20 | case gettingUserData 21 | case gettingUserDataSuccess 22 | case gettingUserDataFailure 23 | } 24 | 25 | protocol ProfileViewModelDelegate: ProfileViewModelInputDelegate, ProfileViewModelOutputDelegate { } 26 | 27 | class ProfileViewModel: ProfileViewModelDelegate { 28 | var currentUser: User? 29 | var profileUseCase: ProfileUseCaseDelegate? 30 | var status: CurrentValueSubject = .init(.none) 31 | var error: CustomError? 32 | 33 | func logoutUser() { 34 | LOGI("User is going to be logged out") 35 | status.value = .loggeoutProcessBegin 36 | profileUseCase?.logout() 37 | } 38 | 39 | func getUserData() { 40 | LOGP("Getting user data status") 41 | status.value = .gettingUserData 42 | profileUseCase?.getCurrentUserData() 43 | } 44 | 45 | } 46 | 47 | extension ProfileViewModel: ProfileUseCaseResponseDelegate { 48 | 49 | //MARK: - Logout response 50 | func onLogoutSuccess() { 51 | currentUser = nil 52 | status.send(.logoutProcessSuccess) 53 | } 54 | 55 | func onLogoutFailure(error: CustomError) { 56 | self.error = error 57 | self.status.value = .logoutProcessFailure 58 | } 59 | 60 | //MARK: - Userdata response 61 | func gettingUserDataSuccess(currentUser: User) { 62 | self.currentUser = currentUser 63 | status.send(.gettingUserDataSuccess) 64 | } 65 | 66 | func gettingUserDataFailure(error: CustomError) { 67 | self.error = error 68 | status.send(.gettingUserDataFailure) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Network/DTO/CountryDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountryDTO.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 03/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | Country DTO model mapped from API JSON data 13 | */ 14 | struct CountryDTO: Codable { 15 | let country: String? 16 | let countryCode: String? 17 | let slug: String? 18 | let newConfirmed: Int32? 19 | let totalConfirmed: Int32? 20 | let newDeaths: Int32? 21 | let totalDeaths: Int32? 22 | let newRecovered: Int32? 23 | let totalRecovered: Int32? 24 | let date: String? 25 | 26 | enum CodingKeys: String, CodingKey { 27 | case country = "Country" 28 | case countryCode = "CountryCode" 29 | case slug = "Slug" 30 | case newConfirmed = "NewConfirmed" 31 | case totalConfirmed = "TotalConfirmed" 32 | case newDeaths = "NewDeaths" 33 | case totalDeaths = "TotalDeaths" 34 | case newRecovered = "NewRecovered" 35 | case totalRecovered = "TotalRecovered" 36 | case date = "Date" 37 | } 38 | 39 | init(from decoder: Decoder) throws { 40 | let values = try decoder.container(keyedBy: CodingKeys.self) 41 | 42 | country = try values.decodeIfPresent(String.self, forKey: .country) 43 | countryCode = try values.decodeIfPresent(String.self, forKey: .countryCode) 44 | slug = try values.decodeIfPresent(String.self, forKey: .slug) 45 | newConfirmed = try values.decodeIfPresent(Int32.self, forKey: .newConfirmed) 46 | 47 | totalConfirmed = try values.decodeIfPresent(Int32.self, forKey: .totalConfirmed) 48 | newDeaths = try values.decodeIfPresent(Int32.self, forKey: .newDeaths) 49 | totalDeaths = try values.decodeIfPresent(Int32.self, forKey: .totalDeaths) 50 | newRecovered = try values.decodeIfPresent(Int32.self, forKey: .newRecovered) 51 | 52 | totalRecovered = try values.decodeIfPresent(Int32.self, forKey: .totalRecovered) 53 | date = try values.decodeIfPresent(String.self, forKey: .date) 54 | 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Repositories/Summary/SummaryRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Covid19Repository.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 03/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class SummaryRepositoryAsync: SummaryRepositoryAsyncDelegate { 12 | 13 | var covidNetwork: SummaryNetworkProtocolAsyncRequest? 14 | var summaryLocal: SummaryPersistenceProtocolRequest? 15 | var countryLocal: CountryPersistenceProtocolRequest? 16 | var isRunningFromTest: Bool? 17 | 18 | func getSummaryData() async throws -> Summary { 19 | var needDataFromNetwork = true 20 | 21 | let localSummaryDTO = summaryLocal?.getLocalSummaryDataDTO() 22 | 23 | if( localSummaryDTO != nil ) { 24 | LOGD("Local summary data found!") 25 | needDataFromNetwork = CustomDateUtils.checkForNetworkUpdate(lastUpdate: localSummaryDTO!.date ?? "") 26 | } 27 | // This is a flag that force collecting data from network if a unit test is running. 28 | // In case of test, data are collected on a JSON file and not really on network 29 | if let _isRunningFromTest = isRunningFromTest { 30 | needDataFromNetwork = _isRunningFromTest 31 | } 32 | 33 | if( needDataFromNetwork ) { 34 | 35 | guard let summary = try await covidNetwork?.getSummaryData() else { 36 | throw CustomError.nilData 37 | } 38 | 39 | summaryLocal?.saveLocalSummaryDTO(data: summary) 40 | if let countries = summary.countries { 41 | self.countryLocal?.saveLocalCountryDataDTO(data: countries) 42 | } 43 | 44 | return SummaryDTOMapper.map(summary: summary) 45 | } else { 46 | 47 | guard let localData = localSummaryDTO else { 48 | throw CustomError.noLocalDataFound 49 | } 50 | 51 | let summaryModel = SummaryDTOMapper.map(summary: localData) 52 | LOGD("Success sent with localData") 53 | return summaryModel 54 | 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /MVVM-Clean/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | API_BASE_URL 6 | ${API_BASE_URL} 7 | Appareance 8 | Dark 9 | CFBundleDevelopmentRegion 10 | $(DEVELOPMENT_LANGUAGE) 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 21 | CFBundleShortVersionString 22 | $(MARKETING_VERSION) 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | NETWORK_REQUEST_HOURS_DELTA 28 | ${NETWORK_REQUEST_HOURS_DELTA} 29 | UIApplicationSceneManifest 30 | 31 | UIApplicationSupportsMultipleScenes 32 | 33 | UISceneConfigurations 34 | 35 | UIWindowSceneSessionRoleApplication 36 | 37 | 38 | UISceneConfigurationName 39 | Default Configuration 40 | UISceneDelegateClassName 41 | $(PRODUCT_MODULE_NAME).SceneDelegate 42 | 43 | 44 | 45 | 46 | UILaunchStoryboardName 47 | LaunchScreen 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | 56 | UISupportedInterfaceOrientations~ipad 57 | 58 | UIInterfaceOrientationPortrait 59 | UIInterfaceOrientationPortraitUpsideDown 60 | UIInterfaceOrientationLandscapeLeft 61 | UIInterfaceOrientationLandscapeRight 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Repositories/Country/CountryRepositoryDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountryRepositoryDelegate.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | //import Foundation 10 | 11 | //protocol CountryRepositoryDelegate { 12 | // 13 | // //MARK: - Data object 14 | // var countryLocal: CountryPersistenceProtocolRequest? { get set } 15 | // var countryNetwork: CountryNetworkProtocolRequest? { get set } 16 | // 17 | // //MARK: - Methods 18 | // 19 | // /// Used to get all countries data. 20 | // /// - Parameters: 21 | // /// - success: Event fired in success case 22 | // /// - failure: Event fired in failure case 23 | // func getCountriesData(success: @escaping ([Country])->Void, failure: @escaping (CustomError)->Void) 24 | // 25 | // /// Used to get country data filtered by various data. 26 | // /// - Parameters: 27 | // /// - countrySlug: Country slug 28 | // /// - status: Covid status 29 | // /// - from: Date from 30 | // /// - to: Date to 31 | // /// - success: Event fired in success case 32 | // /// - failure: Event fired in failure case 33 | // func getCountryData(by countrySlug: String, status: Covid19Status, from: String, to: String, success: @escaping ([Country])->Void, failure: @escaping (CustomError)->Void) 34 | // 35 | //} 36 | 37 | 38 | //MARK: - Async 39 | protocol CountryRepositoryAsyncDelegate { 40 | 41 | //MARK: - Data object 42 | var countryLocal: CountryPersistenceProtocolRequest? { get set } 43 | var countryAsyncNetwork: CountryNetworkProtocolAsyncRequest? { get } 44 | 45 | /// Used to get all countries data. 46 | /// - Returns: <#description#> 47 | func getCountriesAsyncData() async throws -> [Country] 48 | 49 | /// Used to get country data filtered by various data. 50 | /// - Parameters: 51 | /// - countrySlug: Country slug 52 | /// - status: Covid status 53 | /// - from: Date from 54 | /// - to: Date to 55 | /// - Returns: <#description#> 56 | func getCountryAsyncData(by countrySlug: String, status: Covid19Status, from: String, to: String) async throws -> [Country] 57 | 58 | } 59 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Splash/ViewControllers/SplashViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplashViewController.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Combine 11 | 12 | class SplashViewController: UIViewController { 13 | 14 | @IBOutlet var welcomeMessage: UILabel! 15 | 16 | var subscriptions: Set = .init() 17 | var splashViewModel: SplashScreenViewModelDelegate? 18 | var router: SplashRouterInput? 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | // Do any additional setup after loading the view. 23 | localizeUI() 24 | bind() 25 | 26 | // Add 0.5 second in case we would like to show some graphic content for few seconds. 27 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) { 28 | self.splashViewModel?.checkUserState() 29 | } 30 | } 31 | 32 | /// Prepare all label with current localization. 33 | private func localizeUI() { 34 | let localizedWelcomeLabel = NSLocalizedString("splash_message", comment: "") 35 | welcomeMessage.text = localizedWelcomeLabel 36 | } 37 | 38 | /// <#Description#> 39 | private func bind() { 40 | if let viewModel = splashViewModel { 41 | viewModel.status.sink { state in 42 | switch viewModel.status.value { 43 | case .none: 44 | print("Nothing to do") 45 | case .loggedIn: 46 | print("Goes to home view") 47 | self.goestToHomeViewController() 48 | case .notLoggedIn: 49 | print("Goes to login view") 50 | self.goestToLoginViewController() 51 | } 52 | }.store(in: &subscriptions) 53 | } 54 | } 55 | 56 | 57 | /// Get HomeViewCotroller and set as main window 58 | private func goestToHomeViewController() { 59 | router?.navigateToHomeView() 60 | } 61 | 62 | /// Get LoginViewController and set as main window 63 | private func goestToLoginViewController() { 64 | router?.navigateToLoginView() 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Network/Mappers/Country/CountryDTOMapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountryDTOMapper.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct CountryDTOMapper { 12 | 13 | /// Map a single Country DTO object in a single CountryModel object 14 | /// - Parameter country: <#country description#> 15 | /// - Returns: <#description#> 16 | static func map(country: CountryDTO) -> Country { 17 | var countryModel = Country() 18 | 19 | let nf = NumberFormatter() 20 | nf.numberStyle = .decimal 21 | 22 | countryModel.countryName = country.country ?? "Unknow" 23 | if let newConfirmed = country.newConfirmed { 24 | countryModel.newConfirmed = nf.string(from: NSNumber(value: newConfirmed)) ?? "0" 25 | } 26 | 27 | if let totalConfirmed = country.totalConfirmed { 28 | countryModel.totalConfirmed = nf.string(from: NSNumber(value: totalConfirmed)) ?? "0" 29 | } 30 | 31 | if let newRecovered = country.newRecovered { 32 | countryModel.newRecovered = nf.string(from: NSNumber(value: newRecovered)) ?? "0" 33 | } 34 | 35 | if let totalRecovered = country.totalRecovered { 36 | countryModel.totalRecovered = nf.string(from: NSNumber(value: totalRecovered)) ?? "0" 37 | } 38 | 39 | if let newDeath = country.newDeaths { 40 | countryModel.newDeaths = nf.string(from: NSNumber(value: newDeath)) ?? "0" 41 | } 42 | 43 | if let totalDeath = country.totalDeaths { 44 | countryModel.totalDeaths = nf.string(from: NSNumber(value: totalDeath)) ?? "0" 45 | } 46 | 47 | return countryModel 48 | } 49 | 50 | /// Map a Country DTO object array in a CountryModel object array 51 | /// - Parameter countries: <#countries description#> 52 | /// - Returns: <#description#> 53 | static func map(countries: [CountryDTO]) -> [Country] { 54 | var countriesModel = [Country]() 55 | 56 | countries.forEach { (country) in 57 | countriesModel.append( map(country: country) ) 58 | } 59 | 60 | return countriesModel 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Main/Summary/Cell/SummaryTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SummaryTableViewCell.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 30/11/21. 6 | // Copyright © 2021 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SummaryTableViewCell: UITableViewCell { 12 | 13 | static let IDENTIFIER = "summary_cell" 14 | static let XIB_NAME = "SummaryTableViewCell" 15 | static let DEFAULT_HEIGHT: CGFloat = 130.0 16 | 17 | @IBOutlet weak var cellTitle: UILabel! 18 | @IBOutlet weak var leftSectionTitle: UILabel! 19 | @IBOutlet weak var leftSectionData: UILabel! 20 | @IBOutlet weak var rightSectionTitle: UILabel! 21 | @IBOutlet weak var rightSectionData: UILabel! 22 | 23 | override func awakeFromNib() { 24 | super.awakeFromNib() 25 | } 26 | 27 | override func setSelected(_ selected: Bool, animated: Bool) { 28 | super.setSelected(selected, animated: animated) 29 | } 30 | 31 | override func prepareForReuse() { 32 | cellTitle.text = "" 33 | leftSectionTitle.text = "" 34 | leftSectionData.text = "" 35 | rightSectionTitle.text = "" 36 | rightSectionData.text = "" 37 | } 38 | 39 | func setupDeath(summary: SummaryDeath) { 40 | cellTitle.text = summary.title 41 | leftSectionTitle.text = NSLocalizedString("new", comment: "") 42 | leftSectionData.text = summary.newDeath 43 | 44 | rightSectionTitle.text = NSLocalizedString("total", comment: "") 45 | rightSectionData.text = summary.totalDeaths 46 | } 47 | 48 | func setupConfirmed(summary: SummaryConfirmed) { 49 | cellTitle.text = summary.title 50 | leftSectionTitle.text = NSLocalizedString("new", comment: "") 51 | leftSectionData.text = summary.newConfirmedCases 52 | 53 | rightSectionTitle.text = NSLocalizedString("total", comment: "") 54 | rightSectionData.text = summary.totalConfirmedCases 55 | } 56 | 57 | func setupConfirmed(summary: SummaryRecovered) { 58 | cellTitle.text = summary.title 59 | leftSectionTitle.text = NSLocalizedString("new", comment: "") 60 | leftSectionData.text = summary.newRecoveredCases 61 | 62 | rightSectionTitle.text = NSLocalizedString("total", comment: "") 63 | rightSectionData.text = summary.totalRecovered 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /MVVM-Clean/Application/DI/UseCaseAssembly.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UseCaseAssembly.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Swinject 11 | 12 | 13 | class UseCaseAssembly: Assembly { 14 | 15 | func assemble(container: Container) { 16 | 17 | container.register(LoginUseCaseDelegate.self) { resolver in 18 | let loginUseCase = LoginUseCase() 19 | guard let loginRepository = resolver.resolve(LoginRepositoryDelegate.self) else { 20 | fatalError("Assembler was unable to resolve LoginRepositoryDelegate") 21 | } 22 | loginUseCase.loginRepository = loginRepository 23 | return loginUseCase 24 | }.inObjectScope(.transient) 25 | 26 | container.register(ProfileUseCaseDelegate.self) { resolver in 27 | let profileUseCase = ProfileUseCase() 28 | guard let profileRepository = resolver.resolve(ProfileRepositoryDelegate.self) else { 29 | fatalError("Assembler was unable to resolve ProfileRepositoryDelegate") 30 | } 31 | profileUseCase.profileRepository = profileRepository 32 | return profileUseCase 33 | }.inObjectScope(.transient) 34 | 35 | //MARK: - Async 36 | container.register(SummaryUseCaseAsyncDelegate.self) { resolver in 37 | let covidUseCase = SummaryAsyncUseCase() 38 | guard let summaryRepository = resolver.resolve(SummaryRepositoryAsyncDelegate.self) else { 39 | fatalError("Assembler was unable to resolve Covid19RepositoryAsyncDelegate") 40 | } 41 | covidUseCase.summaryRepository = summaryRepository 42 | 43 | return covidUseCase 44 | }.inObjectScope(.transient) 45 | 46 | container.register(CountryUseCaseAsyncDelegate.self) { resolver in 47 | let countryUseCase = CountryAsyncUseCase() 48 | guard let countryRepository = resolver.resolve(CountryRepositoryAsyncDelegate.self) else { 49 | fatalError("Assembler was unable to resolve CountryRepositoryAsyncDelegate") 50 | } 51 | countryUseCase.countryRepository = countryRepository 52 | 53 | return countryUseCase 54 | }.inObjectScope(.transient) 55 | 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /MVVM-Clean/Utility/Loadable/LoadingView.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /MVVM-Clean/Infrastructure/Network/APIConfigurationSwift.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIConfigurationSwift.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 31/03/22. 6 | // Copyright © 2022 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public typealias ParameterSwift = [String: Any] 12 | 13 | protocol APIConfigurationSwift { 14 | 15 | var path: String { get } 16 | var method: HTTPMethodSwift { get } 17 | var header: [String: String]? { get } 18 | var parameters: ParameterSwift? { get } 19 | 20 | /// Transform APIConfigurationSwift protocol to an URLRequest object 21 | /// - Returns: <#description#> 22 | func asURLRequest() throws -> URLRequest 23 | } 24 | 25 | extension APIConfigurationSwift { 26 | 27 | func asURLRequest() throws -> URLRequest { 28 | var urlRequest: URLRequest? 29 | 30 | let url = API.baseURL 31 | let pathWithEncoding = path.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! 32 | 33 | // Parameters 34 | if let parameters = parameters { 35 | switch method { 36 | case .post: 37 | do { 38 | urlRequest = URLRequest(url: URL(string: "\(url)\(pathWithEncoding)")!) 39 | urlRequest!.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: []) 40 | } catch(let error) { 41 | throw error 42 | } 43 | case .get: 44 | var urlComponents = URLComponents(string: "\(url)\(pathWithEncoding)")! 45 | 46 | for param in parameters { 47 | urlComponents.queryItems?.append( URLQueryItem(name: param.key, value: "\(param.value)") ) 48 | } 49 | urlRequest = URLRequest(url: urlComponents.url!) 50 | default: 51 | throw CustomError.generic 52 | } 53 | } else { 54 | urlRequest = URLRequest(url: URL(string: "\(url)\(pathWithEncoding)")!) 55 | } 56 | 57 | guard var request = urlRequest else { 58 | throw CustomError.urlRequestNil 59 | } 60 | 61 | // HTTP Method 62 | request.httpMethod = method.rawValue 63 | 64 | // Common Headers 65 | if let headers = self.header { 66 | for h in headers { 67 | request.addValue(h.value, forHTTPHeaderField: h.key) 68 | } 69 | } 70 | 71 | return request 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Main/Profile/ViewContoller/ProfileViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileViewController.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 17/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Combine 11 | 12 | class ProfileViewController: UIViewController { 13 | 14 | @IBOutlet weak var usernameLabel: UILabel! 15 | @IBOutlet weak var logoutButton: UIButton! 16 | 17 | var router: ProfileNavigationRouterInput? 18 | var profileViewModel: ProfileViewModelDelegate? 19 | var subscriptions: Set = .init() 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | bind() 24 | } 25 | 26 | private func bind() { 27 | 28 | profileViewModel?.status.sink { state in 29 | switch state { 30 | case .none: 31 | LOGP("No action") 32 | 33 | case .gettingUserData: 34 | LOGP("Getting user") 35 | 36 | case .gettingUserDataSuccess: 37 | LOGP("User data success") 38 | if let user = self.profileViewModel?.currentUser { 39 | DispatchQueue.main.async { 40 | self.usernameLabel.text = user.username 41 | } 42 | } 43 | case .gettingUserDataFailure: 44 | LOGE("User data failed") 45 | self.showCancelAlert(title: "Error", message: self.profileViewModel?.error?.errorDescription ?? "") 46 | self.usernameLabel.text = "-" 47 | 48 | case .loggeoutProcessBegin: 49 | LOGP("Logout started") 50 | 51 | case .logoutProcessSuccess: 52 | LOGD("Logout success!") 53 | self.backToLoginViewController() 54 | 55 | case .logoutProcessFailure: 56 | self.showCancelAlert(title: "Error", message: self.profileViewModel?.error?.errorDescription ?? "") 57 | LOGE("Logout failed!") 58 | } 59 | }.store(in: &subscriptions) 60 | 61 | loadUserData() 62 | } 63 | 64 | private func loadUserData() { 65 | profileViewModel?.getUserData() 66 | } 67 | 68 | @IBAction func executeLogout(_ sender: UIButton) { 69 | profileViewModel?.logoutUser() 70 | } 71 | 72 | 73 | private func backToLoginViewController() { 74 | router?.navigateToLoginView() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /MVVM-Clean/Utility/Loadable/Loadable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Loadable.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | protocol Loadable: LoadingViewDelegate { 13 | func showLoading(isFullScreen: Bool, isBackgroundVisible: Bool) 14 | func hideLoading(animate: Bool) 15 | } 16 | 17 | extension Loadable where Self: UIViewController { 18 | 19 | /// Show loadings view on full screen mode 20 | func showLoading(isFullScreen: Bool = true, isBackgroundVisible: Bool = true) { 21 | let nibViews = Bundle.main.loadNibNamed(LoadingView.NIB_NAME, owner: self, options: nil) 22 | if let loadingView = nibViews?.first as? LoadingView { 23 | 24 | if isFullScreen { 25 | let currentWindow: UIWindow? = UIApplication.shared.keyWindow 26 | currentWindow?.addSubview(loadingView) 27 | loadingView.frame = currentWindow!.bounds 28 | } 29 | else { 30 | view.addSubview(loadingView) 31 | loadingView.frame = view.frame 32 | loadingView.delegate = self 33 | } 34 | 35 | loadingView.activityIndicator.startAnimating() 36 | 37 | } 38 | 39 | } 40 | 41 | /// Hide all loader views previously added 42 | func hideLoading(animate: Bool = false) { 43 | let currentWindow: UIWindow? = UIApplication.shared.keyWindow 44 | currentWindow?.getSubviews(type: LoadingView.self) 45 | .forEach({ (loadingView) in 46 | 47 | if animate { 48 | UIView.animate( 49 | withDuration: 0.4, 50 | delay: 0.0, 51 | options: UIView.AnimationOptions.curveEaseIn, 52 | animations: { 53 | loadingView.alpha = 0.0 54 | }, 55 | completion: { 56 | (finished: Bool) -> Void in 57 | loadingView.activityIndicator.stopAnimating() 58 | loadingView.removeFromSuperview() 59 | }) 60 | } 61 | else { 62 | loadingView.activityIndicator.stopAnimating() 63 | loadingView.removeFromSuperview() 64 | } 65 | 66 | }) 67 | } 68 | 69 | func onLoadingViewBackButton(){ 70 | 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /MVVM-Clean/Localization/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | MVVM-Clean 4 | 5 | Created by Alessandro Marcon on 04/08/2020. 6 | Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | */ 8 | 9 | "splash_message" = "MVVM Clean App"; 10 | "login_title" = "Ready for login?"; 11 | "login_subtitle" = "Type \"admin\" as username and \"pass123!\" as password to login"; 12 | "login" = "LOGIN"; 13 | "login_error_message" = "Hey, you entered wrong username or password. Check it and try again!"; 14 | "alert_error_title" = "Warning!"; 15 | "empty_credential" = "Please, fill the informations and try again!"; 16 | "wrong_password" = "Password, should be min 8 chars long"; 17 | "welcome_message" = "COVID19 summary data"; 18 | "last_summary_update" = "Last update %@"; 19 | "generic_error_message" = "An error as accourred. Please, try again!"; 20 | "generic_net_error_message" = "Here, you should get the server network error code %@."; 21 | "generic_localized_net_error_message" = "This will be the localized message from the server."; 22 | "nil_data" = "Data is nil"; 23 | "nil_data_msg" = "No data found on server"; 24 | "new_confirmed_cases" = "Confirmed cases"; 25 | "total_cases" = "Total cases"; 26 | "new_deaths_cases" = "Deaths cases"; 27 | "total_deaths" = "Total deaths cases"; 28 | "new_recovered" = "Recovered cases"; 29 | "total_recovered" = "Total recovered cases"; 30 | "summary_mapper_error" = "SummaryModelMapper or HTTP response is nil and DTO could not be mapped."; 31 | "countries_label" = "Countries"; 32 | "country_mapper_error" = "CountryModelMapper or http response is nil and DTO could not be mapped."; 33 | "new_cases_cell" = "New cases: %@"; 34 | "new_recovered_cell" = "New recovered cases: %@"; 35 | "new_deaths_cell" = "New deaths cases: %@"; 36 | "total_cases_cell" = "Total cases: %@"; 37 | "total_recovered_cell" = "Total recovered cases: %@"; 38 | "total_deaths_cell" = "Total deaths cases: %@"; 39 | "no_local_user_found" = "No user data found in local storage"; 40 | "unable_logout" = "Unable to delete user data. Operation failed. Please, try again later."; 41 | "new" = "New"; 42 | "total" = "Total"; 43 | "no_content_found" = "No content found on server."; 44 | "unauthorized_message" = "User not authorized"; 45 | "badRequest_message" = "Bad request error!"; 46 | "not_found_message" = "Resource not found"; 47 | "forbidden_message" = "Forbidden access"; 48 | "user_not_found_message" = "User not found!"; 49 | "logout_error_message" = "Error on logout"; 50 | "login_error_message" = "Error on login"; 51 | "no_connection_error_message" = "You seems to be offline"; 52 | "url_request_nil" = "Unable to create url request object"; 53 | "no_local_data_found" = "No local summary data found"; 54 | "json_decode_error" = "Error during json decoding"; 55 | -------------------------------------------------------------------------------- /MVVM-Clean/Localization/it.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | MVVM-Clean 4 | 5 | Created by Alessandro Marcon on 04/08/2020. 6 | Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | */ 8 | 9 | "splash_message" = "MVVM Clean App"; 10 | "login_title" = "Pronto per la login?"; 11 | "login_subtitle" = "Inserisci \"admin\" come username e \"pass123!\" come password per entrare"; 12 | "login" = "ENTRA"; 13 | "login_error_message" = "Sembra che tu abbia inserito username o password errate. Controlla e prova di nuovo!"; 14 | "alert_error_title" = "Attenzione!"; 15 | "empty_credential" = "Inserisci le credenziali e prova di nuovo"; 16 | "wrong_password" = "La password deve contenere almeno 8 caratteri"; 17 | "welcome_message" = "COVID19 dati di riepilogo"; 18 | "last_summary_update" = "Ultimo aggiornamento %@"; 19 | "generic_error_message" = "Si è verificato un errore per favore, riprova!"; 20 | "generic_net_error_message" = "Qui, dovresti recuperare l'errore di rete del server codice %@."; 21 | "generic_localized_net_error_message" = "Questo sarà il messaggio localizzato del server."; 22 | "nil_data" = "Data è nil"; 23 | "nil_data_msg" = "Nessun dato ricevuto dal server"; 24 | "new_confirmed_cases" = "Casi confermati"; 25 | "total_cases" = "Casi totali"; 26 | "new_deaths_cases" = "Casi di morte"; 27 | "total_deaths" = "Morti totali"; 28 | "new_recovered" = "Nuovi ricoveri"; 29 | "total_recovered" = "Ricoveri totali"; 30 | "summary_mapper_error" = "SummaryModelMapper o la risposta HTTP è nil e il DTO non può essere mappato."; 31 | "countries_label" = "Paesi"; 32 | "country_mapper_error" = "CountryModelMapper o la risposta HTTP è nil e il DTO non può essere mappato."; 33 | "new_cases_cell" = "Nuovi casi: %@"; 34 | "new_recovered_cell" = "Nuovi ricoveri: %@"; 35 | "new_deaths_cell" = "Nuove morti: %@"; 36 | "total_cases_cell" = "Casi totali: %@"; 37 | "total_recovered_cell" = "Ricoveri totali: %@"; 38 | "total_deaths_cell" = "Morti totali: %@"; 39 | "no_local_user_found" = "Non è stato trovato l'utente nei dati locali"; 40 | "unable_logout" = "Impossibile cancellare i dati utente. Operazione fallita. Per favore, prova più tardi."; 41 | "new" = "Nuovi"; 42 | "total" = "Totali"; 43 | "no_content_found" = "Nessun contenuto trovato."; 44 | "unauthorized_message" = "Utente non autorizzato"; 45 | "badRequest_message" = "Richiesta mal formattata."; 46 | "not_found_message" = "Risorsa non trovata"; 47 | "forbidden_message" = "Accesso non consentito"; 48 | "user_not_found_message" = "Utente non trovato."; 49 | "logout_error_message" = "Problemi in fase di logout"; 50 | "login_error_message" = "Problemi in fase di logout login"; 51 | "no_connection_error_message" = "Sembra che tu sia offline!"; 52 | "url_request_nil" = "Impossibile creare l'oggetto URL Request"; 53 | "no_local_data_found" = "Non sono stati trovati dati locali di riassunto."; 54 | "json_decode_error" = "Errore durante il decode del JSON."; 55 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Main/Country/Cell/CountryTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountryTableViewCell.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 06/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CountryTableViewCell: UITableViewCell { 12 | 13 | static let IDENTIFIER = "country_cell" 14 | static let XIB_NAME = "CountryCell" 15 | static let DEFAULT_HEIGHT: CGFloat = 221.0 16 | 17 | @IBOutlet var countryNameLabel: UILabel! 18 | @IBOutlet var newCasesLabel: UILabel! 19 | @IBOutlet var newRecoveredCasesLabel: UILabel! 20 | @IBOutlet var newDeathsCasesLabel: UILabel! 21 | @IBOutlet var totalCasesLabel: UILabel! 22 | @IBOutlet var totalRecoveredLabel: UILabel! 23 | @IBOutlet var totalDeathsCasesLabel: UILabel! 24 | 25 | 26 | override func awakeFromNib() { 27 | super.awakeFromNib() 28 | } 29 | 30 | override func setSelected(_ selected: Bool, animated: Bool) { 31 | super.setSelected(false, animated: false) 32 | } 33 | 34 | override func prepareForReuse() { 35 | newCasesLabel.text = "" 36 | newRecoveredCasesLabel.text = "" 37 | newDeathsCasesLabel.text = "" 38 | totalCasesLabel.text = "" 39 | totalRecoveredLabel.text = "" 40 | totalDeathsCasesLabel.text = "" 41 | } 42 | 43 | func bind(country: Country) { 44 | countryNameLabel.text = country.countryName 45 | 46 | let newCasesLocalized = NSLocalizedString("new_cases_cell", comment: "") 47 | newCasesLabel.text = String.localizedStringWithFormat(newCasesLocalized, "\(country.newConfirmed)") 48 | 49 | let newRecoveredLocalized = NSLocalizedString("new_recovered_cell", comment: "") 50 | newRecoveredCasesLabel.text = String.localizedStringWithFormat(newRecoveredLocalized, "\(country.newRecovered)") 51 | 52 | let newDeathLocalized = NSLocalizedString("new_deaths_cell", comment: "") 53 | newDeathsCasesLabel.text = String.localizedStringWithFormat(newDeathLocalized, "\(country.newDeaths)") 54 | 55 | let totalCasesLocalized = NSLocalizedString("total_cases_cell", comment: "") 56 | totalCasesLabel.text = String.localizedStringWithFormat(totalCasesLocalized, "\(country.totalConfirmed)") 57 | 58 | let totalRecoveredLocalized = NSLocalizedString("total_recovered_cell", comment: "") 59 | totalRecoveredLabel.text = String.localizedStringWithFormat(totalRecoveredLocalized, "\(country.totalRecovered)") 60 | 61 | let totalDeathLocalized = NSLocalizedString("total_deaths_cell", comment: "") 62 | totalDeathsCasesLabel.text = String.localizedStringWithFormat(totalDeathLocalized, "\(country.totalDeaths)") 63 | 64 | self.isUserInteractionEnabled = false 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MVVM Clean iOS starter project 2 | 3 | ![platform](https://img.shields.io/badge/platform-iOS-lightgrey) 4 | ![swift](https://img.shields.io/badge/Swift-5.0-red) 5 | ![target-ios](https://img.shields.io/badge/Target-iOS%20%7C%2015.0-blue) 6 | ![cocoapods-version](https://img.shields.io/badge/Cocoapods-v.%201.8.4-green) 7 | 8 | ![MVVM Clean iOS home view controller](home.png) 9 | 10 | This is a clean iOS project structured as follow: 11 | 12 | - MVVM 13 | - Clean architecture 14 | - Repository pattern 15 | - Navigation router pattern 16 | - Cocoapods for dependency manager 17 | - Full use of Dependency Injection 18 | - Async Await for network request 19 | - Combine for view model and view controller binding 20 | - Localized, just 2 languages for demo porpouse 21 | - Storyboard for interfaces 22 | - Unit Test 23 | 24 | You can use this project as a template to start building your new iOS app. 25 | 26 | # What's inside. 27 | This is a simple demo project that implement MVVM Clean architecture and repository pattern. It's a COVID-19 dashboard with a very simple (and fake) login view. It collect data from remote or local repository. The local repository, just to keep the app simple, stores the data in UserDefaults but, in a real project, if you need to save a large amount of data, it would be better to use a database like Realm, SQL Lite or Core Data. It include also a Unit test as sample to show how to integrate test on this architecture. 28 | 29 | #### ATTENTION! 30 | COVID-19 data is real and collected by https://covid19api.com/ but the application is not intended to be a medical tool. COVID-19 api are free and here are used just to show how to execute network request and parse them. 31 | 32 | # What do you need to start 33 | 34 | All you need to start with this project is 35 | 36 | - Xcode 37 | - Cocoapods 38 | 39 | If you don't have cocoapods on your mac, or you don't know what cocoapod is, check this out https://cocoapods.org/ 40 | 41 | # Let's start 42 | 43 | 1 44 | After cloning project from Github, open terminal and navigate in project folder 45 | 46 | > cd /your_loca_path/MVVM-Clean 47 | 48 | 2 49 | next, install all pods with command. 50 | 51 | > pod install 52 | 53 | When all pods are installed, open project folder by digit on terminal 54 | 55 | > open . 56 | 57 | double click on **MVVM-Clean.xcworkspace** file and Xcode will be opened. 58 | 59 | # External Library 60 | 61 | These are the libraries used in project with github url and a very short description. 62 | 63 | SDK | URL | DESCRIPTION 64 | -------- | --- | ----------- 65 | Swinject | https://github.com/Swinject/Swinject | Dependency injection framework 66 | 67 | # External resource 68 | 69 | Covid-19 data are collected by [COVID-19 API](https://covid19api.com/) 70 | 71 | The users Icons is made by Freepik from www.flaticon.com -------------------------------------------------------------------------------- /MVVM-Clean/Extensions/UIColor+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Extension.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 08/02/22. 6 | // Copyright © 2022 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIColor { 12 | 13 | static func randomColor() -> UIColor{ 14 | let red = CGFloat(drand48()) 15 | let green = CGFloat(drand48()) 16 | let blue = CGFloat(drand48()) 17 | return UIColor(red: red, green: green, blue: blue, alpha: 1.0) 18 | } 19 | 20 | convenience init(red: Int, green: Int, blue: Int) { 21 | assert(red >= 0 && red <= 255, "Invalid red component") 22 | assert(green >= 0 && green <= 255, "Invalid green component") 23 | assert(blue >= 0 && blue <= 255, "Invalid blue component") 24 | self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1.0) 25 | } 26 | 27 | convenience init(hex:Int) { 28 | self.init(red:(hex >> 16) & 0xff, green:(hex >> 8) & 0xff, blue:hex & 0xff) 29 | } 30 | 31 | public convenience init(hexString: String) { 32 | var cString:String = hexString.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() 33 | 34 | if (cString.hasPrefix("#")) { 35 | cString.remove(at: cString.startIndex) 36 | } 37 | 38 | if ((cString.count) != 6) { 39 | self.init(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0) 40 | }else { 41 | var rgbValue: UInt64 = 0 42 | Scanner(string: cString).scanHexInt64(&rgbValue) 43 | self.init(red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0, green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0, blue: CGFloat(rgbValue & 0x0000FF) / 255.0, alpha: 1.0) 44 | } 45 | } 46 | 47 | convenience init(hexString : String, customAlpha: CGFloat) 48 | { 49 | let fixedString = hexString.replacingOccurrences(of: "#", with: "") 50 | if let rgbValue = UInt(fixedString, radix: 16) { 51 | let red = CGFloat((rgbValue >> 16) & 0xff) / 255 52 | let green = CGFloat((rgbValue >> 8) & 0xff) / 255 53 | let blue = CGFloat((rgbValue ) & 0xff) / 255 54 | self.init(red: red, green: green, blue: blue, alpha: customAlpha) 55 | } else { 56 | self.init(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0) 57 | } 58 | } 59 | 60 | 61 | // Color from https://flatuicolors.com/palette/gb 62 | struct Custom { 63 | //MARK: - Buttons color 64 | static let primary = UIColor(hex: 0x192A56) 65 | static let primaryButton = UIColor(hex: 0x0097E6) 66 | static let primaryText = UIColor(hex: 0xDCDDE1) 67 | static let secondary = UIColor(hex: 0x273C75) 68 | static let secondaryText = UIColor(hex: 0xE1B12C) 69 | static let textfieldBackground = UIColor(hex: 0xDCDDE1) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /MVVM-Clean/AppDelegate/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @available(iOS 13.0, *) 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 18 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 19 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 20 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 21 | 22 | guard let windowScene = (scene as? UIWindowScene) else { return } 23 | window = UIWindow(frame: windowScene.coordinateSpace.bounds) 24 | window?.windowScene = windowScene 25 | 26 | let navController = UINavigationController(rootViewController: SplashConfigurator().configure()) 27 | navController.navigationBar.isHidden = true 28 | window?.rootViewController = navController 29 | window?.makeKeyAndVisible() 30 | } 31 | 32 | func sceneDidDisconnect(_ scene: UIScene) { 33 | // Called as the scene is being released by the system. 34 | // This occurs shortly after the scene enters the background, or when its session is discarded. 35 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 36 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 37 | } 38 | 39 | func sceneDidBecomeActive(_ scene: UIScene) { 40 | // Called when the scene has moved from an inactive state to an active state. 41 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 42 | } 43 | 44 | func sceneWillResignActive(_ scene: UIScene) { 45 | // Called when the scene will move from an active state to an inactive state. 46 | // This may occur due to temporary interruptions (ex. an incoming phone call). 47 | } 48 | 49 | func sceneWillEnterForeground(_ scene: UIScene) { 50 | // Called as the scene transitions from the background to the foreground. 51 | // Use this method to undo the changes made on entering the background. 52 | } 53 | 54 | func sceneDidEnterBackground(_ scene: UIScene) { 55 | // Called as the scene transitions from the foreground to the background. 56 | // Use this method to save data, release shared resources, and store enough scene-specific state information 57 | // to restore the scene back to its current state. 58 | } 59 | 60 | 61 | } 62 | 63 | -------------------------------------------------------------------------------- /MVVM-Clean/Assets/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon-App-20x20@2x.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "Icon-App-20x20@3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "Icon-App-29x29@2x.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "Icon-App-29x29@3x.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "Icon-App-40x40@2x.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "Icon-App-40x40@3x.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "Icon-App-60x60@2x.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "Icon-App-60x60@3x.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "Icon-App-20x20@1x.png", 53 | "idiom" : "ipad", 54 | "scale" : "1x", 55 | "size" : "20x20" 56 | }, 57 | { 58 | "filename" : "Icon-App-20x20@2x-1.png", 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "Icon-App-29x29@1x.png", 65 | "idiom" : "ipad", 66 | "scale" : "1x", 67 | "size" : "29x29" 68 | }, 69 | { 70 | "filename" : "Icon-App-29x29@2x-1.png", 71 | "idiom" : "ipad", 72 | "scale" : "2x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "Icon-App-40x40@1x.png", 77 | "idiom" : "ipad", 78 | "scale" : "1x", 79 | "size" : "40x40" 80 | }, 81 | { 82 | "filename" : "Icon-App-40x40@2x-1.png", 83 | "idiom" : "ipad", 84 | "scale" : "2x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "Icon-App-76x76@1x.png", 89 | "idiom" : "ipad", 90 | "scale" : "1x", 91 | "size" : "76x76" 92 | }, 93 | { 94 | "filename" : "Icon-App-76x76@2x.png", 95 | "idiom" : "ipad", 96 | "scale" : "2x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "Icon-App-83.5x83.5@2x.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "83.5x83.5" 104 | }, 105 | { 106 | "filename" : "ItunesArtwork@2x.png", 107 | "idiom" : "ios-marketing", 108 | "scale" : "1x", 109 | "size" : "1024x1024" 110 | } 111 | ], 112 | "info" : { 113 | "author" : "xcode", 114 | "version" : 1 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Authentication/ViewControllers/LoginViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginViewController.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Combine 11 | 12 | class LoginViewController: BaseViewController { 13 | 14 | @IBOutlet var titleLabel: UILabel! 15 | @IBOutlet var subtitleLabel: UILabel! 16 | @IBOutlet var usernameTextfield: UITextField! 17 | @IBOutlet var passwordTextfield: UITextField! 18 | @IBOutlet var loginButton: UIButton! 19 | 20 | var router: AuthNavigationRouterInput? 21 | var loginViewModel: LoginViewModelDelegate? 22 | var subscriptions: Set = .init() 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | // Do any additional setup after loading the view. 27 | localizeUI() 28 | bind() 29 | } 30 | 31 | /// Prepare all label with current localization. 32 | private func localizeUI() { 33 | titleLabel.text = NSLocalizedString("login_title", comment: "") 34 | subtitleLabel.text = NSLocalizedString("login_subtitle", comment: "") 35 | loginButton.setTitle(NSLocalizedString("login", comment: ""), for: .normal) 36 | } 37 | 38 | private func bind() { 39 | loginViewModel?.status.sink { state in 40 | switch state { 41 | case .none: 42 | LOGP("No action in progress") 43 | case .loginInProgress: 44 | LOGP("Login process begin") 45 | self.showLoading() 46 | case .loginSuccess: 47 | LOGP("Login success. Go to home") 48 | self.hideLoading() 49 | self.goestToHomeViewController() 50 | case .loginError: 51 | LOGP("Login error") 52 | self.hideLoading() 53 | let alertTitle = NSLocalizedString("alert_error_title", comment: "") 54 | self.showCancelAlert(title: alertTitle, message: self.loginViewModel?.error?.errorDescription ?? "Unknow error") 55 | } 56 | }.store(in: &subscriptions) 57 | 58 | } 59 | 60 | 61 | @IBAction func startLogin(_ sender: UIButton) { 62 | let username = usernameTextfield.text ?? "" 63 | let password = passwordTextfield.text ?? "" 64 | 65 | if( username == "" && password == "" ) { 66 | showCancelAlert(title: NSLocalizedString("alert_error_title", comment: ""), message: NSLocalizedString("empty_credential", comment: "")) 67 | } else if( !password.isValidPassword(minSizePassword: 8) ) { 68 | showCancelAlert(title: NSLocalizedString("alert_error_title", comment: ""), message: NSLocalizedString("wrong_password", comment: "")) 69 | } else { 70 | loginViewModel?.executeLogin(username: username, password: password) 71 | } 72 | } 73 | 74 | /// Get HomeViewCotroller and set as main window 75 | private func goestToHomeViewController() { 76 | router?.navigateToHomeView() 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /MVVM-Clean/Application/DI/ViewModelAssembly.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModelAssembly.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Swinject 11 | 12 | class ViewModelAssembly: Assembly { 13 | 14 | func assemble(container: Container) { 15 | 16 | container.register(SplashScreenViewModelDelegate.self) { resolver in 17 | let splashViewModel = SplashScreenViewModel() 18 | 19 | guard let profileUseCase = resolver.resolve(ProfileUseCaseDelegate.self) else { 20 | fatalError("Assembler was unable to resolve ProfileUseCaseDelegate") 21 | } 22 | splashViewModel.profileUseCase = profileUseCase 23 | splashViewModel.profileUseCase?.responseDelegate = splashViewModel 24 | 25 | return splashViewModel 26 | }.inObjectScope(.transient) 27 | 28 | // Login View Model 29 | container.register(LoginViewModelDelegate.self) { resolver in 30 | let loginViewModel = LoginViewModel() 31 | 32 | guard let loginUseCase = resolver.resolve(LoginUseCaseDelegate.self) else { 33 | fatalError("Assembler was unable to resolve LoginUseCaseDelegate") 34 | } 35 | loginViewModel.loginUseCase = loginUseCase 36 | 37 | return loginViewModel 38 | }.inObjectScope(.transient) 39 | 40 | 41 | container.register(SummaryCovidViewModelDelegate.self) { resolver in 42 | let mainViewModel = SummaryCovidViewModel() 43 | guard let summaryUseCaseAsync = resolver.resolve(SummaryUseCaseAsyncDelegate.self) else { 44 | fatalError("Assembler was unable to resolve SummaryUseCaseDelegateAsync") 45 | } 46 | mainViewModel.summaryUseCaseAsync = summaryUseCaseAsync 47 | 48 | return mainViewModel 49 | }.inObjectScope(.transient) 50 | 51 | container.register(CountryCovidViewModelDelegate.self) { resolver in 52 | let countryViewModel = CountryCovidViewModel() 53 | 54 | guard let countryAsyncUseCase = resolver.resolve(CountryUseCaseAsyncDelegate.self) else { 55 | fatalError("Assembler was unable to resolve CountryUseCaseAsyncDelegate") 56 | } 57 | countryViewModel.countryAsyncUseCase = countryAsyncUseCase 58 | 59 | return countryViewModel 60 | }.inObjectScope(.transient) 61 | 62 | container.register(ProfileViewModelDelegate.self) { resolver in 63 | let profileViewModel = ProfileViewModel() 64 | 65 | guard let profileUseCase = resolver.resolve(ProfileUseCaseDelegate.self) else { 66 | fatalError("Assembler was unable to resolve ProfileUseCaseDelegate") 67 | } 68 | profileViewModel.profileUseCase = profileUseCase 69 | profileViewModel.profileUseCase?.responseDelegate = profileViewModel 70 | 71 | return profileViewModel 72 | }.inObjectScope(.transient) 73 | 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /MVVM-Clean/Application/DI/RepositoryAssembly.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepositoryAssembly.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Swinject 11 | 12 | class RepositoryAssembly: Assembly { 13 | 14 | func assemble(container: Container) { 15 | 16 | container.register(ProfileRepositoryDelegate.self) { resolver in 17 | let profileRepository = ProfileRepository() 18 | 19 | guard let profileProtocol = resolver.resolve(ProfilePersistenceProtocolData.self) else { 20 | fatalError("Assembler was unable to resolve ProfileLocalProtocolData") 21 | } 22 | profileRepository.profileLocalData = profileProtocol 23 | 24 | return profileRepository 25 | }.inObjectScope(.transient) 26 | 27 | container.register(LoginRepositoryDelegate.self) { resolver in 28 | let loginRepo = LoginRepository() 29 | guard let profileProtocol = resolver.resolve(ProfilePersistenceProtocolData.self) else { 30 | fatalError("Assembler was unable to resolve ProfileLocalProtocolData") 31 | } 32 | loginRepo.profileProtocol = profileProtocol 33 | 34 | return loginRepo 35 | }.inObjectScope(.transient) 36 | 37 | //MARK: - Async 38 | container.register(SummaryRepositoryAsyncDelegate.self) { resolver in 39 | let covidRepo = SummaryRepositoryAsync() 40 | 41 | guard let summaryNetwork = resolver.resolve(SummaryNetworkProtocolAsyncRequest.self) else { 42 | fatalError("Assembler was unable to resolve SummaryNetworkProtocolAsyncRequest") 43 | } 44 | covidRepo.covidNetwork = summaryNetwork 45 | 46 | guard let summaryLocal = resolver.resolve(SummaryPersistenceProtocolRequest.self) else { 47 | fatalError("Assembler was unable to resolve SummaryLocalProtocolRequest") 48 | } 49 | covidRepo.summaryLocal = summaryLocal 50 | 51 | guard let countryLocal = resolver.resolve(CountryPersistenceProtocolRequest.self) else { 52 | fatalError("Assembler was unable to resolve CountryLocalProtocolRequest") 53 | } 54 | covidRepo.countryLocal = countryLocal 55 | 56 | return covidRepo 57 | }.inObjectScope(.transient) 58 | 59 | container.register(CountryRepositoryAsyncDelegate.self) { resolver in 60 | let countryRepo = CountryAsyncRepository() 61 | 62 | guard let countryLocalData = resolver.resolve(CountryPersistenceProtocolRequest.self) else { 63 | fatalError("Assembler was unable to resolve CountryLocalProtocolRequest") 64 | } 65 | countryRepo.countryLocal = countryLocalData 66 | 67 | guard let countryNetwork = resolver.resolve(CountryNetworkProtocolAsyncRequest.self) else { 68 | fatalError("Assembler was unable to resolve CountryNetworkProtocolAsyncRequest") 69 | } 70 | countryRepo.countryAsyncNetwork = countryNetwork 71 | 72 | return countryRepo 73 | }.inObjectScope(.transient) 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /MVVM-Clean/Utility/Date/CustomDateUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomDateUtils.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 03/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class CustomDateUtils: NSObject { 12 | 13 | enum CustomDateUtilsFormat: String { 14 | case italianDateHourDateFormat = "dd-MM-yyyy HH:mm:ss.SSS" 15 | } 16 | 17 | /// Generate current timestamp 18 | /// 19 | /// - Returns: Return a string rapresent current time stamp dd-MM-yyyy HH:mm:ss.SSS 20 | public static func currentStringDate() -> String { 21 | let formatter = DateFormatter() 22 | formatter.timeZone = TimeZone.current 23 | formatter.locale = Locale.current 24 | formatter.dateFormat = CustomDateUtilsFormat.italianDateHourDateFormat.rawValue 25 | 26 | return formatter.string(from: Date()) 27 | } 28 | 29 | /// Generate current timestamp returned in Date object 30 | /// - Returns: Return a date rapresent current time stamp dd-MM-yyyy HH:mm:ss.SSS 31 | public static func currentDate() -> Date { 32 | let formatter = DateFormatter() 33 | formatter.timeZone = TimeZone.current 34 | formatter.locale = Locale.current 35 | formatter.dateFormat = CustomDateUtilsFormat.italianDateHourDateFormat.rawValue 36 | 37 | let dateString = formatter.string(from: Date()) 38 | return formatter.date(from: dateString)! 39 | } 40 | 41 | /// Get current verbose date in format dd VERBOSE MONTH yyyy (example: 10 March 2020) localized depending on device 42 | public static func currentVerboseDate() -> String { 43 | let formatter = DateFormatter() 44 | formatter.timeZone = TimeZone.current 45 | formatter.locale = Locale.current 46 | formatter.dateFormat = "dd MMMM yyyy" 47 | 48 | return formatter.string(from: Date()) 49 | } 50 | 51 | /// Check delta from last network data and the custom delta 52 | /// - Parameter lastUpdate: Last update received from network 53 | /// - Returns: TRUE if a new HTTP request is needed, FALSE otherwise 54 | public static func checkForNetworkUpdate(lastUpdate: String) -> Bool { 55 | 56 | var networkUpdateNeeded = true 57 | let actualCalendar = Calendar.current 58 | let currentDate = CustomDateUtils.currentDate() 59 | 60 | let formatter = DateFormatter() 61 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" 62 | 63 | guard let lastUpdateDate = formatter.date(from: lastUpdate) else { 64 | return true 65 | } 66 | 67 | let dataComponents = actualCalendar.dateComponents([.hour, .day], from: lastUpdateDate, to: currentDate) 68 | 69 | if( dataComponents.hour ?? 0 <= API.networkHoursDelta && dataComponents.day ?? 0 == 0 ) { 70 | LOGD("Local summary data was saved \(dataComponents.hour ?? 0) hours ago. Configured delta is \(API.networkHoursDelta) hours. No network call needed") 71 | networkUpdateNeeded = false 72 | } else { 73 | LOGD("Local summary data was saved \(dataComponents.day ?? 0) days and \(dataComponents.hour ?? 0) hours ago. Configured delta is \(API.networkHoursDelta) hours. New network call needed") 74 | } 75 | 76 | return networkUpdateNeeded 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Network/Mappers/Summary/SummaryDTOMapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SummaryDTOMapper.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 03/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct SummaryDTOMapper { 12 | 13 | /// Map a single Summary DTO object in a single SummaryModel object 14 | /// - Parameter summary: Summary DTO to map 15 | /// - Returns: SummaryModel mapped from DTO 16 | static func map(summary: SummaryDTO) -> Summary { 17 | var summaryModel = Summary() 18 | 19 | let nf = NumberFormatter() 20 | nf.numberStyle = .decimal 21 | 22 | var confirmed = SummaryConfirmed() 23 | 24 | if let newConfirmedCasesValue = summary.global?.newConfirmed { 25 | confirmed.newConfirmedCases = nf.string(from: NSNumber(value: newConfirmedCasesValue)) ?? "0" 26 | // summaryModel.newConfirmedCases = nf.string(from: NSNumber(value: newConfirmedCasesValue)) ?? "0" 27 | } 28 | 29 | if let confirmedTotalCasesValue = summary.global?.totalConfirmed { 30 | confirmed.totalConfirmedCases = nf.string(from: NSNumber(value: confirmedTotalCasesValue)) ?? "0" 31 | // summaryModel.totalConfirmedCases = nf.string(from: NSNumber(value: confirmedTotalCasesValue)) ?? "0" 32 | } 33 | 34 | summaryModel.summaryConfirmed = confirmed 35 | 36 | var death = SummaryDeath() 37 | 38 | if let newDeathValue = summary.global?.newDeaths { 39 | death.newDeath = nf.string(from: NSNumber(value: newDeathValue)) ?? "0" 40 | // summaryModel.newDeath = nf.string(from: NSNumber(value: newDeathValue)) ?? "0" 41 | } 42 | 43 | if let totalDeathsValue = summary.global?.totalDeaths { 44 | death.totalDeaths = nf.string(from: NSNumber(value: totalDeathsValue)) ?? "0" 45 | // summaryModel.totalDeaths = nf.string(from: NSNumber(value: totalDeathsValue)) ?? "0" 46 | } 47 | 48 | summaryModel.summaryDeath = death 49 | 50 | var recovered = SummaryRecovered() 51 | 52 | if let newRecoveredValue = summary.global?.newRecovered { 53 | recovered.newRecoveredCases = nf.string(from: NSNumber(value: newRecoveredValue)) ?? "0" 54 | // summaryModel.newRecoveredCases = nf.string(from: NSNumber(value: newRecoveredValue)) ?? "0" 55 | } 56 | 57 | if let totalRecoveredValue = summary.global?.totalRecovered { 58 | recovered.totalRecovered = nf.string(from: NSNumber(value: totalRecoveredValue)) ?? "0" 59 | // summaryModel.totalRecovered = nf.string(from: NSNumber(value: totalRecoveredValue)) ?? "0" 60 | } 61 | summaryModel.summaryRecovered = recovered 62 | 63 | return summaryModel 64 | } 65 | 66 | /// Map a Summary DTO object array in a SummaryModel object array 67 | /// - Parameter summaries: Summary DTO array to map 68 | /// - Returns: SummaryModel object array mapped from DTO 69 | static func map(array summaries: [SummaryDTO]) -> [Summary] { 70 | var summariesModel = [Summary]() 71 | 72 | summaries.forEach { (summary) in 73 | summariesModel.append( map(summary: summary) ) 74 | } 75 | 76 | return summariesModel 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Models/CustomError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomError.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum CustomError: Error { 12 | case noContent 13 | case unknow(code: String) 14 | case nilData 15 | case unauthorized 16 | case badRequest 17 | case notFound 18 | case forbidden 19 | case userNotFound 20 | case logoutError 21 | case loginError 22 | case generic 23 | case noConnection 24 | case urlRequestNil 25 | case noLocalDataFound 26 | case jsonDecodeError 27 | 28 | init(errorCode: Int) { 29 | switch errorCode { 30 | case NetworkStatusCode.noContent.rawValue: 31 | self = .noContent 32 | case NetworkStatusCode.unauthorized.rawValue: 33 | self = .unauthorized 34 | case NetworkStatusCode.badRequest.rawValue: 35 | self = .badRequest 36 | case NetworkStatusCode.notFound.rawValue: 37 | self = .notFound 38 | case NetworkStatusCode.forbidden.rawValue: 39 | self = .forbidden 40 | default: 41 | self = .generic 42 | } 43 | } 44 | } 45 | 46 | extension CustomError: LocalizedError { 47 | var errorDescription: String? { 48 | switch self { 49 | case .noContent: 50 | return NSLocalizedString(Localized.no_content_found.rawValue, comment: "") 51 | case .unknow(let code): 52 | let genericErrorMessage = NSLocalizedString(Localized.generic_localized_net_error_message.rawValue, comment: "") 53 | let genericErrorMessageWithFormat = String.localizedStringWithFormat(genericErrorMessage, "\(code)") 54 | return genericErrorMessageWithFormat 55 | case .nilData: 56 | return NSLocalizedString(Localized.nil_data_msg.rawValue, comment: "") 57 | case .unauthorized: 58 | return NSLocalizedString(Localized.unauthorized_message.rawValue, comment: "") 59 | case .badRequest: 60 | return NSLocalizedString(Localized.badRequest_message.rawValue, comment: "") 61 | case .notFound: 62 | return NSLocalizedString(Localized.not_found_message.rawValue, comment: "") 63 | case .forbidden: 64 | return NSLocalizedString(Localized.forbidden_message.rawValue, comment: "") 65 | case .userNotFound: 66 | return NSLocalizedString(Localized.user_not_found_message.rawValue, comment: "") 67 | case .logoutError: 68 | return NSLocalizedString(Localized.logout_error_message.rawValue, comment: "") 69 | case .loginError: 70 | return NSLocalizedString(Localized.login_error_message.rawValue, comment: "") 71 | case .noConnection: 72 | return NSLocalizedString(Localized.no_connection_error_message.rawValue, comment: "") 73 | case .urlRequestNil: 74 | return NSLocalizedString(Localized.url_request_nil.rawValue, comment: "") 75 | case .noLocalDataFound: 76 | return NSLocalizedString(Localized.no_local_data_found.rawValue, comment: "") 77 | case .jsonDecodeError: 78 | return NSLocalizedString(Localized.json_decode_error.rawValue, comment: "") 79 | case .generic: 80 | return NSLocalizedString(Localized.generic_error_message.rawValue, comment: "") 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /MVVM-Clean/Data/Network/Providers/MainProviders/NetworkRequestPerfomer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkRequestPerfomer.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 30/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | //import Foundation 10 | //import Alamofire 11 | // 12 | // 13 | //class NetworkRequestPerfomer { 14 | // 15 | // /// A generic network request performer for execute all request for router that extends APIConfiguration 16 | // /// - Parameters: 17 | // /// - route: Router object 18 | // /// - success: success operation 19 | // /// - failure: fail operation 20 | // /// - Returns: Current network operation request 21 | // @available(iOS, deprecated, message: "Will be replaced", renamed: "AsyncNetworkPerformer.sendReqeust()") 22 | // public static func performRequest(route: APIConfiguration, success: @escaping (T) -> Void, failure: @escaping ((CustomError) -> Void)) -> DataRequest { 23 | // return AF.request(route) 24 | // .responseJSON { response in 25 | // do { 26 | // if response.error != nil { 27 | // let localizedDesctiptionError: String = response.error?.localizedDescription ?? "" 28 | // if response.data != nil { 29 | // if let data = response.data, let utf8Text = String(data: data, encoding: .utf8) { 30 | // print("Data response: \(utf8Text)") 31 | // } 32 | // } else { 33 | // print("Error: \(localizedDesctiptionError)") 34 | // } 35 | // if let statusCode = response.response?.statusCode { 36 | // failure(CustomError.unknow(code: "\(statusCode)")) 37 | // } 38 | // 39 | // } else { 40 | // if let responseData = response.data { 41 | // LOGI("Data response: \(responseData)") 42 | // 43 | // if let jsonArray = try JSONSerialization.jsonObject(with: responseData, options : .allowFragments) as? [String:Any] { 44 | // 45 | // var statusCode: Int? = (jsonArray["statusCode"] as? Int) 46 | // if (statusCode == nil) { 47 | // statusCode = response.response?.statusCode 48 | // } 49 | // 50 | // if( statusCode == NetworkStatusCode.success.rawValue ) { 51 | // success(try JSONDecoder().decode(T.self, from: responseData)) 52 | // } else { 53 | // failure( CustomError(errorCode: statusCode!)) 54 | // } 55 | // } else { 56 | // failure(getGenericError()) 57 | // } 58 | // } else { 59 | // failure(CustomError.nilData) 60 | // } 61 | // } 62 | // } catch { 63 | // failure(CustomError.generic) 64 | // } 65 | // } 66 | // } 67 | // 68 | // 69 | // private static func getGenericError() -> CustomError { 70 | // return CustomError.generic 71 | // } 72 | // 73 | //} 74 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Main/Country/ViewControllers/CountryListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountryListViewController.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Combine 11 | 12 | class CountryListViewController: UIViewController { 13 | 14 | @IBOutlet var countryTableView: UITableView! 15 | 16 | var router: CountryNavigationRouterInput? 17 | var countryViewModel: CountryCovidViewModelDelegate? 18 | var subscriptions: Set = .init() 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | 23 | countryTableView.dataSource = self 24 | countryTableView.delegate = self 25 | 26 | countryTableView.register(UINib(nibName: CountryTableViewCell.XIB_NAME, bundle: nil), forCellReuseIdentifier: CountryTableViewCell.IDENTIFIER) 27 | 28 | bind() 29 | } 30 | 31 | /// Bind view model with view 32 | private func bind() { 33 | countryViewModel?.status.sink { state in 34 | switch state { 35 | case .none: 36 | LOGD("No action") 37 | case .gettingCountryData: 38 | LOGD("Loading country data") 39 | case .countriesDataSuccess: 40 | LOGD("Country data success") 41 | self.reloadData() 42 | case .countriesDataError: 43 | LOGD("Country data error") 44 | DispatchQueue.main.async { 45 | self.showCancelAlert(title: "Error", message: self.countryViewModel?.error?.errorDescription ?? "") 46 | } 47 | } 48 | }.store(in: &subscriptions) 49 | 50 | loadAsyncCountryListData() 51 | } 52 | 53 | 54 | /// Load country list data 55 | private func loadAsyncCountryListData() { 56 | countryViewModel?.countryList() 57 | } 58 | 59 | private func reloadData() { 60 | DispatchQueue.main.async { 61 | self.countryTableView.reloadData() 62 | } 63 | } 64 | } 65 | 66 | //MARK: - UITableView datasource 67 | extension CountryListViewController: UITableViewDataSource { 68 | 69 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 70 | LOGP("Found \(countryViewModel?.countries?.count ?? 0) countries") 71 | return countryViewModel?.countries?.count ?? 0 72 | } 73 | 74 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 75 | if let cell = tableView.dequeueReusableCell(withIdentifier: CountryTableViewCell.IDENTIFIER, for: indexPath) as? CountryTableViewCell { 76 | guard let viewModel = countryViewModel, let country = viewModel.countries?[indexPath.row] else { 77 | return UITableViewCell() 78 | } 79 | cell.bind(country: country) 80 | } 81 | return UITableViewCell() 82 | } 83 | 84 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 85 | LOGP("Selected cell at index \(indexPath.row). Do something with selected cell") 86 | } 87 | } 88 | 89 | //MARK: - UITableView delegate 90 | extension CountryListViewController: UITableViewDelegate { 91 | 92 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 93 | return CountryTableViewCell.DEFAULT_HEIGHT 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/Storyboard/Splash/Splash.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /MVVM-Clean.xcodeproj/xcshareddata/xcschemes/MVVM-Clean.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 39 | 40 | 41 | 42 | 44 | 50 | 51 | 52 | 54 | 60 | 61 | 62 | 63 | 64 | 74 | 76 | 82 | 83 | 84 | 85 | 91 | 93 | 99 | 100 | 101 | 102 | 104 | 105 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /MVVM-Clean/Presentations/UI/Main/Summary/ViewControllers/SummaryViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewController.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Combine 11 | 12 | class SummaryViewController: BaseViewController { 13 | 14 | @IBOutlet var welcomeMessageLabel: UILabel! 15 | @IBOutlet var lastUpdateLabel: UILabel! 16 | @IBOutlet weak var summaryTableView: UITableView! 17 | 18 | @IBOutlet var countriesButton: UIBarButtonItem! 19 | 20 | var navigationRouter: SummaryNavigationRouterInput? 21 | var mainViewModel: SummaryCovidViewModelDelegate? 22 | var subscriptions: Set = .init() 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | localizeUI() 27 | summaryTableView.register(UINib(nibName: SummaryTableViewCell.XIB_NAME, bundle: nil), forCellReuseIdentifier: SummaryTableViewCell.IDENTIFIER) 28 | summaryTableView.dataSource = self 29 | bind() 30 | } 31 | 32 | private func localizeUI() { 33 | welcomeMessageLabel.text = NSLocalizedString("welcome_message", comment: "") 34 | let lastUpdate = NSLocalizedString("last_summary_update", comment: "") 35 | lastUpdateLabel.text = String.localizedStringWithFormat(lastUpdate, "...") 36 | countriesButton.title = NSLocalizedString("countries_label", comment: "") 37 | } 38 | 39 | private func bind() { 40 | 41 | mainViewModel?.status.sink { state in 42 | switch state { 43 | case .none: 44 | LOGD("No action") 45 | case .gettingSummaryData: 46 | LOGD("Loading summary data") 47 | case.summaryDataSuccess: 48 | LOGD("Summary data success") 49 | if let summary = self.mainViewModel?.summary { 50 | self.updateSummary(withData: summary) 51 | } 52 | case .summaryDataError: 53 | LOGD("Summary data error") 54 | self.showCancelAlert(title: "Error", message: self.mainViewModel?.error?.errorDescription ?? "") 55 | } 56 | }.store(in: &subscriptions) 57 | 58 | loadSummaryData() 59 | } 60 | 61 | private func updateSummary(withData: Summary) { 62 | let lastUpdate = NSLocalizedString("last_summary_update", comment: "") 63 | DispatchQueue.main.async { 64 | self.lastUpdateLabel.text = String.localizedStringWithFormat(lastUpdate, "\(withData.lastUpdate)") 65 | self.summaryTableView.reloadData() 66 | } 67 | } 68 | 69 | @IBAction func showProfileViewController(_ sender: UIBarButtonItem) { 70 | navigationRouter?.navigateToProfileView() 71 | } 72 | 73 | private func loadSummaryData() { 74 | LOGD("Load summary data") 75 | mainViewModel?.summaryDataAsync() 76 | } 77 | 78 | @IBAction func showCountriesDataController(_ sender: UIBarButtonItem) { 79 | LOGD("Show countries data view controller") 80 | navigationRouter?.navigateToCountryView() 81 | } 82 | 83 | 84 | } 85 | 86 | extension SummaryViewController: UITableViewDataSource { 87 | 88 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 89 | return mainViewModel == nil ? 0 : 3 90 | } 91 | 92 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 93 | if let cell = summaryTableView.dequeueReusableCell(withIdentifier: SummaryTableViewCell.IDENTIFIER, for: indexPath) as? SummaryTableViewCell { 94 | if( indexPath.row == 0 ) { 95 | if let confirmed = mainViewModel?.summary?.summaryConfirmed { 96 | cell.setupConfirmed(summary: confirmed) 97 | return cell 98 | } else { 99 | return UITableViewCell() 100 | } 101 | } else if( indexPath.row == 1 ) { 102 | if let recovery = mainViewModel?.summary?.summaryRecovered { 103 | cell.setupConfirmed(summary: recovery) 104 | return cell 105 | } else { 106 | return UITableViewCell() 107 | } 108 | } else if( indexPath.row == 2 ) { 109 | if let death = mainViewModel?.summary?.summaryDeath { 110 | cell.setupDeath(summary: death) 111 | return cell 112 | } else { 113 | return UITableViewCell() 114 | } 115 | } else { 116 | return UITableViewCell() 117 | } 118 | 119 | } else { 120 | return UITableViewCell() 121 | } 122 | } 123 | 124 | 125 | } 126 | -------------------------------------------------------------------------------- /MVVM-Clean/Utility/Log/CustomConsoleLog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomConsoleLog.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 03/10/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /* To modify these configuration, change for each target, custom falgs under build settings options **/ 12 | #if DEBUG 13 | var logLevel: Int = CustomConsoleLogLevel.ll_VERBOSEFUNC.rawValue 14 | #else 15 | var logLevel: Int = CustomConsoleLogLevel.ll_ERROR.rawValue 16 | #endif 17 | 18 | 19 | enum CustomConsoleLogLevel: Int { 20 | case ll_ERROR = 1 21 | case ll_WARNING = 2 22 | case ll_INFO = 3 23 | case ll_DEBUG = 4 24 | case ll_VERBOSE = 5 25 | case ll_PRINT = 6 26 | case ll_VERBOSEFUNC = 7 27 | } 28 | 29 | 30 | /// ERROR log message 31 | /// 32 | /// - Parameters: 33 | /// - message: log message 34 | /// - fileName: file where log was printed 35 | /// - functionName: Function name 36 | /// - line: code line 37 | public func LOGE(_ message: String, fileName: String = #file, functionName: String = #function, line: Int = #line) { 38 | 39 | if( logLevel >= CustomConsoleLogLevel.ll_DEBUG.rawValue ) { 40 | let timestamp = CustomDateUtils.currentStringDate() 41 | print("[CCL ERROR] - \(timestamp) - \(fileName).\(functionName):\(line) - \(message)") 42 | } 43 | } 44 | 45 | 46 | /// WARNING log level 47 | /// 48 | /// - Parameters: 49 | /// - message: log message 50 | /// - fileName: file where log was printed 51 | /// - functionName: Function name 52 | /// - line: code line 53 | public func LOGW(_ message: String, fileName: String = #file, functionName: String = #function, line: Int = #line) { 54 | 55 | if( logLevel >= CustomConsoleLogLevel.ll_DEBUG.rawValue ) { 56 | let timestamp = CustomDateUtils.currentStringDate() 57 | print("CCL WARN] - \(timestamp) - \(fileName).\(functionName):\(line) - \(message)") 58 | } 59 | } 60 | 61 | 62 | /// INFO log level 63 | /// 64 | /// - Parameters: 65 | /// - message: log message 66 | /// - fileName: file where log was printed 67 | /// - functionName: Function name 68 | /// - line: code line 69 | public func LOGI(_ message: String, fileName: String = #file, functionName: String = #function, line: Int = #line) { 70 | 71 | if( logLevel >= CustomConsoleLogLevel.ll_DEBUG.rawValue ) { 72 | let timestamp = CustomDateUtils.currentStringDate() 73 | print("[CCL INFO] - \(timestamp) - \(fileName).\(functionName):\(line) - \(message)") 74 | } 75 | } 76 | 77 | 78 | /// DEBUG log level 79 | /// 80 | /// - Parameters: 81 | /// - message: log message 82 | /// - fileName: file where log was printed 83 | /// - functionName: Function name 84 | /// - line: code line 85 | public func LOGD(_ message: String, fileName: String = #file, functionName: String = #function, line: Int = #line) { 86 | 87 | if( logLevel >= CustomConsoleLogLevel.ll_DEBUG.rawValue ) { 88 | let timestamp = CustomDateUtils.currentStringDate() 89 | print("[CCL DEBUG] - \(timestamp) - \(fileName).\(functionName):\(line) - \(message)") 90 | } 91 | } 92 | 93 | 94 | /// VERBOSE log level 95 | /// 96 | /// - Parameters: 97 | /// - message: log message 98 | /// - functionName: Function name 99 | /// - line: code line 100 | public func LOGV(_ message: String, functionName: String = #function, line: Int = #line) { 101 | 102 | if( logLevel >= CustomConsoleLogLevel.ll_VERBOSEFUNC.rawValue ) { 103 | let timestamp = CustomDateUtils.currentStringDate() 104 | print("[CCL VERBOSE] - \(timestamp) - \(functionName):\(line) - \(message)") 105 | } 106 | } 107 | 108 | 109 | /// PRINT log level 110 | /// 111 | /// - Parameters: 112 | /// - message: log message 113 | /// - functionName: Function name 114 | /// - line: code line 115 | public func LOGP(_ message: String, functionName: String = #function, line: Int = #line) { 116 | 117 | if( logLevel >= CustomConsoleLogLevel.ll_PRINT.rawValue ) { 118 | print("[CCL PRINT] - \(functionName):\(line) - \(message)") 119 | } 120 | } 121 | 122 | 123 | /// Static log message to print function begin 124 | /// 125 | /// - Parameters: 126 | /// - functionName: Function name that print log 127 | public func LOGFSTART(functionName: String = #function) { 128 | 129 | if( logLevel >= CustomConsoleLogLevel.ll_VERBOSEFUNC.rawValue ) { 130 | let timestamp = CustomDateUtils.currentStringDate() 131 | print("\(timestamp) - \(functionName): START") 132 | } 133 | } 134 | 135 | 136 | /// Static log message to print function end 137 | /// 138 | /// - Parameters: 139 | /// - functionName: Function name that print log 140 | public func LOGFEND(functionName: String = #function) { 141 | 142 | if( logLevel >= CustomConsoleLogLevel.ll_VERBOSEFUNC.rawValue ) { 143 | let timestamp = CustomDateUtils.currentStringDate() 144 | print("\(timestamp) - \(functionName): END") 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /MVVM-Clean/Application/DI/ViewControllerAssembly.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewControllerAssembly.swift 3 | // MVVM-Clean 4 | // 5 | // Created by Alessandro Marcon on 04/08/2020. 6 | // Copyright © 2020 Alessandro Marcon. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Swinject 11 | 12 | class ViewControllerAssembly: Assembly { 13 | 14 | enum ViewControllerIds: String { 15 | case splash_vc 16 | case login_vc 17 | case main_vc 18 | case country_list_vc 19 | case profile_vc 20 | } 21 | 22 | func assemble(container: Container) { 23 | 24 | // Splash view controller initialization 25 | container.register(SplashViewController.self) { r in 26 | guard let controller: SplashViewController = UIStoryboard(.Splash).instantiateViewController(withIdentifier: ViewControllerIds.splash_vc.rawValue) as? SplashViewController else { 27 | fatalError("Assembler was unable to resolve SplashViewController") 28 | } 29 | guard let model = r.resolve(SplashScreenViewModelDelegate.self) else { 30 | fatalError("Assembler was unable to resolve SplashScreenViewModelDelegate") 31 | } 32 | controller.splashViewModel = model 33 | 34 | controller.router = SplashRouter() 35 | 36 | return controller 37 | }.inObjectScope(.transient) 38 | 39 | // Login View Controller 40 | container.register(LoginViewController.self) { r in 41 | guard let controller: LoginViewController = UIStoryboard(.Auth).instantiateViewController(withIdentifier: ViewControllerIds.login_vc.rawValue) as? LoginViewController else { 42 | fatalError("Assembler was unable to resolve LoginViewController") 43 | } 44 | 45 | guard let loginViewModel = r.resolve(LoginViewModelDelegate.self) else { 46 | fatalError("Assembler was unable to resolve LoginViewModelDelegate") 47 | } 48 | controller.loginViewModel = loginViewModel 49 | 50 | let router = AuthNavigationRouter(vc: controller) 51 | controller.router = router 52 | router.vc = controller 53 | 54 | return controller 55 | }.inObjectScope(.transient) 56 | 57 | // Main View Controller 58 | container.register(SummaryViewController.self) { r in 59 | guard let controller: SummaryViewController = UIStoryboard(.Main).instantiateViewController(withIdentifier: ViewControllerIds.main_vc.rawValue) as? SummaryViewController else { 60 | fatalError("Assembler was unable to resolve MainViewController") 61 | } 62 | 63 | guard let mainViewModel = r.resolve(SummaryCovidViewModelDelegate.self) else { 64 | fatalError("Assembler was unable to resolve MainViewModelDelegate") 65 | } 66 | controller.mainViewModel = mainViewModel 67 | 68 | let navigationRouter = SummaryNavigationRouter(vc: controller) 69 | controller.navigationRouter = navigationRouter 70 | 71 | return controller 72 | }.inObjectScope(.transient) 73 | 74 | // Countries list View Controller 75 | container.register(CountryListViewController.self) { r in 76 | guard let controller: CountryListViewController = UIStoryboard(.Main).instantiateViewController(withIdentifier: ViewControllerIds.country_list_vc.rawValue) as? CountryListViewController else { 77 | fatalError("Assembler was unable to resolve CountryListViewController") 78 | } 79 | 80 | guard let countryViewModel = r.resolve(CountryCovidViewModelDelegate.self) else { 81 | fatalError("Assembler was unable to resolve CountryCovidViewModelDelegate") 82 | } 83 | controller.countryViewModel = countryViewModel 84 | 85 | let navigationRouter = CountryNavigationRouter(vc: controller) 86 | controller.router = navigationRouter 87 | 88 | return controller 89 | }.inObjectScope(.transient) 90 | 91 | // Profile View Controller 92 | container.register(ProfileViewController.self) { r in 93 | guard let controller: ProfileViewController = UIStoryboard(.Main).instantiateViewController(withIdentifier: ViewControllerIds.profile_vc.rawValue) as? ProfileViewController else { 94 | fatalError("Assembler was unable to resolve ProfileViewController") 95 | } 96 | 97 | guard let profileViewModel = r.resolve(ProfileViewModelDelegate.self) else { 98 | fatalError("Assembler was unable to resolve ProfileViewModelDelegate") 99 | } 100 | controller.profileViewModel = profileViewModel 101 | 102 | let router = ProfileNavigationRouter(vc: controller) 103 | controller.router = router 104 | 105 | return controller 106 | }.inObjectScope(.transient) 107 | 108 | } 109 | } 110 | --------------------------------------------------------------------------------