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
viewDidLoad
is only called once. Maybe you’re confusing it withviewDidAppear
? \$\endgroup\$viewDidLoad
could be called multiple times. But it hasn’t been that way for a while. SeeviewDidUnload
. If you ever see it called multiple times, nowadays, you have some other serious bug in your project. \$\endgroup\$