Compare commits

..

10 Commits

Author SHA1 Message Date
d9ca737896 more updates to ReadMe
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
2025-01-21 09:38:19 -06:00
415f7aa030 updated Readme
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
2025-01-21 09:37:14 -06:00
bf70ccb19a finished the readme
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
2025-01-21 09:35:01 -06:00
8efd109a46 updated test cases for new Testing Framework macros.
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
2025-01-21 09:20:41 -06:00
6f7a43d15e commented code
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
2025-01-21 09:15:12 -06:00
069cc8c06a commented the code and refactored to new shared.
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
2025-01-21 09:14:15 -06:00
c127a6c465 commented the code and removed self.
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
2025-01-21 09:13:41 -06:00
f5c1a64e40 add shared property
commented the code

Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
2025-01-21 09:13:21 -06:00
f07089a1fc commented the code.
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
2025-01-21 09:12:56 -06:00
0ad993ca6b refactored to use 1 method and commented the code.
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
2025-01-21 09:11:06 -06:00
14 changed files with 203 additions and 51 deletions

View File

@ -12,11 +12,9 @@ extension String {
/// Non-numeric characters are removed, and formatting is applied based on the length of the string. /// Non-numeric characters are removed, and formatting is applied based on the length of the string.
/// - Returns: A formatted phone number as a string. /// - Returns: A formatted phone number as a string.
internal func formatUSNumber() -> String { internal func formatUSNumber() -> String {
// format the number // mask for the phone numver
return format(with: "XXX-XXX-XXXX", phone: self) let mask = "XXX-XXX-XXXX"
}
internal func format(with mask: String, phone: String) -> String {
let numbers = filter { $0.isNumber } let numbers = filter { $0.isNumber }
var result = "" var result = ""
var index = numbers.startIndex // numbers iterator var index = numbers.startIndex // numbers iterator
@ -35,6 +33,6 @@ extension String {
} }
} }
return result return result
} }
} }

View File

@ -8,6 +8,8 @@ import Foundation
import CryptoKit import CryptoKit
extension URL { extension URL {
/// This will has the URL absoluteString into a that is consistent.
internal var uniqueIdentifier: String { internal var uniqueIdentifier: String {
let data = Data(absoluteString.utf8) let data = Data(absoluteString.utf8)
let hash = SHA256.hash(data: data) let hash = SHA256.hash(data: data)

View File

@ -8,6 +8,8 @@ import Foundation
/// Wrapper JSON Class for the Employees /// Wrapper JSON Class for the Employees
public struct Employees: Codable { public struct Employees: Codable {
/// Array of Employees
public var employees: [Employee] public var employees: [Employee]
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {

View File

@ -5,6 +5,12 @@
// Created by Matt Bruce on 1/20/25. // Created by Matt Bruce on 1/20/25.
// //
/// This will be the interface for the API for Employees
public protocol EmployeeServiceProtocol { 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 func getEmployees(_ serviceMode: EmployeeServiceMode) async throws -> Employees
} }

View File

@ -12,6 +12,7 @@ public enum EmployeeServiceMode: String, CaseIterable {
case malformed case malformed
case empty case empty
/// Enpoint in which to grabe employees from.
public var endpoint: String { public var endpoint: String {
switch self { switch self {
case .production: case .production:
@ -26,11 +27,18 @@ public enum EmployeeServiceMode: String, CaseIterable {
/// Service Layer for Employees /// Service Layer for Employees
public class EmployeeService: EmployeeServiceProtocol { public class EmployeeService: EmployeeServiceProtocol {
// MARK: - Properties
public static let shared = EmployeeService() // Default shared instance
// MARK: - Initializer
public init() {} public init() {}
/// Service to get Employees // MARK: - Public Methods
/// - Returns: Array of Employee Structs
/// This will get a list of all employees
/// - Parameter serviceMode: Mode in which to hit.
/// - Returns: An Employees struct
public func getEmployees(_ serviceMode: EmployeeServiceMode = .production) async throws -> Employees { public func getEmployees(_ serviceMode: EmployeeServiceMode = .production) async throws -> Employees {
return try await NetworkService.shared.fetchData(from: serviceMode.endpoint, as: Employees.self) return try await NetworkService.shared.fetchData(from: serviceMode.endpoint, as: Employees.self)
} }

View File

@ -8,49 +8,80 @@
import Foundation import Foundation
import UIKit import UIKit
/// A service that handles image caching using memory, disk, and network in priority order.
public class ImageCacheService { public class ImageCacheService {
// MARK: - Properties
public static let shared = ImageCacheService() // Default shared instance public static let shared = ImageCacheService() // Default shared instance
/// Memory cache for storing images in RAM.
private let memoryCache = NSCache<NSString, UIImage>() private let memoryCache = NSCache<NSString, UIImage>()
/// File manager for handling disk operations.
private let fileManager = FileManager.default private let fileManager = FileManager.default
/// Directory where cached images are stored on disk.
private let cacheDirectory: URL = { private let cacheDirectory: URL = {
FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
}() }()
// MARK: - Initializer
public init() {} 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? { public func loadImage(from url: URL) async -> UIImage? {
let uniqueKey = url.uniqueIdentifier let uniqueKey = url.uniqueIdentifier
let cacheKey = uniqueKey as NSString let cacheKey = uniqueKey as NSString
// Step 1: Check the memory cache
if let cachedImage = memoryCache.object(forKey: cacheKey) { if let cachedImage = memoryCache.object(forKey: cacheKey) {
return cachedImage return cachedImage
} }
// Step 2: Check the disk cache
let diskImagePath = cacheDirectory.appendingPathComponent(uniqueKey) let diskImagePath = cacheDirectory.appendingPathComponent(uniqueKey)
if let diskImage = UIImage(contentsOfFile: diskImagePath.path) { 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 return diskImage
} }
// Step 3: Fetch the image from the network
if let networkImage = await fetchFromNetwork(url: url) { if let networkImage = await fetchFromNetwork(url: url) {
memoryCache.setObject(networkImage, forKey: cacheKey) memoryCache.setObject(networkImage, forKey: cacheKey) // Cache in memory
try? saveImageToDisk(image: networkImage, at: diskImagePath) try? saveImageToDisk(image: networkImage, at: diskImagePath) // Cache on disk
return networkImage return networkImage
} }
// Step 4: Return nil if all options fail
return nil 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? { private func fetchFromNetwork(url: URL) async -> UIImage? {
do { do {
let (data, _) = try await URLSession.shared.data(from: url) let (data, _) = try await URLSession.shared.data(from: url)
return UIImage(data: data) return UIImage(data: data)
} catch { } catch {
print("Failed to fetch image from network for URL: \(url). Error: \(error)")
return nil 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 { private func saveImageToDisk(image: UIImage, at path: URL) throws {
guard let data = image.pngData() else { return } guard let data = image.pngData() else { return }
try data.write(to: path) try data.write(to: path)

View File

@ -26,15 +26,22 @@ public enum NetworkServiceError: Error {
public class NetworkService { public class NetworkService {
// MARK: - Properties
public static let shared = NetworkService() // Default shared instance public static let shared = NetworkService() // Default shared instance
private let session: URLSession private let session: URLSession
// MARK: - Initializer
/// Public initializer to allow customization /// Public initializer to allow customization
public init(session: URLSession = .shared) { public init(session: URLSession = .shared) {
self.session = session self.session = session
} }
// MARK: - Public Methods
/// Fetches data from a URL and decodes it into a generic Decodable type. /// Fetches data from a URL and decodes it into a generic Decodable type.
/// - Parameters: /// - Parameters:
/// - endpoint: The url to fetch data from. /// - endpoint: The url to fetch data from.

View File

@ -9,13 +9,30 @@ import UIKit
import Combine import Combine
class EmployeesViewController: UIViewController { class EmployeesViewController: UIViewController {
// MARK: - Properties
/// List for the employees
private let tableView = UITableView() private let tableView = UITableView()
/// Will only show when fetching data occurs
private let activityIndicator = UIActivityIndicatorView(style: .large) private let activityIndicator = UIActivityIndicatorView(style: .large)
/// Allows the user to pick between service modes
private let modeSegmentedControl = UISegmentedControl(items: EmployeeServiceMode.allCases.map{ $0.rawValue } ) private let modeSegmentedControl = UISegmentedControl(items: EmployeeServiceMode.allCases.map{ $0.rawValue } )
/// ViewModel in which drives the screen
private let viewModel = EmployeesViewModel() private let viewModel = EmployeesViewModel()
/// Holds onto the ViewModels Subscribers
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
/// Will show specific state of the viewModel
private var footerView: TableFooterView? private var footerView: TableFooterView?
// MARK: - Public Methods
public override func viewDidLoad() { public override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
setupUI() setupUI()
@ -23,6 +40,9 @@ class EmployeesViewController: UIViewController {
viewModel.fetchEmployees() viewModel.fetchEmployees()
} }
// MARK: - Private Methods
/// Setup the UI by adding the views to the main view
private func setupUI() { private func setupUI() {
view.backgroundColor = .white view.backgroundColor = .white
@ -46,6 +66,7 @@ class EmployeesViewController: UIViewController {
navigationItem.titleView = modeSegmentedControl navigationItem.titleView = modeSegmentedControl
} }
/// Using the ViewModel setup combine handlers
private func bindViewModel() { private func bindViewModel() {
viewModel.$employees viewModel.$employees
.receive(on: RunLoop.main) .receive(on: RunLoop.main)
@ -74,7 +95,8 @@ class EmployeesViewController: UIViewController {
} }
.store(in: &cancellables) .store(in: &cancellables)
} }
/// Show state in specific use-cases for the EmployeesViewModel
private func updateFooter() { private func updateFooter() {
var message: String? { var message: String? {
guard !viewModel.isLoading else { return nil } guard !viewModel.isLoading else { return nil }
@ -96,11 +118,16 @@ class EmployeesViewController: UIViewController {
} }
} }
// Mark: - Objective-C Methods
extension EmployeesViewController { extension EmployeesViewController {
/// Fetch the Employees
@objc private func didPullToRefresh() { @objc private func didPullToRefresh() {
viewModel.fetchEmployees() 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) { @objc private func onServiceModeChange(_ sender: UISegmentedControl) {
let selectedMode: EmployeeServiceMode let selectedMode: EmployeeServiceMode
switch sender.selectedSegmentIndex { switch sender.selectedSegmentIndex {
@ -113,6 +140,7 @@ extension EmployeesViewController {
} }
} }
/// Mark: - UITableViewDataSource
extension EmployeesViewController: UITableViewDataSource { extension EmployeesViewController: UITableViewDataSource {
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.employees.count return viewModel.employees.count

View File

@ -8,8 +8,11 @@
import Foundation import Foundation
import UIKit import UIKit
/// ViewModel that will be used along with the EmployeeTableViewCell.
@MainActor @MainActor
public class EmployeeCellViewModel: ObservableObject { public class EmployeeCellViewModel: ObservableObject {
// MARK: - Properties
private let employee: Employee private let employee: Employee
public private(set) var uuid: String public private(set) var uuid: String
@ -21,18 +24,21 @@ public class EmployeeCellViewModel: ObservableObject {
public private(set) var employeeType: String public private(set) var employeeType: String
@Published public private(set) var smallPhoto: UIImage? @Published public private(set) var smallPhoto: UIImage?
// MARK: - Initializer
public init(employee: Employee) { public init(employee: Employee) {
self.employee = employee self.employee = employee
// Initialize properties // Initialize properties
self.uuid = employee.uuid.uuidString uuid = employee.uuid.uuidString
self.fullName = employee.fullName fullName = employee.fullName
self.phoneNumber = employee.phoneNumber?.formatUSNumber() phoneNumber = employee.phoneNumber?.formatUSNumber()
self.emailAddress = employee.emailAddress emailAddress = employee.emailAddress
self.biography = employee.biography biography = employee.biography
self.team = employee.team team = employee.team
self.employeeType = employee.employeeType.description employeeType = employee.employeeType.description
// Fetch the image for the url if it exists
if let endpoint = employee.photoURLSmall { if let endpoint = employee.photoURLSmall {
Task{ Task{
if let smallPhotoURL = URL(string: endpoint) { if let smallPhotoURL = URL(string: endpoint) {

View File

@ -7,18 +7,17 @@
import Foundation import Foundation
/// ViewModel that will be bound to an Employees model and used
/// specifically with the EmployeesViewController.
@MainActor @MainActor
public class EmployeesViewModel: ObservableObject { public class EmployeesViewModel: ObservableObject {
private let service: EmployeeService
private var serviceMode: EmployeeServiceMode = .production private var serviceMode: EmployeeServiceMode = .production
@Published public private(set) var employees: [Employee] = [] @Published public private(set) var employees: [Employee] = []
@Published public private(set) var errorMessage: String? = nil @Published public private(set) var errorMessage: String? = nil
@Published public private(set) var isLoading: Bool = false @Published public private(set) var isLoading: Bool = false
public init(service: EmployeeService = EmployeeService()) { public init() {}
self.service = service
}
public func fetchEmployees() { public func fetchEmployees() {
// resetting values out the values before fetching new data // resetting values out the values before fetching new data
@ -28,17 +27,17 @@ public class EmployeesViewModel: ObservableObject {
Task { Task {
do { do {
// Fetch employees using the async method // Fetch employees using the async method
let wrapper = try await service.getEmployees(serviceMode) let wrapper = try await EmployeeService.shared.getEmployees(serviceMode)
// Update published properties // Update published properties
self.employees = wrapper.employees employees = wrapper.employees
self.isLoading = false isLoading = false
} catch { } catch {
// Handle errors // Handle errors
self.employees = [] employees = []
self.isLoading = false isLoading = false
self.errorMessage = "An unexpected error occurred, please try to refresh" errorMessage = "An unexpected error occurred, please try to refresh"
} }
} }

View File

@ -7,9 +7,17 @@
import UIKit import UIKit
import Combine import Combine
/// This is the Cell used in the EmployeesTableViewController to show
/// the properties of an Employee model.
public class EmployeeTableViewCell: UITableViewCell { public class EmployeeTableViewCell: UITableViewCell {
static let identifier = "EmployeeTableViewCell"
/// Used in the TableView registration
static let identifier = "EmployeeTableViewCell"
// MARK: - Properties
/// UI Elements
private let photoImageView = UIImageView() private let photoImageView = UIImageView()
private let nameLabel = UILabel() private let nameLabel = UILabel()
private let emailLabel = UILabel() private let emailLabel = UILabel()
@ -19,7 +27,10 @@ public class EmployeeTableViewCell: UITableViewCell {
private let bioLabel = UILabel() private let bioLabel = UILabel()
private let stackView = UIStackView() private let stackView = UIStackView()
private var cancellable: AnyCancellable? /// Used for grabbing the photo
private var smallPhotoSubscriber: AnyCancellable?
// MARK: - Initializer
public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier) super.init(style: style, reuseIdentifier: reuseIdentifier)
@ -30,6 +41,8 @@ public class EmployeeTableViewCell: UITableViewCell {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
// MARK: - Private Methods
private func setupUI() { private func setupUI() {
// Configure photoImageView // Configure photoImageView
photoImageView.contentMode = .scaleAspectFill photoImageView.contentMode = .scaleAspectFill
@ -93,9 +106,13 @@ public class EmployeeTableViewCell: UITableViewCell {
]) ])
} }
// MARK: - Public Methods
/// Override for setting back to a default state
public override func prepareForReuse() { public override func prepareForReuse() {
super.prepareForReuse() super.prepareForReuse()
cancellable = nil smallPhotoSubscriber = nil
photoImageView.image = UIImage(systemName: "person.crop.circle") photoImageView.image = UIImage(systemName: "person.crop.circle")
nameLabel.text = nil nameLabel.text = nil
emailLabel.text = nil emailLabel.text = nil
@ -105,10 +122,12 @@ public class EmployeeTableViewCell: UITableViewCell {
bioLabel.text = nil bioLabel.text = nil
} }
/// Configures the UI Elements with the properties of the EmployeeCellViewModel.
/// - Parameter viewModel: Employee Model wrapped for Cell.
public func configure(with viewModel: EmployeeCellViewModel) { public func configure(with viewModel: EmployeeCellViewModel) {
// Bind the image to the photoImageView // Bind the image to the photoImageView
cancellable = viewModel.$smallPhoto smallPhotoSubscriber = viewModel.$smallPhoto
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] image in .sink { [weak self] image in
self?.photoImageView.image = image self?.photoImageView.image = image

View File

@ -6,8 +6,11 @@
// //
import UIKit import UIKit
/// Meant to be used as a Message for State in a TableView.
public class TableFooterView: UIView { public class TableFooterView: UIView {
// MARK: - Properties
/// Label used to show the message /// Label used to show the message
private let messageLabel: UILabel = { private let messageLabel: UILabel = {
let label = UILabel() let label = UILabel()
@ -20,6 +23,8 @@ public class TableFooterView: UIView {
return label return label
}() }()
// MARK: - Initializer
init(message: String) { init(message: String) {
super.init(frame: .zero) super.init(frame: .zero)
setupUI() setupUI()
@ -30,6 +35,8 @@ public class TableFooterView: UIView {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
// MARK: - Private Methods
/// Setup the UI /// Setup the UI
private func setupUI() { private func setupUI() {
addSubview(messageLabel) addSubview(messageLabel)
@ -41,7 +48,8 @@ public class TableFooterView: UIView {
]) ])
} }
// MARK: - Public Methods
/// Updates the Current Message /// Updates the Current Message
/// - Parameter message: message to show /// - Parameter message: message to show
public func update(message: String) { public func update(message: String) {

View File

@ -6,7 +6,6 @@
// //
import Testing import Testing
import XCTest
@testable import EmployeeDirectory @testable import EmployeeDirectory
@ -14,36 +13,57 @@ struct EmployeeDirectoryTests {
@Test func getEmployeesValid() async throws { @Test func getEmployeesValid() async throws {
do { do {
let wrapper = try await EmployeeService().getEmployees(.production) let wrapper = try await EmployeeService.shared.getEmployees(.production)
#expect(wrapper.employees.count == 11) #expect(wrapper.employees.count == 11)
} catch { } catch {
XCTFail("Unexpected error: \(error)") #expect(Bool(false), "Unexpected error: \(error)")
} }
} }
@Test func getEmployeesMalformed() async throws { @Test func getEmployeesMalformed() async throws {
do { do {
let wrapper = try await EmployeeService().getEmployees(.malformed) _ = try await EmployeeService.shared.getEmployees(.malformed)
XCTFail("Expected to throw, but got \(wrapper.employees.count) employees") #expect(Bool(false), "Expected invalidResponse error, but no error was thrown")
} catch let error as NetworkServiceError { } catch let error as NetworkServiceError {
switch error { switch error {
case .invalidResponse: case .decodingError(let decodingError):
XCTAssertTrue(true, "Correctly threw invalidResponse error") #expect(Bool(true), "Expected NetworkServiceError.decodingError, but got \(decodingError)")
default: default:
XCTFail("Expected NetworkServiceError.invalidResponse, but got \(error)") #expect(Bool(false), "Expected NetworkServiceError.decodingError, but got \(error)")
} }
} catch { } catch {
XCTFail("Unexpected error: \(error)") #expect(Bool(false), "Unexpected error: \(error)")
} }
} }
@Test func getEmployeesEmpty() async throws { @Test func getEmployeesEmpty() async throws {
do { do {
let wrapper = try await EmployeeService().getEmployees(.empty) let wrapper = try await EmployeeService.shared.getEmployees(.empty)
#expect(wrapper.employees.count == 0) #expect(wrapper.employees.count == 0)
} catch { } catch {
XCTFail("Unexpected error: \(error)") #expect(Bool(false), "Unexpected error: \(error)")
} }
} }
} }
// Testing extension since this isn't needed in the code.
extension NetworkServiceError: @retroactive Equatable {
public static func == (lhs: NetworkServiceError, rhs: NetworkServiceError) -> Bool {
switch (lhs, rhs) {
case (.invalidResponse, .invalidResponse):
return true
case (.invalidURL, .invalidURL):
return true
case (.decodingError(let lhsError), .decodingError(let rhsError)):
return lhsError.localizedDescription == rhsError.localizedDescription
case (.networkError(let lhsError), .networkError(let rhsError)):
return lhsError.code == rhsError.code
case (.unknownError(let lhsError), .unknownError(let rhsError)):
return lhsError.localizedDescription == rhsError.localizedDescription
default:
return false
}
}
}

View File

@ -1,21 +1,39 @@
# Employee Directory # Employee Directory
## Build tools & versions used ## Build tools & versions used
Xcode 16.2, built with Swift 5.9.
## Steps to run the app ## Steps to run the app
No additional steps are required. Simply build and run the project. The app supports iPhone and iPad in portrait mode.
## What areas of the app did you focus on? ## What areas of the app did you focus on?
I focused on designing a robust architecture using a separation of concerns approach. I also implemented a functional UI for the employee directory.
## What was the reason for your focus? What problems were you trying to solve? ## What was the reason for your focus? What problems were you trying to solve?
My primary goal was to ensure the app's architecture was clean, modular, and easy to maintain or extend. I aimed to minimize tightly coupled code and ensure future features could be added with minimal changes. This approach improves testability and makes the codebase easier for others to understand and work with.
## How long did you spend on this project? ## How long did you spend on this project?
Approximately 5 hours:
- 4 hours on development and testing.
- 1 hour on code refinement, documentation, and ensuring best practices.
## Did you make any trade-offs for this project? What would you have done differently with more time? ## Did you make any trade-offs for this project? What would you have done differently with more time?
**Trade-offs**:
- The networking layer currently lacks a request builder. This limits the ability to add query parameters or authentication headers in a structured way.
- The UI uses standard UIKit components, which could be improved for a more modern or visually appealing design.
**With more time**, I would:
- Implement a dedicated `RequestBuilder` to make the networking layer more flexible and extensible.
- Enhance the UI by incorporating more modern design elements, animations, and responsiveness.
- Expand test coverage to include additional edge cases and integration tests.
## What do you think is the weakest part of your project? ## What do you think is the weakest part of your project?
The weakest areas are:
- The UI, which is functional but minimal, using standard UIKit elements.
- The networking layer, which, while functional, would benefit from additional abstraction (e.g., `RequestBuilder`) to handle authentication or dynamic query parameters more effectively.
## Did you copy any code or dependencies? Please make sure to attribute them here! ## Did you copy any code or dependencies? Please make sure to attribute them here!
### String.swift extension for dealing with TelephoneNumber from a generic regEx formatter I found on stackoverflow. I adapted a regular expression-based string extension for phone number formatting from a Stack Overflow post. While the logic was modified for this project, credit is due to the original author for the inspiration.
## Is there any other information youd like us to know?
## Is there any other information youd like us to know?
When building projects from scratch, I prioritize setting up the API/Service/Model layer and validating it with tests before focusing on the UI. My strengths lie in designing clean, scalable app architectures. While I am proficient in UIKit, I am less experienced with SwiftUI but eager to continue improving in that area.