// // 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() /// 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.register(EmployeeTableViewCell.self, forCellReuseIdentifier: EmployeeTableViewCell.identifier) view.addSubview(tableView) tableView.frame = view.bounds tableView.delegate = self //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) // Configure Mode Selector modeSegmentedControl.selectedSegmentIndex = 0 modeSegmentedControl.addTarget(self, action: #selector(onServiceModeChange), for: .valueChanged) navigationItem.titleView = modeSegmentedControl } /// Using the ViewModel setup combine handlers private func bindViewModel() { viewModel.$employees .receive(on: RunLoop.main) .sink { [weak self] employees in 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, please try to refresh." : 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 } } } extension EmployeesViewController: UITableViewDelegate{ func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let employee = viewModel.employees[indexPath.row] let details = EmployeeDetailViewController() details.viewModel = .init(employee: employee) navigationController?.pushViewController(details, animated: true) } } // 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) } }