Compare commits
No commits in common. "coding-pairing-1" and "main" have entirely different histories.
coding-pai
...
main
@ -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
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
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(_ serviceMode: EmployeeServiceMode) async throws -> Employees
|
||||
}
|
||||
|
||||
@ -11,25 +11,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
let keyAppVersion = "savedAppVersion"
|
||||
|
||||
func getAppVersion() -> String {
|
||||
if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
|
||||
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
|
||||
return "\(version) (\(build))"
|
||||
}
|
||||
return "Unknown Version"
|
||||
}
|
||||
|
||||
// Retrieve saved version
|
||||
func getSavedAppVersion() -> String? {
|
||||
return UserDefaults.standard.string(forKey: keyAppVersion)
|
||||
}
|
||||
|
||||
func saveAppVersion(_ version: String) {
|
||||
UserDefaults.standard.set(version, forKey: keyAppVersion)
|
||||
UserDefaults.standard.synchronize()
|
||||
}
|
||||
|
||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||
|
||||
@ -44,19 +25,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
let navigationController = UINavigationController(rootViewController: employeeListVC)
|
||||
window.rootViewController = navigationController
|
||||
|
||||
// get app version
|
||||
let currentAppVersion = getAppVersion()
|
||||
|
||||
// get app cached version
|
||||
let savedAppVersion = getSavedAppVersion() ?? "none"
|
||||
|
||||
// check to see if there is a diff and if so clear cache
|
||||
if savedAppVersion != currentAppVersion {
|
||||
EmployeeCacheService.shared.clear()
|
||||
// save the current app version for next run
|
||||
saveAppVersion(currentAppVersion)
|
||||
}
|
||||
|
||||
// Set the window to the scene
|
||||
self.window = window
|
||||
window.makeKeyAndVisible()
|
||||
|
||||
@ -1,46 +0,0 @@
|
||||
//
|
||||
// EmployeeCacheService.swift
|
||||
// EmployeeDirectory
|
||||
//
|
||||
// Created by Matt Bruce on 2/6/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
/// A service that handles image caching using memory, disk, and network in priority order.
|
||||
public class EmployeeCacheService {
|
||||
private lazy var fileURL = cacheDirectory.appendingPathComponent("employees.json")
|
||||
|
||||
// MARK: - Properties
|
||||
public static let shared = EmployeeCacheService() // Default shared instance
|
||||
|
||||
/// Memory cache for storing images in RAM.
|
||||
private let emplyoees: Employees? = nil
|
||||
|
||||
/// File manager for handling disk operations.
|
||||
private let fileManager = FileManager.default
|
||||
|
||||
/// Directory where cached images are stored on disk.
|
||||
private let cacheDirectory: URL = {
|
||||
FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
||||
}()
|
||||
|
||||
// MARK: - Initializer
|
||||
|
||||
public init() {}
|
||||
|
||||
public func save(from employees: Employees) throws {
|
||||
let data = try JSONEncoder().encode(employees)
|
||||
try data.write(to: fileURL)
|
||||
}
|
||||
|
||||
public func load() throws -> Employees {
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
return try JSONDecoder().decode(Employees.self, from: data)
|
||||
}
|
||||
|
||||
public func clear() {
|
||||
try? FileManager.default.removeItem(at: fileURL)
|
||||
}
|
||||
}
|
||||
@ -7,22 +7,11 @@
|
||||
import Foundation
|
||||
|
||||
/// 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 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 {
|
||||
@ -36,10 +25,8 @@ internal enum EmployeeServiceMode: String, CaseIterable {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Service Layer for Employees
|
||||
public class EmployeeService: EmployeeServiceProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
public static let shared = EmployeeService() // Default shared instance
|
||||
|
||||
@ -52,59 +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 {
|
||||
let cache = EmployeeCacheService.shared
|
||||
|
||||
//try to load the employees
|
||||
if let employees = try? cache.load() {
|
||||
return employees
|
||||
} else {
|
||||
//fetch the employees
|
||||
let employees = try await NetworkService.shared.fetchData(from: EmployeeServiceMode.production.endpoint, as: Employees.self)
|
||||
|
||||
//save to cache
|
||||
try? EmployeeCacheService.shared.save(from: employees)
|
||||
|
||||
//return class
|
||||
return employees
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(_ serviceMode: EmployeeServiceMode = .production) async throws -> Employees {
|
||||
return try await NetworkService.shared.fetchData(from: serviceMode.endpoint, as: Employees.self)
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,46 +31,24 @@ class EmployeesViewController: UIViewController {
|
||||
/// Will show specific state of the viewModel
|
||||
private var footerView: TableFooterView?
|
||||
|
||||
private var dataSource: UITableViewDiffableDataSource<Int, Employee>!
|
||||
|
||||
// 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<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.register(EmployeeTableViewCell.self, forCellReuseIdentifier: EmployeeTableViewCell.identifier)
|
||||
tableView.dataSource = self
|
||||
view.addSubview(tableView)
|
||||
tableView.frame = view.bounds
|
||||
|
||||
@ -92,8 +70,10 @@ class EmployeesViewController: UIViewController {
|
||||
private func bindViewModel() {
|
||||
viewModel.$employees
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] employees in
|
||||
self?.applySnapshot(employees: employees)
|
||||
.sink { [weak self] _ in
|
||||
self?.updateFooter()
|
||||
self?.tableView.reloadData()
|
||||
self?.tableView.refreshControl?.endRefreshing()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
@ -104,8 +84,6 @@ class EmployeesViewController: UIViewController {
|
||||
self?.activityIndicator.startAnimating()
|
||||
} else {
|
||||
self?.activityIndicator.stopAnimating()
|
||||
self?.tableView.refreshControl?.endRefreshing()
|
||||
self?.updateFooter()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@ -158,6 +136,22 @@ extension EmployeesViewController {
|
||||
case 2: selectedMode = .empty
|
||||
default: return
|
||||
}
|
||||
viewModel.changeService(to: selectedMode.service)
|
||||
viewModel.changeMode(to: selectedMode)
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark: - UITableViewDataSource
|
||||
extension EmployeesViewController: UITableViewDataSource {
|
||||
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return viewModel.employees.count
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ import Foundation
|
||||
/// specifically with the EmployeesViewController.
|
||||
@MainActor
|
||||
public class EmployeesViewModel: ObservableObject {
|
||||
private var employeeService: EmployeeServiceProtocol = EmployeeService()
|
||||
private var serviceMode: EmployeeServiceMode = .production
|
||||
|
||||
@Published public private(set) var employees: [Employee] = []
|
||||
@Published public private(set) var errorMessage: String? = nil
|
||||
@ -27,7 +27,7 @@ public class EmployeesViewModel: ObservableObject {
|
||||
Task {
|
||||
do {
|
||||
// Fetch employees using the async method
|
||||
let wrapper = try await employeeService.getEmployees()
|
||||
let wrapper = try await EmployeeService.shared.getEmployees(serviceMode)
|
||||
|
||||
// Update published properties
|
||||
employees = wrapper.employees
|
||||
@ -43,8 +43,8 @@ public class EmployeesViewModel: ObservableObject {
|
||||
|
||||
}
|
||||
|
||||
public func changeService(to employeeService: EmployeeServiceProtocol) {
|
||||
self.employeeService = employeeService
|
||||
public func changeMode(to mode: EmployeeServiceMode) {
|
||||
serviceMode = mode
|
||||
fetchEmployees()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user