6
\$\begingroup\$

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

Rob
2,65716 silver badges27 bronze badges
asked Apr 13, 2019 at 6:51
\$\endgroup\$
5
  • 1
    \$\begingroup\$ Be aware: ViewDidLoad sometimes get called twicee. \$\endgroup\$ Commented Apr 13, 2019 at 11:00
  • \$\begingroup\$ @muescha - No it doesn’t. For a given instance of the view controller, viewDidLoad is only called once. Maybe you’re confusing it with viewDidAppear? \$\endgroup\$ Commented Apr 18, 2019 at 22:12
  • \$\begingroup\$ No. See question „why-is-viewdidload-getting-called-twice" stackoverflow.com/questions/32796362/… \$\endgroup\$ Commented Apr 21, 2019 at 13:31
  • 1
    \$\begingroup\$ I have created one example app with some tests with MVVM design type. https://github.com/ivamaheshwari/dubizzleX Also sharing one article which helped me understanding the concept for iOS. https://medium.com/flawless-app-stories/mvvm-in-ios-swift-aa1448a66fb4 Hope this is helpful. Thanks \$\endgroup\$ Commented Jan 31, 2021 at 9:45
  • \$\begingroup\$ @muescha - No. Back in iOS 5 and earlier, views were loaded and unloaded, so viewDidLoad could be called multiple times. But it hasn’t been that way for a while. See viewDidUnload. If you ever see it called multiple times, nowadays, you have some other serious bug in your project. \$\endgroup\$ Commented Jan 21, 2022 at 4:30

1 Answer 1

6
\$\begingroup\$

A couple of thoughts.

  • The presence of init(viewModel:) in the view controller (with its associated comment about using this during testing) seems to suggest that you plan on testing the view controller. But one of the central tenets of MVP, MVVM, and the like, is that the goal is to have a UIKit-independent representation. Part of the reasons we do that is because that is the object that we’ll test, not the view controller.

    Bottom line, I’d be inclined to retire init(viewModel:) from the view controller and restrict the unit testing to the view model.

    For example, a MVP might look like:

    enter image description here

    Or, in MVVM, you’ll see structures like:

    enter image description here

    (Both images from Medium's iOS Architecture Patterns.)

    But in both cases, the mediator (the view model or presenter or whatever you want to call it), should not be UIKit-specific, and it’s that mediator on which we’ll do our unit testing.

  • The view controller has a method updateUI, which is adding the view to the hierarchy. I’d suggest you decouple the initial "configure the view" from the "view model has informed me of some model change".

  • While this distinction is often ignored (with the term "view model" often being used quite loosely), technically, MVVM suggests that one would employ "data binding", where the initial configuration of the view controller sets up the connection between presenter events and UIKit control updates and vice versa. It’s hard to tell, but this feels like you’re got the beginning of something with a more MVP je ne sais quoi than MVVM. That’s not necessarily wrong, but, technically, they’re just different.

    Quite frankly, many use the term of "view model" within the UIKit world quite loosely, often applying the term that are actually following something that is technically closer to MVP.

  • In BreachViewModel is updating the text property of the UILabel called nameLabel within the view. To my two prior points, the view model, itself, should strive to be UIKit dependent. It really should not be reaching into a subview of the view and updating the text itself. If this was MVVM, you’d bind the label to the view model and have the update take place that way. If this was MVP, the presenter should just inform the view controller that the value has changed and the view controller would update the UIKit control.

    But avoid having anything UIKit specific in the view model.

  • A few observations in fetchData:

    • You have paths of execution where you don’t call the completion handler. You generally always want to call the completion handler, reporting success with data or failure with error information. Perhaps adopt the Result<T, U> pattern you did with ClosureHTTPManager (like we did in the answer to your earlier question). If you adopt async-await, it prevents this class of mistake.

    • You are decoding your JSON twice. Obviously, you only want to do that once.

  • This is personally a matter of taste, but I’m not crazy about the view model doing JSON parsing. That seems like the job of some API layer, not the view model. I like to see the view model limited to taking parsed data, updating models, applying business rules, etc.

I think the aforementioned iOS Architecture Patterns is an interesting discussion of many of these issues. I also think Dave DeLong’s A Better MVC is an interesting read.


Generally, if you were really determined to adopt MVVM (as opposed to MVP-style patterns), you’d use a framework like SwiftUI (with ObservableObject or Observation framework) or SwiftRX (or possibly the older Bond) to facilitate the bindings.

In iOS 18, you can use Observation framework, too, but it’s inelegant. In iOS 26, the situation has improved, offering the new updateProperties method that will be called automatically when the @Observable object updates:

class ViewController: UITableViewController {
 private let viewModel = BreachViewModel()
 override func viewDidLoad() {
 super.viewDidLoad()
 configureTableView()
 }
 override func updateProperties() {
 updateTableView()
 }
}

And:

import Foundation
import Observation
@Observable
class BreachViewModel {
 var breaches: [Breach] = []
 @ObservationIgnored private let apiManager: ApiManager
 /// Create breaches view model.
 ///
 /// - Parameter apiManager: Optional `ApiManager`. Generally omitted from the call point. But, in unit tests, you might instantiate the view model with:
 ///
 /// let viewModel = BreachViewModel(
 /// apiManager: ApiManager(networkManager: mockedNetworkManager)
 /// )
 init(apiManager: ApiManager = .shared) {
 self.apiManager = apiManager
 }
 func fetchBreaches() async throws {
 self.breaches = try await apiManager.breaches()
 }
}

And

class ApiManager {
 static let shared = ApiManager()
 private let baseUrl = URL(string: ...)!
 private let breachesPath = "..."
 private let networkManager: NetworkManagerProtocol
 init(networkManager: NetworkManagerProtocol = NetworkManager.shared) {
 self.networkManager = networkManager
 }
 func breaches() async throws -> [Breach] {
 let url = baseUrl.appending(path: breachesPath)
 let data = try await networkManager.data(from: url)
 let responseObject = try JSONDecoder().decode(BreachResponse.self, from: data)
 return responseObject.payload.breaches // the specifics of how you extract `breaches` from your payload will obviously vary
 }
}

And

protocol NetworkManagerProtocol {
 func data(from url: URL) async throws -> Data
}
class NetworkManager: NetworkManagerProtocol {
 static let shared = NetworkManager()
 enum HTTPError: Error {
 case invalidResponse(Data, URLResponse)
 }
 public func data(from url: URL) async throws -> Data {
 let (data, response) = try await URLSession.shared.data(from: url)
 guard
 let response = response as? HTTPURLResponse,
 200 ..< 300 ~= response.statusCode
 else {
 throw HTTPError.invalidResponse(data, response)
 }
 return data
 }
}

The details matter less than a clear separation of responsibilities:

  • The view controller is responsible for managing the UI;
  • The view model is a thin, OS-independent, layer of model objects necessary to support the view controller and initiates interactions with other services;
  • The API layer is where you embed things like endpoints, authentication, request preparation, and the parsing responsibilities; and
  • The network layer (which should be protocol-based to support mocking for tests) is where you perform the actual network requests.

This UIKit rendition still isn’t technically MVVM (because you still have to manually observe model updates and trigger UI updates; there is no automatic two-way binding of controls to model objects), but the Observation framework has evolved to a point that you can minimize much of the noise previously associated with UIKit projects. If you want true MVVM data-binding, SwiftUI offers more natural integration.


A more complete example can be found on GitHub. I’m just going against https://httpbin.org/json (so I’ve contorted myself to take their "slide show" data model and relabel it as "breaches"), but it illustrates the basic division of labor that I employ above.

answered Apr 18, 2019 at 23:14
\$\endgroup\$
0

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.