Compare commits
No commits in common. "a1214cf38b49afa2e06cfe2046b5e1e8084e7ea4" and "255356ff19bcd550ffbad06681b86bc8df35b07d" have entirely different histories.
a1214cf38b
...
255356ff19
@ -1,16 +0,0 @@
|
|||||||
//
|
|
||||||
// URL.swift
|
|
||||||
// EmployeeDirectory
|
|
||||||
//
|
|
||||||
// Created by Matt Bruce on 1/20/25.
|
|
||||||
//
|
|
||||||
import Foundation
|
|
||||||
import CryptoKit
|
|
||||||
|
|
||||||
extension URL {
|
|
||||||
internal var uniqueIdentifier: String {
|
|
||||||
let data = Data(absoluteString.utf8)
|
|
||||||
let hash = SHA256.hash(data: data)
|
|
||||||
return hash.compactMap { String(format: "%02x", $0) }.joined()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
//
|
|
||||||
// Employee.swift
|
|
||||||
// EmployeeDirectory
|
|
||||||
//
|
|
||||||
// Created by Matt Bruce on 1/20/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
/// EmployeeType enum
|
|
||||||
/// - How the employee is classified.
|
|
||||||
public enum EmployeeType: String, Codable, CustomStringConvertible {
|
|
||||||
case fullTime = "FULL_TIME"
|
|
||||||
case partTime = "PART_TIME"
|
|
||||||
case contractor = "CONTRACTOR"
|
|
||||||
|
|
||||||
/// Format the employee type for display
|
|
||||||
public var description: String {
|
|
||||||
switch self {
|
|
||||||
case .fullTime: return "Full-Time"
|
|
||||||
case .partTime: return "Part-Time"
|
|
||||||
case .contractor: return "Contractor"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Employee Object
|
|
||||||
/// JSON Object defintion
|
|
||||||
/// - https://square.github.io/microsite/mobile-interview-project/
|
|
||||||
public struct Employee: Codable {
|
|
||||||
|
|
||||||
/// The unique identifier for the employee. Represented as a UUID.
|
|
||||||
public let uuid: UUID
|
|
||||||
|
|
||||||
/// The full name of the employee.
|
|
||||||
public let fullName: String
|
|
||||||
|
|
||||||
/// The phone number of the employee, sent as an unformatted string (eg, 5556661234).
|
|
||||||
public let phoneNumber: String?
|
|
||||||
|
|
||||||
/// The email address of the employee.
|
|
||||||
public let emailAddress: String
|
|
||||||
|
|
||||||
/// A short, tweet-length (~300 chars) string that the employee provided to describe themselves.
|
|
||||||
public let biography: String?
|
|
||||||
|
|
||||||
/// The URL of the employee’s small photo. Useful for list view.
|
|
||||||
public let photoURLSmall: String?
|
|
||||||
|
|
||||||
/// The URL of the employee’s full-size photo.
|
|
||||||
public let photoURLLarge: String?
|
|
||||||
|
|
||||||
/// The team they are on, represented as a human readable string.
|
|
||||||
public let team: String
|
|
||||||
|
|
||||||
/// How the employee is classified.
|
|
||||||
public let employeeType: EmployeeType
|
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
|
||||||
case uuid
|
|
||||||
case fullName = "full_name"
|
|
||||||
case phoneNumber = "phone_number"
|
|
||||||
case emailAddress = "email_address"
|
|
||||||
case biography
|
|
||||||
case photoURLSmall = "photo_url_small"
|
|
||||||
case photoURLLarge = "photo_url_large"
|
|
||||||
case team
|
|
||||||
case employeeType = "employee_type"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
//
|
|
||||||
// Employees.swift
|
|
||||||
// EmployeeDirectory
|
|
||||||
//
|
|
||||||
// Created by Matt Bruce on 1/20/25.
|
|
||||||
//
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
/// Wrapper JSON Class for the Employees
|
|
||||||
public struct Employees: Codable {
|
|
||||||
public var employees: [Employee]
|
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
|
||||||
case employees
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
//
|
|
||||||
// EmployeeServiceable.swift
|
|
||||||
// EmployeeDirectory
|
|
||||||
//
|
|
||||||
// Created by Matt Bruce on 1/20/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
public protocol EmployeeServiceProtocol {
|
|
||||||
func getEmployees(_ serviceMode: EmployeeServiceMode) async throws -> Employees
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
//
|
|
||||||
// EmployeeService.swift
|
|
||||||
// EmployeeDirectory
|
|
||||||
//
|
|
||||||
// Created by Matt Bruce on 1/20/25.
|
|
||||||
//
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
/// These are the testing URL Endpoints for different states
|
|
||||||
public enum EmployeeServiceMode: String, CaseIterable {
|
|
||||||
case production
|
|
||||||
case malformed
|
|
||||||
case empty
|
|
||||||
|
|
||||||
public var endpoint: String {
|
|
||||||
switch self {
|
|
||||||
case .production:
|
|
||||||
return "https://s3.amazonaws.com/sq-mobile-interview/employees.json"
|
|
||||||
case .malformed:
|
|
||||||
return "https://s3.amazonaws.com/sq-mobile-interview/employees_malformed.json"
|
|
||||||
case .empty:
|
|
||||||
return "https://s3.amazonaws.com/sq-mobile-interview/employees_empty.json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Service Layer for Employees
|
|
||||||
public class EmployeeService: EmployeeServiceProtocol {
|
|
||||||
|
|
||||||
public init() {}
|
|
||||||
|
|
||||||
/// Service to get Employees
|
|
||||||
/// - Returns: Array of Employee Structs
|
|
||||||
public func getEmployees(_ serviceMode: EmployeeServiceMode = .production) async throws -> Employees {
|
|
||||||
return try await NetworkService.shared.fetchData(from: serviceMode.endpoint, as: Employees.self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
//
|
|
||||||
// ImageCacheService.swift
|
|
||||||
// EmployeeDirectory
|
|
||||||
//
|
|
||||||
// Created by Matt Bruce on 1/20/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
public class ImageCacheService {
|
|
||||||
public static let shared = ImageCacheService() // Default shared instance
|
|
||||||
|
|
||||||
private let memoryCache = NSCache<NSString, UIImage>()
|
|
||||||
private let fileManager = FileManager.default
|
|
||||||
private let cacheDirectory: URL = {
|
|
||||||
FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
|
||||||
}()
|
|
||||||
|
|
||||||
public init() {}
|
|
||||||
|
|
||||||
public func loadImage(from url: URL) async -> UIImage? {
|
|
||||||
let uniqueKey = url.uniqueIdentifier
|
|
||||||
let cacheKey = uniqueKey as NSString
|
|
||||||
|
|
||||||
if let cachedImage = memoryCache.object(forKey: cacheKey) {
|
|
||||||
return cachedImage
|
|
||||||
}
|
|
||||||
|
|
||||||
let diskImagePath = cacheDirectory.appendingPathComponent(uniqueKey)
|
|
||||||
if let diskImage = UIImage(contentsOfFile: diskImagePath.path) {
|
|
||||||
memoryCache.setObject(diskImage, forKey: cacheKey)
|
|
||||||
return diskImage
|
|
||||||
}
|
|
||||||
|
|
||||||
if let networkImage = await fetchFromNetwork(url: url) {
|
|
||||||
memoryCache.setObject(networkImage, forKey: cacheKey)
|
|
||||||
try? saveImageToDisk(image: networkImage, at: diskImagePath)
|
|
||||||
return networkImage
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func fetchFromNetwork(url: URL) async -> UIImage? {
|
|
||||||
do {
|
|
||||||
let (data, _) = try await URLSession.shared.data(from: url)
|
|
||||||
return UIImage(data: data)
|
|
||||||
} catch {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func saveImageToDisk(image: UIImage, at path: URL) throws {
|
|
||||||
guard let data = image.pngData() else { return }
|
|
||||||
try data.write(to: path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -6,124 +6,14 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
|
||||||
|
|
||||||
class EmployeesViewController: UIViewController {
|
class EmployeesViewController: UIViewController {
|
||||||
private let tableView = UITableView()
|
|
||||||
private let activityIndicator = UIActivityIndicatorView(style: .large)
|
|
||||||
private let modeSegmentedControl = UISegmentedControl(items: EmployeeServiceMode.allCases.map{ $0.rawValue } )
|
|
||||||
private let viewModel = EmployeesViewModel()
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
|
||||||
private var footerView: TableFooterView?
|
|
||||||
|
|
||||||
public override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
setupUI()
|
// Do any additional setup after loading the view.
|
||||||
bindViewModel()
|
|
||||||
viewModel.fetchEmployees()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
//add pull to refresh
|
|
||||||
tableView.refreshControl = UIRefreshControl()
|
|
||||||
tableView.refreshControl?.addTarget(self, action: #selector(didPullToRefresh), for: .valueChanged)
|
|
||||||
|
|
||||||
// Configure Activity Indicator
|
|
||||||
activityIndicator.center = view.center
|
|
||||||
view.addSubview(activityIndicator)
|
|
||||||
|
|
||||||
// Configure Mode Selector
|
|
||||||
modeSegmentedControl.selectedSegmentIndex = 0
|
|
||||||
modeSegmentedControl.addTarget(self, action: #selector(onServiceModeChange), for: .valueChanged)
|
|
||||||
navigationItem.titleView = modeSegmentedControl
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func bindViewModel() {
|
|
||||||
viewModel.$employees
|
|
||||||
.receive(on: RunLoop.main)
|
|
||||||
.sink { [weak self] _ in
|
|
||||||
self?.updateFooter()
|
|
||||||
self?.tableView.reloadData()
|
|
||||||
self?.tableView.refreshControl?.endRefreshing()
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
|
|
||||||
viewModel.$isLoading
|
|
||||||
.receive(on: RunLoop.main)
|
|
||||||
.sink { [weak self] isLoading in
|
|
||||||
if isLoading {
|
|
||||||
self?.activityIndicator.startAnimating()
|
|
||||||
} else {
|
|
||||||
self?.activityIndicator.stopAnimating()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
|
|
||||||
viewModel.$errorMessage
|
|
||||||
.receive(on: RunLoop.main)
|
|
||||||
.sink { [weak self] _ in
|
|
||||||
self?.updateFooter()
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateFooter() {
|
|
||||||
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 message, !viewModel.isLoading {
|
|
||||||
// Lazy initialize footerView if needed
|
|
||||||
if footerView == nil {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension EmployeesViewController {
|
|
||||||
@objc private func didPullToRefresh() {
|
|
||||||
viewModel.fetchEmployees()
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func onServiceModeChange(_ sender: UISegmentedControl) {
|
|
||||||
let selectedMode: EmployeeServiceMode
|
|
||||||
switch sender.selectedSegmentIndex {
|
|
||||||
case 0: selectedMode = .production
|
|
||||||
case 1: selectedMode = .malformed
|
|
||||||
case 2: selectedMode = .empty
|
|
||||||
default: return
|
|
||||||
}
|
|
||||||
viewModel.changeMode(to: selectedMode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,44 +0,0 @@
|
|||||||
//
|
|
||||||
// EmployeeCellViewModel.swift
|
|
||||||
// EmployeeDirectory
|
|
||||||
//
|
|
||||||
// Created by Matt Bruce on 1/20/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
public class EmployeeCellViewModel: ObservableObject {
|
|
||||||
private let employee: Employee
|
|
||||||
|
|
||||||
public private(set) var uuid: String
|
|
||||||
public private(set) var fullName: String
|
|
||||||
public private(set) var phoneNumber: String?
|
|
||||||
public private(set) var emailAddress: String
|
|
||||||
public private(set) var biography: String?
|
|
||||||
public private(set) var team: String
|
|
||||||
public private(set) var employeeType: String
|
|
||||||
@Published public private(set) var smallPhoto: UIImage?
|
|
||||||
|
|
||||||
public init(employee: Employee) {
|
|
||||||
self.employee = employee
|
|
||||||
|
|
||||||
// Initialize properties
|
|
||||||
self.uuid = employee.uuid.uuidString
|
|
||||||
self.fullName = employee.fullName
|
|
||||||
self.phoneNumber = employee.phoneNumber
|
|
||||||
self.emailAddress = employee.emailAddress
|
|
||||||
self.biography = employee.biography
|
|
||||||
self.team = employee.team
|
|
||||||
self.employeeType = employee.employeeType.description
|
|
||||||
|
|
||||||
if let endpoint = employee.photoURLSmall {
|
|
||||||
Task{
|
|
||||||
if let smallPhotoURL = URL(string: endpoint) {
|
|
||||||
smallPhoto = await ImageCacheService.shared.loadImage(from: smallPhotoURL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
//
|
|
||||||
// EmployeesViewModel.swift
|
|
||||||
// EmployeeDirectory
|
|
||||||
//
|
|
||||||
// Created by Matt Bruce on 1/20/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
public class EmployeesViewModel: ObservableObject {
|
|
||||||
private let service: EmployeeService
|
|
||||||
private var serviceMode: EmployeeServiceMode = .production
|
|
||||||
|
|
||||||
@Published public private(set) var employees: [Employee] = []
|
|
||||||
@Published public private(set) var errorMessage: String? = nil
|
|
||||||
@Published public private(set) var isLoading: Bool = false
|
|
||||||
|
|
||||||
public init(service: EmployeeService = EmployeeService()) {
|
|
||||||
self.service = service
|
|
||||||
}
|
|
||||||
|
|
||||||
public func fetchEmployees() {
|
|
||||||
// resetting values out the values before fetching new data
|
|
||||||
errorMessage = nil
|
|
||||||
isLoading = true
|
|
||||||
|
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
// Fetch employees using the async method
|
|
||||||
let wrapper = try await service.getEmployees(serviceMode)
|
|
||||||
|
|
||||||
// Update published properties
|
|
||||||
self.employees = wrapper.employees
|
|
||||||
self.isLoading = false
|
|
||||||
|
|
||||||
} catch {
|
|
||||||
// Handle errors
|
|
||||||
self.employees = []
|
|
||||||
self.isLoading = false
|
|
||||||
self.errorMessage = "An unexpected error occurred, please try to refresh"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public func changeMode(to mode: EmployeeServiceMode) {
|
|
||||||
serviceMode = mode
|
|
||||||
fetchEmployees()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,93 +0,0 @@
|
|||||||
//
|
|
||||||
// EmployeeTableViewCell.swift
|
|
||||||
// EmployeeDirectory
|
|
||||||
//
|
|
||||||
// Created by Matt Bruce on 1/20/25.
|
|
||||||
//
|
|
||||||
import UIKit
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
public class EmployeeTableViewCell: UITableViewCell {
|
|
||||||
static let identifier = "EmployeeTableViewCell"
|
|
||||||
|
|
||||||
private let photoImageView = UIImageView()
|
|
||||||
private let nameLabel = UILabel()
|
|
||||||
private let emailLabel = UILabel()
|
|
||||||
private let stackView = UIStackView()
|
|
||||||
|
|
||||||
private var cancellable: AnyCancellable?
|
|
||||||
|
|
||||||
public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
|
||||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
|
||||||
setupUI()
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setupUI() {
|
|
||||||
// Configure photoImageView
|
|
||||||
photoImageView.contentMode = .scaleAspectFill
|
|
||||||
photoImageView.clipsToBounds = true
|
|
||||||
photoImageView.layer.cornerRadius = 25
|
|
||||||
photoImageView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
photoImageView.image = UIImage(systemName: "person.crop.circle")
|
|
||||||
|
|
||||||
// Configure labels with text styles
|
|
||||||
nameLabel.font = UIFont.preferredFont(forTextStyle: .headline) // Bold, prominent text
|
|
||||||
nameLabel.adjustsFontForContentSizeCategory = true // Adapts to Dynamic Type
|
|
||||||
|
|
||||||
emailLabel.font = UIFont.preferredFont(forTextStyle: .body)
|
|
||||||
emailLabel.textColor = .blue
|
|
||||||
emailLabel.adjustsFontForContentSizeCategory = true // Adapts to Dynamic Type
|
|
||||||
|
|
||||||
// Configure stackView
|
|
||||||
stackView.axis = .vertical
|
|
||||||
stackView.spacing = 5
|
|
||||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
|
|
||||||
// Add labels to stackView
|
|
||||||
stackView.addArrangedSubview(nameLabel)
|
|
||||||
stackView.addArrangedSubview(emailLabel)
|
|
||||||
|
|
||||||
// Add subviews
|
|
||||||
contentView.addSubview(photoImageView)
|
|
||||||
contentView.addSubview(stackView)
|
|
||||||
|
|
||||||
// Add constraints
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
photoImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10),
|
|
||||||
photoImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
|
||||||
photoImageView.widthAnchor.constraint(equalToConstant: 50),
|
|
||||||
photoImageView.heightAnchor.constraint(equalToConstant: 50),
|
|
||||||
|
|
||||||
stackView.leadingAnchor.constraint(equalTo: photoImageView.trailingAnchor, constant: 10),
|
|
||||||
stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10),
|
|
||||||
stackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
|
|
||||||
stackView.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -10)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
public override func prepareForReuse() {
|
|
||||||
super.prepareForReuse()
|
|
||||||
photoImageView.image = UIImage(systemName: "person.crop.circle")
|
|
||||||
nameLabel.text = nil
|
|
||||||
emailLabel.text = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
public func configure(with viewModel: EmployeeCellViewModel) {
|
|
||||||
|
|
||||||
// Bind the image to the photoImageView
|
|
||||||
cancellable = viewModel.$smallPhoto
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak self] image in
|
|
||||||
self?.photoImageView.image = image
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bind data to UI components
|
|
||||||
nameLabel.text = viewModel.fullName
|
|
||||||
emailLabel.text = viewModel.emailAddress
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
//
|
|
||||||
// EmptyStateFooterView.swift
|
|
||||||
// EmployeeDirectory
|
|
||||||
//
|
|
||||||
// Created by Matt Bruce on 1/20/25.
|
|
||||||
//
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
public class TableFooterView: UIView {
|
|
||||||
|
|
||||||
/// Label used to show the message
|
|
||||||
private let messageLabel: UILabel = {
|
|
||||||
let label = UILabel()
|
|
||||||
label.textColor = .gray
|
|
||||||
label.textAlignment = .center
|
|
||||||
label.font = UIFont.preferredFont(forTextStyle: .body) // Use a text style for Dynamic Type
|
|
||||||
label.adjustsFontForContentSizeCategory = true // Enable Dynamic Type adjustments
|
|
||||||
label.numberOfLines = 0
|
|
||||||
label.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
return label
|
|
||||||
}()
|
|
||||||
|
|
||||||
init(message: String) {
|
|
||||||
super.init(frame: .zero)
|
|
||||||
setupUI()
|
|
||||||
update(message: message)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Setup the UI
|
|
||||||
private func setupUI() {
|
|
||||||
addSubview(messageLabel)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
messageLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
||||||
messageLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
||||||
messageLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
|
|
||||||
messageLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Updates the Current Message
|
|
||||||
/// - Parameter message: message to show
|
|
||||||
public func update(message: String) {
|
|
||||||
messageLabel.text = message
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -6,44 +6,12 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Testing
|
import Testing
|
||||||
import XCTest
|
|
||||||
|
|
||||||
@testable import EmployeeDirectory
|
@testable import EmployeeDirectory
|
||||||
|
|
||||||
struct EmployeeDirectoryTests {
|
struct EmployeeDirectoryTests {
|
||||||
|
|
||||||
@Test func getEmployeesValid() async throws {
|
@Test func example() async throws {
|
||||||
do {
|
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
|
||||||
let wrapper = try await EmployeeService().getEmployees(.production)
|
|
||||||
#expect(wrapper.employees.count == 11)
|
|
||||||
} catch {
|
|
||||||
XCTFail("Unexpected error: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test func getEmployeesMalformed() async throws {
|
|
||||||
do {
|
|
||||||
let wrapper = try await EmployeeService().getEmployees(.malformed)
|
|
||||||
XCTFail("Expected to throw, but got \(wrapper.employees.count) employees")
|
|
||||||
} catch let error as NetworkServiceError {
|
|
||||||
switch error {
|
|
||||||
case .invalidResponse:
|
|
||||||
XCTAssertTrue(true, "Correctly threw invalidResponse error")
|
|
||||||
default:
|
|
||||||
XCTFail("Expected NetworkServiceError.invalidResponse, but got \(error)")
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
XCTFail("Unexpected error: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test func getEmployeesEmpty() async throws {
|
|
||||||
do {
|
|
||||||
let wrapper = try await EmployeeService().getEmployees(.empty)
|
|
||||||
#expect(wrapper.employees.count == 0)
|
|
||||||
} catch {
|
|
||||||
XCTFail("Unexpected error: \(error)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user