Skip to main content
Tweeted twitter.com/StackCodeReview/status/1355893604061999105

ViewController class ViewController: UIViewController {

class ViewController: UIViewController {
    
    var breachesViewModel: BreachViewModelType!
    var breachView : BreachView?
    
    // to be called during testing
    init(viewModel: BreachViewModelType) {
        breachesViewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }
    
    // required when called from storyboard
    required init?(coder aDecoder: NSCoder) {
        breachesViewModel = BreachViewModel()
        super.init(coder: aDecoder)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()       
        breachesViewModel.fetchData{ [weak self] breaches in
            guard let self = self else {return}
            DispatchQueue.main.async {
                self.updateUI()
            }
        }
    }

    func updateUI() {
        breachView = BreachView(frame: view.frame)
        breachesViewModel.configure(breachView!, number: 3)
        view.addSubview(breachView!)
    }
}

}

ViewModel class BreachViewModel : BreachViewModelType { var breaches = BreachModel

class BreachViewModel : BreachViewModelType {
    var breaches = [BreachModel]()
    
    init() {
        // add init for ClosureHTTPManager here, to allow it to be teestable in the future
    }
    
    func fetchData(completion: @escaping ([BreachModel]) -> Void) {
        ClosureHTTPManager.shared.get(urlString: baseUrl + breachesExtensionURL, completionBlock: { [weak self] result in
            guard let self = self else {return}
            switch result {
            case .failure(let error):
                print ("failure", error)
            case .success(let dta) :
                let decoder = JSONDecoder()
                do
                {
                    self.breaches = try decoder.decode([BreachModel].self, from: dta)
                    completion(try decoder.decode([BreachModel].self, from: dta))
                } catch {
                    // deal with error from JSON decoding!
                }
            }            
        })
    }
    
    func numberItemsToDisplay() -> Int {
        return breaches.count
    }
    
    func configure (_ view: BreachView, number index: Int) {
        // set the name and data in the view
        view.nameLabel.text = breaches[index].name
    }
        
}
    class ClosureHTTPManager {
        static let shared: ClosureHTTPManager = ClosureHTTPManager()
    
        enum HTTPError: Error 

 {
        case invalidURL
        case invalidResponse(Data?, URLResponse?)
    }
    
    public func get(urlString: String, completionBlock: @escaping (Result<Data, Error>) -> Void) {
        guard let url = URL(string: urlString) else {
            completionBlock(.failure(HTTPError.invalidURL))
            return
        }
        
        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()
    }
    }

ViewController class ViewController: UIViewController {

var breachesViewModel: BreachViewModelType!
var breachView : BreachView?

// to be called during testing
init(viewModel: BreachViewModelType) {
    breachesViewModel = viewModel
    super.init(nibName: nil, bundle: nil)
}

// required when called from storyboard
required init?(coder aDecoder: NSCoder) {
    breachesViewModel = BreachViewModel()
    super.init(coder: aDecoder)
}

override func viewDidLoad() {
    super.viewDidLoad()       
    breachesViewModel.fetchData{ [weak self] breaches in
        guard let self = self else {return}
        DispatchQueue.main.async {
            self.updateUI()
        }
    }
}

func updateUI() {
    breachView = BreachView(frame: view.frame)
    breachesViewModel.configure(breachView!, number: 3)
    view.addSubview(breachView!)
}

}

ViewModel class BreachViewModel : BreachViewModelType { var breaches = BreachModel

init() {
    // add init for ClosureHTTPManager here, to allow it to be teestable in the future
}

func fetchData(completion: @escaping ([BreachModel]) -> Void) {
    ClosureHTTPManager.shared.get(urlString: baseUrl + breachesExtensionURL, completionBlock: { [weak self] result in
        guard let self = self else {return}
        switch result {
        case .failure(let error):
            print ("failure", error)
        case .success(let dta) :
            let decoder = JSONDecoder()
            do
            {
                self.breaches = try decoder.decode([BreachModel].self, from: dta)
                completion(try decoder.decode([BreachModel].self, from: dta))
            } catch {
                // deal with error from JSON decoding!
            }
        }            
    })
}

func numberItemsToDisplay() -> Int {
    return breaches.count
}

func configure (_ view: BreachView, number index: Int) {
    // set the name and data in the view
    view.nameLabel.text = breaches[index].name
}
    
}
    class ClosureHTTPManager {
        static let shared: ClosureHTTPManager = ClosureHTTPManager()
    
        enum HTTPError: Error 

 {
        case invalidURL
        case invalidResponse(Data?, URLResponse?)
    }
    
    public func get(urlString: String, completionBlock: @escaping (Result<Data, Error>) -> Void) {
        guard let url = URL(string: urlString) else {
            completionBlock(.failure(HTTPError.invalidURL))
            return
        }
        
        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()
    }
    }

ViewController:

class ViewController: UIViewController {
    
    var breachesViewModel: BreachViewModelType!
    var breachView : BreachView?
    
    // to be called during testing
    init(viewModel: BreachViewModelType) {
        breachesViewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }
    
    // required when called from storyboard
    required init?(coder aDecoder: NSCoder) {
        breachesViewModel = BreachViewModel()
        super.init(coder: aDecoder)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()       
        breachesViewModel.fetchData{ [weak self] breaches in
            guard let self = self else {return}
            DispatchQueue.main.async {
                self.updateUI()
            }
        }
    }

    func updateUI() {
        breachView = BreachView(frame: view.frame)
        breachesViewModel.configure(breachView!, number: 3)
        view.addSubview(breachView!)
    }
}

ViewModel:

class BreachViewModel : BreachViewModelType {
    var breaches = [BreachModel]()
    
    init() {
        // add init for ClosureHTTPManager here, to allow it to be teestable in the future
    }
    
    func fetchData(completion: @escaping ([BreachModel]) -> Void) {
        ClosureHTTPManager.shared.get(urlString: baseUrl + breachesExtensionURL, completionBlock: { [weak self] result in
            guard let self = self else {return}
            switch result {
            case .failure(let error):
                print ("failure", error)
            case .success(let dta) :
                let decoder = JSONDecoder()
                do
                {
                    self.breaches = try decoder.decode([BreachModel].self, from: dta)
                    completion(try decoder.decode([BreachModel].self, from: dta))
                } catch {
                    // deal with error from JSON decoding!
                }
            }            
        })
    }
    
    func numberItemsToDisplay() -> Int {
        return breaches.count
    }
    
    func configure (_ view: BreachView, number index: Int) {
        // set the name and data in the view
        view.nameLabel.text = breaches[index].name
    }
        
}
class ClosureHTTPManager {
    static let shared: ClosureHTTPManager = ClosureHTTPManager()

    enum HTTPError: Error {
        case invalidURL
        case invalidResponse(Data?, URLResponse?)
    }

    public func get(urlString: String, completionBlock: @escaping (Result<Data, Error>) -> Void) {
        guard let url = URL(string: urlString) else {
            completionBlock(.failure(HTTPError.invalidURL))
            return
        }
        
        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

MVVM in Swift iOS

I've been implementing MVVM in Swift. I've looked at several implementations, many of which violate some aspects of MVVM and wanted to have a go with my own version that contains a Web request service.

View:

class BreachView: UIView {
    var nameLabel = UILabel()
    public override init(frame: CGRect) {
        let labelframe = CGRect(x: 0, y: 50, width: frame.width, height: 20)
        nameLabel.frame = labelframe
        nameLabel.backgroundColor = .gray
        super.init(frame: frame)
        self.addSubview(nameLabel)
        backgroundColor = .red
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

ViewController class ViewController: UIViewController {

var breachesViewModel: BreachViewModelType!
var breachView : BreachView?

// to be called during testing
init(viewModel: BreachViewModelType) {
    breachesViewModel = viewModel
    super.init(nibName: nil, bundle: nil)
}

// required when called from storyboard
required init?(coder aDecoder: NSCoder) {
    breachesViewModel = BreachViewModel()
    super.init(coder: aDecoder)
}

override func viewDidLoad() {
    super.viewDidLoad()       
    breachesViewModel.fetchData{ [weak self] breaches in
        guard let self = self else {return}
        DispatchQueue.main.async {
            self.updateUI()
        }
    }
}

func updateUI() {
    breachView = BreachView(frame: view.frame)
    breachesViewModel.configure(breachView!, number: 3)
    view.addSubview(breachView!)
}

}

Protocol for dependency injection:

protocol BreachViewModelType {
    func fetchData(completion: @escaping ([BreachModel]) -> Void)
    func configure (_ view: BreachView, number index: Int)
}

ViewModel class BreachViewModel : BreachViewModelType { var breaches = BreachModel

init() {
    // add init for ClosureHTTPManager here, to allow it to be teestable in the future
}

func fetchData(completion: @escaping ([BreachModel]) -> Void) {
    ClosureHTTPManager.shared.get(urlString: baseUrl + breachesExtensionURL, completionBlock: { [weak self] result in
        guard let self = self else {return}
        switch result {
        case .failure(let error):
            print ("failure", error)
        case .success(let dta) :
            let decoder = JSONDecoder()
            do
            {
                self.breaches = try decoder.decode([BreachModel].self, from: dta)
                completion(try decoder.decode([BreachModel].self, from: dta))
            } catch {
                // deal with error from JSON decoding!
            }
        }            
    })
}

func numberItemsToDisplay() -> Int {
    return breaches.count
}

func configure (_ view: BreachView, number index: Int) {
    // set the name and data in the view
    view.nameLabel.text = breaches[index].name
}
    
}

and HTTP manager

    class ClosureHTTPManager {
        static let shared: ClosureHTTPManager = ClosureHTTPManager()
    
        enum HTTPError: Error 

{
        case invalidURL
        case invalidResponse(Data?, URLResponse?)
    }
    
    public func get(urlString: String, completionBlock: @escaping (Result<Data, Error>) -> Void) {
        guard let url = URL(string: urlString) else {
            completionBlock(.failure(HTTPError.invalidURL))
            return
        }
        
        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()
    }
    }

Calling the API from

let baseUrl : String = "https://haveibeenpwned.com/api/v2"
let breachesExtensionURL : String = "/breaches"

Any comments on whether the implementation conforms to MVVM or not, typos, changes etc. are appreciated.

Git link: https://github.com/stevencurtis/MVVMWithNetworkService