added search

Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
Matt Bruce 2025-01-31 12:33:11 -06:00
parent 0f9515b90b
commit 94202990bb
3 changed files with 89 additions and 11 deletions

View File

@ -301,7 +301,8 @@
INFOPLIST_FILE = EmployeeDirectory/Info.plist;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = UIInterfaceOrientationPortrait;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -327,7 +328,8 @@
INFOPLIST_FILE = EmployeeDirectory/Info.plist;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = UIInterfaceOrientationPortrait;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",

View File

@ -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)
@ -70,10 +73,39 @@ class EmployeesViewController: UIViewController {
view.backgroundColor = .white
// Configure TableView
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.register(EmployeeTableViewCell.self, forCellReuseIdentifier: EmployeeTableViewCell.identifier)
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)
@ -82,10 +114,10 @@ class EmployeesViewController: UIViewController {
activityIndicator.center = view.center
view.addSubview(activityIndicator)
// Configure Mode Selector
modeSegmentedControl.selectedSegmentIndex = 0
modeSegmentedControl.addTarget(self, action: #selector(onServiceModeChange), for: .valueChanged)
navigationItem.titleView = modeSegmentedControl
/// setup Search
searchBar.placeholder = "Search employees"
searchBar.delegate = self
navigationItem.titleView = searchBar
}
/// Using the ViewModel setup combine handlers
@ -93,6 +125,7 @@ class EmployeesViewController: UIViewController {
viewModel.$employees
.receive(on: RunLoop.main)
.sink { [weak self] employees in
self?.updateFooter()
self?.applySnapshot(employees: employees)
}
.store(in: &cancellables)
@ -122,7 +155,7 @@ class EmployeesViewController: UIViewController {
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)
return viewModel.errorMessage ?? (viewModel.employees.isEmpty ? "No employees found" : nil)
}
if let message, !viewModel.isLoading {
@ -161,3 +194,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
}
}

View File

@ -6,6 +6,7 @@
//
import Foundation
import Combine
/// ViewModel that will be bound to an Employees model and used
/// specifically with the EmployeesViewController.
@ -13,11 +14,26 @@ import Foundation
public class EmployeesViewModel: ObservableObject {
private var employeeService: EmployeeServiceProtocol = EmployeeService()
private var allEmployees: [Employee] = [] // Holds unfiltered employees
@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 var searchText: String = "" // User input from search bar
private var cancellables = Set<AnyCancellable>()
public init() {
setupSearch()
}
public init() {}
/// 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)
}
public func fetchEmployees() {
// resetting values out the values before fetching new data
@ -30,7 +46,8 @@ public class EmployeesViewModel: ObservableObject {
let wrapper = try await employeeService.getEmployees()
// Update published properties
employees = wrapper.employees
allEmployees = wrapper.employees
filterEmployees(by: searchText)
isLoading = false
} catch {
@ -47,5 +64,19 @@ public class EmployeesViewModel: ObservableObject {
self.employeeService = employeeService
fetchEmployees()
}
/// 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())
}
}
}
}