Compare commits

..

No commits in common. "146c90f2eebcee383919367798a6698cc5c7dc7b" and "b9bd3ae3104b80c9cace42316f277ed3665314f6" have entirely different histories.

3 changed files with 27 additions and 53 deletions

View File

@ -27,7 +27,7 @@ public enum EmployeeType: String, Codable, CustomStringConvertible {
/// Employee Object /// Employee Object
/// JSON Object defintion /// JSON Object defintion
/// - https://square.github.io/microsite/mobile-interview-project/ /// - https://square.github.io/microsite/mobile-interview-project/
public struct Employee: Hashable, Codable { public struct Employee: Codable {
/// The unique identifier for the employee. Represented as a UUID. /// The unique identifier for the employee. Represented as a UUID.
public let uuid: UUID public let uuid: UUID

View File

@ -34,46 +34,24 @@ class EmployeesViewController: UIViewController {
// Prevents multiple calls during rapid scrolling // Prevents multiple calls during rapid scrolling
private var isFetchingNextPage: Bool = false private var isFetchingNextPage: Bool = false
private var dataSource: UITableViewDiffableDataSource<Int, Employee>!
// MARK: - Public Methods // MARK: - Public Methods
public override func viewDidLoad() { public override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
setupUI() setupUI()
setupDataSource()
bindViewModel() bindViewModel()
viewModel.fetchEmployees() viewModel.fetchEmployees()
} }
// MARK: - Private Methods // 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 /// Setup the UI by adding the views to the main view
private func setupUI() { private func setupUI() {
view.backgroundColor = .white view.backgroundColor = .white
// Configure TableView // Configure TableView
tableView.register(EmployeeTableViewCell.self, forCellReuseIdentifier: EmployeeTableViewCell.identifier) tableView.register(EmployeeTableViewCell.self, forCellReuseIdentifier: EmployeeTableViewCell.identifier)
tableView.dataSource = self
tableView.delegate = self tableView.delegate = self
view.addSubview(tableView) view.addSubview(tableView)
tableView.frame = view.bounds tableView.frame = view.bounds
@ -131,8 +109,9 @@ class EmployeesViewController: UIViewController {
private func bindViewModel() { private func bindViewModel() {
viewModel.$employees viewModel.$employees
.receive(on: RunLoop.main) .receive(on: RunLoop.main)
.sink { [weak self] employees in .sink { [weak self] _ in
self?.applySnapshot(employees: employees) self?.updateFooter()
self?.tableView.reloadData()
} }
.store(in: &cancellables) .store(in: &cancellables)
@ -156,26 +135,6 @@ class EmployeesViewController: UIViewController {
.store(in: &cancellables) .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 /// Show state in specific use-cases for the EmployeesViewModel
private func updateFooter() { private func updateFooter() {
var footerMessage: String? var footerMessage: String?
@ -253,3 +212,19 @@ extension EmployeesViewController {
viewModel.changeMode(to: selectedMode) 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 @MainActor
public class EmployeesViewModel: ObservableObject { public class EmployeesViewModel: ObservableObject {
private var serviceMode: EmployeeServiceMode = .production private var serviceMode: EmployeeServiceMode = .production
private var employeeService: EmployeeServiceProtocol = MockEmployeeService.shared private var employeeService: EmployeeServiceProtocol = EmployeeService.shared
@Published public private(set) var employees: [Employee] = [] @Published public private(set) var employees: [Employee] = []
@Published public private(set) var errorMessage: String? = nil @Published public private(set) var errorMessage: String? = nil
@Published public private(set) var isLoading: Bool = false @Published public private(set) var isLoading: Bool = false
@ -34,7 +34,6 @@ public class EmployeesViewModel: ObservableObject {
/// Observe changes to sortField and sortOrder and debounce fetch calls /// Observe changes to sortField and sortOrder and debounce fetch calls
private func observeSortingChanges() { private func observeSortingChanges() {
Publishers.CombineLatest($sortField, $sortOrder) Publishers.CombineLatest($sortField, $sortOrder)
.dropFirst()
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.sink { [weak self] _, _ in .sink { [weak self] _, _ in
self?.resetAndFetchEmployees() self?.resetAndFetchEmployees()
@ -56,16 +55,16 @@ public class EmployeesViewModel: ObservableObject {
sortField: sortField, sortField: sortField,
sortOrder: sortOrder) sortOrder: sortOrder)
totalEmployees = wrapper.total
currentPage = page
hasMorePages = wrapper.employees.count < totalEmployees
// Update published properties // Update published properties
if page == 1 { if page == 1 {
employees = wrapper.employees // Replace list for the first page employees = wrapper.employees // Replace list for the first page
} else { } else {
employees.append(contentsOf: wrapper.employees) // Append for subsequent pages employees.append(contentsOf: wrapper.employees) // Append for subsequent pages
} }
totalEmployees = wrapper.total
currentPage = page
hasMorePages = employees.count < totalEmployees
} catch { } catch {
// Handle errors // Handle errors
errorMessage = "An unexpected error occurred, please try to refresh." errorMessage = "An unexpected error occurred, please try to refresh."