Compare commits

..

No commits in common. "requestBuilder-search" and "develop" have entirely different histories.

11 changed files with 67 additions and 11546 deletions

View File

@ -1,55 +0,0 @@
//
// RequestBuilder.swift
// EmployeeDirectory
//
// Created by Matt Bruce on 1/21/25.
//
import Foundation
/// Enum representing HTTP methods
public enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
case patch = "PATCH"
case head = "HEAD"
case options = "OPTIONS"
}
/// A utility for constructing HTTP requests
public struct RequestBuilder {
private var url: URL
private var method: HTTPMethod
private var headers: [String: String] = [:]
private var body: Data?
public init(url: URL, method: HTTPMethod = .get) {
self.url = url
self.method = method
}
/// Add a header to the request
@discardableResult
public mutating func addHeader(key: String, value: String) -> Self {
headers[key] = value
return self
}
/// Set the body of the request
@discardableResult
public mutating func setBody(_ data: Data?) -> Self {
body = data
return self
}
/// Build the `URLRequest`
public func build() -> URLRequest {
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
request.allHTTPHeaderFields = headers
request.httpBody = body
return request
}
}

View File

@ -27,7 +27,7 @@ public enum EmployeeType: String, Codable, CustomStringConvertible {
/// Employee Object
/// JSON Object defintion
/// - https://square.github.io/microsite/mobile-interview-project/
public struct Employee: Hashable, Codable {
public struct Employee: Codable {
/// The unique identifier for the employee. Represented as a UUID.
public let uuid: UUID

View File

@ -11,29 +11,8 @@ public struct Employees: Codable {
/// Array of Employees
public var employees: [Employee]
public let total: Int
public let page: Int
public let perPage: Int
private enum CodingKeys: String, CodingKey {
case employees
case total
case page
case perPage = "per_page"
}
public init(employees: [Employee], total: Int = 0, page: Int = 1, perPage: Int = 10) {
self.employees = employees
self.total = total
self.page = page
self.perPage = perPage
}
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.employees = try container.decode([Employee].self, forKey: .employees)
self.total = try container.decodeIfPresent(Int.self, forKey: .total) ?? self.employees.count
self.page = try container.decodeIfPresent(Int.self, forKey: .page) ?? 1
self.perPage = try container.decodeIfPresent(Int.self, forKey: .perPage) ?? self.total
}
}

View File

@ -5,10 +5,12 @@
// Created by Matt Bruce on 1/20/25.
//
/// This will be the interface for the API for Employees
public protocol EmployeeServiceProtocol {
/// This will get a list of all employees
/// - Parameter serviceMode: Mode in which to hit.
/// - Returns: An Employees struct
func getEmployees() async throws -> Employees
func getEmployees(page: Int, perPage: Int, sortField: EmployeeSortField, sortOrder: EmployeeSortOrder) async throws -> Employees
func getEmployees(_ serviceMode: EmployeeServiceMode) async throws -> Employees
}

View File

@ -6,35 +6,12 @@
//
import Foundation
public enum EmployeeSortField: String {
case fullName
case team
case employeeType
}
public enum EmployeeSortOrder: String {
case ascending = "asc"
case descending = "desc"
}
/// These are the testing URL Endpoints for different states
internal enum EmployeeServiceMode: String, CaseIterable {
public enum EmployeeServiceMode: String, CaseIterable {
case production
case malformed
case empty
public var service: EmployeeServiceProtocol {
switch self {
case .production:
return MockEmployeeService.shared
//return EmployeeService.shared
case .malformed:
return EmployeeMalformedService.shared
case .empty:
return EmployeeEmptyService.shared
}
}
/// Enpoint in which to grabe employees from.
public var endpoint: String {
switch self {
@ -62,82 +39,7 @@ public class EmployeeService: EmployeeServiceProtocol {
/// This will get a list of all employees
/// - Parameter serviceMode: Mode in which to hit.
/// - Returns: An Employees struct
public func getEmployees() async throws -> Employees {
return try await NetworkService.shared.fetchData(from: EmployeeServiceMode.production.endpoint, as: Employees.self)
}
/// Fetch employees with pagination support
/// - Parameters:
/// - page: The page number to fetch.
/// - perPage: The number of employees per page.
/// - Returns: A paginated Employees object.
public func getEmployees(page: Int, perPage: Int,
sortField: EmployeeSortField = .fullName,
sortOrder: EmployeeSortOrder = .ascending) async throws -> Employees {
guard var urlComponents = URLComponents(string: EmployeeServiceMode.production.endpoint) else {
throw NetworkServiceError.invalidURL
}
urlComponents.queryItems = [
URLQueryItem(name: "page", value: "\(page)"),
URLQueryItem(name: "perPage", value: "\(perPage)"),
URLQueryItem(name: "sortField", value: "\(sortField.rawValue)"),
URLQueryItem(name: "sortOrder", value: "\(sortOrder.rawValue)")
]
guard let url = urlComponents.url else {
throw NetworkServiceError.invalidURL
}
let request = RequestBuilder(url: url, method: .get).build()
return try await NetworkService.shared.fetchData(with: request, as: Employees.self)
}
}
/// Service Layer for Employees
public class EmployeeMalformedService: EmployeeServiceProtocol {
// MARK: - Properties
public static let shared = EmployeeMalformedService() // Default shared instance
// MARK: - Initializer
public init() {}
// MARK: - Public Methods
/// This will get a list of all employees
/// - Parameter serviceMode: Mode in which to hit.
/// - Returns: An Employees struct
public func getEmployees() async throws -> Employees {
return try await NetworkService.shared.fetchData(from: EmployeeServiceMode.malformed.endpoint, as: Employees.self)
}
public func getEmployees(page: Int, perPage: Int, sortField: EmployeeSortField, sortOrder: EmployeeSortOrder) async throws -> Employees {
return try await NetworkService.shared.fetchData(from: EmployeeServiceMode.malformed.endpoint, as: Employees.self)
}
}
/// Service Layer for Employees
public class EmployeeEmptyService: EmployeeServiceProtocol {
// MARK: - Properties
public static let shared = EmployeeEmptyService() // Default shared instance
// MARK: - Initializer
public init() {}
// MARK: - Public Methods
/// This will get a list of all employees
/// - Parameter serviceMode: Mode in which to hit.
/// - Returns: An Employees struct
public func getEmployees() async throws -> Employees {
return try await NetworkService.shared.fetchData(from: EmployeeServiceMode.empty.endpoint, as: Employees.self)
}
public func getEmployees(page: Int, perPage: Int, sortField: EmployeeSortField, sortOrder: EmployeeSortOrder) async throws -> Employees {
return try await NetworkService.shared.fetchData(from: EmployeeServiceMode.empty.endpoint, as: Employees.self)
public func getEmployees(_ serviceMode: EmployeeServiceMode = .production) async throws -> Employees {
return try await NetworkService.shared.fetchData(from: serviceMode.endpoint, as: Employees.self)
}
}

View File

@ -1,85 +0,0 @@
//
// MockEmployeeService.swift
// EmployeeDirectory
//
// Created by Matt Bruce on 1/21/25.
//
import Foundation
public class MockEmployeeService: EmployeeServiceProtocol {
// MARK: - Properties
public static let shared = MockEmployeeService() // Default shared instance
private var wrapper: Employees
private var sortField: EmployeeSortField = .fullName
private var sortOrder: EmployeeSortOrder = .ascending
// MARK: - Initializer
public init() {
let jsonFileName = "localTest"
guard let url = Bundle.main.url(forResource: jsonFileName, withExtension: "json"),
let data = try? Data(contentsOf: url),
let localData = try? JSONDecoder().decode(Employees.self, from: data) else {
wrapper = .init(employees: [], total: 0, page: 0, perPage: 0)
return
}
wrapper = localData
sortEmployees()
}
public func getEmployees() async throws -> Employees {
return wrapper
}
public func getEmployees(page: Int, perPage: Int,
sortField: EmployeeSortField = .fullName,
sortOrder: EmployeeSortOrder = .ascending) async throws -> Employees {
//resort mock data
if sortField != self.sortField || sortOrder != self.sortOrder {
self.sortField = sortField
self.sortOrder = sortOrder
sortEmployees()
}
let totalUsers = wrapper.employees.count
let startIndex = (page - 1) * perPage
let endIndex = min(startIndex + perPage, totalUsers)
guard startIndex < totalUsers else {
return .init(employees: [], total: totalUsers, page: page, perPage: perPage) // Return empty if out of bounds
}
//paged
let pagedEmployees = Array(wrapper.employees[startIndex..<endIndex])
return .init(employees: pagedEmployees, total: totalUsers, page: page, perPage: perPage)
}
private func sortEmployees() {
wrapper = .init(employees: wrapper.employees.sorted(by: sortField, with: sortOrder),
total: wrapper.employees.count,
page: 1,
perPage: 10)
}
}
extension Array where Element == Employee {
func sorted(by field: EmployeeSortField, with option: EmployeeSortOrder) -> [Employee] {
switch field {
case .fullName:
return option == .ascending
? sorted { $0.fullName < $1.fullName }
: sorted { $0.fullName > $1.fullName }
case .team:
return option == .ascending
? sorted { $0.team < $1.team || ($0.team == $1.team && $0.fullName < $1.fullName) }
: sorted { $0.team > $1.team || ($0.team == $1.team && $0.fullName < $1.fullName) }
case .employeeType:
return option == .ascending
? sorted { $0.employeeType.rawValue < $1.employeeType.rawValue || ($0.employeeType.rawValue == $1.employeeType.rawValue && $0.fullName < $1.fullName) }
: sorted { $0.employeeType.rawValue > $1.employeeType.rawValue || ($0.employeeType.rawValue == $1.employeeType.rawValue && $0.fullName < $1.fullName) }
}
}
}

View File

@ -49,24 +49,14 @@ public class NetworkService {
/// - Throws: A `ServiceError` for network, decoding, or unexpected errors.
/// - Returns: The decoded object of the specified type.
public func fetchData<T: Decodable>(from endpoint: String, as type: T.Type) async throws -> T {
do {
//ensure a valid URL
guard let url = URL(string: endpoint) else {
throw NetworkServiceError.invalidURL
}
return try await fetchData(with: URLRequest(url: url), as: type)
}
/// Fetches data using a URLRequest and decodes it into a generic Decodable type.
/// - Parameters:
/// - request: The URLRequest to execute.
/// - type: The type to decode the data into.
/// - Throws: A `NetworkServiceError` for network, decoding, or unexpected errors.
/// - Returns: The decoded object of the specified type.
public func fetchData<T: Decodable>(with request: URLRequest, as type: T.Type) async throws -> T {
do {
// Perform network request
let (data, response) = try await session.data(for: request)
let (data, response) = try await URLSession.shared.data(for: URLRequest(url: url))
// Validate HTTP response
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {

File diff suppressed because it is too large Load Diff

View File

@ -16,9 +16,6 @@ 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)
@ -34,81 +31,26 @@ class EmployeesViewController: UIViewController {
/// Will show specific state of the viewModel
private var footerView: TableFooterView?
// Prevents multiple calls during rapid scrolling
private var isFetchingNextPage: Bool = false
private var dataSource: UITableViewDiffableDataSource<Int, Employee>!
// MARK: - Public Methods
public override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupDataSource()
bindViewModel()
viewModel.fetchEmployees()
}
// MARK: - Private Methods
/// Setting up the TableView Datasource
private func setupDataSource() {
dataSource = UITableViewDiffableDataSource<Int, Employee>(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<Int, Employee>()
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.delegate = self
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)
])
tableView.dataSource = self
view.addSubview(tableView)
tableView.frame = view.bounds
//add pull to refresh
tableView.refreshControl = UIRefreshControl()
@ -118,66 +60,30 @@ class EmployeesViewController: UIViewController {
activityIndicator.center = view.center
view.addSubview(activityIndicator)
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() {
let alert = UIAlertController(title: "Sort Employees", message: "Select a sort option", preferredStyle: .actionSheet)
alert.addAction(UIAlertAction(title: "Full Name (Asc)", style: .default) { [weak self] _ in
self?.viewModel.sortField = .fullName
self?.viewModel.sortOrder = .ascending
})
alert.addAction(UIAlertAction(title: "Full Name (Desc)", style: .default) { [weak self] _ in
self?.viewModel.sortField = .fullName
self?.viewModel.sortOrder = .descending
})
alert.addAction(UIAlertAction(title: "Team (Asc)", style: .default) { [weak self] _ in
self?.viewModel.sortField = .team
self?.viewModel.sortOrder = .ascending
})
alert.addAction(UIAlertAction(title: "Team (Desc)", style: .default) { [weak self] _ in
self?.viewModel.sortField = .team
self?.viewModel.sortOrder = .descending
})
alert.addAction(UIAlertAction(title: "Employee Type (Asc)", style: .default) { [weak self] _ in
self?.viewModel.sortField = .employeeType
self?.viewModel.sortOrder = .ascending
})
alert.addAction(UIAlertAction(title: "Employee Type (Desc)", style: .default) { [weak self] _ in
self?.viewModel.sortField = .employeeType
self?.viewModel.sortOrder = .ascending
})
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
present(alert, animated: true, completion: nil)
// 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
.sink { [weak self] _ in
self?.updateFooter()
self?.applySnapshot(employees: employees)
self?.tableView.reloadData()
self?.tableView.refreshControl?.endRefreshing()
}
.store(in: &cancellables)
viewModel.$isLoading
.receive(on: RunLoop.main)
.sink { [weak self] isLoading in
guard let self, let refreshControl = self.tableView.refreshControl else { return }
if isLoading {
activityIndicator.startAnimating()
self?.activityIndicator.startAnimating()
} else {
activityIndicator.stopAnimating()
refreshControl.endRefreshing()
updateFooter()
self?.activityIndicator.stopAnimating()
}
}
.store(in: &cancellables)
@ -192,63 +98,29 @@ class EmployeesViewController: UIViewController {
/// Show state in specific use-cases for the EmployeesViewModel
private func updateFooter() {
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." : nil) {
footerMessage = message
}
// Show loading footer if there are more pages to load
else if (viewModel.isLoading || isFetchingNextPage) && viewModel.hasMorePages {
footerMessage = "Loading more employees..."
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 footerMessage {
if let message, !viewModel.isLoading {
// Lazy initialize footerView if needed
if footerView == nil {
footerView = TableFooterView(message: footerMessage)
} else {
footerView?.update(message: footerMessage)
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: - UITableViewDelegate
extension EmployeesViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
let scrollViewHeight = scrollView.frame.size.height
if offsetY > contentHeight - scrollViewHeight - 100 && !isFetchingNextPage {
isFetchingNextPage = true
updateFooter()
viewModel.loadNextPage()
// Reset the flag after a short delay to allow new fetches
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
self?.isFetchingNextPage = false
}
}
}
}
// Mark: - Objective-C Methods
extension EmployeesViewController {
/// Show sort options
@objc private func showSortOptions() {
presentSortingActionSheet()
}
/// Fetch the Employees
@objc private func didPullToRefresh() {
viewModel.fetchEmployees()
@ -264,18 +136,22 @@ extension EmployeesViewController {
case 2: selectedMode = .empty
default: return
}
viewModel.changeService(to: selectedMode.service)
viewModel.changeMode(to: selectedMode)
}
}
extension EmployeesViewController: UISearchBarDelegate {
public func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
viewModel.searchText = searchText // Updates search text in ViewModel
/// Mark: - UITableViewDataSource
extension EmployeesViewController: UITableViewDataSource {
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.employees.count
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
searchBar.text = ""
searchBar.resignFirstResponder()
viewModel.searchText = "" // Clear search and reset list
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: EmployeeTableViewCell.identifier, for: indexPath) as? EmployeeTableViewCell else {
return UITableViewCell()
}
let employee = viewModel.employees[indexPath.row]
cell.configure(with: EmployeeCellViewModel(employee: employee))
return cell
}
}

View File

@ -6,126 +6,46 @@
//
import Foundation
import Combine
/// ViewModel that will be bound to an Employees model and used
/// specifically with the EmployeesViewController.
@MainActor
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<AnyCancellable>()
private var currentPage = 1
private let perPage = 10
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)
.dropFirst()
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.sink { [weak self] _, _ in
self?.resetAndFetchEmployees()
}
.store(in: &cancellables)
}
/// Fetch employees for the given page
public func fetchEmployees(page: Int = 1) {
// Prevent duplicate calls
guard !isLoading else { return }
public init() {}
public func fetchEmployees() {
// resetting values out the values before fetching new data
errorMessage = nil
isLoading = true
Task {
do {
// Fetch employees using the paginated API
let wrapper = try await employeeService.getEmployees(page: page,
perPage: perPage,
sortField: sortField,
sortOrder: sortOrder)
totalEmployees = wrapper.total
currentPage = page
hasMorePages = wrapper.employees.count < totalEmployees
// Fetch employees using the async method
let wrapper = try await EmployeeService.shared.getEmployees(serviceMode)
// Update published properties
if page == 1 {
allEmployees = wrapper.employees // Replace list for the first page
} else {
allEmployees.append(contentsOf: wrapper.employees) // Append for subsequent pages
}
filterEmployees(by: searchText)
employees = wrapper.employees
isLoading = false
} catch {
// Handle errors
errorMessage = "An unexpected error occurred, please try to refresh."
}
isLoading = false
}
}
/// Load the next page of employees
public func loadNextPage() {
guard hasMorePages else { return }
fetchEmployees(page: currentPage + 1)
}
/// Resets the current employee list and fetches data from page 1
private func resetAndFetchEmployees() {
currentPage = 1
employees = []
allEmployees = []
hasMorePages = true
fetchEmployees(page: 1)
}
public func changeService(to employeeService: EmployeeServiceProtocol) {
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())
}
isLoading = false
errorMessage = "An unexpected error occurred, please try to refresh"
}
}
}
public func changeMode(to mode: EmployeeServiceMode) {
serviceMode = mode
fetchEmployees()
}
}

View File

@ -128,7 +128,6 @@ public class EmployeeTableViewCell: UITableViewCell {
// Bind the image to the photoImageView
smallPhotoSubscriber = viewModel.$smallPhoto
.compactMap { $0 }
.receive(on: DispatchQueue.main)
.sink { [weak self] image in
self?.photoImageView.image = image