// // ViewController.swift // EmployeeDirectory // // Created by Matt Bruce on 1/20/25. // import UIKit import Combine class EmployeesViewController: UIViewController { // MARK: - Properties /// 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) /// Allows the user to pick between service modes private let modeSegmentedControl = UISegmentedControl(items: EmployeeServiceMode.allCases.map{ $0.rawValue } ) /// ViewModel in which drives the screen private let viewModel = EmployeesViewModel() /// Holds onto the ViewModels Subscribers private var cancellables = Set() /// Will show specific state of the viewModel private var footerView: TableFooterView? private var dataSource: UITableViewDiffableDataSource! // MARK: - Public Methods public override func viewDidLoad() { super.viewDidLoad() setupUI() setupDataSource() bindViewModel() viewModel.fetchEmployees() } // MARK: - Private Methods /// Setup TableView dataSource private func setupDataSource() { dataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, employee in guard let cell = tableView.dequeueReusableCell(withIdentifier: EmployeeTableViewCell.identifier,for: indexPath) as? EmployeeTableViewCell else { return UITableViewCell() } cell.configure(with: EmployeeCellViewModel(employee: employee)) return cell } } /// Snapshot Handling private func applySnapshot(employees: [Employee]) { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([0]) snapshot.appendItems(employees, toSection: 0) dataSource.apply(snapshot, animatingDifferences: true) } /// Setup the UI by adding the views to the main view private func setupUI() { view.backgroundColor = .white // Configure TableView tableView.translatesAutoresizingMaskIntoConstraints = false tableView.register(EmployeeTableViewCell.self, forCellReuseIdentifier: EmployeeTableViewCell.identifier) 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) // Configure Activity Indicator activityIndicator.center = view.center view.addSubview(activityIndicator) /// setup Search searchBar.placeholder = "Search employees" searchBar.delegate = self navigationItem.titleView = searchBar } /// Using the ViewModel setup combine handlers private func bindViewModel() { viewModel.$employees .receive(on: RunLoop.main) .sink { [weak self] employees in self?.updateFooter() self?.applySnapshot(employees: employees) } .store(in: &cancellables) viewModel.$isLoading .receive(on: RunLoop.main) .sink { [weak self] isLoading in if isLoading { self?.activityIndicator.startAnimating() } else { self?.activityIndicator.stopAnimating() self?.tableView.refreshControl?.endRefreshing() self?.updateFooter() } } .store(in: &cancellables) viewModel.$errorMessage .receive(on: RunLoop.main) .sink { [weak self] _ in self?.updateFooter() } .store(in: &cancellables) } /// 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" : nil) } if let message, !viewModel.isLoading { // Lazy initialize footerView if needed if footerView == nil { footerView = TableFooterView(message: message) } else { // Update the message footerView?.update(message: message) } footerView?.frame = CGRect(x: 0, y: 0, width: tableView.frame.width, height: 150) tableView.tableFooterView = footerView } else { tableView.tableFooterView = nil } } } // Mark: - Objective-C Methods extension EmployeesViewController { /// Fetch the Employees @objc private func didPullToRefresh() { viewModel.fetchEmployees() } /// This will handle services changes to test conditions to ensure UI works correctly. /// - Parameter sender: Mode in which to test @objc private func onServiceModeChange(_ sender: UISegmentedControl) { let selectedMode: EmployeeServiceMode switch sender.selectedSegmentIndex { case 0: selectedMode = .production case 1: selectedMode = .malformed case 2: selectedMode = .empty default: return } 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 } }