Compare commits

..

5 Commits

Author SHA1 Message Date
146c90f2ee updating comments
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
2025-01-21 13:33:35 -06:00
7501ce5936 now using diffable datasource
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
2025-01-21 13:31:46 -06:00
3238defe84 updated with new logic for dealing with employee changes
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
2025-01-21 13:25:51 -06:00
fa5f782968 updated animation
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
2025-01-21 13:21:20 -06:00
6280aa42cb added to keep track of oldEmployees
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
2025-01-21 13:21:13 -06:00
3 changed files with 53 additions and 27 deletions

View File

@ -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

View File

@ -34,24 +34,46 @@ class EmployeesViewController: UIViewController {
// Prevents multiple calls during rapid scrolling
private var isFetchingNextPage: Bool = false
private var dataSource: UITableViewDiffableDataSource<Int, Employee>!
// MARK: - Public Methods
public override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupDataSource()
bindViewModel()
viewModel.fetchEmployees()
}
// MARK: - Private Methods
/// Setting up the TableView Datasource
private func setupDataSource() {
dataSource = UITableViewDiffableDataSource<Int, Employee>(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<Int, Employee>()
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
tableView.delegate = self
view.addSubview(tableView)
tableView.frame = view.bounds
@ -109,9 +131,8 @@ class EmployeesViewController: UIViewController {
private func bindViewModel() {
viewModel.$employees
.receive(on: RunLoop.main)
.sink { [weak self] _ in
self?.updateFooter()
self?.tableView.reloadData()
.sink { [weak self] employees in
self?.applySnapshot(employees: employees)
}
.store(in: &cancellables)
@ -135,6 +156,26 @@ class EmployeesViewController: UIViewController {
.store(in: &cancellables)
}
private func animateEmployeeChanges(from oldEmployees: [Employee], to newEmployees: [Employee]) {
let oldCount = oldEmployees.count
let newCount = newEmployees.count
// Case: Removing all employees
if oldCount > 0 && newCount == 0 {
let indexPaths = (0..<oldCount).map { IndexPath(row: $0, section: 0) }
tableView.performBatchUpdates {
tableView.deleteRows(at: indexPaths, with: .fade)
}
return
}
// Case: Resetting with new data (or switching modes)
if newCount > 0 {
tableView.reloadData()
return
}
}
/// Show state in specific use-cases for the EmployeesViewModel
private func updateFooter() {
var footerMessage: String?
@ -212,19 +253,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
}
}

View File

@ -13,7 +13,7 @@ import Combine
@MainActor
public class EmployeesViewModel: ObservableObject {
private var serviceMode: EmployeeServiceMode = .production
private var employeeService: EmployeeServiceProtocol = EmployeeService.shared
private var employeeService: EmployeeServiceProtocol = MockEmployeeService.shared
@Published public private(set) var employees: [Employee] = []
@Published public private(set) var errorMessage: String? = nil
@Published public private(set) var isLoading: Bool = false
@ -34,6 +34,7 @@ public class EmployeesViewModel: ObservableObject {
/// Observe changes to sortField and sortOrder and debounce fetch calls
private func observeSortingChanges() {
Publishers.CombineLatest($sortField, $sortOrder)
.dropFirst()
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.sink { [weak self] _, _ in
self?.resetAndFetchEmployees()
@ -55,16 +56,16 @@ public class EmployeesViewModel: ObservableObject {
sortField: sortField,
sortOrder: sortOrder)
totalEmployees = wrapper.total
currentPage = page
hasMorePages = wrapper.employees.count < totalEmployees
// Update published properties
if page == 1 {
employees = wrapper.employees // Replace list for the first page
} else {
employees.append(contentsOf: wrapper.employees) // Append for subsequent pages
}
totalEmployees = wrapper.total
currentPage = page
hasMorePages = employees.count < totalEmployees
} catch {
// Handle errors
errorMessage = "An unexpected error occurred, please try to refresh."