Compare commits
No commits in common. "146c90f2eebcee383919367798a6698cc5c7dc7b" and "b9bd3ae3104b80c9cace42316f277ed3665314f6" have entirely different histories.
146c90f2ee
...
b9bd3ae310
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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."
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user