From 28bbade08fe8dcf75d93ad27f83acc9f9815a80c Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 21 Jan 2025 11:15:41 -0600 Subject: [PATCH 1/4] fixed bug Signed-off-by: Matt Bruce --- EmployeeDirectory/Views/EmployeeTableViewCell.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/EmployeeDirectory/Views/EmployeeTableViewCell.swift b/EmployeeDirectory/Views/EmployeeTableViewCell.swift index af15ad8..722cfd8 100644 --- a/EmployeeDirectory/Views/EmployeeTableViewCell.swift +++ b/EmployeeDirectory/Views/EmployeeTableViewCell.swift @@ -128,6 +128,7 @@ public class EmployeeTableViewCell: UITableViewCell { // Bind the image to the photoImageView smallPhotoSubscriber = viewModel.$smallPhoto + .compactMap { $0 } .receive(on: DispatchQueue.main) .sink { [weak self] image in self?.photoImageView.image = image From 52d02ea3a1fff9758e6c9685335c913e50ccf689 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 21 Jan 2025 13:59:17 -0600 Subject: [PATCH 2/4] now using diffable datasource Signed-off-by: Matt Bruce # Conflicts: # EmployeeDirectory/ViewControllers/EmployeesViewController.swift Signed-off-by: Matt Bruce --- EmployeeDirectory/Models/Employee.swift | 2 +- .../EmployeesViewController.swift | 48 ++++++++++--------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/EmployeeDirectory/Models/Employee.swift b/EmployeeDirectory/Models/Employee.swift index 08e4b82..c55b68e 100644 --- a/EmployeeDirectory/Models/Employee.swift +++ b/EmployeeDirectory/Models/Employee.swift @@ -27,7 +27,7 @@ public enum EmployeeType: String, Codable, CustomStringConvertible { /// Employee Object /// JSON Object defintion /// - https://square.github.io/microsite/mobile-interview-project/ -public struct Employee: Codable { +public struct Employee: Hashable, Codable { /// The unique identifier for the employee. Represented as a UUID. public let uuid: UUID diff --git a/EmployeeDirectory/ViewControllers/EmployeesViewController.swift b/EmployeeDirectory/ViewControllers/EmployeesViewController.swift index cfb192e..b732bcb 100644 --- a/EmployeeDirectory/ViewControllers/EmployeesViewController.swift +++ b/EmployeeDirectory/ViewControllers/EmployeesViewController.swift @@ -31,24 +31,46 @@ class EmployeesViewController: UIViewController { /// Will show specific state of the viewModel private var footerView: TableFooterView? + private var dataSource: UITableViewDiffableDataSource! + // MARK: - Public Methods public override func viewDidLoad() { super.viewDidLoad() setupUI() + setupDataSource() bindViewModel() viewModel.fetchEmployees() } // MARK: - Private Methods - + + + /// Setup TableView dataSource + private func setupDataSource() { + dataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, employee in + guard let cell = tableView.dequeueReusableCell(withIdentifier: EmployeeTableViewCell.identifier,for: indexPath) as? EmployeeTableViewCell else { + return UITableViewCell() + } + cell.configure(with: EmployeeCellViewModel(employee: employee)) + return cell + } + } + + /// Snapshot Handling + private func applySnapshot(employees: [Employee]) { + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([0]) + snapshot.appendItems(employees, toSection: 0) + dataSource.apply(snapshot, animatingDifferences: true) + } + /// Setup the UI by adding the views to the main view private func setupUI() { view.backgroundColor = .white // Configure TableView tableView.register(EmployeeTableViewCell.self, forCellReuseIdentifier: EmployeeTableViewCell.identifier) - tableView.dataSource = self view.addSubview(tableView) tableView.frame = view.bounds @@ -70,10 +92,8 @@ class EmployeesViewController: UIViewController { private func bindViewModel() { viewModel.$employees .receive(on: RunLoop.main) - .sink { [weak self] _ in - self?.updateFooter() - self?.tableView.reloadData() - self?.tableView.refreshControl?.endRefreshing() + .sink { [weak self] employees in + self?.applySnapshot(employees: employees) } .store(in: &cancellables) @@ -139,19 +159,3 @@ extension EmployeesViewController { viewModel.changeMode(to: selectedMode) } } - -/// Mark: - UITableViewDataSource -extension EmployeesViewController: UITableViewDataSource { - public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return viewModel.employees.count - } - - public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: EmployeeTableViewCell.identifier, for: indexPath) as? EmployeeTableViewCell else { - return UITableViewCell() - } - let employee = viewModel.employees[indexPath.row] - cell.configure(with: EmployeeCellViewModel(employee: employee)) - return cell - } -} From 0f4cd465b1c25ad81f4e7a1f65915677bf142467 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 21 Jan 2025 14:02:32 -0600 Subject: [PATCH 3/4] fixed a few bugs for refreshControl and footerView Signed-off-by: Matt Bruce --- EmployeeDirectory/ViewControllers/EmployeesViewController.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/EmployeeDirectory/ViewControllers/EmployeesViewController.swift b/EmployeeDirectory/ViewControllers/EmployeesViewController.swift index b732bcb..76d130e 100644 --- a/EmployeeDirectory/ViewControllers/EmployeesViewController.swift +++ b/EmployeeDirectory/ViewControllers/EmployeesViewController.swift @@ -104,6 +104,8 @@ class EmployeesViewController: UIViewController { self?.activityIndicator.startAnimating() } else { self?.activityIndicator.stopAnimating() + self?.tableView.refreshControl?.endRefreshing() + self?.updateFooter() } } .store(in: &cancellables) From 0f9515b90b15d4df0f9000317f5b640d42572d9a Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 21 Jan 2025 14:12:07 -0600 Subject: [PATCH 4/4] refactored out passing service mode Signed-off-by: Matt Bruce --- .../Protocols/EmployeeServiceProtocol.swift | 3 +- .../Services/EmployeeService.swift | 55 ++++++++++++++++++- .../EmployeesViewController.swift | 2 +- .../ViewModels/EmployeesViewModel.swift | 8 +-- 4 files changed, 58 insertions(+), 10 deletions(-) diff --git a/EmployeeDirectory/Protocols/EmployeeServiceProtocol.swift b/EmployeeDirectory/Protocols/EmployeeServiceProtocol.swift index 809baf2..3737c61 100644 --- a/EmployeeDirectory/Protocols/EmployeeServiceProtocol.swift +++ b/EmployeeDirectory/Protocols/EmployeeServiceProtocol.swift @@ -10,7 +10,6 @@ public protocol EmployeeServiceProtocol { /// This will get a list of all employees - /// - Parameter serviceMode: Mode in which to hit. /// - Returns: An Employees struct - func getEmployees(_ serviceMode: EmployeeServiceMode) async throws -> Employees + func getEmployees() async throws -> Employees } diff --git a/EmployeeDirectory/Services/EmployeeService.swift b/EmployeeDirectory/Services/EmployeeService.swift index 3104502..27b9780 100644 --- a/EmployeeDirectory/Services/EmployeeService.swift +++ b/EmployeeDirectory/Services/EmployeeService.swift @@ -7,11 +7,22 @@ import Foundation /// These are the testing URL Endpoints for different states -public enum EmployeeServiceMode: String, CaseIterable { +internal enum EmployeeServiceMode: String, CaseIterable { case production case malformed case empty + public var service: EmployeeServiceProtocol { + switch self { + case .production: + return EmployeeService.shared + case .malformed: + return EmployeeMalformedService.shared + case .empty: + return EmployeeEmptyService.shared + } + } + /// Enpoint in which to grabe employees from. public var endpoint: String { switch self { @@ -39,7 +50,45 @@ public class EmployeeService: EmployeeServiceProtocol { /// This will get a list of all employees /// - Parameter serviceMode: Mode in which to hit. /// - Returns: An Employees struct - public func getEmployees(_ serviceMode: EmployeeServiceMode = .production) async throws -> Employees { - return try await NetworkService.shared.fetchData(from: serviceMode.endpoint, as: Employees.self) + public func getEmployees() async throws -> Employees { + return try await NetworkService.shared.fetchData(from: EmployeeServiceMode.production.endpoint, as: Employees.self) + } +} + +/// Service Layer for Employees +public class EmployeeMalformedService: EmployeeServiceProtocol { + // MARK: - Properties + public static let shared = EmployeeMalformedService() // Default shared instance + + // MARK: - Initializer + + public init() {} + + // MARK: - Public Methods + + /// This will get a list of all employees + /// - Parameter serviceMode: Mode in which to hit. + /// - Returns: An Employees struct + public func getEmployees() async throws -> Employees { + return try await NetworkService.shared.fetchData(from: EmployeeServiceMode.malformed.endpoint, as: Employees.self) + } +} + +/// Service Layer for Employees +public class EmployeeEmptyService: EmployeeServiceProtocol { + // MARK: - Properties + public static let shared = EmployeeEmptyService() // Default shared instance + + // MARK: - Initializer + + public init() {} + + // MARK: - Public Methods + + /// This will get a list of all employees + /// - Parameter serviceMode: Mode in which to hit. + /// - Returns: An Employees struct + public func getEmployees() async throws -> Employees { + return try await NetworkService.shared.fetchData(from: EmployeeServiceMode.empty.endpoint, as: Employees.self) } } diff --git a/EmployeeDirectory/ViewControllers/EmployeesViewController.swift b/EmployeeDirectory/ViewControllers/EmployeesViewController.swift index 76d130e..c1d1914 100644 --- a/EmployeeDirectory/ViewControllers/EmployeesViewController.swift +++ b/EmployeeDirectory/ViewControllers/EmployeesViewController.swift @@ -158,6 +158,6 @@ extension EmployeesViewController { case 2: selectedMode = .empty default: return } - viewModel.changeMode(to: selectedMode) + viewModel.changeService(to: selectedMode.service) } } diff --git a/EmployeeDirectory/ViewModels/EmployeesViewModel.swift b/EmployeeDirectory/ViewModels/EmployeesViewModel.swift index 796b422..ab34896 100644 --- a/EmployeeDirectory/ViewModels/EmployeesViewModel.swift +++ b/EmployeeDirectory/ViewModels/EmployeesViewModel.swift @@ -11,7 +11,7 @@ import Foundation /// specifically with the EmployeesViewController. @MainActor public class EmployeesViewModel: ObservableObject { - private var serviceMode: EmployeeServiceMode = .production + private var employeeService: EmployeeServiceProtocol = EmployeeService() @Published public private(set) var employees: [Employee] = [] @Published public private(set) var errorMessage: String? = nil @@ -27,7 +27,7 @@ public class EmployeesViewModel: ObservableObject { Task { do { // Fetch employees using the async method - let wrapper = try await EmployeeService.shared.getEmployees(serviceMode) + let wrapper = try await employeeService.getEmployees() // Update published properties employees = wrapper.employees @@ -43,8 +43,8 @@ public class EmployeesViewModel: ObservableObject { } - public func changeMode(to mode: EmployeeServiceMode) { - serviceMode = mode + public func changeService(to employeeService: EmployeeServiceProtocol) { + self.employeeService = employeeService fetchEmployees() } }