commented the code.

Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
Matt Bruce 2025-01-21 09:12:56 -06:00
parent 0ad993ca6b
commit f07089a1fc
7 changed files with 91 additions and 7 deletions

View File

@ -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)

View File

@ -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 {

View File

@ -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
}

View File

@ -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<NSString, UIImage>()
/// 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() {}
// 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)

View File

@ -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.

View File

@ -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<AnyCancellable>()
/// 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)
@ -75,6 +96,7 @@ 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

View File

@ -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,6 +48,7 @@ public class TableFooterView: UIView {
])
}
// MARK: - Public Methods
/// Updates the Current Message
/// - Parameter message: message to show