From 7e0c2fd310092ca7227a6b48adfd5a10e8ead19a Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 21 Jan 2025 11:49:09 -0600 Subject: [PATCH] added paging Signed-off-by: Matt Bruce --- .../EmployeesViewController.swift | 47 +++++++++++++++--- .../ViewModels/EmployeesViewModel.swift | 49 +++++++++++++------ 2 files changed, 75 insertions(+), 21 deletions(-) diff --git a/EmployeeDirectory/ViewControllers/EmployeesViewController.swift b/EmployeeDirectory/ViewControllers/EmployeesViewController.swift index cfb192e..c1b4c13 100644 --- a/EmployeeDirectory/ViewControllers/EmployeesViewController.swift +++ b/EmployeeDirectory/ViewControllers/EmployeesViewController.swift @@ -31,6 +31,9 @@ class EmployeesViewController: UIViewController { /// Will show specific state of the viewModel private var footerView: TableFooterView? + // Prevents multiple calls during rapid scrolling + private var isFetchingNextPage: Bool = false + // MARK: - Public Methods public override func viewDidLoad() { @@ -49,6 +52,7 @@ class EmployeesViewController: UIViewController { // Configure TableView tableView.register(EmployeeTableViewCell.self, forCellReuseIdentifier: EmployeeTableViewCell.identifier) tableView.dataSource = self + tableView.delegate = self view.addSubview(tableView) tableView.frame = view.bounds @@ -98,26 +102,55 @@ class EmployeesViewController: UIViewController { /// Show state in specific use-cases for the EmployeesViewModel private func updateFooter() { - var message: String? { - guard !viewModel.isLoading else { return nil } - return viewModel.errorMessage ?? (viewModel.employees.isEmpty ? "No employees found, please try to refresh." : nil) + var footerMessage: String? + + // Check for error messages or empty state first + if let message = viewModel.errorMessage ?? (viewModel.employees.isEmpty && !viewModel.isLoading ? "No employees found, please try to refresh." : nil) { + footerMessage = message + + } + // Show loading footer if there are more pages to load + else if (viewModel.isLoading || isFetchingNextPage) && viewModel.hasMorePages { + footerMessage = "Loading more employees..." } - if let message, !viewModel.isLoading { + if let footerMessage { // Lazy initialize footerView if needed if footerView == nil { - footerView = TableFooterView(message: message) - } else { // Update the message - footerView?.update(message: message) + footerView = TableFooterView(message: footerMessage) + } else { + footerView?.update(message: footerMessage) } footerView?.frame = CGRect(x: 0, y: 0, width: tableView.frame.width, height: 150) tableView.tableFooterView = footerView + } else { tableView.tableFooterView = nil } } } +// MARK: - UITableViewDelegate +extension EmployeesViewController: UITableViewDelegate { + func scrollViewDidScroll(_ scrollView: UIScrollView) { + let offsetY = scrollView.contentOffset.y + let contentHeight = scrollView.contentSize.height + let scrollViewHeight = scrollView.frame.size.height + + if offsetY > contentHeight - scrollViewHeight - 100 && !isFetchingNextPage { + isFetchingNextPage = true + updateFooter() + viewModel.loadNextPage() + + // Reset the flag after a short delay to allow new fetches + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + self?.isFetchingNextPage = false + } + } + } +} + + // Mark: - Objective-C Methods extension EmployeesViewController { diff --git a/EmployeeDirectory/ViewModels/EmployeesViewModel.swift b/EmployeeDirectory/ViewModels/EmployeesViewModel.swift index 796b422..411f5d9 100644 --- a/EmployeeDirectory/ViewModels/EmployeesViewModel.swift +++ b/EmployeeDirectory/ViewModels/EmployeesViewModel.swift @@ -16,36 +16,57 @@ public class EmployeesViewModel: ObservableObject { @Published public private(set) var employees: [Employee] = [] @Published public private(set) var errorMessage: String? = nil @Published public private(set) var isLoading: Bool = false - + @Published public private(set) var hasMorePages: Bool = true + + private var currentPage = 1 + private let perPage = 10 + private var totalEmployees = 0 + public init() {} - - public func fetchEmployees() { - // resetting values out the values before fetching new data - errorMessage = nil + + /// Fetch employees for the given page + public func fetchEmployees(page: Int = 1) { + // Prevent duplicate calls + guard !isLoading else { return } isLoading = true Task { do { - // Fetch employees using the async method - let wrapper = try await EmployeeService.shared.getEmployees(serviceMode) + // Fetch employees using the paginated API + let wrapper = try await MockEmployeeService.shared.getEmployees(.empty ,page: page, perPage: perPage) // Update published properties - employees = wrapper.employees + 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 isLoading = false } catch { // Handle errors - employees = [] isLoading = false - errorMessage = "An unexpected error occurred, please try to refresh" + errorMessage = "An unexpected error occurred, please try to refresh." } } - } - + + /// Load the next page of employees + public func loadNextPage() { + guard hasMorePages else { return } + fetchEmployees(page: currentPage + 1) + } + + /// Change the service mode (e.g., production, malformed, empty) public func changeMode(to mode: EmployeeServiceMode) { serviceMode = mode - fetchEmployees() + currentPage = 1 + employees = [] + hasMorePages = true + fetchEmployees(page: 1) } } -