├── .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 | [](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 | 
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 |
--------------------------------------------------------------------------------