├── .gitignore ├── MockingProject ├── SupportingFiles │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── round-profile.imageset │ │ │ ├── user.png │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── AppDelegate.swift │ ├── SceneDelegate.swift │ └── Info.plist ├── Model │ ├── EmployeeResponse.swift │ ├── EmployeesResponse.swift │ ├── EmployeesEvent.swift │ └── Employee.swift ├── Helpers │ ├── DIHelpers │ │ ├── AppDelegate+Injection.swift │ │ ├── APIManager+Injection.swift │ │ ├── SwinjectStoryboardContainer.swift │ │ ├── SwinjectContainer.swift │ │ └── ServiceLocator.swift │ ├── Observable.swift │ ├── EnvironmentHelper.swift │ └── EventBus.swift ├── Extensions │ ├── ThirdPartyExtensions.swift │ └── UIViewController+Extensions.swift ├── Repositories │ ├── DataModel │ │ ├── EmployeeResponseDTO.swift │ │ ├── EmployeesResponseDTO.swift │ │ └── EmployeeDTO.swift │ ├── EmployeeRepository.swift │ ├── APIRouter.swift │ └── BaseNetworkManager.swift └── Modules │ ├── DataBinding │ ├── View │ │ ├── Cells │ │ │ ├── EmployeeCell.swift │ │ │ └── EmployeeCell.xib │ │ └── Controllers │ │ │ ├── DetailController.swift │ │ │ ├── CobineController.swift │ │ │ ├── ObservableController.swift │ │ │ ├── EventBusController.swift │ │ │ └── RxSwiftController.swift │ ├── ViewModel │ │ ├── CombineViewModel.swift │ │ ├── HomeViewModel.swift │ │ ├── ObservableViewModel.swift │ │ ├── EventBusViewModel.swift │ │ └── RxSwiftViewModel.swift │ └── README.md │ └── DependencyInjection │ ├── ViewModel │ ├── ResolverViewModel.swift │ └── SwinjectViewModel.swift │ ├── Controllers │ ├── SwinjectController.swift │ ├── ResolverController.swift │ └── ServiceLocatorController.swift │ └── README.md ├── MockingProject.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcuserdata │ └── paaquesiafful.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── xcshareddata │ └── xcschemes │ └── MockingProject.xcscheme ├── MockingProject.xcworkspace ├── xcuserdata │ └── paaquesiafful.xcuserdatad │ │ └── IDEFindNavigatorScopes.plist ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── MockingProjectTests ├── Resources │ ├── employee24.json │ ├── Info.plist │ └── employees.json ├── HomeControllerTests.swift ├── EmployeeTests.swift └── APIManagerTests.swift ├── .swiftlint.yml ├── Podfile ├── README.md ├── Test-Info.plist ├── Production-Info.plist └── Podfile.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /Pods 2 | /Pods/** -------------------------------------------------------------------------------- /MockingProject/SupportingFiles/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /MockingProject/SupportingFiles/Assets.xcassets/round-profile.imageset/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FitzAfful/MockingProject/HEAD/MockingProject/SupportingFiles/Assets.xcassets/round-profile.imageset/user.png -------------------------------------------------------------------------------- /MockingProject.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MockingProject.xcworkspace/xcuserdata/paaquesiafful.xcuserdatad/IDEFindNavigatorScopes.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /MockingProjectTests/Resources/employee24.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": "success", 3 | "data": { 4 | "id": "24", 5 | "employee_name": "Doris Wilder", 6 | "employee_salary": "85600", 7 | "employee_age": "23", 8 | "profile_image": "" 9 | } 10 | } -------------------------------------------------------------------------------- /MockingProject.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /MockingProject.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MockingProject.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MockingProject/Model/EmployeeResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIRouter.swift 3 | // MockingProject 4 | // 5 | // Created by Paa Quesi Afful on 01/04/2020. 6 | // Copyright © 2020 MockingProject. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | public struct EmployeeResponse { 13 | 14 | var data: Employee 15 | let status: String 16 | } 17 | -------------------------------------------------------------------------------- /MockingProject/Model/EmployeesResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIRouter.swift 3 | // MockingProject 4 | // 5 | // Created by Paa Quesi Afful on 01/04/2020. 6 | // Copyright © 2020 MockingProject. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | public struct EmployeesResponse { 13 | 14 | var data: [Employee] 15 | let status: String 16 | 17 | } 18 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - MockingProject 3 | 4 | disabled_rules: 5 | - identifier_name 6 | - line_length 7 | - function_body_length 8 | - force_cast 9 | - switch_case_alignment 10 | 11 | excluded: 12 | - Pods 13 | 14 | line_length: 15 | warning: 180 16 | error: 400 17 | ignores_function_declarations: true 18 | ignores_comments: true 19 | ignores_urls: true 20 | 21 | switch_case_alignment: 22 | indented_cases: true 23 | -------------------------------------------------------------------------------- /MockingProject/SupportingFiles/Assets.xcassets/round-profile.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "user.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /MockingProject/Helpers/DIHelpers/AppDelegate+Injection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate+Injection.swift 3 | // Development 4 | // 5 | // Created by Fitzgerald Afful on 12/05/2020. 6 | // Copyright © 2020 Fitzgerald Afful. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Resolver 11 | 12 | extension Resolver: ResolverRegistering { 13 | public static func registerAllServices() { 14 | registerEmployeeRepository() 15 | registerViewModel() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MockingProjectTests/HomeControllerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeControllerTests.swift 3 | // MockingProjectTests 4 | // 5 | // Created by Fitzgerald Afful on 04/04/2020. 6 | // Copyright © 2020 Fitzgerald Afful. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class HomeControllerTests: XCTestCase { 12 | 13 | override func setUp() { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /MockingProject/Extensions/ThirdPartyExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIRouter.swift 3 | // MockingProject 4 | // 5 | // Created by Paa Quesi Afful on 01/04/2020. 6 | // Copyright © 2020 MockingProject. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import Nuke 12 | 13 | //Nuke Extension 14 | extension UIImageView { 15 | func setImage(url: String?) { 16 | guard let myURL = url, !myURL.isEmpty else { 17 | return 18 | } 19 | let mainURL = URL(string: myURL)! 20 | Nuke.loadImage(with: mainURL, into: self) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /MockingProject/Helpers/DIHelpers/APIManager+Injection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIManager+Injection.swift 3 | // Development 4 | // 5 | // Created by Fitzgerald Afful on 12/05/2020. 6 | // Copyright © 2020 Fitzgerald Afful. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Resolver 11 | 12 | extension Resolver { 13 | public static func registerEmployeeRepository() { 14 | register { APIEmployeeRepository() as EmployeeRepository } 15 | } 16 | 17 | public static func registerViewModel() { 18 | register { ResolverViewModel(repository: self.resolve()) } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MockingProject/Repositories/DataModel/EmployeeResponseDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIRouter.swift 3 | // MockingProject 4 | // 5 | // Created by Paa Quesi Afful on 01/04/2020. 6 | // Copyright © 2020 MockingProject. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | public struct EmployeeResponseDTO: Codable, Equatable { 13 | 14 | var data: EmployeeDTO 15 | let status: String 16 | } 17 | 18 | public extension EmployeeResponseDTO { 19 | func map() -> EmployeeResponse { 20 | return EmployeeResponse(data: self.data.map(), status: self.status) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MockingProject/Model/EmployeesEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftBusEmployee.swift 3 | // Development 4 | // 5 | // Created by Fitzgerald Afful on 18/04/2020. 6 | // Copyright © 2020 Fitzgerald Afful. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class EmployeesEvent: NSObject { 12 | var error: Bool 13 | var errorMessage: String? 14 | var employees: [Employee]? 15 | 16 | init(error: Bool, errorMessage: String? = nil, employees: [Employee]? = nil) { 17 | self.error = error 18 | self.errorMessage = errorMessage 19 | self.employees = employees 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /MockingProject/Helpers/Observable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Observable.swift 3 | // Development 4 | // 5 | // Created by Fitzgerald Afful on 17/04/2020. 6 | // Copyright © 2020 Fitzgerald Afful. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class Observable { 12 | 13 | var value: T { 14 | didSet { 15 | listener?(value) 16 | } 17 | } 18 | 19 | private var listener: ((T) -> Void)? 20 | 21 | init(_ value: T) { 22 | self.value = value 23 | } 24 | 25 | func bind(_ closure: @escaping (T) -> Void) { 26 | closure(value) 27 | listener = closure 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /MockingProject/Model/Employee.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmployeeDTO.swift 3 | // Development 4 | // 5 | // Created by Fitzgerald Afful on 23/05/2020. 6 | // Copyright © 2020 Fitzgerald Afful. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Employee { 12 | var employeeId: String 13 | var employeeName: String 14 | var employeeSalary: String 15 | var profileImage: String? 16 | 17 | enum CodingKeys: String, CodingKey { 18 | case employeeId = "id" 19 | case employeeName = "employee_name" 20 | case employeeSalary = "employee_salary" 21 | case profileImage = "profile_image" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MockingProject/Helpers/DIHelpers/SwinjectStoryboardContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwinjectStoryboardContainer.swift 3 | // Development 4 | // 5 | // Created by Fitzgerald Afful on 21/05/2020. 6 | // Copyright © 2020 Fitzgerald Afful. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Swinject 11 | import SwinjectStoryboard 12 | import SwinjectAutoregistration 13 | 14 | extension SwinjectStoryboard { 15 | @objc class func setup() { 16 | let mainContainer = SwinjectContainer.sharedContainer.container 17 | 18 | defaultContainer.storyboardInitCompleted(SwinjectController.self) { _, controller in 19 | controller.viewModel = mainContainer.resolve(HomeViewModelProtocol.self) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MockingProject/Modules/DataBinding/View/Cells/EmployeeCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIRouter.swift 3 | // MockingProject 4 | // 5 | // Created by Paa Quesi Afful on 01/04/2020. 6 | // Copyright © 2020 MockingProject. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class EmployeeCell: UITableViewCell { 12 | 13 | @IBOutlet weak var employeeImageView: UIImageView! 14 | @IBOutlet weak var employeeTitleLabel: UILabel! 15 | @IBOutlet weak var employeeSalaryLabel: UILabel! 16 | 17 | var item: Employee? { 18 | didSet { 19 | guard let employee = item else { return } 20 | self.employeeTitleLabel.text = employee.employeeName 21 | self.employeeSalaryLabel.text = employee.employeeSalary 22 | self.employeeImageView.setImage(url: employee.profileImage) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MockingProjectTests/Resources/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 | -------------------------------------------------------------------------------- /MockingProject/Helpers/DIHelpers/SwinjectContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwinjectHelper.swift 3 | // Development 4 | // 5 | // Created by Fitzgerald Afful on 21/05/2020. 6 | // Copyright © 2020 Fitzgerald Afful. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Swinject 11 | 12 | class SwinjectContainer { 13 | 14 | static let sharedContainer = SwinjectContainer() 15 | let container = Container() 16 | 17 | private init() { 18 | setupDefaultContainers() 19 | } 20 | 21 | private func setupDefaultContainers() { 22 | container.register(EmployeeRepository.self, factory: { _ in APIEmployeeRepository() }) 23 | 24 | container.register(HomeViewModelProtocol.self, factory: { resolver in 25 | return SwinjectViewModel(repository: resolver.resolve(EmployeeRepository.self)!) 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /MockingProject/Repositories/DataModel/EmployeesResponseDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIRouter.swift 3 | // MockingProject 4 | // 5 | // Created by Paa Quesi Afful on 01/04/2020. 6 | // Copyright © 2020 MockingProject. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | public struct EmployeesResponseDTO: Codable, Equatable { 13 | 14 | var data: [EmployeeDTO] 15 | let status: String 16 | 17 | public static func == (lhs: EmployeesResponseDTO, rhs: EmployeesResponseDTO) -> Bool { 18 | return (lhs.data == rhs.data) 19 | } 20 | } 21 | 22 | public extension EmployeesResponseDTO { 23 | func map() -> EmployeesResponse { 24 | var array: [Employee] = [] 25 | for item in self.data { 26 | array.append(item.map()) 27 | } 28 | return EmployeesResponse(data: array, status: self.status) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /MockingProject/Helpers/DIHelpers/ServiceLocator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DIServiceLocator.swift 3 | // Development 4 | // 5 | // Created by Fitzgerald Afful on 21/05/2020. 6 | // Copyright © 2020 Fitzgerald Afful. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol ServiceLocator { 12 | func resolve() -> T? 13 | } 14 | 15 | final class DIServiceLocator: ServiceLocator { 16 | 17 | static let shared = DIServiceLocator() 18 | 19 | private lazy var services: [String: Any] = [:] 20 | private func typeName(some: Any) -> String { 21 | return (some is Any.Type) ? 22 | "\(some)" : "\(type(of: some))" 23 | } 24 | 25 | func register(_ service: T) { 26 | let key = typeName(some: T.self) 27 | print("Registering \(key)") 28 | services[key] = service 29 | print(services) 30 | } 31 | 32 | func resolve() -> T? { 33 | let key = typeName(some: T.self) 34 | print("Resolving \(key)") 35 | return services[key] as? T 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /MockingProject/Repositories/DataModel/EmployeeDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIRouter.swift 3 | // MockingProject 4 | // 5 | // Created by Paa Quesi Afful on 01/04/2020. 6 | // Copyright © 2020 MockingProject. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class EmployeeDTO: Codable, Equatable { 12 | var employeeId: String 13 | var employeeName: String 14 | var employeeSalary: String 15 | var profileImage: String? 16 | 17 | enum CodingKeys: String, CodingKey { 18 | case employeeId = "id" 19 | case employeeName = "employee_name" 20 | case employeeSalary = "employee_salary" 21 | case profileImage = "profile_image" 22 | } 23 | 24 | public static func == (lhs: EmployeeDTO, rhs: EmployeeDTO) -> Bool { 25 | return (lhs.employeeId == rhs.employeeId) 26 | } 27 | } 28 | 29 | public extension EmployeeDTO { 30 | func map() -> Employee { 31 | return Employee(employeeId: self.employeeId, employeeName: self.employeeName, employeeSalary: self.employeeSalary, profileImage: self.profileImage) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | 3 | platform :ios, '11.0' 4 | use_frameworks! 5 | inhibit_all_warnings! 6 | 7 | target 'Development' do 8 | 9 | #UI Dev 10 | pod 'Nuke' 11 | pod 'IQKeyboardManagerSwift' 12 | pod 'ESPullToRefresh' 13 | pod 'FTIndicator' 14 | 15 | #Networking 16 | pod 'Alamofire' 17 | 18 | #Testing 19 | pod 'Mocker' 20 | 21 | #Dependency Injection 22 | pod 'Swinject' 23 | pod 'SwinjectAutoregistration' 24 | pod 'SwinjectStoryboard' 25 | pod 'Resolver' 26 | 27 | 28 | #Reactive / Functional Programming 29 | pod 'RxSwift' 30 | pod 'RxCocoa' 31 | 32 | #CI/CD 33 | pod 'AppCenter' 34 | pod 'SwiftLint' 35 | 36 | #GraphQL 37 | pod 'Apollo' 38 | 39 | target 'MockingProjectTests' do 40 | inherit! :search_paths 41 | end 42 | 43 | target 'Test' do 44 | inherit! :search_paths 45 | pod 'RxBlocking' 46 | pod 'RxTest' 47 | end 48 | 49 | target 'Production' do 50 | inherit! :search_paths 51 | end 52 | end 53 | 54 | -------------------------------------------------------------------------------- /MockingProject/Modules/DataBinding/View/Controllers/DetailController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIRouter.swift 3 | // MockingProject 4 | // 5 | // Created by Paa Quesi Afful on 01/04/2020. 6 | // Copyright © 2020 MockingProject. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class DetailController: UIViewController, MyEnvironmentHelper { 12 | 13 | @IBOutlet weak var imageView: UIImageView! 14 | @IBOutlet weak var titleLabel: UILabel! 15 | @IBOutlet weak var descriptionTextView: UITextView! 16 | 17 | var item: Employee? 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | setEmployee() 21 | } 22 | 23 | func setEmployee() { 24 | guard let employee = item else { return } 25 | imageView.setImage(url: employee.profileImage) 26 | self.title = "Environment - \(getEnvValue()!)" 27 | titleLabel.text = employee.employeeName 28 | let description = "Salary: " + employee.employeeSalary 29 | self.descriptionTextView.text = description 30 | 31 | } 32 | 33 | func initializeFromStoryboard() -> DetailController { 34 | let controller = AppStoryboard.main.instance.instantiateViewController(withIdentifier: DetailController.storyboardID) as! DetailController 35 | return controller 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /MockingProject/Helpers/EnvironmentHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EnvironmentHelper.swift 3 | // Development 4 | // 5 | // Created by Fitzgerald Afful on 09/04/2020. 6 | // Copyright © 2020 Fitzgerald Afful. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class EnvironmentHelper { 12 | static var shared: EnvironmentHelper = { 13 | return EnvironmentHelper() 14 | 15 | }() 16 | 17 | /*This is in line with multiple Environments. You'll notice in the Project Settings, I have 3 targets. Development, Test and Production. 18 | All 3 targets have their own unique bundle indentifiers and Info.plist files which have been renamed. 19 | In each of the info.plist file, there's a variable envVar. envVar shows you which environment you are working in at the moment. */ 20 | func getEnvValue() -> String? { 21 | return Bundle.main.object(forInfoDictionaryKey: "envVar") as? String 22 | } 23 | } 24 | 25 | //This can be created in another form. As Below 26 | 27 | protocol MyEnvironmentHelper {} 28 | 29 | extension MyEnvironmentHelper { 30 | func getEnvValue() -> String? { 31 | return Bundle.main.object(forInfoDictionaryKey: "envVar") as? String 32 | } 33 | } 34 | 35 | //The Above example is used in DetailController 36 | -------------------------------------------------------------------------------- /MockingProject/Repositories/EmployeeRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIRouter.swift 3 | // MockingProject 4 | // 5 | // Created by Paa Quesi Afful on 01/04/2020. 6 | // Copyright © 2020 MockingProject. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | protocol EmployeeRepository { 13 | func getEmployees(completion:@escaping (DataResponse) -> Void) 14 | func getSingleEmployee(employeeId: String, completion:@escaping (DataResponse) -> Void) 15 | } 16 | 17 | public class APIEmployeeRepository: EmployeeRepository { 18 | 19 | private let session: Session 20 | 21 | init(_ session: Session = Session.default) { 22 | self.session = session 23 | } 24 | 25 | func getEmployees(completion:@escaping (DataResponse) -> Void) { 26 | session.request(APIRouter.getEmployees).responseDecodable { (response) in 27 | completion(response) 28 | } 29 | } 30 | 31 | func getSingleEmployee(employeeId: String, completion:@escaping (DataResponse) -> Void) { 32 | session.request(APIRouter.getSingleEmployee(employeeId: employeeId)).responseDecodable { (response) in 33 | completion(response) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /MockingProject.xcodeproj/xcuserdata/paaquesiafful.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | MockingProject copy.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 12 11 | 12 | MockingProject.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | Production.xcscheme_^#shared#^_ 18 | 19 | orderHint 20 | 25 21 | 22 | Test copy.xcscheme_^#shared#^_ 23 | 24 | orderHint 25 | 13 26 | 27 | Test.xcscheme_^#shared#^_ 28 | 29 | orderHint 30 | 24 31 | 32 | 33 | SuppressBuildableAutocreation 34 | 35 | 84D22D102435EE3000358B5F 36 | 37 | primary 38 | 39 | 40 | 84D22D262435EE3300358B5F 41 | 42 | primary 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /MockingProject/Modules/DependencyInjection/ViewModel/ResolverViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DepInjectionViewModel.swift 3 | // Development 4 | // 5 | // Created by Fitzgerald Afful on 20/05/2020. 6 | // Copyright © 2020 Fitzgerald Afful. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | class ResolverViewModel: HomeViewModelProtocol { 13 | 14 | var employeeRepository: EmployeeRepository? 15 | var employees: [Employee] = [] 16 | 17 | init(repository: EmployeeRepository = APIEmployeeRepository()) { 18 | self.employeeRepository = repository 19 | } 20 | 21 | func fetchEmployees(completion: @escaping ([Employee]?, String?) -> Void) { 22 | employeeRepository!.getEmployees { (result: DataResponse) in 23 | switch result.result { 24 | case .success(let response): 25 | if response.status == "success" { 26 | self.employees = response.map().data 27 | completion(self.employees, nil) 28 | return 29 | } 30 | completion(nil, BaseNetworkManager().getErrorMessage(response: result)) 31 | case .failure: 32 | completion(nil, BaseNetworkManager().getErrorMessage(response: result)) 33 | } 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /MockingProject/Modules/DependencyInjection/ViewModel/SwinjectViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeViewModel.swift 3 | // MockingProject 4 | // 5 | // Created by Fitzgerald Afful on 04/04/2020. 6 | // Copyright © 2020 Fitzgerald Afful. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | class SwinjectViewModel: HomeViewModelProtocol { 13 | 14 | var employeeRepository: EmployeeRepository? 15 | var employees: [Employee] = [] 16 | 17 | init(repository: EmployeeRepository = APIEmployeeRepository()) { 18 | self.employeeRepository = repository 19 | } 20 | 21 | func fetchEmployees(completion: @escaping ([Employee]?, String?) -> Void) { 22 | self.employeeRepository!.getEmployees { (result: DataResponse) in 23 | switch result.result { 24 | case .success(let response): 25 | if response.status == "success" { 26 | self.employees = response.map().data 27 | completion(self.employees, nil) 28 | return 29 | } 30 | completion(nil, BaseNetworkManager().getErrorMessage(response: result)) 31 | case .failure: 32 | completion(nil, BaseNetworkManager().getErrorMessage(response: result)) 33 | } 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /MockingProject/Modules/DataBinding/ViewModel/CombineViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockingProject 3 | // 4 | // Created by Fitzgerald Afful on 04/04/2020. 5 | // Copyright © 2020 Fitzgerald Afful. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | import Alamofire 10 | import Combine 11 | 12 | class CombineViewModel: ObservableObject { 13 | var errorMessage: String? 14 | var error: Bool = false 15 | 16 | @Published var employees: [Employee] = [] 17 | var employeeRepository: EmployeeRepository? 18 | 19 | init(repository: EmployeeRepository = APIEmployeeRepository()) { 20 | self.employeeRepository = repository 21 | } 22 | 23 | func setEmployeeRepository(repository: EmployeeRepository) { 24 | self.employeeRepository = repository 25 | } 26 | 27 | func fetchEmployees() { 28 | self.employeeRepository!.getEmployees { (result: DataResponse) in 29 | switch result.result { 30 | case .success(let response): 31 | if response.status == "success" { 32 | self.employees = response.map().data 33 | } else { 34 | self.setError(BaseNetworkManager().getErrorMessage(response: result)) 35 | } 36 | case .failure: 37 | self.setError(BaseNetworkManager().getErrorMessage(response: result)) 38 | } 39 | } 40 | } 41 | 42 | func setError(_ message: String) { 43 | self.errorMessage = message 44 | self.error = true 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /MockingProjectTests/EmployeeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmployeeTests.swift 3 | // MockingProjectTests 4 | // 5 | // Created by Fitzgerald Afful on 04/04/2020. 6 | // Copyright © 2020 Fitzgerald Afful. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Development 11 | @testable import Alamofire 12 | @testable import Mocker 13 | 14 | class EmployeeTests: XCTestCase { 15 | 16 | var bundle: Bundle! 17 | 18 | override func setUp() { 19 | bundle = Bundle(for: EmployeeTests.self) 20 | } 21 | 22 | func testSingleEmployeeResponseJSONMapping() throws { 23 | guard let url = bundle.url(forResource: "employee24", withExtension: "json") else { 24 | XCTFail("Missing file: Employee24.json") 25 | return 26 | } 27 | 28 | let json = try Data(contentsOf: url) 29 | let employee = try! JSONDecoder().decode(EmployeeResponse.self, from: json) 30 | 31 | XCTAssertEqual(employee.data.employeeName, "Doris Wilder") 32 | XCTAssertEqual(employee.data.employeeSalary, "85600") 33 | } 34 | 35 | func testEmployeesResponseJSONMapping() throws { 36 | guard let url = bundle.url(forResource: "employees", withExtension: "json") else { 37 | XCTFail("Missing file: Employees.json") 38 | return 39 | } 40 | 41 | let json = try Data(contentsOf: url) 42 | let data = try! JSONDecoder().decode(EmployeesResponse.self, from: json) 43 | 44 | XCTAssertEqual(data.data[0].employeeName, "Tiger Nixon") 45 | XCTAssertEqual(data.data[0].employeeSalary, "320800") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /MockingProject/Modules/DataBinding/ViewModel/HomeViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeViewModel.swift 3 | // MockingProject 4 | // 5 | // Created by Fitzgerald Afful on 04/04/2020. 6 | // Copyright © 2020 Fitzgerald Afful. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | protocol HomeViewModelProtocol { 13 | func fetchEmployees(completion: @escaping ([Employee]?, String?) -> Void) 14 | var employees: [Employee] { get set } 15 | } 16 | 17 | class HomeViewModel: HomeViewModelProtocol { 18 | 19 | var employeeRepository: EmployeeRepository? 20 | var employees: [Employee] = [] 21 | 22 | init(repository: EmployeeRepository = APIEmployeeRepository()) { 23 | self.employeeRepository = repository 24 | } 25 | 26 | func setEmployeeRepository(repository: EmployeeRepository) { 27 | self.employeeRepository = repository 28 | } 29 | 30 | func fetchEmployees(completion: @escaping ([Employee]?, String?) -> Void) { 31 | self.employeeRepository!.getEmployees { (result: DataResponse) in 32 | switch result.result { 33 | case .success(let response): 34 | if response.status == "success" { 35 | self.employees = response.map().data 36 | completion(self.employees, nil) 37 | return 38 | } 39 | completion(nil, BaseNetworkManager().getErrorMessage(response: result)) 40 | case .failure: 41 | completion(nil, BaseNetworkManager().getErrorMessage(response: result)) 42 | } 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /MockingProject/SupportingFiles/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 | -------------------------------------------------------------------------------- /MockingProject/Modules/DataBinding/ViewModel/ObservableViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockingProject 3 | // 4 | // Created by Fitzgerald Afful on 04/04/2020. 5 | // Copyright © 2020 Fitzgerald Afful. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | import Alamofire 10 | 11 | protocol ObservableViewModelProtocol { 12 | func fetchEmployees() 13 | func setError(_ message: String) 14 | var employees: Observable<[Employee]> { get set } //1 15 | var errorMessage: Observable { get set } 16 | var error: Observable { get set } 17 | } 18 | 19 | class ObservableViewModel: ObservableViewModelProtocol { 20 | var errorMessage: Observable = Observable(nil) 21 | var error: Observable = Observable(false) 22 | 23 | var employeeRepository: EmployeeRepository? 24 | var employees: Observable<[Employee]> = Observable([]) //2 25 | 26 | init(repository: EmployeeRepository = APIEmployeeRepository()) { 27 | self.employeeRepository = repository 28 | } 29 | 30 | func setEmployeeRepository(repository: EmployeeRepository) { 31 | self.employeeRepository = repository 32 | } 33 | 34 | func fetchEmployees() { 35 | self.employeeRepository!.getEmployees { (result: DataResponse) in 36 | switch result.result { 37 | case .success(let response): 38 | if response.status == "success" { 39 | self.employees.value = response.map().data //3 40 | return 41 | } 42 | self.setError(BaseNetworkManager().getErrorMessage(response: result)) 43 | case .failure: 44 | self.setError(BaseNetworkManager().getErrorMessage(response: result)) 45 | } 46 | } 47 | } 48 | 49 | func setError(_ message: String) { 50 | self.errorMessage.value = message 51 | self.error.value = true 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MockingProject 2 | 3 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/1e4b5be3374b4633b73864f82deae55c)](https://app.codacy.com/manual/fitzafful/MockingProject?utm_source=github.com&utm_medium=referral&utm_content=FitzAfful/MockingProject&utm_campaign=Badge_Grade_Dashboard) 4 | 5 | ![Build Status](https://build.appcenter.ms/v0.1/apps/d53174af-00c0-42be-bdb8-d6ed8916fc9d/branches/development/badge) 6 | 7 | A little project to refresh my memory on old stuff I forgot about and new stuff I'm learning. I comment parts I think will be difficult to understand. 8 | 9 | ## Features 10 | - [Data Binding in MVVM](https://github.com/FitzAfful/MockingProject/blob/master/MockingProject/Modules/DataBinding) 11 | - [Dependency Injection in Swift](https://github.com/FitzAfful/MockingProject/blob/master/MockingProject/Modules/DependencyInjection) 12 | - [Helpers for Reusable functions](https://github.com/FitzAfful/MockingProject/blob/master/MockingProject/Helpers/EnvironmentalHelpers.swift) 13 | - [Mocking Network Calls](https://github.com/FitzAfful/MockingProject/blob/master/MockingProjectTests/APIManagerTests.swift) 14 | - [Multiple Environments Support - Development/Test/Production](https://github.com/FitzAfful/MockingProject/blob/master/MockingProject/Helpers/EnvironmentalHelpers.swift) 15 | 16 | ## Tech/framework used 17 | Built with 18 | - [Mocker for Mocking Calls/Objects](https://github.com/WeTransfer/Mocker) 19 | - [Resolver for Dependency Injection](https://github.com/hmlongco/Resolver) 20 | - [Swinject for Dependency Injection](https://github.com/Swinject/Swinject) 21 | - [Alamofire for Network Calls](https://github.com/Alamofire/Alamofire) 22 | - [Swiftlint for Code Analysis](https://github.com/realm/SwiftLint) 23 | - [Microsoft App Center](https://appcenter.ms) 24 | 25 | ## Installation 26 | To run this project, have Cocoapods installed and run `pod install` in the parent folder. 27 | 28 | #### Anything else that seems useful 29 | WIll update as and when. 30 | -------------------------------------------------------------------------------- /MockingProject/Modules/DataBinding/ViewModel/EventBusViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockingProject 3 | // 4 | // Created by Fitzgerald Afful on 04/04/2020. 5 | // Copyright © 2020 Fitzgerald Afful. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | import Alamofire 10 | 11 | protocol EventBusModelProtocol { 12 | func fetchEmployees() 13 | var employees: [Employee] { get set } 14 | func setError(_ message: String) 15 | var errorMessage: String? { get set } 16 | var error: Bool { get set } 17 | } 18 | 19 | class EventBusViewModel: EventBusModelProtocol { 20 | var errorMessage: String? 21 | var error: Bool = false 22 | var employeeRepository: EmployeeRepository? 23 | var employees: [Employee] = [] 24 | 25 | init(repository: EmployeeRepository = APIEmployeeRepository()) { 26 | self.employeeRepository = repository 27 | } 28 | 29 | func setEmployeeRepository(repository: EmployeeRepository) { 30 | self.employeeRepository = repository 31 | } 32 | 33 | func fetchEmployees() { 34 | self.employeeRepository!.getEmployees { (result: DataResponse) in 35 | switch result.result { 36 | case .success(let response): 37 | if response.status == "success" { 38 | self.employees = response.map().data 39 | } else { 40 | self.setError(BaseNetworkManager().getErrorMessage(response: result)) 41 | } 42 | self.callEvent() 43 | case .failure: 44 | self.setError(BaseNetworkManager().getErrorMessage(response: result)) 45 | self.callEvent() 46 | } 47 | } 48 | } 49 | 50 | func setError(_ message: String) { 51 | self.errorMessage = message 52 | self.error = true 53 | } 54 | 55 | func callEvent() { 56 | EventBus.post("fetchEmployees", sender: EmployeesEvent(error: error, errorMessage: errorMessage, employees: employees)) 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /MockingProject/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /MockingProject/Repositories/APIRouter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIRouter.swift 3 | // MockingProject 4 | // 5 | // Created by Paa Quesi Afful on 01/04/2020. 6 | // Copyright © 2020 MockingProject. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | enum APIRouter: APIConfiguration { 13 | 14 | case getEmployees 15 | case getSingleEmployee(employeeId: String) 16 | 17 | internal var method: HTTPMethod { 18 | switch self { 19 | case .getEmployees: return .get 20 | case .getSingleEmployee: return .get 21 | } 22 | } 23 | 24 | internal var path: String { 25 | switch self { 26 | case .getEmployees: return NetworkingConstants.baseUrl + "employees" 27 | case .getSingleEmployee(let employeeId): return NetworkingConstants.baseUrl + "employee/\(employeeId)" 28 | } 29 | } 30 | 31 | internal var parameters: [String: Any] { 32 | switch self { 33 | default: return [:] 34 | } 35 | } 36 | 37 | internal var body: [String: Any] { 38 | switch self { 39 | default: return [:] 40 | } 41 | } 42 | 43 | internal var headers: HTTPHeaders { 44 | switch self { 45 | default: return ["Content-Type": "application/json", "Accept": "application/json"] 46 | } 47 | } 48 | 49 | func asURLRequest() throws -> URLRequest { 50 | var urlComponents = URLComponents(string: path)! 51 | var queryItems: [URLQueryItem] = [] 52 | for item in parameters { 53 | queryItems.append(URLQueryItem(name: item.key, value: "\(item.value)")) 54 | } 55 | if !(queryItems.isEmpty) { 56 | urlComponents.queryItems = queryItems 57 | } 58 | let url = urlComponents.url! 59 | var urlRequest = URLRequest(url: url) 60 | 61 | print("URL: \(url)") 62 | 63 | urlRequest.httpMethod = method.rawValue 64 | urlRequest.allHTTPHeaderFields = headers.dictionary 65 | 66 | if !(body.isEmpty) { 67 | urlRequest = try URLEncoding().encode(urlRequest, with: body) 68 | let jsonData1 = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) 69 | urlRequest.httpBody = jsonData1 70 | } 71 | return urlRequest 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /MockingProject/Extensions/UIViewController+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIRouter.swift 3 | // MockingProject 4 | // 5 | // Created by Paa Quesi Afful on 01/04/2020. 6 | // Copyright © 2020 MockingProject. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | enum AppStoryboard: String { 13 | 14 | case main 15 | 16 | var instance: UIStoryboard { 17 | return UIStoryboard(name: self.rawValue, bundle: Bundle.main) 18 | } 19 | 20 | func viewController(viewControllerClass: T.Type, function: String = #function, line: Int = #line, file: String = #file) -> T { 21 | 22 | let storyboardID = (viewControllerClass as UIViewController.Type).storyboardID 23 | guard let scene = instance.instantiateViewController(withIdentifier: storyboardID) as? T else { 24 | fatalError("ViewController \(storyboardID), not found in \(self.rawValue) Storyboard.\nFile : \(file) \nLine Number : \(line)") 25 | } 26 | 27 | return scene 28 | } 29 | 30 | func initialViewController() -> UIViewController? { 31 | return instance.instantiateInitialViewController() 32 | } 33 | } 34 | 35 | extension UIViewController { 36 | 37 | class var storyboardID: String { 38 | return "\(self)" 39 | } 40 | 41 | static func instantiate(fromAppStoryboard appStoryboard: AppStoryboard) -> Self { 42 | return appStoryboard.viewController(viewControllerClass: self) 43 | } 44 | 45 | func showAlert(title: String, message: String) { 46 | let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) 47 | alert.addAction(UIAlertAction(title: "Okay", style: .cancel, handler: nil)) 48 | self.present(alert, animated: true, completion: nil) 49 | } 50 | 51 | func showAlert(withTitle title: String, message: String) { 52 | let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) 53 | alert.addAction(UIAlertAction(title: "Dismiss", style: .cancel, handler: nil)) 54 | present(alert, animated: true, completion: nil) 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /MockingProject/SupportingFiles/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MockingProject 4 | // 5 | // Created by Fitzgerald Afful on 02/04/2020. 6 | // Copyright © 2020 Fitzgerald Afful. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AppCenter 11 | import AppCenterAnalytics 12 | import AppCenterCrashes 13 | 14 | @UIApplicationMain 15 | class AppDelegate: UIResponder, UIApplicationDelegate { 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | MSAppCenter.start("ede02f1f-31af-4da6-8c8f-395519dda28a", withServices: [ 19 | MSAnalytics.self, 20 | MSCrashes.self 21 | ]) 22 | _ = SwinjectContainer.sharedContainer 23 | 24 | //ServiceLocator Registrations 25 | let serviceLocator = DIServiceLocator.shared 26 | serviceLocator.register(APIEmployeeRepository() as EmployeeRepository) 27 | 28 | guard let repository: EmployeeRepository = serviceLocator.resolve() else { return true } 29 | serviceLocator.register(HomeViewModel(repository: repository)) 30 | 31 | return true 32 | } 33 | 34 | // MARK: UISceneSession Lifecycle 35 | 36 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 37 | // Called when a new scene session is being created. 38 | // Use this method to select a configuration to create the new scene with. 39 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 40 | } 41 | 42 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 43 | // Called when the user discards a scene session. 44 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 45 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Test-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | envVar 6 | Test 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | UISceneConfigurations 30 | 31 | UIWindowSceneSessionRoleApplication 32 | 33 | 34 | UISceneConfigurationName 35 | Default Configuration 36 | UISceneDelegateClassName 37 | $(PRODUCT_MODULE_NAME).SceneDelegate 38 | UISceneStoryboardFile 39 | Main 40 | 41 | 42 | 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIMainStoryboardFile 47 | Main 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | UISupportedInterfaceOrientations~ipad 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationPortraitUpsideDown 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /Production-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | envVar 6 | Production 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | UISceneConfigurations 30 | 31 | UIWindowSceneSessionRoleApplication 32 | 33 | 34 | UISceneConfigurationName 35 | Default Configuration 36 | UISceneDelegateClassName 37 | $(PRODUCT_MODULE_NAME).SceneDelegate 38 | UISceneStoryboardFile 39 | Main 40 | 41 | 42 | 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIMainStoryboardFile 47 | Main 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | UISupportedInterfaceOrientations~ipad 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationPortraitUpsideDown 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /MockingProject/Modules/DataBinding/ViewModel/RxSwiftViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockingProject 3 | // 4 | // Created by Fitzgerald Afful on 04/04/2020. 5 | // Copyright © 2020 Fitzgerald Afful. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | import Alamofire 10 | import RxSwift 11 | import RxCocoa 12 | 13 | class RxSwiftViewModel { 14 | 15 | private let disposeBag = DisposeBag() 16 | private let _employees = BehaviorRelay<[Employee]>(value: []) 17 | private let _error = BehaviorRelay(value: false) 18 | private let _errorMessage = BehaviorRelay(value: nil) 19 | 20 | var employees: Driver<[Employee]> { 21 | return _employees.asDriver() 22 | } 23 | 24 | var hasError: Bool { 25 | return _error.value 26 | } 27 | 28 | var errorMessage: Driver { 29 | return _errorMessage.asDriver() 30 | } 31 | 32 | var numberOfEmployees: Int { 33 | return _employees.value.count 34 | } 35 | 36 | var employeeRepository: EmployeeRepository? 37 | 38 | init(repository: EmployeeRepository = APIEmployeeRepository()) { 39 | self.employeeRepository = repository 40 | } 41 | 42 | func setEmployeeRepository(repository: EmployeeRepository) { 43 | self.employeeRepository = repository 44 | } 45 | 46 | func fetchEmployees() { 47 | self.employeeRepository!.getEmployees { (result: DataResponse) in 48 | switch result.result { 49 | case .success(let response): 50 | if response.status == "success" { 51 | self._error.accept(false) 52 | self._errorMessage.accept(nil) 53 | self._employees.accept(response.map().data) 54 | return 55 | } 56 | self.setError(BaseNetworkManager().getErrorMessage(response: result)) 57 | case .failure: 58 | self.setError(BaseNetworkManager().getErrorMessage(response: result)) 59 | } 60 | } 61 | } 62 | 63 | func setError(_ message: String) { 64 | self._error.accept(true) 65 | self._errorMessage.accept(message) 66 | } 67 | 68 | func modelForIndex(at index: Int) -> Employee? { 69 | guard index < _employees.value.count else { 70 | return nil 71 | } 72 | return _employees.value[index] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /MockingProject/SupportingFiles/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // MockingProject 4 | // 5 | // Created by Fitzgerald Afful on 02/04/2020. 6 | // Copyright © 2020 Fitzgerald Afful. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | //guard let _ = (scene as? UIWindowScene) else { return } 20 | if scene as? UIWindowScene == nil {return } 21 | } 22 | 23 | func sceneDidDisconnect(_ scene: UIScene) { 24 | // Called as the scene is being released by the system. 25 | // This occurs shortly after the scene enters the background, or when its session is discarded. 26 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 27 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 28 | } 29 | 30 | func sceneDidBecomeActive(_ scene: UIScene) { 31 | // Called when the scene has moved from an inactive state to an active state. 32 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 33 | } 34 | 35 | func sceneWillResignActive(_ scene: UIScene) { 36 | // Called when the scene will move from an active state to an inactive state. 37 | // This may occur due to temporary interruptions (ex. an incoming phone call). 38 | } 39 | 40 | func sceneWillEnterForeground(_ scene: UIScene) { 41 | // Called as the scene transitions from the background to the foreground. 42 | // Use this method to undo the changes made on entering the background. 43 | } 44 | 45 | func sceneDidEnterBackground(_ scene: UIScene) { 46 | // Called as the scene transitions from the foreground to the background. 47 | // Use this method to save data, release shared resources, and store enough scene-specific state information 48 | // to restore the scene back to its current state. 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /MockingProject/SupportingFiles/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | envVar 6 | Development 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | UISceneConfigurations 30 | 31 | UIWindowSceneSessionRoleApplication 32 | 33 | 34 | UISceneConfigurationName 35 | Default Configuration 36 | UISceneDelegateClassName 37 | $(PRODUCT_MODULE_NAME).SceneDelegate 38 | UISceneStoryboardFile 39 | Main 40 | 41 | 42 | 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIMainStoryboardFile 47 | Main 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | UISupportedInterfaceOrientations~ipad 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationPortraitUpsideDown 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | NSAppTransportSecurity 66 | 67 | NSExceptionDomains 68 | 69 | restapiexample.com 70 | 71 | NSExceptionAllowsInsecureHTTPLoads 72 | 73 | NSExceptionMinimumTLSVersion 74 | TLSv1.1 75 | NSIncludesSubdomains 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /MockingProject/Repositories/BaseNetworkManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIRouter.swift 3 | // MockingProject 4 | // 5 | // Created by Paa Quesi Afful on 01/04/2020. 6 | // Copyright © 2020 MockingProject. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | public class BaseNetworkManager { 13 | 14 | public func getErrorMessage(response: DataResponse) -> String where T: Codable { 15 | var message = NetworkingConstants.networkErrorMessage 16 | if let data = response.data { 17 | if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { 18 | if let error = json["errors"] as? NSDictionary { 19 | message = error["message"] as! String 20 | } else if let error = json["error"] as? NSDictionary { 21 | if let message1 = error["message"] as? String { 22 | message = message1 23 | } 24 | } else if let messages = json["message"] as? String { 25 | message = messages 26 | } 27 | } 28 | } 29 | return message 30 | } 31 | } 32 | 33 | class DictionaryEncoder { 34 | private let jsonEncoder = JSONEncoder() 35 | 36 | /// Encodes given Encodable value into an array or dictionary 37 | func encode(_ value: T) throws -> Any where T: Encodable { 38 | let jsonData = try jsonEncoder.encode(value) 39 | return try JSONSerialization.jsonObject(with: jsonData, options: .allowFragments) 40 | } 41 | } 42 | 43 | class DictionaryDecoder { 44 | private let jsonDecoder = JSONDecoder() 45 | 46 | /// Decodes given Decodable type from given array or dictionary 47 | func decode(_ type: T.Type, from json: Any) throws -> T where T: Decodable { 48 | let jsonData = try JSONSerialization.data(withJSONObject: json, options: []) 49 | return try jsonDecoder.decode(type, from: jsonData) 50 | } 51 | } 52 | 53 | extension Encodable { 54 | var dictionary: [String: Any] { 55 | var param: [String: Any] = [: ] 56 | do { 57 | let param1 = try DictionaryEncoder().encode(self) 58 | param = param1 as! [String: Any] 59 | } catch { 60 | print("Couldnt parse parameter") 61 | } 62 | return param 63 | } 64 | } 65 | 66 | struct NetworkingConstants { 67 | static let baseUrl = "http://dummy.restapiexample.com/api/v1/" 68 | static let networkErrorMessage = "Please check your internet connection and try again." 69 | } 70 | 71 | protocol APIConfiguration: URLRequestConvertible { 72 | var method: HTTPMethod { get } 73 | var path: String { get } 74 | var body: [String: Any] { get } 75 | var headers: HTTPHeaders { get } 76 | var parameters: [String: Any] { get } 77 | } 78 | 79 | enum NetworkError: LocalizedError { 80 | case responseStatusError(message: String) 81 | } 82 | 83 | extension NetworkError { 84 | var errorDescription: String { 85 | switch self { 86 | case let .responseStatusError(message): 87 | return "\(message)" 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Alamofire (5.0.5) 3 | - Apollo (0.27.1): 4 | - Apollo/Core (= 0.27.1) 5 | - Apollo/Core (0.27.1) 6 | - AppCenter (3.1.0): 7 | - AppCenter/Analytics (= 3.1.0) 8 | - AppCenter/Crashes (= 3.1.0) 9 | - AppCenter/Analytics (3.1.0): 10 | - AppCenter/Core 11 | - AppCenter/Core (3.1.0) 12 | - AppCenter/Crashes (3.1.0): 13 | - AppCenter/Core 14 | - ESPullToRefresh (2.9.2) 15 | - FTIndicator (1.2.9): 16 | - FTIndicator/FTNotificationIndicator (= 1.2.9) 17 | - FTIndicator/FTProgressIndicator (= 1.2.9) 18 | - FTIndicator/FTToastIndicator (= 1.2.9) 19 | - FTIndicator/FTNotificationIndicator (1.2.9) 20 | - FTIndicator/FTProgressIndicator (1.2.9) 21 | - FTIndicator/FTToastIndicator (1.2.9) 22 | - IQKeyboardManagerSwift (6.5.5) 23 | - Mocker (2.1.0) 24 | - Nuke (8.4.1) 25 | - Resolver (1.0.7) 26 | - RxBlocking (5.1.1): 27 | - RxSwift (~> 5) 28 | - RxCocoa (5.1.1): 29 | - RxRelay (~> 5) 30 | - RxSwift (~> 5) 31 | - RxRelay (5.1.1): 32 | - RxSwift (~> 5) 33 | - RxSwift (5.1.1) 34 | - RxTest (5.1.1): 35 | - RxSwift (~> 5) 36 | - SwiftLint (0.39.2) 37 | - Swinject (2.7.1) 38 | - SwinjectAutoregistration (2.7.0): 39 | - Swinject (~> 2.7) 40 | - SwinjectStoryboard (2.2.0): 41 | - Swinject (~> 2.6) 42 | 43 | DEPENDENCIES: 44 | - Alamofire 45 | - Apollo 46 | - AppCenter 47 | - ESPullToRefresh 48 | - FTIndicator 49 | - IQKeyboardManagerSwift 50 | - Mocker 51 | - Nuke 52 | - Resolver 53 | - RxBlocking 54 | - RxCocoa 55 | - RxSwift 56 | - RxTest 57 | - SwiftLint 58 | - Swinject 59 | - SwinjectAutoregistration 60 | - SwinjectStoryboard 61 | 62 | SPEC REPOS: 63 | https://github.com/CocoaPods/Specs.git: 64 | - Alamofire 65 | - Apollo 66 | - AppCenter 67 | - ESPullToRefresh 68 | - FTIndicator 69 | - IQKeyboardManagerSwift 70 | - Mocker 71 | - Nuke 72 | - Resolver 73 | - RxBlocking 74 | - RxCocoa 75 | - RxRelay 76 | - RxSwift 77 | - RxTest 78 | - SwiftLint 79 | - Swinject 80 | - SwinjectAutoregistration 81 | - SwinjectStoryboard 82 | 83 | SPEC CHECKSUMS: 84 | Alamofire: df2f8f826963b08b9a870791ad48e07a10090b2e 85 | Apollo: e5c057c155bf28bcfbf928e0d69e3ce643f1a1c6 86 | AppCenter: a1c30c47b7882a04a615ffa5ab26c007326436d8 87 | ESPullToRefresh: efed00d52597065e9a84e1e52fa444fdb1a1dee0 88 | FTIndicator: f7f071fd159e5befa1d040a9ef2e3ab53fa9322c 89 | IQKeyboardManagerSwift: 0fb93310284665245591f50f7a5e38de615960b7 90 | Mocker: 55f3d932b93e1568f3cf15bd697348c06fc676ee 91 | Nuke: d780e3507a86b86c589ab3cc5cd302d5456f06fb 92 | Resolver: da30396fcabb46790959209430a4a0a9e6e39849 93 | RxBlocking: 5f700a78cad61ce253ebd37c9a39b5ccc76477b4 94 | RxCocoa: 32065309a38d29b5b0db858819b5bf9ef038b601 95 | RxRelay: d77f7d771495f43c556cbc43eebd1bb54d01e8e9 96 | RxSwift: 81470a2074fa8780320ea5fe4102807cb7118178 97 | RxTest: 711632d5644dffbeb62c936a521b5b008a1e1faa 98 | SwiftLint: 22ccbbe3b8008684be5955693bab135e0ed6a447 99 | Swinject: ddf78b8486dd9b71a667b852cad919ab4484478e 100 | SwinjectAutoregistration: 330f5012642a8b5c89a8a4adb0c5e52df07382c0 101 | SwinjectStoryboard: 32512ef16c2b0ff5b8f823b23539c4a50f6d3383 102 | 103 | PODFILE CHECKSUM: ccb53ca930b258c96357f2876f9e6c9945510e52 104 | 105 | COCOAPODS: 1.8.4 106 | -------------------------------------------------------------------------------- /MockingProject/Modules/DataBinding/View/Controllers/CobineController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIRouter.swift 3 | // MockingProject 4 | // 5 | // Created by Paa Quesi Afful on 01/04/2020. 6 | // Copyright © 2020 MockingProject. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ESPullToRefresh 11 | import Combine 12 | 13 | class CombineController: UIViewController { 14 | 15 | @IBOutlet weak var tableView: UITableView! 16 | @IBOutlet weak var emptyView: UIView! 17 | @IBOutlet weak var activityIndicator: UIActivityIndicatorView! 18 | 19 | lazy var viewModel: CombineViewModel = { 20 | let viewModel = CombineViewModel() 21 | return viewModel 22 | }() 23 | 24 | private var cancellables: Set = [] 25 | 26 | override func viewDidLoad() { 27 | super.viewDidLoad() 28 | showLoader() 29 | setupTableView() 30 | bindViewModel() 31 | } 32 | 33 | private func bindViewModel() { 34 | viewModel.$employees.sink { [weak self] _ in 35 | self?.showTableView() 36 | }.store(in: &cancellables) 37 | } 38 | 39 | func setupTableView() { 40 | self.tableView.register(UINib(nibName: "EmployeeCell", bundle: nil), forCellReuseIdentifier: "EmployeeCell") 41 | self.tableView.dataSource = self 42 | self.tableView.delegate = self 43 | self.tableView.tableFooterView = UIView() 44 | self.tableView.es.addPullToRefresh { 45 | self.viewModel.fetchEmployees() 46 | } 47 | self.tableView.es.startPullToRefresh() 48 | } 49 | 50 | func showLoader() { 51 | self.tableView.isHidden = true 52 | self.emptyView.isHidden = true 53 | self.activityIndicator.isHidden = false 54 | self.activityIndicator.startAnimating() 55 | } 56 | 57 | func showEmptyView() { 58 | self.tableView.isHidden = true 59 | self.emptyView.isHidden = false 60 | self.activityIndicator.isHidden = true 61 | } 62 | 63 | func showTableView() { 64 | DispatchQueue.main.async { 65 | self.tableView.es.stopPullToRefresh() 66 | self.tableView.reloadData() 67 | self.tableView.isHidden = false 68 | self.emptyView.isHidden = true 69 | self.activityIndicator.isHidden = true 70 | } 71 | } 72 | 73 | func moveToDetails(item: Employee) { 74 | DispatchQueue.main.async { 75 | let detailController = DetailController().initializeFromStoryboard() 76 | detailController.item = item 77 | self.navigationController?.pushViewController(detailController, animated: true) 78 | } 79 | } 80 | 81 | } 82 | 83 | extension CombineController: UITableViewDelegate { 84 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 85 | let item = self.viewModel.employees[indexPath.row] 86 | self.moveToDetails(item: item) 87 | } 88 | } 89 | 90 | extension CombineController: UITableViewDataSource { 91 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 92 | return self.viewModel.employees.count 93 | } 94 | 95 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 96 | let cell = tableView.dequeueReusableCell(withIdentifier: "EmployeeCell", for: indexPath) as! EmployeeCell 97 | cell.item = self.viewModel.employees[indexPath.row] 98 | return cell 99 | } 100 | 101 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 102 | return 96.0 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /MockingProject/Modules/DependencyInjection/Controllers/SwinjectController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIRouter.swift 3 | // MockingProject 4 | // 5 | // Created by Paa Quesi Afful on 01/04/2020. 6 | // Copyright © 2020 MockingProject. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ESPullToRefresh 11 | import Alamofire 12 | 13 | class SwinjectController: UIViewController { 14 | 15 | @IBOutlet weak var tableView: UITableView! 16 | @IBOutlet weak var emptyView: UIView! 17 | @IBOutlet weak var activityIndicator: UIActivityIndicatorView! 18 | 19 | var viewModel: HomeViewModelProtocol! 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | showLoader() 24 | setupTableView() 25 | } 26 | 27 | func setupTableView() { 28 | self.tableView.register(UINib(nibName: "EmployeeCell", bundle: nil), forCellReuseIdentifier: "EmployeeCell") 29 | self.tableView.dataSource = self 30 | self.tableView.delegate = self 31 | self.tableView.tableFooterView = UIView() 32 | self.tableView.es.addPullToRefresh { 33 | self.getEmployees() 34 | } 35 | self.tableView.es.startPullToRefresh() 36 | } 37 | 38 | func getEmployees() { 39 | viewModel.fetchEmployees { (employees, errorMessage) in 40 | if employees != nil { 41 | self.showTableView() 42 | } else if errorMessage != nil { 43 | self.showTableView() 44 | self.showAlert(title: "Error", message: errorMessage!) 45 | } 46 | } 47 | } 48 | 49 | func showLoader() { 50 | self.tableView.isHidden = true 51 | self.emptyView.isHidden = true 52 | self.activityIndicator.isHidden = false 53 | self.activityIndicator.startAnimating() 54 | } 55 | 56 | func showEmptyView() { 57 | self.tableView.isHidden = true 58 | self.emptyView.isHidden = false 59 | self.activityIndicator.isHidden = true 60 | } 61 | 62 | func showTableView() { 63 | DispatchQueue.main.async { 64 | self.tableView.es.stopPullToRefresh() 65 | if self.viewModel.employees.isEmpty { 66 | self.showEmptyView() 67 | } else { 68 | self.tableView.reloadData() 69 | self.tableView.isHidden = false 70 | self.emptyView.isHidden = true 71 | self.activityIndicator.isHidden = true 72 | } 73 | } 74 | } 75 | 76 | func moveToDetails(item: Employee) { 77 | DispatchQueue.main.async { 78 | let detailController = DetailController().initializeFromStoryboard() 79 | detailController.item = item 80 | self.navigationController?.pushViewController(detailController, animated: true) 81 | } 82 | } 83 | 84 | } 85 | 86 | extension SwinjectController: UITableViewDelegate { 87 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 88 | let item = self.viewModel.employees[indexPath.row] 89 | self.moveToDetails(item: item) 90 | } 91 | } 92 | 93 | extension SwinjectController: UITableViewDataSource { 94 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 95 | return self.viewModel.employees.count 96 | } 97 | 98 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 99 | let cell = tableView.dequeueReusableCell(withIdentifier: "EmployeeCell", for: indexPath) as! EmployeeCell 100 | cell.item = self.self.viewModel.employees[indexPath.row] 101 | return cell 102 | } 103 | 104 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 105 | return 96.0 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /MockingProject/Modules/DependencyInjection/Controllers/ResolverController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIRouter.swift 3 | // MockingProject 4 | // 5 | // Created by Paa Quesi Afful on 01/04/2020. 6 | // Copyright © 2020 MockingProject. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ESPullToRefresh 11 | import Alamofire 12 | import Resolver 13 | 14 | class ResolverController: UIViewController, Resolving { 15 | 16 | @IBOutlet weak var tableView: UITableView! 17 | @IBOutlet weak var emptyView: UIView! 18 | @IBOutlet weak var activityIndicator: UIActivityIndicatorView! 19 | 20 | private var viewModel: ResolverViewModel = Resolver.resolve() 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | showLoader() 25 | setupTableView() 26 | } 27 | 28 | func setupTableView() { 29 | self.tableView.register(UINib(nibName: "EmployeeCell", bundle: nil), forCellReuseIdentifier: "EmployeeCell") 30 | self.tableView.dataSource = self 31 | self.tableView.delegate = self 32 | self.tableView.tableFooterView = UIView() 33 | self.tableView.es.addPullToRefresh { 34 | self.getEmployees() 35 | } 36 | self.tableView.es.startPullToRefresh() 37 | } 38 | 39 | func getEmployees() { 40 | viewModel.fetchEmployees { (employees, errorMessage) in 41 | if employees != nil { 42 | self.showTableView() 43 | } else if errorMessage != nil { 44 | self.showTableView() 45 | self.showAlert(title: "Error", message: errorMessage!) 46 | } 47 | } 48 | } 49 | 50 | func showLoader() { 51 | self.tableView.isHidden = true 52 | self.emptyView.isHidden = true 53 | self.activityIndicator.isHidden = false 54 | self.activityIndicator.startAnimating() 55 | } 56 | 57 | func showEmptyView() { 58 | self.tableView.isHidden = true 59 | self.emptyView.isHidden = false 60 | self.activityIndicator.isHidden = true 61 | } 62 | 63 | func showTableView() { 64 | DispatchQueue.main.async { 65 | self.tableView.es.stopPullToRefresh() 66 | if self.viewModel.employees.isEmpty { 67 | self.showEmptyView() 68 | } else { 69 | self.tableView.reloadData() 70 | self.tableView.isHidden = false 71 | self.emptyView.isHidden = true 72 | self.activityIndicator.isHidden = true 73 | } 74 | } 75 | } 76 | 77 | func moveToDetails(item: Employee) { 78 | DispatchQueue.main.async { 79 | let detailController = DetailController().initializeFromStoryboard() 80 | detailController.item = item 81 | self.navigationController?.pushViewController(detailController, animated: true) 82 | } 83 | } 84 | 85 | } 86 | 87 | extension ResolverController: UITableViewDelegate { 88 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 89 | let item = self.viewModel.employees[indexPath.row] 90 | self.moveToDetails(item: item) 91 | } 92 | } 93 | 94 | extension ResolverController: UITableViewDataSource { 95 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 96 | return self.viewModel.employees.count 97 | } 98 | 99 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 100 | let cell = tableView.dequeueReusableCell(withIdentifier: "EmployeeCell", for: indexPath) as! EmployeeCell 101 | cell.item = self.self.viewModel.employees[indexPath.row] 102 | return cell 103 | } 104 | 105 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 106 | return 96.0 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /MockingProject/Modules/DataBinding/View/Controllers/ObservableController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIRouter.swift 3 | // MockingProject 4 | // 5 | // Created by Paa Quesi Afful on 01/04/2020. 6 | // Copyright © 2020 MockingProject. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ESPullToRefresh 11 | import Alamofire 12 | 13 | class ObservableController: UIViewController { 14 | 15 | @IBOutlet weak var tableView: UITableView! 16 | @IBOutlet weak var emptyView: UIView! 17 | @IBOutlet weak var activityIndicator: UIActivityIndicatorView! 18 | 19 | lazy var viewModel: ObservableViewModel = { 20 | let viewModel = ObservableViewModel() 21 | return viewModel 22 | }() 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | showLoader() 27 | setupTableView() 28 | } 29 | 30 | func setupTableView() { 31 | self.tableView.register(UINib(nibName: "EmployeeCell", bundle: nil), forCellReuseIdentifier: "EmployeeCell") 32 | self.tableView.dataSource = self 33 | self.tableView.delegate = self 34 | self.tableView.tableFooterView = UIView() 35 | viewModel.employees.bind { (_) in 36 | self.showTableView() 37 | } 38 | self.tableView.es.addPullToRefresh { 39 | self.viewModel.fetchEmployees() 40 | } 41 | self.tableView.es.startPullToRefresh() 42 | } 43 | 44 | func showErrorMessage(_ message: String) { 45 | self.showTableView() 46 | self.showAlert(title: "Error", message: message) 47 | } 48 | 49 | func showLoader() { 50 | self.tableView.isHidden = true 51 | self.emptyView.isHidden = true 52 | self.activityIndicator.isHidden = false 53 | self.activityIndicator.startAnimating() 54 | } 55 | 56 | func showTableView() { 57 | DispatchQueue.main.async { 58 | self.tableView.es.stopPullToRefresh() 59 | if self.viewModel.employees.value.isEmpty { 60 | self.showEmptyView() 61 | } else { 62 | self.tableView.reloadData() 63 | self.tableView.isHidden = false 64 | self.emptyView.isHidden = true 65 | self.activityIndicator.isHidden = true 66 | } 67 | } 68 | } 69 | 70 | func showEmptyView() { 71 | self.tableView.isHidden = true 72 | self.emptyView.isHidden = false 73 | self.activityIndicator.isHidden = true 74 | } 75 | 76 | func moveToDetails(item: Employee) { 77 | DispatchQueue.main.async { 78 | let detailController = DetailController().initializeFromStoryboard() 79 | detailController.item = item 80 | self.navigationController?.pushViewController(detailController, animated: true) 81 | } 82 | } 83 | 84 | } 85 | 86 | extension ObservableController: UITableViewDelegate { 87 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 88 | let item = self.viewModel.employees.value[indexPath.row] 89 | self.moveToDetails(item: item) 90 | } 91 | } 92 | 93 | extension ObservableController: UITableViewDataSource { 94 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 95 | return self.viewModel.employees.value.count 96 | } 97 | 98 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 99 | let cell = tableView.dequeueReusableCell(withIdentifier: "EmployeeCell", for: indexPath) as! EmployeeCell 100 | cell.item = self.viewModel.employees.value[indexPath.row] 101 | return cell 102 | } 103 | 104 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 105 | return 96.0 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /MockingProject/Modules/DependencyInjection/Controllers/ServiceLocatorController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIRouter.swift 3 | // MockingProject 4 | // 5 | // Created by Paa Quesi Afful on 01/04/2020. 6 | // Copyright © 2020 MockingProject. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ESPullToRefresh 11 | import Alamofire 12 | 13 | class ServiceLocatorController: UIViewController { 14 | 15 | @IBOutlet weak var tableView: UITableView! 16 | @IBOutlet weak var emptyView: UIView! 17 | @IBOutlet weak var activityIndicator: UIActivityIndicatorView! 18 | 19 | private var viewModel: HomeViewModel! 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | 24 | let serviceLocator = DIServiceLocator.shared 25 | 26 | guard let model: HomeViewModel = serviceLocator.resolve() else { return } 27 | self.viewModel = model 28 | 29 | showLoader() 30 | setupTableView() 31 | } 32 | 33 | func setupTableView() { 34 | self.tableView.register(UINib(nibName: "EmployeeCell", bundle: nil), forCellReuseIdentifier: "EmployeeCell") 35 | self.tableView.dataSource = self 36 | self.tableView.delegate = self 37 | self.tableView.tableFooterView = UIView() 38 | self.tableView.es.addPullToRefresh { 39 | self.getEmployees() 40 | } 41 | self.tableView.es.startPullToRefresh() 42 | } 43 | 44 | func getEmployees() { 45 | viewModel.fetchEmployees { (employees, errorMessage) in 46 | if employees != nil { 47 | self.showTableView() 48 | } else if errorMessage != nil { 49 | self.showTableView() 50 | self.showAlert(title: "Error", message: errorMessage!) 51 | } 52 | } 53 | } 54 | 55 | func showLoader() { 56 | self.tableView.isHidden = true 57 | self.emptyView.isHidden = true 58 | self.activityIndicator.isHidden = false 59 | self.activityIndicator.startAnimating() 60 | } 61 | 62 | func showEmptyView() { 63 | self.tableView.isHidden = true 64 | self.emptyView.isHidden = false 65 | self.activityIndicator.isHidden = true 66 | } 67 | 68 | func showTableView() { 69 | DispatchQueue.main.async { 70 | self.tableView.es.stopPullToRefresh() 71 | if self.viewModel.employees.isEmpty { 72 | self.showEmptyView() 73 | } else { 74 | self.tableView.reloadData() 75 | self.tableView.isHidden = false 76 | self.emptyView.isHidden = true 77 | self.activityIndicator.isHidden = true 78 | } 79 | } 80 | } 81 | 82 | func moveToDetails(item: Employee) { 83 | DispatchQueue.main.async { 84 | let detailController = DetailController().initializeFromStoryboard() 85 | detailController.item = item 86 | self.navigationController?.pushViewController(detailController, animated: true) 87 | } 88 | } 89 | 90 | } 91 | 92 | extension ServiceLocatorController: UITableViewDelegate { 93 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 94 | let item = self.viewModel.employees[indexPath.row] 95 | self.moveToDetails(item: item) 96 | } 97 | } 98 | 99 | extension ServiceLocatorController: UITableViewDataSource { 100 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 101 | return self.viewModel.employees.count 102 | } 103 | 104 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 105 | let cell = tableView.dequeueReusableCell(withIdentifier: "EmployeeCell", for: indexPath) as! EmployeeCell 106 | cell.item = self.self.viewModel.employees[indexPath.row] 107 | return cell 108 | } 109 | 110 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 111 | return 96.0 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /MockingProject/Modules/DataBinding/View/Controllers/EventBusController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIRouter.swift 3 | // MockingProject 4 | // 5 | // Created by Paa Quesi Afful on 01/04/2020. 6 | // Copyright © 2020 MockingProject. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ESPullToRefresh 11 | import Alamofire 12 | 13 | class EventBusController: UIViewController { 14 | 15 | @IBOutlet weak var tableView: UITableView! 16 | @IBOutlet weak var emptyView: UIView! 17 | @IBOutlet weak var activityIndicator: UIActivityIndicatorView! 18 | 19 | lazy var viewModel: EventBusViewModel = { 20 | let viewModel = EventBusViewModel() 21 | return viewModel 22 | }() 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | showLoader() 27 | setupEventBusSubscriber() 28 | setupTableView() 29 | } 30 | 31 | func setupEventBusSubscriber() { 32 | _ = EventBus.onMainThread(self, name: "fetchEmployees") { result in 33 | if let event = result!.object as? EmployeesEvent { 34 | if event.employees != nil { 35 | self.showTableView() 36 | } else if let message = event.errorMessage { 37 | self.showAlert(title: "Error", message: message) 38 | } 39 | } 40 | } 41 | } 42 | 43 | func setupTableView() { 44 | self.tableView.register(UINib(nibName: "EmployeeCell", bundle: nil), forCellReuseIdentifier: "EmployeeCell") 45 | self.tableView.dataSource = self 46 | self.tableView.delegate = self 47 | self.tableView.tableFooterView = UIView() 48 | self.tableView.es.addPullToRefresh { 49 | self.getEmployees() 50 | } 51 | self.tableView.es.startPullToRefresh() 52 | } 53 | 54 | func getEmployees() { 55 | viewModel.fetchEmployees() 56 | } 57 | 58 | func showLoader() { 59 | self.tableView.isHidden = true 60 | self.emptyView.isHidden = true 61 | self.activityIndicator.isHidden = false 62 | self.activityIndicator.startAnimating() 63 | } 64 | 65 | func showEmptyView() { 66 | self.tableView.isHidden = true 67 | self.emptyView.isHidden = false 68 | self.activityIndicator.isHidden = true 69 | } 70 | 71 | func showTableView() { 72 | DispatchQueue.main.async { 73 | self.tableView.es.stopPullToRefresh() 74 | if self.viewModel.employees.isEmpty { 75 | self.showEmptyView() 76 | } else { 77 | self.tableView.reloadData() 78 | self.tableView.isHidden = false 79 | self.emptyView.isHidden = true 80 | self.activityIndicator.isHidden = true 81 | } 82 | } 83 | } 84 | 85 | func moveToDetails(item: Employee) { 86 | DispatchQueue.main.async { 87 | let detailController = DetailController().initializeFromStoryboard() 88 | detailController.item = item 89 | self.navigationController?.pushViewController(detailController, animated: true) 90 | } 91 | } 92 | 93 | } 94 | 95 | extension EventBusController: UITableViewDelegate { 96 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 97 | let item = self.viewModel.employees[indexPath.row] 98 | self.moveToDetails(item: item) 99 | } 100 | } 101 | 102 | extension EventBusController: UITableViewDataSource { 103 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 104 | return self.viewModel.employees.count 105 | } 106 | 107 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 108 | let cell = tableView.dequeueReusableCell(withIdentifier: "EmployeeCell", for: indexPath) as! EmployeeCell 109 | cell.item = self.self.viewModel.employees[indexPath.row] 110 | return cell 111 | } 112 | 113 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 114 | return 96.0 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /MockingProject/Modules/DataBinding/View/Controllers/RxSwiftController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIRouter.swift 3 | // MockingProject 4 | // 5 | // Created by Paa Quesi Afful on 01/04/2020. 6 | // Copyright © 2020 MockingProject. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ESPullToRefresh 11 | import Alamofire 12 | import RxSwift 13 | import RxCocoa 14 | 15 | class RxSwiftController: UIViewController { 16 | 17 | @IBOutlet weak var tableView: UITableView! 18 | @IBOutlet weak var emptyView: UIView! 19 | @IBOutlet weak var activityIndicator: UIActivityIndicatorView! 20 | let disposeBag = DisposeBag() 21 | 22 | lazy var viewModel: RxSwiftViewModel = { 23 | let viewModel = RxSwiftViewModel() 24 | return viewModel 25 | }() 26 | 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | showLoader() 30 | setupTableView() 31 | setupBindings() 32 | } 33 | 34 | func setupBindings() { 35 | viewModel.employees.drive(onNext: {[unowned self] (_) in 36 | self.showTableView() 37 | }).disposed(by: disposeBag) 38 | 39 | viewModel.errorMessage.drive(onNext: { (_message) in 40 | if let message = _message { 41 | self.showAlert(title: "Error", message: message) 42 | } 43 | }).disposed(by: disposeBag) 44 | } 45 | 46 | func setupTableView() { 47 | self.tableView.register(UINib(nibName: "EmployeeCell", bundle: nil), forCellReuseIdentifier: "EmployeeCell") 48 | self.tableView.dataSource = self 49 | self.tableView.delegate = self 50 | self.tableView.tableFooterView = UIView() 51 | 52 | self.tableView.es.addPullToRefresh { 53 | self.viewModel.fetchEmployees() 54 | } 55 | self.tableView.es.startPullToRefresh() 56 | } 57 | 58 | func showErrorMessage(_ message: String) { 59 | self.showTableView() 60 | self.showAlert(title: "Error", message: message) 61 | } 62 | 63 | func showLoader() { 64 | self.tableView.isHidden = true 65 | self.emptyView.isHidden = true 66 | self.activityIndicator.isHidden = false 67 | self.activityIndicator.startAnimating() 68 | } 69 | 70 | func showTableView() { 71 | DispatchQueue.main.async { 72 | self.tableView.es.stopPullToRefresh() 73 | if self.viewModel.numberOfEmployees == 0 { 74 | self.showEmptyView() 75 | } else { 76 | self.tableView.reloadData() 77 | self.tableView.isHidden = false 78 | self.emptyView.isHidden = true 79 | self.activityIndicator.isHidden = true 80 | } 81 | } 82 | } 83 | 84 | func showEmptyView() { 85 | self.tableView.isHidden = true 86 | self.emptyView.isHidden = false 87 | self.activityIndicator.isHidden = true 88 | } 89 | 90 | func moveToDetails(item: Employee) { 91 | DispatchQueue.main.async { 92 | let detailController = DetailController().initializeFromStoryboard() 93 | detailController.item = item 94 | self.navigationController?.pushViewController(detailController, animated: true) 95 | } 96 | } 97 | 98 | } 99 | 100 | extension RxSwiftController: UITableViewDelegate { 101 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 102 | if let item = viewModel.modelForIndex(at: indexPath.row) { 103 | self.moveToDetails(item: item) 104 | } 105 | } 106 | } 107 | 108 | extension RxSwiftController: UITableViewDataSource { 109 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 110 | return self.viewModel.numberOfEmployees 111 | } 112 | 113 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 114 | let cell = tableView.dequeueReusableCell(withIdentifier: "EmployeeCell", for: indexPath) as! EmployeeCell 115 | if let item = viewModel.modelForIndex(at: indexPath.row) { 116 | cell.item = item 117 | } 118 | return cell 119 | } 120 | 121 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 122 | return 96.0 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /MockingProject.xcodeproj/xcshareddata/xcschemes/MockingProject.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 39 | 40 | 41 | 42 | 44 | 50 | 51 | 52 | 53 | 54 | 64 | 66 | 72 | 73 | 74 | 75 | 81 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /MockingProjectTests/APIManagerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockingProjectTests.swift 3 | // MockingProjectTests 4 | // 5 | // Created by Fitzgerald Afful on 02/04/2020. 6 | // Copyright © 2020 Fitzgerald Afful. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Development 11 | @testable import Alamofire 12 | @testable import Mocker 13 | 14 | class APIManagerTests: XCTestCase { 15 | 16 | private var manager: APIManager! 17 | 18 | override func setUp() { 19 | let sessionManager: Session = { 20 | let configuration: URLSessionConfiguration = { 21 | let configuration = URLSessionConfiguration.default 22 | configuration.protocolClasses = [MockingURLProtocol.self] + (configuration.protocolClasses ?? []) 23 | return configuration 24 | }() 25 | return Session(configuration: configuration) 26 | }() 27 | manager = APIManager(manager: sessionManager) 28 | } 29 | 30 | 31 | func test_getEmployees() { 32 | 33 | let apiEndpoint = URL(string: APIRouter.getEmployees.path)! 34 | let requestExpectation = expectation(description: "Request should finish with Employees") 35 | let responseFile = "employees" 36 | guard let mockedData = dataFromTestBundleFile(fileName: responseFile, withExtension: "json") else { 37 | XCTFail("Error from JSON DeSerialization.jsonObject") 38 | return 39 | } 40 | guard let mockResponse = try? JSONDecoder().decode(EmployeesResponse.self, from: mockedData) else { 41 | XCTFail("Error from JSON DeSerialization.jsonObject") 42 | return 43 | } 44 | 45 | let mock = Mock(url: apiEndpoint, dataType: .json, statusCode: 200, data: [.get: mockedData]) 46 | mock.register() 47 | 48 | manager.getEmployees { (result) in 49 | XCTAssertEqual(result.result.success!, mockResponse) 50 | XCTAssertNil(result.error) 51 | requestExpectation.fulfill() 52 | } 53 | 54 | wait(for: [requestExpectation], timeout: 10.0) 55 | } 56 | 57 | func test_getSingleEmployee() { 58 | let employeeId = "24" 59 | let apiEndpoint = URL(string: APIRouter.getSingleEmployee(employeeId: employeeId).path)! 60 | let requestExpectation = expectation(description: "Request should finish with Employees") 61 | let responseFile = "employee24" 62 | guard let mockedData = dataFromTestBundleFile(fileName: responseFile, withExtension: "json") else { 63 | XCTFail("Error from JSON DeSerialization.jsonObject") 64 | return 65 | } 66 | guard let mockResponse = try? JSONDecoder().decode(EmployeeResponse.self, from: mockedData) else { 67 | XCTFail("Error from JSON DeSerialization.jsonObject") 68 | return 69 | } 70 | 71 | let mock = Mock(url: apiEndpoint, dataType: .json, statusCode: 200, data: [.get: mockedData]) 72 | mock.register() 73 | 74 | manager.getSingleEmployee(employeeId: employeeId) { (result) in 75 | XCTAssertEqual(result.result.success!, mockResponse) 76 | XCTAssertNil(result.error) 77 | requestExpectation.fulfill() 78 | } 79 | 80 | wait(for: [requestExpectation], timeout: 10.0) 81 | } 82 | 83 | 84 | func verifyAndConvertToDictionary(data: Data?) -> [String: Any]? { 85 | 86 | XCTAssertNotNil(data) 87 | guard let data = data else { return nil } 88 | 89 | do { 90 | let dataObject = try JSONSerialization.jsonObject(with: data, options: []) 91 | guard let dataDict = dataObject as? [String: Any] else { 92 | XCTFail("data object is not of type [String: Any]. dataObject=\(dataObject )") 93 | return nil 94 | } 95 | 96 | return dataDict 97 | } catch { 98 | XCTFail("Error from JSONSerialization.jsonObject; error=\(error)") 99 | return nil 100 | } 101 | } 102 | 103 | func dataFromTestBundleFile(fileName: String, withExtension fileExtension: String) -> Data? { 104 | 105 | let testBundle = Bundle(for: APIManagerTests.self) 106 | let resourceUrl = testBundle.url(forResource: fileName, withExtension: fileExtension)! 107 | do { 108 | let data = try Data(contentsOf: resourceUrl) 109 | return data 110 | } catch { 111 | XCTFail("Error reading data from resource file \(fileName).\(fileExtension)") 112 | return nil 113 | } 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /MockingProjectTests/Resources/employees.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": "success", 3 | "data": [ 4 | { 5 | "id": "1", 6 | "employee_name": "Tiger Nixon", 7 | "employee_salary": "320800", 8 | "employee_age": "61", 9 | "profile_image": "" 10 | }, 11 | { 12 | "id": "2", 13 | "employee_name": "Garrett Winters", 14 | "employee_salary": "170750", 15 | "employee_age": "63", 16 | "profile_image": "" 17 | }, 18 | { 19 | "id": "3", 20 | "employee_name": "Ashton Cox", 21 | "employee_salary": "86000", 22 | "employee_age": "66", 23 | "profile_image": "" 24 | }, 25 | { 26 | "id": "4", 27 | "employee_name": "Cedric Kelly", 28 | "employee_salary": "433060", 29 | "employee_age": "22", 30 | "profile_image": "" 31 | }, 32 | { 33 | "id": "5", 34 | "employee_name": "Airi Satou", 35 | "employee_salary": "162700", 36 | "employee_age": "33", 37 | "profile_image": "" 38 | }, 39 | { 40 | "id": "6", 41 | "employee_name": "Brielle Williamson", 42 | "employee_salary": "372000", 43 | "employee_age": "61", 44 | "profile_image": "" 45 | }, 46 | { 47 | "id": "7", 48 | "employee_name": "Herrod Chandler", 49 | "employee_salary": "137500", 50 | "employee_age": "59", 51 | "profile_image": "" 52 | }, 53 | { 54 | "id": "8", 55 | "employee_name": "Rhona Davidson", 56 | "employee_salary": "327900", 57 | "employee_age": "55", 58 | "profile_image": "" 59 | }, 60 | { 61 | "id": "9", 62 | "employee_name": "Colleen Hurst", 63 | "employee_salary": "205500", 64 | "employee_age": "39", 65 | "profile_image": "" 66 | }, 67 | { 68 | "id": "10", 69 | "employee_name": "Sonya Frost", 70 | "employee_salary": "103600", 71 | "employee_age": "23", 72 | "profile_image": "" 73 | }, 74 | { 75 | "id": "11", 76 | "employee_name": "Jena Gaines", 77 | "employee_salary": "90560", 78 | "employee_age": "30", 79 | "profile_image": "" 80 | }, 81 | { 82 | "id": "12", 83 | "employee_name": "Quinn Flynn", 84 | "employee_salary": "342000", 85 | "employee_age": "22", 86 | "profile_image": "" 87 | }, 88 | { 89 | "id": "13", 90 | "employee_name": "Charde Marshall", 91 | "employee_salary": "470600", 92 | "employee_age": "36", 93 | "profile_image": "" 94 | }, 95 | { 96 | "id": "14", 97 | "employee_name": "Haley Kennedy", 98 | "employee_salary": "313500", 99 | "employee_age": "43", 100 | "profile_image": "" 101 | }, 102 | { 103 | "id": "15", 104 | "employee_name": "Tatyana Fitzpatrick", 105 | "employee_salary": "385750", 106 | "employee_age": "19", 107 | "profile_image": "" 108 | }, 109 | { 110 | "id": "16", 111 | "employee_name": "Michael Silva", 112 | "employee_salary": "198500", 113 | "employee_age": "66", 114 | "profile_image": "" 115 | }, 116 | { 117 | "id": "17", 118 | "employee_name": "Paul Byrd", 119 | "employee_salary": "725000", 120 | "employee_age": "64", 121 | "profile_image": "" 122 | }, 123 | { 124 | "id": "18", 125 | "employee_name": "Gloria Little", 126 | "employee_salary": "237500", 127 | "employee_age": "59", 128 | "profile_image": "" 129 | }, 130 | { 131 | "id": "19", 132 | "employee_name": "Bradley Greer", 133 | "employee_salary": "132000", 134 | "employee_age": "41", 135 | "profile_image": "" 136 | }, 137 | { 138 | "id": "20", 139 | "employee_name": "Dai Rios", 140 | "employee_salary": "217500", 141 | "employee_age": "35", 142 | "profile_image": "" 143 | }, 144 | { 145 | "id": "21", 146 | "employee_name": "Jenette Caldwell", 147 | "employee_salary": "345000", 148 | "employee_age": "30", 149 | "profile_image": "" 150 | }, 151 | { 152 | "id": "22", 153 | "employee_name": "Yuri Berry", 154 | "employee_salary": "675000", 155 | "employee_age": "40", 156 | "profile_image": "" 157 | }, 158 | { 159 | "id": "23", 160 | "employee_name": "Caesar Vance", 161 | "employee_salary": "106450", 162 | "employee_age": "21", 163 | "profile_image": "" 164 | }, 165 | { 166 | "id": "24", 167 | "employee_name": "Doris Wilder", 168 | "employee_salary": "85600", 169 | "employee_age": "23", 170 | "profile_image": "" 171 | } 172 | ] 173 | } -------------------------------------------------------------------------------- /MockingProject/Modules/DataBinding/View/Cells/EmployeeCell.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 | 35 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /MockingProject/Helpers/EventBus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventBus.swift 3 | // Development 4 | // 5 | // Created by Fitzgerald Afful on 18/04/2020. 6 | // Copyright © 2020 Fitzgerald Afful. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | open class EventBus { 12 | 13 | struct Static { 14 | static let instance = EventBus() 15 | static let queue = DispatchQueue(label: "EventBus", attributes: []) 16 | } 17 | 18 | struct NamedObserver { 19 | let observer: NSObjectProtocol 20 | let name: String 21 | } 22 | 23 | var cache = [UInt: [NamedObserver]]() 24 | 25 | //////////////////////////////////// 26 | // Publish 27 | //////////////////////////////////// 28 | 29 | open class func post(_ name: String) { 30 | NotificationCenter.default.post(name: Notification.Name(rawValue: name), object: nil) 31 | } 32 | 33 | open class func post(_ name: String, sender: AnyObject?) { 34 | NotificationCenter.default.post(name: Notification.Name(rawValue: name), object: sender) 35 | } 36 | 37 | open class func post(_ name: String, sender: NSObject?) { 38 | NotificationCenter.default.post(name: Notification.Name(rawValue: name), object: sender) 39 | } 40 | 41 | open class func post(_ name: String, userInfo: [AnyHashable: Any]?) { 42 | NotificationCenter.default.post(name: Notification.Name(rawValue: name), object: nil, userInfo: userInfo) 43 | } 44 | 45 | open class func post(_ name: String, sender: AnyObject?, userInfo: [AnyHashable: Any]?) { 46 | NotificationCenter.default.post(name: Notification.Name(rawValue: name), object: sender, userInfo: userInfo) 47 | } 48 | 49 | open class func postToMainThread(_ name: String) { 50 | DispatchQueue.main.async(execute: { 51 | NotificationCenter.default.post(name: Notification.Name(rawValue: name), object: nil) 52 | }) 53 | } 54 | 55 | open class func postToMainThread(_ name: String, sender: AnyObject?) { 56 | DispatchQueue.main.async(execute: { 57 | NotificationCenter.default.post(name: Notification.Name(rawValue: name), object: sender) 58 | }) 59 | } 60 | 61 | open class func postToMainThread(_ name: String, sender: NSObject?) { 62 | DispatchQueue.main.async(execute: { 63 | NotificationCenter.default.post(name: Notification.Name(rawValue: name), object: sender) 64 | }) 65 | } 66 | 67 | open class func postToMainThread(_ name: String, userInfo: [AnyHashable: Any]?) { 68 | DispatchQueue.main.async(execute: { 69 | NotificationCenter.default.post(name: Notification.Name(rawValue: name), object: nil, userInfo: userInfo) 70 | }) 71 | } 72 | 73 | open class func postToMainThread(_ name: String, sender: AnyObject?, userInfo: [AnyHashable: Any]?) { 74 | DispatchQueue.main.async(execute: { 75 | NotificationCenter.default.post(name: Notification.Name(rawValue: name), object: sender, userInfo: userInfo) 76 | }) 77 | } 78 | 79 | //////////////////////////////////// 80 | // Subscribe 81 | //////////////////////////////////// 82 | 83 | open class func on(_ target: AnyObject, name: String, sender: AnyObject?, queue: OperationQueue?, handler: @escaping ((Notification?) -> Void)) -> NSObjectProtocol { 84 | let id = UInt(bitPattern: ObjectIdentifier(target)) 85 | let observer = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: name), object: sender, queue: queue, using: handler) 86 | let namedObserver = NamedObserver(observer: observer, name: name) 87 | 88 | Static.queue.sync { 89 | if let namedObservers = Static.instance.cache[id] { 90 | Static.instance.cache[id] = namedObservers + [namedObserver] 91 | } else { 92 | Static.instance.cache[id] = [namedObserver] 93 | } 94 | } 95 | 96 | return observer 97 | } 98 | 99 | open class func onMainThread(_ target: AnyObject, name: String, handler: @escaping ((Notification?) -> Void)) -> NSObjectProtocol { 100 | return EventBus.on(target, name: name, sender: nil, queue: OperationQueue.main, handler: handler) 101 | } 102 | 103 | open class func onMainThread(_ target: AnyObject, name: String, sender: AnyObject?, handler: @escaping ((Notification?) -> Void)) -> NSObjectProtocol { 104 | return EventBus.on(target, name: name, sender: sender, queue: OperationQueue.main, handler: handler) 105 | } 106 | 107 | open class func onBackgroundThread(_ target: AnyObject, name: String, handler: @escaping ((Notification?) -> Void)) -> NSObjectProtocol { 108 | return EventBus.on(target, name: name, sender: nil, queue: OperationQueue(), handler: handler) 109 | } 110 | 111 | open class func onBackgroundThread(_ target: AnyObject, name: String, sender: AnyObject?, handler: @escaping ((Notification?) -> Void)) -> NSObjectProtocol { 112 | return EventBus.on(target, name: name, sender: sender, queue: OperationQueue(), handler: handler) 113 | } 114 | 115 | //////////////////////////////////// 116 | // Unregister 117 | //////////////////////////////////// 118 | 119 | open class func unregister(_ target: AnyObject) { 120 | let id = UInt(bitPattern: ObjectIdentifier(target)) 121 | let center = NotificationCenter.default 122 | 123 | Static.queue.sync { 124 | if let namedObservers = Static.instance.cache.removeValue(forKey: id) { 125 | for namedObserver in namedObservers { 126 | center.removeObserver(namedObserver.observer) 127 | } 128 | } 129 | } 130 | } 131 | 132 | open class func unregister(_ target: AnyObject, name: String) { 133 | let id = UInt(bitPattern: ObjectIdentifier(target)) 134 | let center = NotificationCenter.default 135 | 136 | Static.queue.sync { 137 | if let namedObservers = Static.instance.cache[id] { 138 | Static.instance.cache[id] = namedObservers.filter({ (namedObserver: NamedObserver) -> Bool in 139 | if namedObserver.name == name { 140 | center.removeObserver(namedObserver.observer) 141 | return false 142 | } else { 143 | return true 144 | } 145 | }) 146 | } 147 | } 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /MockingProject/Modules/DependencyInjection/README.md: -------------------------------------------------------------------------------- 1 | # Dependency Injection: Introduction 2 | 3 | ## Definition 4 | 5 | Dependency injection is a technique whereby one object (or static method) supplies the dependencies of another object. A dependency is an object that can be used (a service). By [Bhavya Karia](https://www.freecodecamp.org/news/a-quick-intro-to-dependency-injection-what-it-is-and-when-to-use-it-7578c84fa88f/). There are generally 4 patterns of Dependency Injection. Its also an attempt at implementing the D in SOLID principles which says: High level modules should not depend on low level modules. Both should depend on abstractions. B, Abstractions should not depend upon details. Details should depend on abstractions. 6 | 7 | ## Technique 1: Initializer/Constructor DI 8 | A lot of people use this method without even acknowledging it as Dependency Injection. Here, the dependency is passed in the class' initializer. Below, we pass our Dependency object (dependency) to our class Initializer class through it's init. Then we can use it in other places in the class InitializerClass. This technique becomes complicated when our InitializerClass has a lot of dependencies. Our init method will grow so big. 9 | 10 | ``` 11 | class InitializerClass { 12 | 13 | private var dependency: Dependency 14 | 15 | init(_ dependency: Dependency) { 16 | self.dependency = dependency 17 | } 18 | 19 | func useDependency() { 20 | self.dependency.useMyMethod() 21 | } 22 | } 23 | ``` 24 | 25 | ## Technique 2: Property DI 26 | 27 | In classes like View Controllers, we cannot use Initializer DI mainly because we mostly do not care about initializers for them (not that they do not exist). Instead, we pass dependencies as properties (eg. how we pass data to particular View Controllers generally). In the example below, we successfully pass Dependency() to InitializerController. 28 | 29 | ``` 30 | class InitializerController: UIViewController { 31 | var dependency: Dependency! 32 | } 33 | 34 | //In another controller or class, 35 | let initController = InitializerController() 36 | initController.dependency = Dependency() 37 | ``` 38 | 39 | 40 | ## Technique 3: Factory DI: 41 | The Factory DI pattern combines both patterns we've used above into something different but elegant. Factories enable you to fully decouple the usage and creation of an object and therefore helps in refactoring. We create our Factory as a protocol (so we can implement it in different ways even during testing). BaseDIFactory then implements it and returns our object. If we want to create our controller, we instantiate our BaseDIFactory and ask it to generate the controller for us. 42 | 43 | ``` 44 | protocol DIFactory { 45 | func generateDependency() -> Dependency 46 | func generateInitializerController() -> InitializerController 47 | } 48 | 49 | class BaseDIFactory: DIFactory { 50 | 51 | func generateDependency() -> Dependency { 52 | return Dependency() 53 | } 54 | 55 | func generateInitializerController() -> InitializerController { 56 | let initController = InitializerController() 57 | initController.dependency = generateDependency() 58 | return initController 59 | } 60 | } 61 | 62 | let factory = BaseDIFactory() 63 | let controller = factory.generateInitializerController() 64 | // Our controller already comes with it's dependency 65 | ``` 66 | 67 | ## Technique 4: Service Locator DI 68 | The Service Locator serves as a registry of dependencies for a given dependencies and it consists of 2 basic parts. The Registry and the Locator/Resolver. The registry registers our dependencies, then the locator / resolver helps us find our dependencies. Optionally, you can include a Container (A container keep your registrations at one particular place so your code isnt all over the place). The simplest form of the Service Locator pattern is below 69 | 70 | 71 | ``` 72 | protocol ServiceLocator { 73 | func resolve() -> T? 74 | } 75 | 76 | final class DIServiceLocator: ServiceLocator { 77 | 78 | static let shared = DIServiceLocator() 79 | 80 | private lazy var services: Dictionary = [:] 81 | private func typeName(some: Any) -> String { 82 | return (some is Any.Type) ? 83 | "\(some)" : "\(type(of: some))" 84 | } 85 | 86 | func register(_ service: T) { 87 | let key = typeName(some: T.self) 88 | services[key] = service 89 | } 90 | 91 | func resolve() -> T? { 92 | let key = typeName(some: T.self) 93 | return services[key] as? T 94 | } 95 | } 96 | ``` 97 | The above protocol and implementation contains the registrar (first method) and the resolver (second method). 98 | 99 | To register using our locator, 100 | ``` 101 | let locator = DIServiceLocator() 102 | let dependency = Dependency() 103 | locator.register(dependency) 104 | ``` 105 | 106 | To resolve our registered dependency, 107 | ``` 108 | let locator = DIServiceLocator() 109 | guard let dependency: Dependency = locator.resolve() else { return } 110 | ``` 111 | 112 | ## Libraries 113 | There are libraries around like [Resolver](https://github.com/hmlongco/Resolver), [Swinject](https://github.com/Swinject/Swinject) and [Cleanse](https://github.com/square/Cleanse) which create use cases based on the Service Locator pattern for much more complex situations so you can check them out. 114 | In the controllers in this module, I use Resolver and Swinject.. each with it's view model and Controller in it's simplest form so its easy to understand. 115 | 116 | ### To use Resolver.. 117 | 1. Register services (as shown in MockingProject/Helpers/DIHelpers/AppDelegate+Injection) 118 | ``` 119 | extension Resolver: ResolverRegistering { 120 | public static func registerAllServices() { 121 | registerAPIManager() 122 | registerViewModel() 123 | } 124 | } 125 | 126 | extension Resolver { 127 | public static func registerAPIManager() { 128 | register { APIManager() } 129 | } 130 | 131 | public static func registerViewModel() { 132 | register { ResolverViewModel(manager: self.resolve()) } 133 | } 134 | } 135 | ``` 136 | 137 | 2. Resolve ViewModel in Controller by... 138 | ``` 139 | class ResolverController: UIViewController, Resolving { 140 | private var viewModel: ResolverViewModel = Resolver.resolve() 141 | } 142 | ``` 143 | 144 | ### To use Swinject. 145 | 1. Create a container which contain all your registrations 146 | ``` 147 | class SwinjectContainer { 148 | 149 | static let sharedContainer = SwinjectContainer() 150 | let container = Container() 151 | 152 | private init() { 153 | setupDefaultContainers() 154 | } 155 | 156 | private func setupDefaultContainers() { 157 | container.register(APIManager.self, factory: { _ in APIManager() }) 158 | 159 | container.register(HomeViewModelProtocol.self, factory: { resolver in 160 | return SwinjectViewModel(manager: resolver.resolve(APIManager.self)!) 161 | }) 162 | } 163 | } 164 | 165 | extension SwinjectStoryboard { 166 | @objc class func setup() { 167 | let mainContainer = SwinjectContainer.sharedContainer.container 168 | 169 | defaultContainer.storyboardInitCompleted(SwinjectController.self) { _, controller in 170 | controller.viewModel = mainContainer.resolve(HomeViewModelProtocol.self) 171 | } 172 | } 173 | } 174 | 175 | ``` 176 | 177 | 2. Call Container init method in AppDelegate's didFinishLaunchingWithOptions method 178 | ``` 179 | _ = SwinjectContainer.sharedContainer 180 | ``` 181 | 182 | 3. Resolving will occur automatically now 183 | 184 | ## Extras 185 | For extensive reading, check out 186 | 187 | - [Modern Dependency Injection with Swift](https://medium.com/better-programming/modern-dependency-injection-in-swift-952286b308be) 188 | - [iOS Dependency Injection Using Swinject](https://medium.com/flawless-app-stories/ios-dependency-injection-using-swinject-9c4ceff99e41) 189 | - [Advanced Dependency Injection on iOS with Swift 5](https://www.vadimbulavin.com/dependency-injection-in-swift/) 190 | - [Dependency Injection Strategies in Swift](https://quickbirdstudios.com/blog/swift-dependency-injection-service-locators/) 191 | - [Swift 5.1 Takes Dependency Injection to the Next Level](https://medium.com/better-programming/taking-swift-dependency-injection-to-the-next-level-b71114c6a9c6) 192 | -------------------------------------------------------------------------------- /MockingProject/Modules/DataBinding/README.md: -------------------------------------------------------------------------------- 1 | # Data Binding: Introduction 2 | 3 | Below, I explain 4 techniques you can use to implement Data Binding in your MVVM project. All techniques in the post below have their respective views and viewModels in this module of the project. You can also read the whole post on [Medium](https://medium.com/flawless-app-stories/data-binding-in-mvvm-on-ios-714eb15e3913). 4 | 5 | ## Definitions 6 | 7 | Data Binding is simply the process that establishes a connection between the app UI (View Controller) and the data (Not Model, But View Model) it displays. There are different ways of data binding so we’ll look at a couple. Please note, that Data Binding does not apply only to MVVM but to other patterns too. 8 | 9 | ## Technique 1: Observables 10 | This appears to be the easiest and most commonly used. Libraries like [Bond](https://github.com/DeclarativeHub/Bond) allow you to bind easily but we’re going to create our own Helper class called Observable. It’s initialized with the value we want to observe (or pass around), and we have a function bind that does the binding and gets us our value. listener is our closure called when the value is set. 11 | 12 | ``` 13 | class Observable { 14 | 15 | var value: T { 16 | didSet { 17 | listener?(value) 18 | } 19 | } 20 | 21 | private var listener: ((T) -> Void)? 22 | 23 | init(_ value: T) { 24 | self.value = value 25 | } 26 | 27 | func bind(_ closure: @escaping (T) -> Void) { 28 | closure(value) 29 | listener = closure 30 | } 31 | } 32 | ``` 33 | 34 | Let’s move to our ViewModel. Before we focus on our numbered lines, all we’re trying to do here is get our ViewModel to implement the Protocol we created for it, then fetch data (Employees) from our APIManager class. 35 | 36 | ``` 37 | protocol ObservableViewModelProtocol { 38 | func fetchEmployees() 39 | func setError(_ message: String) 40 | var employees: Observable<[Employee]> { get set } //1 41 | var errorMessage: Observable { get set } 42 | var error: Observable { get set } 43 | } 44 | 45 | 46 | class ObservableViewModel: ObservableViewModelProtocol { 47 | var errorMessage: Observable = Observable(nil) 48 | var error: Observable = Observable(false) 49 | 50 | var apiManager: APIManager? 51 | var employees: Observable<[Employee]> = Observable([]) //2 52 | init(manager: APIManager = APIManager()) { 53 | self.apiManager = manager 54 | } 55 | 56 | func setAPIManager(manager: APIManager) { 57 | self.apiManager = manager 58 | } 59 | 60 | func fetchEmployees() { 61 | self.apiManager!.getEmployees { (result: DataResponse) in 62 | switch result.result { 63 | case .success(let response): 64 | if response.status == "success" { 65 | self.employees = Observable(response.data) //3 66 | return 67 | } 68 | self.setError(BaseNetworkManager().getErrorMessage(response: result)) 69 | case .failure: 70 | self.setError(BaseNetworkManager().getErrorMessage(response: result)) 71 | } 72 | } 73 | } 74 | 75 | func setError(_ message: String) { 76 | self.errorMessage = Observable(message) 77 | self.error = Observable(true) 78 | } 79 | 80 | } 81 | ``` 82 | 83 | //1 is how we declare our Observable on our Employees array in our protocol. //2 is how we implement //1 in our ViewModel. And finally //3 is we set/add data to our Observable. We can now go ahead to bind it to viewDidLoad in our View Controller. 84 | 85 | ``` 86 | /********* Binding to array in viewDidLoad */ 87 | viewModel.employees.bind { (_) in 88 | self.showTableView() 89 | } 90 | /********************************/ 91 | ``` 92 | 93 | Tadaa. Anytime employees are added or set, self.showTableView will get called in our Controller. 94 | 95 | ## Technique 2: Event Bus / Notification Center 96 | 97 | EventBuses are more popular on Android. On iOS, they are well-structured wrappers for the NotificationCenter. You can go for César Ferreira’s [SwiftEventBus](https://github.com/cesarferreira/SwiftEventBus) or [this refactored version](https://gist.github.com/FitzAfful/2af698e9f4ce4f2b2a5fac5d69688080) (which I use). 98 | 99 | 1. Create an Event that will be pushed by the EventBus to all subscribers. It usually contains the stuff we want to pass around. So EmployeesEvent contains our employees, error Boolean and errorMessage String. 100 | 101 | ``` 102 | class EmployeesEvent: NSObject { 103 | var error: Bool 104 | var errorMessage: String? 105 | var employees: [Employee]? 106 | 107 | init(error: Bool, errorMessage: String? = nil, employees: [Employee]? = nil) { 108 | self.error = error 109 | self.errorMessage = errorMessage 110 | self.employees = employees 111 | } 112 | } 113 | ``` 114 | 115 | 2. Publish (Or Post) the Event from the ViewModel using the EventBus. 116 | 117 | ``` 118 | func callEvent() { 119 | //Post Event (Publish Event) 120 | EventBus.post("fetchEmployees", sender: EmployeesEvent(error: error, errorMessage: errorMessage, employees: employees)) 121 | } 122 | 123 | ``` 124 | 125 | 3. Subscribe to Event from View Controller. So setupEventBusSubscriber is called from our viewDidLoad. 126 | 127 | ``` 128 | func setupEventBusSubscriber() { 129 | _ = EventBus.onMainThread(self, name: "fetchEmployees") { result in 130 | if let event = result!.object as? EmployeesEvent { 131 | if event.employees != nil { 132 | self.showTableView() 133 | } else if let message = event.errorMessage { 134 | self.showAlert(title: "Error", message: message) 135 | } 136 | } 137 | } 138 | } 139 | ``` 140 | Contents of our EventBus’ onMainThread implementation run every time callEvent is called in our ViewModel. 141 | 142 | ## Technique 3: FRP Technique (ReactiveCocoa / RxSwift): 143 | The Functional / Reactive Programming approach. You can either go with [ReactiveCocoa](https://github.com/ReactiveCocoa/ReactiveCocoa) or [RxSwift](https://github.com/ReactiveX/RxSwift). RayWenderlich does a good analysis of both [here](https://www.raywenderlich.com/126522/reactivecocoa-vs-rxswift), you might want to check out. But for the sake of this project, I’ll use RxSwift. 144 | 145 | Similar to our Observable technique, our ViewModel looks like this: 146 | 147 | ``` 148 | import Foundation 149 | import Alamofire 150 | import RxSwift 151 | import RxCocoa 152 | 153 | class RxSwiftViewModel { 154 | 155 | private let disposeBag = DisposeBag() 156 | private let _employees = BehaviorRelay<[Employee]>(value: []) 157 | private let _error = BehaviorRelay(value: false) 158 | private let _errorMessage = BehaviorRelay(value: nil) 159 | 160 | var employees: Driver<[Employee]> { 161 | return _employees.asDriver() 162 | } 163 | 164 | var hasError: Bool { 165 | return _error.value 166 | } 167 | 168 | var errorMessage: Driver { 169 | return _errorMessage.asDriver() 170 | } 171 | 172 | var numberOfEmployees: Int { 173 | return _employees.value.count 174 | } 175 | 176 | var apiManager: APIManager? 177 | 178 | init(manager: APIManager = APIManager()) { 179 | self.apiManager = manager 180 | } 181 | 182 | func setAPIManager(manager: APIManager) { 183 | self.apiManager = manager 184 | } 185 | 186 | func fetchEmployees() { 187 | self.apiManager!.getEmployees { (result: DataResponse) in 188 | switch result.result { 189 | case .success(let response): 190 | if response.status == "success" { 191 | self._error.accept(false) 192 | self._errorMessage.accept(nil) 193 | self._employees.accept(response.data) 194 | return 195 | } 196 | self.setError(BaseNetworkManager().getErrorMessage(response: result)) 197 | case .failure: 198 | self.setError(BaseNetworkManager().getErrorMessage(response: result)) 199 | } 200 | } 201 | } 202 | 203 | func setError(_ message: String) { 204 | self._error.accept(true) 205 | self._errorMessage.accept(message) 206 | } 207 | 208 | func modelForIndex(at index: Int) -> Employee? { 209 | guard index < _employees.value.count else { 210 | return nil 211 | } 212 | return _employees.value[index] 213 | } 214 | } 215 | ``` 216 | 217 | The employeesproperty like the error and errorMessage variables are computed variables that return Driver (An Observable that our controls in our Views will bind to) from each of their respective private properties. (Dont forget to import both RxSwift and RxCocoa — You can add their dependencies using Cocoapods, Carthage or Swift Package Manager). 218 | 219 | And Controller: 220 | 221 | ``` 222 | import RxSwift 223 | import RxCocoa 224 | 225 | class RxSwiftController: UIViewController { 226 | 227 | @IBOutlet weak var tableView: UITableView! 228 | @IBOutlet weak var emptyView: UIView! 229 | @IBOutlet weak var activityIndicator: UIActivityIndicatorView! 230 | let disposeBag = DisposeBag() 231 | 232 | lazy var viewModel: RxSwiftViewModel = { 233 | let viewModel = RxSwiftViewModel() 234 | return viewModel 235 | }() 236 | 237 | override func viewDidLoad() { 238 | super.viewDidLoad() 239 | showLoader() 240 | setupTableView() 241 | setupBindings() 242 | } 243 | 244 | func setupBindings() { 245 | viewModel.employees.drive(onNext: {[unowned self] (_) in 246 | self.showTableView() 247 | }).disposed(by: disposeBag) 248 | 249 | viewModel.errorMessage.drive(onNext: { (_message) in 250 | if let message = _message { 251 | self.showAlert(title: "Error", message: message) 252 | } 253 | }).disposed(by: disposeBag) 254 | } 255 | 256 | //... other delegate methods go here 257 | } 258 | ``` 259 | 260 | In the Controller, we have our DisposeBag, a RxSwift object that aids in releasing any references they may have to any observables they’re observing. setupBindings method is to observe the employees property from the View Model. Whenever employees is updated, showTableView method is called to reload the list. 261 | 262 | 263 | 264 | 265 | ## Technique 4: Combine 266 | The Combine framework (added in Swift 5.1) provides a unified publish-and-subscribe API for channeling and processing asynchronous signals. 267 | 268 | 1. We make a publisher (in our ViewModel): To do that, we import Combine and let our ViewModel inherit from Combine’s ObservableOject. Our employees array which we want to observe will then be wrapped with a @Published property wrapper. This publisher emits the current value whenever the property (employees) changes. 269 | 270 | ``` 271 | import Foundation 272 | import Alamofire 273 | import Combine 274 | 275 | class CombineViewModel: ObservableObject { 276 | 277 | var apiManager: APIManager? 278 | @Published var employees: [Employee] = [] //1 279 | init(manager: APIManager = APIManager()) { 280 | self.apiManager = manager 281 | } 282 | 283 | func setAPIManager(manager: APIManager) { 284 | self.apiManager = manager 285 | } 286 | 287 | func fetchEmployees() { 288 | self.apiManager!.getEmployees { (result: DataResponse) in 289 | switch result.result { 290 | case .success(let response): 291 | if response.status == "success" { 292 | self.employees = response.data 293 | } 294 | case .failure: 295 | print("Failure") 296 | } 297 | } 298 | } 299 | 300 | } 301 | ``` 302 | 303 | 2. Then, attach a subscriber to the publisher (in our View Controller): In bindViewModel, we subscribe to $employees using one of Combine’s default subscriber keywords — sink, and it allows us to update our view only when specific published properties change. 304 | ``` 305 | import UIKit 306 | import Combine 307 | 308 | class CombineController: UIViewController { 309 | 310 | @IBOutlet weak var tableView: UITableView! 311 | @IBOutlet weak var emptyView: UIView! 312 | @IBOutlet weak var activityIndicator: UIActivityIndicatorView! 313 | 314 | lazy var viewModel: CombineViewModel = { 315 | let viewModel = CombineViewModel() 316 | return viewModel 317 | }() 318 | 319 | private var cancellables: Set = [] 320 | 321 | override func viewDidLoad() { 322 | super.viewDidLoad() 323 | showLoader() 324 | setupTableView() 325 | bindViewModel() 326 | } 327 | 328 | private func bindViewModel() { 329 | viewModel.$employees.sink { [weak self] _ in 330 | self?.showTableView() 331 | }.store(in: &cancellables) 332 | } 333 | 334 | //... Other delegate methods 335 | 336 | } 337 | ``` 338 | 339 | And We store the subscriber in an instance property so that it is retained (and so that it will be released automatically at the latest when the surrounding instance goes out of existence) or canceled by ourselves. 340 | 341 | - [Full Post on Medium](https://medium.com/flawless-app-stories/data-binding-in-mvvm-on-ios-714eb15e3913) 342 | -------------------------------------------------------------------------------- /MockingProject/SupportingFiles/Base.lproj/Main.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 | 27 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | --------------------------------------------------------------------------------