I’d suggest using
Datetypes inBreachModel(which I’d personally just callBreachand make it astruct):struct Breach: Codable { let name: String let title: String let domain: String private(set) lazy var breachDate: Date? = { return Breach.dateOnlyFormatter.date(from: breachDateString) }() let breachDateString: String let addedDate: Date let modifiedDate: Date let pwnCount: Int let description: String private enum CodingKeys: String, CodingKey { case name = "Name" case title = "Title" case domain = "Domain" case breachDateString = "BreachDate" case addedDate = "AddedDate" case modifiedDate = "ModifiedDate" case pwnCount = "PwnCount" case description = "Description" } static let dateOnlyFormatter: DateFormatter = { let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") formatter.dateFormat = "yyyy-MM-dd" return formatter }() }
// SitewideTableViewController.swift
class SitewideTableViewController: UITableViewController {
var pwnedBreaches: [Breach]?
override func viewDidLoad() {
super.viewDidLoad()
ApiManager.shared.fetchBreaches { [weak self] result in
guard let self = self else { return }
switch result {
case .failure(let error):
print(error)
case .success(let breaches):
self.pwnedBreaches = breaches.sortedByName()
self.tableView.reloadData()
}
}
}
}
// MARK: - UITableViewDataSource
extension SitewideTableViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return pwnedBreaches?.count ?? 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Sitewide", for: indexPath)
cell.textLabel?.text = pwnedBreaches?[indexPath.row].name
return cell
}
}
// Breach.swift
struct Breach: Codable {
let name: String
let title: String
let domain: String
private(set) lazy var breachDate: Date? = {
return Breach.dateOnlyFormatter.date(from: breachDateString)
}()
let breachDateString: String
let addedDate: Date
let modifiedDate: Date
let pwnCount: Int
let description: String
private enum CodingKeys: String, CodingKey {
case name = "Name"
case title = "Title"
case domain = "Domain"
case breachDateString = "BreachDate"
case addedDate = "AddedDate"
case modifiedDate = "ModifiedDate"
case pwnCount = "PwnCount"
case description = "Description"
}
static let dateOnlyFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd"
return formatter
}()
}
extension RandomAccessCollection where Element == Breach {
func sortedByName() -> [Breach] {
return sorted { a, b in a.name < b.name }
}
}
// Result.swift
//
// `Result` not needed if you are using Swift 5, as it already has defined this for us.
enum Result<T, U> {
case success(T)
case failure(U)
}
// ApiManager.swift
class ApiManager {
static let shared = ApiManager()
let baseUrl = URL(string: "https://haveibeenpwned.com/api/v2")!
let breachesExtensionURL = "breaches"
static let dateTimeFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssX"
formatter.timeZone = TimeZone(secondsFromGMT: 0)
return formatter
}()
func fetchBreaches(completion: @escaping (Result<[Breach], Error>) -> Void) {
let url = baseUrl.appendingPathComponent(breachesExtensionURL)
HTTPManager.shared.get(url) { result in
switch result {
case .failure(let error):
DispatchQueue.main.async { completion(.failure(error)) }
case .success(let data):
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(ApiManager.dateTimeFormatter)
do {
let breaches = try decoder.decode([Breach].self, from: data)
DispatchQueue.main.async { completion(.success(breaches)) }
} catch {
print(String(data: data, encoding: .utf8) ?? "Unable to retrieve string representation")
DispatchQueue.main.async { completion(.failure(error)) }
}
}
}
}
}
class HTTPManager {
static let shared = HTTPManager()
enum HTTPError: Error {
case invalidResponse(Data?, URLResponse?)
}
public func get(_ url: URL, completionBlock: @escaping (Result<Data, Error>) -> Void) {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard error == nil else {
completionBlock(.failure(error!))
return
}
guard
let responseData = data,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode else {
completionBlock(.failure(HTTPError.invalidResponse(data, response)))
return
}
completionBlock(.success(responseData))
}
task.resume()
}
}