block-employee-directory/EmployeeDirectory/ViewModels/EmployeesViewModel.swift
Matt Bruce 9c8782dea8 Merge branch 'search' into requestBuilder
# Conflicts:
#	EmployeeDirectory/ViewControllers/EmployeesViewController.swift
#	EmployeeDirectory/ViewModels/EmployeesViewModel.swift

Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
2025-01-31 12:53:45 -06:00

132 lines
4.6 KiB
Swift

//
// EmployeesViewModel.swift
// EmployeeDirectory
//
// Created by Matt Bruce on 1/20/25.
//
import Foundation
import Combine
/// ViewModel that will be bound to an Employees model and used
/// specifically with the EmployeesViewController.
@MainActor
public class EmployeesViewModel: ObservableObject {
private var serviceMode: EmployeeServiceMode = .production
private var employeeService: EmployeeServiceProtocol = MockEmployeeService.shared
private var allEmployees: [Employee] = []
@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
@Published public var searchText: String = ""
@Published public var sortField: EmployeeSortField = .fullName
@Published public var sortOrder: EmployeeSortOrder = .ascending
private var cancellables = Set<AnyCancellable>()
private var currentPage = 1
private let perPage = 10
private var totalEmployees = 0
public init() {
setupSearch()
observeSortingChanges()
}
/// Sets up Combine to filter employees as the search text changes
private func setupSearch() {
$searchText
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) // Add delay to avoid frequent filtering
.removeDuplicates()
.sink { [weak self] query in
self?.filterEmployees(by: query)
}
.store(in: &cancellables)
}
/// 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()
}
.store(in: &cancellables)
}
/// Fetch employees for the given page
public func fetchEmployees(page: Int = 1) {
// Prevent duplicate calls
guard !isLoading else { return }
// resetting values out the values before fetching new data
errorMessage = nil
isLoading = true
Task {
do {
// Fetch employees using the paginated API
let wrapper = try await employeeService.getEmployees(page: page,
perPage: perPage,
sortField: sortField,
sortOrder: sortOrder)
totalEmployees = wrapper.total
currentPage = page
hasMorePages = wrapper.employees.count < totalEmployees
// Update published properties
if page == 1 {
allEmployees = wrapper.employees // Replace list for the first page
} else {
allEmployees.append(contentsOf: wrapper.employees) // Append for subsequent pages
}
filterEmployees(by: searchText)
} catch {
// Handle errors
errorMessage = "An unexpected error occurred, please try to refresh."
}
isLoading = false
}
}
/// Load the next page of employees
public func loadNextPage() {
guard hasMorePages else { return }
fetchEmployees(page: currentPage + 1)
}
/// Resets the current employee list and fetches data from page 1
private func resetAndFetchEmployees() {
currentPage = 1
employees = []
allEmployees = []
hasMorePages = true
fetchEmployees(page: 1)
}
public func changeService(to employeeService: EmployeeServiceProtocol) {
self.employeeService = employeeService
resetAndFetchEmployees()
}
/// Filters employees based on search query
private func filterEmployees(by query: String) {
if query.isEmpty {
employees = allEmployees // Reset to all employees if no search text
} else {
employees = allEmployees.filter { employee in
employee.fullName.lowercased().contains(query.lowercased()) ||
employee.team.lowercased().contains(query.lowercased()) ||
employee.emailAddress.lowercased().contains(query.lowercased())
}
}
}
}