diff --git a/EmployeeDirectory/ViewControllers/EmployeesViewController.swift b/EmployeeDirectory/ViewControllers/EmployeesViewController.swift index 4087933..42ac30e 100644 --- a/EmployeeDirectory/ViewControllers/EmployeesViewController.swift +++ b/EmployeeDirectory/ViewControllers/EmployeesViewController.swift @@ -16,6 +16,9 @@ class EmployeesViewController: UIViewController { /// List for the employees private let tableView = UITableView() + /// Filtering the employees + private let searchBar = UISearchBar() + /// Will only show when fetching data occurs private let activityIndicator = UIActivityIndicatorView(style: .large) @@ -73,11 +76,40 @@ class EmployeesViewController: UIViewController { view.backgroundColor = .white // Configure TableView + tableView.translatesAutoresizingMaskIntoConstraints = false tableView.register(EmployeeTableViewCell.self, forCellReuseIdentifier: EmployeeTableViewCell.identifier) tableView.delegate = self - view.addSubview(tableView) - tableView.frame = view.bounds + tableView.estimatedRowHeight = 80 // Provide a reasonable default height + tableView.rowHeight = UITableView.automaticDimension // Enable dynamic row height + + // Configure Segmented Control + let segmentedContainer = UIView() + segmentedContainer.translatesAutoresizingMaskIntoConstraints = false + segmentedContainer.addSubview(modeSegmentedControl) + + modeSegmentedControl.selectedSegmentIndex = 0 + modeSegmentedControl.addTarget(self, action: #selector(onServiceModeChange), for: .valueChanged) + modeSegmentedControl.translatesAutoresizingMaskIntoConstraints = false + let stackView = UIStackView(arrangedSubviews: [segmentedContainer, tableView]) + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.distribution = .fill + stackView.spacing = 5 + + view.addSubview(stackView) + + NSLayoutConstraint.activate([ + stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + modeSegmentedControl.topAnchor.constraint(equalTo: segmentedContainer.topAnchor), + modeSegmentedControl.bottomAnchor.constraint(equalTo: segmentedContainer.bottomAnchor, constant: -5), + modeSegmentedControl.leadingAnchor.constraint(equalTo: segmentedContainer.leadingAnchor, constant: 16), + modeSegmentedControl.trailingAnchor.constraint(equalTo: segmentedContainer.trailingAnchor, constant: -16) + ]) + //add pull to refresh tableView.refreshControl = UIRefreshControl() tableView.refreshControl?.addTarget(self, action: #selector(didPullToRefresh), for: .valueChanged) @@ -85,14 +117,14 @@ class EmployeesViewController: UIViewController { // Configure Activity Indicator activityIndicator.center = view.center view.addSubview(activityIndicator) - - // Configure Mode Selector - modeSegmentedControl.selectedSegmentIndex = 0 - modeSegmentedControl.addTarget(self, action: #selector(onServiceModeChange), for: .valueChanged) - navigationItem.titleView = modeSegmentedControl let sortButton = UIBarButtonItem(title: "Sort", style: .plain, target: self, action: #selector(showSortOptions)) navigationItem.rightBarButtonItem = sortButton + + /// setup Search + searchBar.placeholder = "Search employees" + searchBar.delegate = self + navigationItem.titleView = searchBar } private func presentSortingActionSheet() { @@ -124,7 +156,6 @@ class EmployeesViewController: UIViewController { }) alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) present(alert, animated: true, completion: nil) - } /// Using the ViewModel setup combine handlers @@ -132,6 +163,7 @@ class EmployeesViewController: UIViewController { viewModel.$employees .receive(on: RunLoop.main) .sink { [weak self] employees in + self?.updateFooter() self?.applySnapshot(employees: employees) } .store(in: &cancellables) @@ -183,7 +215,7 @@ class EmployeesViewController: UIViewController { var footerMessage: String? // Check for error messages or empty state first - if let message = viewModel.errorMessage ?? (viewModel.employees.isEmpty && viewModel.isLoading == false ? "No employees found, please try to refresh." : nil) { + if let message = viewModel.errorMessage ?? (viewModel.employees.isEmpty && viewModel.isLoading == false ? "No employees found." : nil) { footerMessage = message } @@ -255,3 +287,15 @@ extension EmployeesViewController { viewModel.changeService(to: selectedMode.service) } } + +extension EmployeesViewController: UISearchBarDelegate { + public func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { + viewModel.searchText = searchText // Updates search text in ViewModel + } + + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + searchBar.text = "" + searchBar.resignFirstResponder() + viewModel.searchText = "" // Clear search and reset list + } +} diff --git a/EmployeeDirectory/ViewModels/EmployeesViewModel.swift b/EmployeeDirectory/ViewModels/EmployeesViewModel.swift index 38024c7..7c0bf92 100644 --- a/EmployeeDirectory/ViewModels/EmployeesViewModel.swift +++ b/EmployeeDirectory/ViewModels/EmployeesViewModel.swift @@ -16,11 +16,12 @@ 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() @@ -30,9 +31,21 @@ public class EmployeesViewModel: ObservableObject { 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) @@ -49,6 +62,8 @@ public class EmployeesViewModel: ObservableObject { // Prevent duplicate calls guard !isLoading else { return } + + // resetting values out the values before fetching new data errorMessage = nil isLoading = true Task { @@ -65,10 +80,11 @@ public class EmployeesViewModel: ObservableObject { // Update published properties if page == 1 { - employees = wrapper.employees // Replace list for the first page + allEmployees = wrapper.employees // Replace list for the first page } else { - employees.append(contentsOf: wrapper.employees) // Append for subsequent pages + allEmployees.append(contentsOf: wrapper.employees) // Append for subsequent pages } + filterEmployees(by: searchText) } catch { // Handle errors @@ -89,6 +105,7 @@ public class EmployeesViewModel: ObservableObject { private func resetAndFetchEmployees() { currentPage = 1 employees = [] + allEmployees = [] hasMorePages = true fetchEmployees(page: 1) } @@ -97,4 +114,18 @@ public class EmployeesViewModel: ObservableObject { 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()) + } + } + } + }