Compare commits
25 Commits
255356ff19
...
a1214cf38b
| Author | SHA1 | Date | |
|---|---|---|---|
| a1214cf38b | |||
| 887cfd99ea | |||
| 9d3a23e69d | |||
| 6e4ff7a569 | |||
| f3086d35d5 | |||
| e1521c47a8 | |||
| 4f072677a5 | |||
| fa5d16f143 | |||
| 522861e434 | |||
| 09c8a1ef5a | |||
| 4848fb3160 | |||
| a0adea163f | |||
| aced710388 | |||
| 5d8285935f | |||
| f664e81e5b | |||
| 96468b123b | |||
| 53fc099d6e | |||
| 26864403fd | |||
| 532914d357 | |||
| 16bdd05082 | |||
| dc25bd490d | |||
| e4268fd22f | |||
| bfb721bfd9 | |||
| a517ced1cc | |||
| 9bc63d31cd |
16
EmployeeDirectory/Extensions/URL.swift
Normal file
16
EmployeeDirectory/Extensions/URL.swift
Normal file
@ -0,0 +1,16 @@
|
||||
//
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
70
EmployeeDirectory/Models/Employee.swift
Normal file
70
EmployeeDirectory/Models/Employee.swift
Normal file
@ -0,0 +1,70 @@
|
||||
//
|
||||
// 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"
|
||||
}
|
||||
}
|
||||
16
EmployeeDirectory/Models/Employees.swift
Normal file
16
EmployeeDirectory/Models/Employees.swift
Normal file
@ -0,0 +1,16 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
10
EmployeeDirectory/Protocols/EmployeeServiceProtocol.swift
Normal file
10
EmployeeDirectory/Protocols/EmployeeServiceProtocol.swift
Normal file
@ -0,0 +1,10 @@
|
||||
//
|
||||
// EmployeeServiceable.swift
|
||||
// EmployeeDirectory
|
||||
//
|
||||
// Created by Matt Bruce on 1/20/25.
|
||||
//
|
||||
|
||||
public protocol EmployeeServiceProtocol {
|
||||
func getEmployees(_ serviceMode: EmployeeServiceMode) async throws -> Employees
|
||||
}
|
||||
37
EmployeeDirectory/Services/EmployeeService.swift
Normal file
37
EmployeeDirectory/Services/EmployeeService.swift
Normal file
@ -0,0 +1,37 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
58
EmployeeDirectory/Services/ImageCacheService.swift
Normal file
58
EmployeeDirectory/Services/ImageCacheService.swift
Normal file
@ -0,0 +1,58 @@
|
||||
//
|
||||
// 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,14 +6,124 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
class EmployeesViewController: UIViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
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() {
|
||||
super.viewDidLoad()
|
||||
// Do any additional setup after loading the view.
|
||||
setupUI()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
44
EmployeeDirectory/ViewModels/EmployeeCellViewModel.swift
Normal file
44
EmployeeDirectory/ViewModels/EmployeeCellViewModel.swift
Normal file
@ -0,0 +1,44 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
52
EmployeeDirectory/ViewModels/EmployeesViewModel.swift
Normal file
52
EmployeeDirectory/ViewModels/EmployeesViewModel.swift
Normal file
@ -0,0 +1,52 @@
|
||||
//
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
||||
93
EmployeeDirectory/Views/EmployeeTableViewCell.swift
Normal file
93
EmployeeDirectory/Views/EmployeeTableViewCell.swift
Normal file
@ -0,0 +1,93 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
}
|
||||
}
|
||||
51
EmployeeDirectory/Views/TableFooterView.swift
Normal file
51
EmployeeDirectory/Views/TableFooterView.swift
Normal file
@ -0,0 +1,51 @@
|
||||
//
|
||||
// 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,12 +6,44 @@
|
||||
//
|
||||
|
||||
import Testing
|
||||
import XCTest
|
||||
|
||||
@testable import EmployeeDirectory
|
||||
|
||||
struct EmployeeDirectoryTests {
|
||||
|
||||
@Test func example() async throws {
|
||||
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
|
||||
|
||||
@Test func getEmployeesValid() async throws {
|
||||
do {
|
||||
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