diff --git a/EmployeeDirectory/Extensions/URL.swift b/EmployeeDirectory/Extensions/URL.swift index e146d27..74329cf 100644 --- a/EmployeeDirectory/Extensions/URL.swift +++ b/EmployeeDirectory/Extensions/URL.swift @@ -8,6 +8,8 @@ import Foundation import CryptoKit extension URL { + + /// This will has the URL absoluteString into a that is consistent. internal var uniqueIdentifier: String { let data = Data(absoluteString.utf8) let hash = SHA256.hash(data: data) diff --git a/EmployeeDirectory/Models/Employees.swift b/EmployeeDirectory/Models/Employees.swift index 2f2ebaa..f7bf329 100644 --- a/EmployeeDirectory/Models/Employees.swift +++ b/EmployeeDirectory/Models/Employees.swift @@ -8,6 +8,8 @@ import Foundation /// Wrapper JSON Class for the Employees public struct Employees: Codable { + + /// Array of Employees public var employees: [Employee] private enum CodingKeys: String, CodingKey { diff --git a/EmployeeDirectory/Protocols/EmployeeServiceProtocol.swift b/EmployeeDirectory/Protocols/EmployeeServiceProtocol.swift index 4cbe15d..809baf2 100644 --- a/EmployeeDirectory/Protocols/EmployeeServiceProtocol.swift +++ b/EmployeeDirectory/Protocols/EmployeeServiceProtocol.swift @@ -5,6 +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(_ serviceMode: EmployeeServiceMode) async throws -> Employees } diff --git a/EmployeeDirectory/Services/ImageCacheService.swift b/EmployeeDirectory/Services/ImageCacheService.swift index ba45f2a..6aadff4 100644 --- a/EmployeeDirectory/Services/ImageCacheService.swift +++ b/EmployeeDirectory/Services/ImageCacheService.swift @@ -8,49 +8,80 @@ import Foundation import UIKit +/// A service that handles image caching using memory, disk, and network in priority order. public class ImageCacheService { + // MARK: - Properties public static let shared = ImageCacheService() // Default shared instance + /// Memory cache for storing images in RAM. private let memoryCache = NSCache() + + /// 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] + FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] }() + // MARK: - Initializer + public init() {} + // MARK: - Public Methods + + /// Loads an image from memory, disk, or network. + /// + /// - Parameter url: The URL of the image to load. + /// - Returns: The loaded `UIImage` or `nil` if the image could not be loaded. public func loadImage(from url: URL) async -> UIImage? { let uniqueKey = url.uniqueIdentifier let cacheKey = uniqueKey as NSString - + + // Step 1: Check the memory cache if let cachedImage = memoryCache.object(forKey: cacheKey) { return cachedImage } + // Step 2: Check the disk cache let diskImagePath = cacheDirectory.appendingPathComponent(uniqueKey) if let diskImage = UIImage(contentsOfFile: diskImagePath.path) { - memoryCache.setObject(diskImage, forKey: cacheKey) + memoryCache.setObject(diskImage, forKey: cacheKey) // Cache in memory for faster access next time return diskImage } + // Step 3: Fetch the image from the network if let networkImage = await fetchFromNetwork(url: url) { - memoryCache.setObject(networkImage, forKey: cacheKey) - try? saveImageToDisk(image: networkImage, at: diskImagePath) + memoryCache.setObject(networkImage, forKey: cacheKey) // Cache in memory + try? saveImageToDisk(image: networkImage, at: diskImagePath) // Cache on disk return networkImage } + // Step 4: Return nil if all options fail return nil } + // MARK: - Private Methods + + /// Fetches an image from the network. + /// + /// - Parameter url: The URL of the image to fetch. + /// - Returns: The fetched `UIImage` or `nil` if the network request fails. private func fetchFromNetwork(url: URL) async -> UIImage? { do { let (data, _) = try await URLSession.shared.data(from: url) return UIImage(data: data) } catch { + print("Failed to fetch image from network for URL: \(url). Error: \(error)") return nil } } + /// Saves an image to disk at the specified path. + /// + /// - Parameters: + /// - image: The `UIImage` to save. + /// - path: The file path where the image should be saved. private func saveImageToDisk(image: UIImage, at path: URL) throws { guard let data = image.pngData() else { return } try data.write(to: path) diff --git a/EmployeeDirectory/Services/NetworkService.swift b/EmployeeDirectory/Services/NetworkService.swift index 80582dd..cddd430 100644 --- a/EmployeeDirectory/Services/NetworkService.swift +++ b/EmployeeDirectory/Services/NetworkService.swift @@ -26,15 +26,22 @@ public enum NetworkServiceError: Error { public class NetworkService { + // MARK: - Properties + public static let shared = NetworkService() // Default shared instance private let session: URLSession + + // MARK: - Initializer + /// Public initializer to allow customization public init(session: URLSession = .shared) { self.session = session } + // MARK: - Public Methods + /// Fetches data from a URL and decodes it into a generic Decodable type. /// - Parameters: /// - endpoint: The url to fetch data from. diff --git a/EmployeeDirectory/ViewControllers/EmployeesViewController.swift b/EmployeeDirectory/ViewControllers/EmployeesViewController.swift index 6fc1740..cfb192e 100644 --- a/EmployeeDirectory/ViewControllers/EmployeesViewController.swift +++ b/EmployeeDirectory/ViewControllers/EmployeesViewController.swift @@ -9,13 +9,30 @@ 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? + // MARK: - Public Methods + public override func viewDidLoad() { super.viewDidLoad() setupUI() @@ -23,6 +40,9 @@ class EmployeesViewController: UIViewController { viewModel.fetchEmployees() } + // MARK: - Private Methods + + /// Setup the UI by adding the views to the main view private func setupUI() { view.backgroundColor = .white @@ -46,6 +66,7 @@ class EmployeesViewController: UIViewController { navigationItem.titleView = modeSegmentedControl } + /// Using the ViewModel setup combine handlers private func bindViewModel() { viewModel.$employees .receive(on: RunLoop.main) @@ -74,7 +95,8 @@ class EmployeesViewController: UIViewController { } .store(in: &cancellables) } - + + /// Show state in specific use-cases for the EmployeesViewModel private func updateFooter() { var message: String? { guard !viewModel.isLoading else { return nil } @@ -96,11 +118,16 @@ class EmployeesViewController: UIViewController { } } +// 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 { @@ -113,6 +140,7 @@ extension EmployeesViewController { } } +/// Mark: - UITableViewDataSource extension EmployeesViewController: UITableViewDataSource { public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return viewModel.employees.count diff --git a/EmployeeDirectory/Views/TableFooterView.swift b/EmployeeDirectory/Views/TableFooterView.swift index 4266cc3..1b7748e 100644 --- a/EmployeeDirectory/Views/TableFooterView.swift +++ b/EmployeeDirectory/Views/TableFooterView.swift @@ -6,8 +6,11 @@ // import UIKit +/// Meant to be used as a Message for State in a TableView. public class TableFooterView: UIView { + // MARK: - Properties + /// Label used to show the message private let messageLabel: UILabel = { let label = UILabel() @@ -20,6 +23,8 @@ public class TableFooterView: UIView { return label }() + // MARK: - Initializer + init(message: String) { super.init(frame: .zero) setupUI() @@ -30,6 +35,8 @@ public class TableFooterView: UIView { fatalError("init(coder:) has not been implemented") } + // MARK: - Private Methods + /// Setup the UI private func setupUI() { addSubview(messageLabel) @@ -41,7 +48,8 @@ public class TableFooterView: UIView { ]) } - + // MARK: - Public Methods + /// Updates the Current Message /// - Parameter message: message to show public func update(message: String) {