Skip to main content
Bounty Awarded with 50 reputation awarded by stevenpcurtis
use computed property for breachdate
Source Link
Rob
  • 2.7k
  • 17
  • 27
  1. I’d suggest using Date types in BreachModel (which I’d personally just call Breach and make it a struct):

     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()
    }
}
  1. I’d suggest using Date types in BreachModel (which I’d personally just call Breach and make it a struct):

     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()
    }
}
  1. I’d suggest using Date types in BreachModel (which I’d personally just call Breach and make it a struct):

     struct Breach: Codable {
         let name: String
         let title: String
         let domain: String
         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
    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()
    }
}
`pwnedData` as optional
Source Link
Rob
  • 2.7k
  • 17
  • 27
  1. I notice that your pwnedData (which I might suggest renaming to pwnedBreaches because it’s an array of Breach objects, not an array of Data objects) is initialized as an empty [BreachModel] before you retrieve the data. It’s not terribly critical in this particular case, but as a general rule, it is useful to distinguish between “this property has not been set” and “it has been set but there are no records.”

Bottom line, I’d suggest making this an optional (where nil means that it hasn’t been set yet, and [] means that it has been set to an empty array).

  1. I notice that your pwnedData (which I might suggest renaming to pwnedBreaches because it’s an array of Breach objects, not an array of Data objects) is initialized as an empty [BreachModel] before you retrieve the data. It’s not terribly critical in this particular case, but as a general rule, it is useful to distinguish between “this property has not been set” and “it has been set but there are no records.”

Bottom line, I’d suggest making this an optional (where nil means that it hasn’t been set yet, and [] means that it has been set to an empty array).

use renamed pwnedBreaches
Source Link
Rob
  • 2.7k
  • 17
  • 27
// 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 pwnedDatapwnedBreaches?.count ?? 0
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Sitewide", for: indexPath)
        cell.textLabel?.text = pwnedDatapwnedBreaches?[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 = URL(string: 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 = 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()
    }
}
// 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 pwnedData?.count ?? 0
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Sitewide", for: indexPath)
        cell.textLabel?.text = pwnedData?[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 = "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 = URL(string: 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 = 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()
    }
}
// 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()
    }
}
Source Link
Rob
  • 2.7k
  • 17
  • 27
Loading