Merge branch 'search' into requestBuilder
# Conflicts: # EmployeeDirectory/ViewControllers/EmployeesViewController.swift # EmployeeDirectory/ViewModels/EmployeesViewModel.swift Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
commit
9c8782dea8
@ -16,6 +16,9 @@ class EmployeesViewController: UIViewController {
|
|||||||
/// List for the employees
|
/// List for the employees
|
||||||
private let tableView = UITableView()
|
private let tableView = UITableView()
|
||||||
|
|
||||||
|
/// Filtering the employees
|
||||||
|
private let searchBar = UISearchBar()
|
||||||
|
|
||||||
/// Will only show when fetching data occurs
|
/// Will only show when fetching data occurs
|
||||||
private let activityIndicator = UIActivityIndicatorView(style: .large)
|
private let activityIndicator = UIActivityIndicatorView(style: .large)
|
||||||
|
|
||||||
@ -73,11 +76,40 @@ class EmployeesViewController: UIViewController {
|
|||||||
view.backgroundColor = .white
|
view.backgroundColor = .white
|
||||||
|
|
||||||
// Configure TableView
|
// Configure TableView
|
||||||
|
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
tableView.register(EmployeeTableViewCell.self, forCellReuseIdentifier: EmployeeTableViewCell.identifier)
|
tableView.register(EmployeeTableViewCell.self, forCellReuseIdentifier: EmployeeTableViewCell.identifier)
|
||||||
tableView.delegate = self
|
tableView.delegate = self
|
||||||
view.addSubview(tableView)
|
tableView.estimatedRowHeight = 80 // Provide a reasonable default height
|
||||||
tableView.frame = view.bounds
|
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
|
//add pull to refresh
|
||||||
tableView.refreshControl = UIRefreshControl()
|
tableView.refreshControl = UIRefreshControl()
|
||||||
tableView.refreshControl?.addTarget(self, action: #selector(didPullToRefresh), for: .valueChanged)
|
tableView.refreshControl?.addTarget(self, action: #selector(didPullToRefresh), for: .valueChanged)
|
||||||
@ -85,14 +117,14 @@ class EmployeesViewController: UIViewController {
|
|||||||
// Configure Activity Indicator
|
// Configure Activity Indicator
|
||||||
activityIndicator.center = view.center
|
activityIndicator.center = view.center
|
||||||
view.addSubview(activityIndicator)
|
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))
|
let sortButton = UIBarButtonItem(title: "Sort", style: .plain, target: self, action: #selector(showSortOptions))
|
||||||
navigationItem.rightBarButtonItem = sortButton
|
navigationItem.rightBarButtonItem = sortButton
|
||||||
|
|
||||||
|
/// setup Search
|
||||||
|
searchBar.placeholder = "Search employees"
|
||||||
|
searchBar.delegate = self
|
||||||
|
navigationItem.titleView = searchBar
|
||||||
}
|
}
|
||||||
|
|
||||||
private func presentSortingActionSheet() {
|
private func presentSortingActionSheet() {
|
||||||
@ -124,7 +156,6 @@ class EmployeesViewController: UIViewController {
|
|||||||
})
|
})
|
||||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
|
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
|
||||||
present(alert, animated: true, completion: nil)
|
present(alert, animated: true, completion: nil)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Using the ViewModel setup combine handlers
|
/// Using the ViewModel setup combine handlers
|
||||||
@ -132,6 +163,7 @@ class EmployeesViewController: UIViewController {
|
|||||||
viewModel.$employees
|
viewModel.$employees
|
||||||
.receive(on: RunLoop.main)
|
.receive(on: RunLoop.main)
|
||||||
.sink { [weak self] employees in
|
.sink { [weak self] employees in
|
||||||
|
self?.updateFooter()
|
||||||
self?.applySnapshot(employees: employees)
|
self?.applySnapshot(employees: employees)
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
@ -183,7 +215,7 @@ class EmployeesViewController: UIViewController {
|
|||||||
var footerMessage: String?
|
var footerMessage: String?
|
||||||
|
|
||||||
// Check for error messages or empty state first
|
// 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
|
footerMessage = message
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -255,3 +287,15 @@ extension EmployeesViewController {
|
|||||||
viewModel.changeService(to: selectedMode.service)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -16,11 +16,12 @@ public class EmployeesViewModel: ObservableObject {
|
|||||||
private var serviceMode: EmployeeServiceMode = .production
|
private var serviceMode: EmployeeServiceMode = .production
|
||||||
private var employeeService: EmployeeServiceProtocol = MockEmployeeService.shared
|
private var employeeService: EmployeeServiceProtocol = MockEmployeeService.shared
|
||||||
|
|
||||||
|
private var allEmployees: [Employee] = []
|
||||||
@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
|
||||||
@Published public private(set) var hasMorePages: Bool = true
|
@Published public private(set) var hasMorePages: Bool = true
|
||||||
|
@Published public var searchText: String = ""
|
||||||
@Published public var sortField: EmployeeSortField = .fullName
|
@Published public var sortField: EmployeeSortField = .fullName
|
||||||
@Published public var sortOrder: EmployeeSortOrder = .ascending
|
@Published public var sortOrder: EmployeeSortOrder = .ascending
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
@ -30,9 +31,21 @@ public class EmployeesViewModel: ObservableObject {
|
|||||||
private var totalEmployees = 0
|
private var totalEmployees = 0
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
|
setupSearch()
|
||||||
observeSortingChanges()
|
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
|
/// Observe changes to sortField and sortOrder and debounce fetch calls
|
||||||
private func observeSortingChanges() {
|
private func observeSortingChanges() {
|
||||||
Publishers.CombineLatest($sortField, $sortOrder)
|
Publishers.CombineLatest($sortField, $sortOrder)
|
||||||
@ -49,6 +62,8 @@ public class EmployeesViewModel: ObservableObject {
|
|||||||
|
|
||||||
// Prevent duplicate calls
|
// Prevent duplicate calls
|
||||||
guard !isLoading else { return }
|
guard !isLoading else { return }
|
||||||
|
|
||||||
|
// resetting values out the values before fetching new data
|
||||||
errorMessage = nil
|
errorMessage = nil
|
||||||
isLoading = true
|
isLoading = true
|
||||||
Task {
|
Task {
|
||||||
@ -65,10 +80,11 @@ public class EmployeesViewModel: ObservableObject {
|
|||||||
|
|
||||||
// Update published properties
|
// Update published properties
|
||||||
if page == 1 {
|
if page == 1 {
|
||||||
employees = wrapper.employees // Replace list for the first page
|
allEmployees = wrapper.employees // Replace list for the first page
|
||||||
} else {
|
} else {
|
||||||
employees.append(contentsOf: wrapper.employees) // Append for subsequent pages
|
allEmployees.append(contentsOf: wrapper.employees) // Append for subsequent pages
|
||||||
}
|
}
|
||||||
|
filterEmployees(by: searchText)
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
// Handle errors
|
// Handle errors
|
||||||
@ -89,6 +105,7 @@ public class EmployeesViewModel: ObservableObject {
|
|||||||
private func resetAndFetchEmployees() {
|
private func resetAndFetchEmployees() {
|
||||||
currentPage = 1
|
currentPage = 1
|
||||||
employees = []
|
employees = []
|
||||||
|
allEmployees = []
|
||||||
hasMorePages = true
|
hasMorePages = true
|
||||||
fetchEmployees(page: 1)
|
fetchEmployees(page: 1)
|
||||||
}
|
}
|
||||||
@ -97,4 +114,18 @@ public class EmployeesViewModel: ObservableObject {
|
|||||||
self.employeeService = employeeService
|
self.employeeService = employeeService
|
||||||
resetAndFetchEmployees()
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user