MVVM in Swift 4: Using Delegates
Introduction
The challenge in deciding how to structure a codebase lies in deciding a way to make it maintainable and testable while still keeping it simple and easy to understand.
There are several architectural patterns that have been invented that attempt to solve this problem. Every pattern has its own pros and cons. This article describes the MVVM pattern and demonstrates an easy way to implement it in Swift 4 using delegates.
The MVVM pattern
The Model-View-ViewModel (MVVM) pattern intends to provide a clean separation of concern between the presentation logic and the domain logic. There are three core components in MVVM pattern: the model, the view and the view model. Each component has a distinct role.
The Model represent your domain objects. Models are designed irrespective of how data is displayed in different views within the application, but are purely optimized based on the best way to represent data structures the application deals with.
The View represents the user interface. It describes how information is displayed on the screen. It is also responsible for receiving user interactions and passing them on to the view model.
The View Model is a key piece of MVVM pattern. View model acts as a bridge between the view and the model. Each view class has a corresponding view model where it gets its data from. View model retieves data from the model, manipulates the data into a format required by the view, and notifies the view to show this data.
MVVM using Delegates
In iOS, views can be represented by UIViewController objects while view models can be a simple Swift class. Let us define some ground rules for all view model types by declaring the following protocols.
protocol ViewModelDelegate: class {
func willLoadData()
func didLoadData()
}
protocol ViewModelType {
func bootstrap()
var delegate: ViewModelDelegate? { get set }
}
The above protocol requires that all view models must have a bootstrap method which can be used to initialize it, and all view models should be able to communicate back to the view that owns it using delegate methods.
Consider the following use of the above protocols.
// view
class TasksViewController: UIViewController {
...
// the view model should be injected into this class
private var model: TasksViewModel!
override func viewDidLoad() {
super.viewDidLoad()
// initialize the view model
model.delegate = self
model.bootstrap()
}
}
// notifications from the view model
extension TasksViewController: ViewModelDelegate {
func willLoadData() {
activityIndicator?.startAnimating()
}
func didLoadData() {
// reloads tableView data from model.taskNames
tableView.reloadData()
activityIndicator?.stopAnimating()
}
}
// view model
class TasksViewModel: ViewModelType {
var taskNames: [String] = []
private weak var delegate: ViewModelDelegate?
private var dataSource: UsersDataSource!
func bootstrap() {
// fire delegate method willLoadData
// invoke dataSource.fetchUsers and load it into taskNames array
// fire delegate method didLoadData
}
}
TasksViewController (view) tells TasksViewModel (its view model) to start fetching new data by invoking the bootstrap method on it. The view model fires the willLoadData delegate method indicating that it’s loading data thus the view should display a loading indicator. Once it has fetched new data, it then fires didLoadData delegate method telling the view that it can hide the loading indicator and show latest data contained in taskNames.
Please note, the view model does not have any knowledge of the view. In this implementation, view models can only interact with views as a generic ViewModelDelegate type. However, the view owns and can directly access properties and methods in its view model. Similarly, the data source object has no knowledge of individual view models, but the view model directly references and can access properties and methods inside the data source. This is one of the core rules inscribed by the MVVM design pattern.
Learning by example
Let's take an example app that is required to fetch a list of users from a REST API and then the display names and email addresses of those users on the screen. The users should be listed under one of three sections, "A to I", "J to Q," and "R to Z," based on the first character of the individual names.
The following sequence diagram describes how all the components could be wired.
Here is an example source code that explores how this can be implemented with MVVM pattern, using delegates.
Additional notes
This article describes a very basic way to use delegates to implement MVVM pattern. It would be useful and easy to extend it to handle errors, limit data fetching, etc.
Some words about unit testing — the view models are generally injected into the views. This way view logic can be unit tested by using mock view models. Similarly, the data source object is also injected into view models, making it possible to test the presentation logic in the view models by mocking the data source.
at https://github.com/nishadh/simple-mvvm-example/blob/master/simple-mvvm-example/Users/UsersViewModel.swift
private func loadData() {
delegate?.willLoadData()
dataSource.fetchUsers { [weak self] result in
DispatchQueue.main.async {
guard let ws = self else { return }
switch result {
case .failure(_):
ws.sections = []
if DispatchQueue.main.async only { UI display code }, will be better or not ?
DispatchQueue.main.async {
ws.delegate?.didLoadData()
}
Thanks !
https://github.com/manishkkatoch/SimpleTwoWayBindingIOS
Any suggestions for TwoWayBingind framwwork? Thanks!
email : yita.wu55@gmail.com
https://code.i-harness.com/en/q/5819c
what is domain logic ? is this correct ? Thanks!
email : yita.wu55@gmail.com
Domain logic aka Business logic represent business rules. Example, suppose one rule is that “only premium customers can store files larger than 1 GB.” This can be written in business layer so that if logged in user is not a premium user and the file about to be stored is larger than 1 GB, the operation is rejected and an appropriate error code is returned. Of course, this error code can (and usually will) be translated to proper error message and displayed to the user by the presentation layer.
Thank you !