MVVM Model-View-ViewModel iOS
MVVM Model-View-ViewModel iOS
View
In the View folder, we will not only have the View files and the components that form them (as in MVC), but also the
Controller files (subclasses of UIViewController), as shown in Figure 4-4.
Remember that in MVVM, Controllers usually only have coordination/routing functions (to navigate between screens)
and, in some cases, pass information (via a Delegate pattern, for example).
ViewModel
This folder only contains the ViewModels, which, as we have seen, connect the Model to the View (Figure 4-5).
Figure 4-5 ViewModel layer files
Before starting to see how the different screens of our application are configured, as we have done in the case of MVC and
MVP, we are going to see how we will carry out the Data Binding process between the ViewModel and the View.
As we have seen before, there are different ways to carry out this procedure, but in our case, we will use RxSwift.
What Is RxSwift?
RxSwift is a library that allows us to develop asynchronous code in our applications, simplifying the way our code will act
in front of the new data that arrives, treating them in a sequential and isolated way.
In this book, we will not delve into the study of RxSwift and reactive programming, suffice it to say that reactive program-
ming allows you to dynamically respond to changes in data and user events.
RxSwift works with what we call Observables, which are wrapper objects for any type of data, to which we can subscribe or
link so that any change that occurs in said Observables triggers a series of previously programmed operations.
Installing RxSwift
The installation of RxSwift, as it is an external library, can be done in different ways (Swift Package Manager, Carthage,
CocoaPods). In our case, we will use the Swift Package Manager (SPM), which is the system integrated with Xcode to intro-
duce third-party libraries in our applications.
First of all, we access the Xcode main menu and select File ➤ Add Packages… (Figure 4-6).
Figure 4-6 Add Packages… menu selection
The Xcode Swift Package manager will appear on the screen (Figure 4-7).
Figure 4-7 Swift Package Manager screen
At this point, what we do is copy the address of the RxSwift git repository (which we will find on their website), and
we will place it in the search field that appears in the upper right (Figure 4-8).
https://github.com/ReactiveX/RxSwift.git
By doing this, Xcode takes care of searching for this library and tells us if we want to install it.
Before adding RxSwift, what we will do is change the “Dependency Rule” option from “Branch” to “Up To Next Major
Version.” Once this change is made, select “Add Package.”
Next, a screen will appear in which we can select which components or products of the RxSwift library we want to
install. For our application, we will select RxCocoa, RxRelay, and RxSwift (for main code), along with RxTest (for testing
purposes), and select “Add Package” again (Figure 4-9).
Figure 4-9 Products selector for RxSwift
Once the installation is finished, we can see that our project already presents RxSwift as a dependency (Figure 4-10).
Figure 4-10 Under Package Dependencies, all SPM dependencies installed are shown
To use any of these products in our code, we simply have to import them (the ones we need at any given time):
import RxCocoa
import RxRelay
import RxSwift
Input/Output Approach
To work in a more organized way with RxSwift and the bind between the different components and events, we will use a
simplified procedure based on a fairly widespread convention whose origin is the Kickstarter company.5
• Input: It refers to all the events and interactions that occur in the View and that affect the ViewModel (write a text,
press a button…).
• Output: These are the changes that occur in the model and that must be reflected in the View.
Let’s see a simple example of the application of this convention. We will start with the code of the ViewModel (Listing
4-2).
class ExampleViewModel {
var output: Output!
var input: Input!
struct Input {
let text: PublishRelay<String>
}
struct Output {
let title: Driver<String>
}
init() {
let text = PublishRelay<String>()
let capsTitle = text
.map({
return text.uppercased()
})
.asDriver(onErrorJustReturn: "")
input = Input(text: text)
output = Output(title: capsTitle)
}
}
Listing 4-2 Example of RxSwift Input/Output approach in the ViewModel
Although, as we have said, we are not going to delve into a library as extensive and complex as RxSwift, we will explain
the different elements used in our application and what their function is.
• PublishRelay is a component of RxSwift whose function is to broadcast the most recent item it has watched (and
subsequent ones) to all those watchers that have subscribed. In this case, the passed item is of type String and is the
text that we put in the UITextField element.
• Driver is an observable that runs on the main thread (so it’s used to update the View). In this case, what it does is
pass the value that comes from the UITextField element (through the PublishRelay created and that it is observing).
Since we’re passing it to a UI component, we have to do it on the main thread.
Now, let’s see how we bind the ViewModel with the elements of the View (Listing 4-3).
class ExampleView {
func bind() {
textfield.rx.text
.bind(to: viewModel.input.text )
.disposed(by: disposeBag)
viewModel.output.title
.drive(titleLabel.rx.text)
.disposed(by: disposeBag)
}
}
Listing 4-3 Element binding in the View
• The first binds the text parameter of the UITextField element to the text variable (as Input) of the ViewModel. In
this way, when we write in that field, the ViewModel will receive it.
• The second block binds the title variable (such as Output) of the ViewModel to the text parameter of the UILabel el-
ement. Therefore, as we type, the text in the ViewModel is converted to uppercase and forwarded to the View for dis-
play.
NoteRxSwift provides several extensions for most of the objects and classes we use in Swift development. These exten-
sions are accessed via the rx particle, as in tableView.rx.
When we want to destroy an observable that we have created, we must call the dispose( ) method. To make this job eas-
ier, we have DisposeBag from RxSwift. Therefore, every time we create an observable, we must add it to the created
diposeBag object using the .disposed(by: disposeBag) method.
MyToDos Application Screens
As we have just seen, the core of the MVVM architecture is the ViewModel, which is in charge of maintaining the state of
the View and modifying it every time that state changes (thanks to the Data Binding).
Although the MVVM architecture is similar to the MVP architecture (which we have already seen in Chapter 3), where the
ViewModel would have similar functions to the Presenter, MVVM solves the coupling problem between the View and
Presenter of the MVP (in which the View has a reference to the Presenter and vice versa): in the MVVM this coupling does
not exist thanks to the use of Data Binding.
Therefore, we are going to focus on how to program the ViewModel and the View (along with the Controller) and the
binding between them.
In our MVVM project, AppDelegate and SceneDelegate present the same code as in the case of the MVP, in which we add
an instance of the HomeViewController to the UINavigationController component, without passing the dependencies to the
TasksListService and TaskService services since these dependencies will be established in the HomeViewModel component
(Listing 4-4).
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let navigationController = UINavigationController(rootViewController: HomeViewController())
navigationController.navigationBar.isHidden = true
navigationController.interactivePopGestureRecognizer?.isEnabled = false
window.backgroundColor = .white
window.rootViewController = navigationController
self.window = window
window.makeKeyAndVisible()
}
}
Listing 4-4 SceneDelegate changes to load HomeViewController
Home Screen
On the Home screen, the main component is the HomeViewModel, which is bound to the HomeView (Data Binding) (Figure
4-11).
The Controller for this screen is similar to the one we saw in the MVP model: now it is in charge of instantiating the
HomeViewModel and passing it to the View (Listing 4-5).
On the other hand, the HomeViewController also handles the routing between screens (in HomeView, the user can
select to access a list or create a new one, and this delegates the navigation to the HomeViewController), as shown in
Listing 4-6.
At the end of this chapter, we will see how to remove this navigation part of the UIViewController by using a Coordinator.
HomeView
HomeView changes quite a bit concerning the one we saw in the MVC or the MVP, not from the point of view of the com-
ponents that form it, but from the point of view of how they are aware of the changes in the state of the ViewModel
through Data Binding.
The fact of using a library like RxSwift allows us to easily link each of the components of the view (and its properties) to
the model.
To establish all the links between the ViewModel and the components of the View, we will create a method that
groups them (Listing 4-7).
We are now going to see what is the code we will introduce in this method, and that will allow us to link the components
of the View with the ViewModel.
We start with the UITableView component, which is the one that shows us which lists of tasks we have created.
First, we set the table’s delegate (for the UITableViewDelegate protocol) and which we will only use to set the height
of the row (Listing 4-8).
tableView.rx
.setDelegate(self)
.disposed(by: disposeBag)
Listing 4-8 Setting tableView delegate with RxSwift
Next, we need to pass information about the number of sections, rows, and items that make up the table to the table.
From the point of view of the Input/Output convention that we are going to use, the task lists will be an output of the
ViewModel (output.lists). Linking this data to the UITableView element is done through the statement
tableView.rx.items(cellIdentifier: ... (Listing 4-9).
viewModel.output.lists
.drive(tableView.rx.items(cellIdentifier: ToDoListCell.reuseId, cellType: ToDoListCell.self)) { (_, list, cell) in
cell.setCellParametersForList(list)
}
.disposed(by: disposeBag)
Listing 4-9 Binding tasks lists data to tableView
Next, we set the instruction that allows us to subscribe to the user’s cell selection action. This is done via the in-
put.selectRow parameter. Every time the user selects a cell, the IndexPath information of the selected cell is sent to the
ViewModel (Listing 4-10).
When we select a table cell and pass the corresponding IndexPath value to the ViewModel, it will take care of selecting
the corresponding TaskListModel object and will emit it as output. Therefore, we must bind this output
(output.selectedList) from the View.
tableView.rx.itemSelected
.bind(to: viewModel.input.selectRow)
.disposed(by: disposeBag)
viewModel.output.selectedList
.drive(onNext: { [self] list in
delegate?.selectedList(list)
})
.disposed(by: disposeBag)
Listing 4-10 Subscription to the itemSelected event
The last instruction related to the UITableView component is to delete a cell (Listing 4-11). Every time the user makes
the gesture to delete a cell, the ViewModel will be asked to delete it in the Model. This event of deleting a list will
correspond to an input of the ViewModel (input.deleteRow).
tableView.rx.itemDeleted
.bind(to: viewModel.input.deleteRow)
.disposed(by: disposeBag)
Listing 4-11 Subscription to the itemDeleted event
Once we have finished binding the UITableView component, we move on to binding the EmptyState component and the
Once we have finished binding the UITableView component, we move on to binding the EmptyState component and the
AddListButton component.
In the first case, what we want is to hide or show the EmptyState depending on whether or not there are task lists
created. The ViewModel must communicate to the View if it should show the EmptyState or not, so this information will
be an output (output.hideEmptyState), as shown in Listing 4-12.
viewModel.output.hideEmptyState
.drive(emptyState.rx.isHidden)
.disposed(by: disposeBag)
Listing 4-12 Binding output.hideEmptyState property of the ViewModel to the EmptyState’s isHidden property
When in the ViewModel hideEmptyState takes a false value, the EmptyState will be visible, while if it takes a true value, it
will be hidden.
Regarding the AddListButton, while in MVC or MVP, we assigned a target that called a method that we had created,
with RxSwift we simply assigned the code to be executed to the tap method (Listing 4-13).
addListButton.rx.tap
.asDriver()
.drive(onNext: { [self] in
delegate?.addList()
})
.disposed(by: disposeBag)
Listing 4-13 Setting tap event for AddListButton
At the end of the bindToViewModel method, what we do is make a call to reload the table which, as we will see now in
the ViewModel, calls the database to obtain the created task lists.
viewModel.input.reload.accept(())
As we have commented in the introduction to RxSwift, we are not going to delve into the possibilities offered by this li-
brary (which are many). We simply show how to use some of them in our example project.
HomeViewModel
As we have seen at the beginning of this chapter, the ViewModel is responsible for obtaining the data from the Model and
preparing it to be displayed by the View and also manages the business logic of this View.
As we have seen in the development of the HomeView, the HomeViewModel must have three inputs (reload, deleteRow,
and selectRow) and three outputs (lists, selectedList, and hideEmptyState). Therefore, we will define the struct Input and
the struct Output as follows (Listing 4-14).
class HomeViewModel {
var output: Output!
var input: Input!
struct Input {
let reload: PublishRelay<Void>
let deleteRow: PublishRelay<IndexPath>
let selectRow: PublishRelay<IndexPath>
}
struct Output {
let hideEmptyState: Driver<Bool>
let lists: Driver<[TasksListModel]>
let selectedList: Driver<TasksListModel>
}
...
}
Listing 4-14 Definition of Input and Output for the HomeViewModel
Once we have defined Input and Output, we set their behavior in the initialization of the HomeViewModel (Listing
).
4-15).
class HomeViewModel {
...
private let lists = BehaviorRelay<[TasksListModel]>(value: [])
private let taskList = BehaviorRelay<TasksListModel>(value: TasksListModel())
private var tasksListService: TasksListServiceProtocol!
init(tasksListService: TasksListServiceProtocol) {
self.tasksListService = tasksListService
// Inputs
let reload = PublishRelay<Void>()
_ = reload.subscribe(onNext: { [self] _ in
fetchTasksLists()
})
let deleteRow = PublishRelay<IndexPath>()
_ = deleteRow.subscribe(onNext: { [self] indexPath in
tasksListService.deleteList(listAtIndexPath(indexPath))
})
let selectRow = PublishRelay<IndexPath>()
_ = selectRow.subscribe(onNext: { [self] indexPath in
taskList.accept(listAtIndexPath(indexPath))
})
self.input = Input(reload: reload, deleteRow: deleteRow, selectRow: selectRow)
// Outputs
let items = lists
.asDriver(onErrorJustReturn: [])
let hideEmptyState = lists
.map({ items in
return !items.isEmpty
})
.asDriver(onErrorJustReturn: false)
let selectedList = taskList.asDriver()
output = Output(hideEmptyState: hideEmptyState, lists: items, selectedList: selectedList)
...
}
...
}
Listing 4-15 Establishment of the behavior of the parameters associated with the inputs and outputs
The input.reload parameter will execute the method that fetches the lists recorded in the database.
The input.deleteRow parameter will receive the position in the table of the cell we want to delete (as IndexPath).
The input.selectRow parameter will receive the position in the table of the cell we have selected (as IndexPath).
The output.lists parameter will emit an array of TasksListModel, each time it receives it from the database.
The output.selectedList parameter will emit a TasksListModel object, corresponding to the cell selected by the user.
The output.hideEmptyState parameter will emit a boolean that will be true if there are task lists in the database and false if
there aren’t.
Finally, we set the methods related to accessing the Model, such as calls to the database, or setting a database change
observer (Listing 4-16).
class HomeViewModel {
...
init(tasksListService: TasksListServiceProtocol) {
...
NotificationCenter.default.addObserver(self,
selector: #selector(contextObjectsDidChange),
name: NSNotification.Name.NSManagedObjectContextObjectsDidChange,
object: CoreDataManager.shared.mainContext)
}
@objc func contextObjectsDidChange() {
@objc func contextObjectsDidChange() {
fetchTasksLists()
}
func fetchTasksLists() {
lists.accept(tasksListService.fetchLists())
}
func listAtIndexPath(_ indexPath: IndexPath) -> TasksListModel {
lists.value[indexPath.row]
}
}
Listing 4-16 Relationship between the ViewModel and the Model
This screen is responsible for adding task lists and the communication between its components. The communication
between these components according to an MVVM architecture is shown in Figure 4-12.
AddListViewController
The AddListViewController only takes care of installing the AddListViewModel and passing it to the AddListView, and
handling the navigation back to the home screen (Listing 4-17).
AddListView
As we have seen in previous chapters, AddListView has the following elements that the user can interact with and that we
As we have seen in previous chapters, AddListView has the following elements that the user can interact with and that we
must link to the AddListViewModel: a UITextField element, two UIButton elements (one to add the list and another to go
back), and an IconSelector.
We are now going to see how we perform the Data Binding with the AddListViewModel for each of these elements in the
bindViewToModel method.
The titleTextField element is where we will enter the title of the list. We have to remember that to create a task this field
cannot be empty; otherwise, the addListButton button will be disabled.
Therefore, we must bind the content of titleTextField both to the addListButton button to enable it
(addListButton.rx.isEnabled) and to the title input in the AddListViewModel (input.title) (Listing 4-18).
titleTextfield
.rx.text
.map({ !($0?.isEmpty)! })
.bind(to: addListButton.rx.isEnabled)
.disposed(by: disposeBag)
titleTextfield.rx.text
.map({ $0! })
.bind(to: viewModel.input.title )
.disposed(by: disposeBag)
Listing 4-18 Binding of the content of titleTextField to the state of addListButton
With rx.text, we access the content of the UITextField; then through map, we evaluate whether or not it is empty and we
pass the state to the isEnabled parameter of the addListButton (so that if it is empty, isEnabled is false, and if it contains
text, isEnabled is true).
Next, we configure the behavior of the addListButton and backButton buttons in front of the tap event (Listing 4-19).
addListButton.rx.tap
.bind(to: viewModel.input.addList)
.disposed(by: disposeBag)
backButton.rx.tap
.bind(to: viewModel.input.dismiss)
.disposed(by: disposeBag)
viewModel.output.dismiss
.drive(onNext: { [self] _ in
delegate?.navigateBack()
})
.disposed(by: disposeBag
Listing 4-19 Bind addListButton and backButton to their corresponding tap events
The function of addListButton is to tell the AddListViewModel to save the new task list to the database (and then report
that it’s already added, so we’re back on the Home screen). We communicate this to the input.addList parameter.
The backButton’s function is to return to the HomeScreen, such as when finishing adding a new task list. For this reason,
instead of associating a direct call to the delegate.navigateBack method, we will create an Output in the ViewModel that
fulfills that call function (output.dismiss) and an Input that allows it to be called (input.dismiss).
For the case of the iconSelectorView, we will modify the IconSelectorView component to work with RxSwift instead of
using the delegate pattern. To do this, what we do is eliminate the protocol of this component (which we use in the MVC
and MVP architectures) and introduce a new variable of type BehaviorRelay, which will emit the name of the selected icon.
We will also modify the method by which when selecting one of the icons, its name was sent through the delegate. We
simply pass that value to the created variable for it to pass:
Now we can bind the iconSelectorView to the AddListViewModel to pass the icon name to it as an input (input.icon)
(Listing 4-20):
iconSelectorView.selectedIcon
.bind(to: viewModel.input.icon)
.disposed(by: disposeBag)
Listing 4-20 Binding of the name of the selected icon with the AddListViewModel
Finally, we will bind to the addedList method of the AddListViewModel, so that when this method is executed, the applica-
tion navigates back to the Home.
AddListViewModel
The AddListModel class is pretty straightforward. What is done in this class when instantiating it is to pass a reference to
the TaskListService (as a protocol) so that it can interact with the Model, and in addition, we initialize a TaskListModel ob-
ject.
Following the Input/Output convention, we set the input and output parameters of the AddListViewModel.
class AddListViewModel {
var output: Output!
var input: Input!
struct Input {
let icon: PublishRelay<String>
let title: PublishRelay<String>
let addList: PublishRelay<Void>
let dismiss: PublishRelay<Void>
}
struct Output {
let dismiss: Driver<Void>
}
...
}
Now we only have to set the behavior of these parameters in the initialization of the AddListViewModel (Listing 4-21).
class AddListViewModel {
...
private var tasksListService: TasksListServiceProtocol!
private(set) var list: TasksListModel!
private let dismiss = BehaviorRelay<Void>(value: ())
init(tasksListService: TasksListServiceProtocol) {
self.tasksListService = tasksListService
self.list = TasksListModel(id: ProcessInfo().globallyUniqueString, icon: "checkmark.seal.fill",
createdAt: Date())
// Inputs
let icon = PublishRelay<String>()
_ = icon.subscribe(onNext: { [self] newIcon in
list.icon = newIcon
})
let title = PublishRelay<String>()
_ = title.subscribe(onNext: { [self] newTitle in
list.title = newTitle
})
let addList = PublishRelay<Void>()
_ = addList.subscribe(onNext: { [self] _ in
tasksListService.saveTasksList(list)
dismiss.accept(())
})
let dismissView = PublishRelay<Void>()
_ = dismissView.subscribe(onNext: { [self] _ in
dismiss.accept(())
})
input = Input(icon: icon, title: title, addList: addList, dismiss: dismissView)
// Outputs
let backNavigation = dismiss.asDriver()
output = Output(dismiss: backNavigation)
...
}
}
Listing 4-21 Establishment of the behavior of the parameters associated with the inputs and outputs
The input.icon and input.title parameters will receive the corresponding values for the selected icon and the entered title.
For the input.addList parameter, it will execute the method that adds the new list to the database and then call the dismiss
method (which we’ve seen is bound to the delegate’s navigateBack method on the AddListView).
The dismiss method will be the same method that we call via the input.dismiss parameter (which we have bound to the
backButton in the AddListView).
This screen is responsible for displaying the tasks that make up a list, marking them as done, deleting them, and adding
new ones. The communication between its components is shown in Figure 4-13.
TaskListViewController
This controller is responsible for, on the one hand, instantiating the TaskListViewModel and passing it to the View and, on
the other hand, managing navigation to the add tasks screen or the Home screen.
On this screen we show the tasks that make up the list that we have selected in the Home screen, so in its initialization,
we must pass the object taskListModel, which then passes it in turn to the TaskListViewModel (Listing 4-22).
TaskListView
TaskListView has, from the point of view of the components that make it up, functionality similar to HomeView: it presents
a UITableView component that must be filled with the tasks that make up a list, a button to be able to add a new task, and
we can delete tasks. But, in addition, it includes the title of the task list, a button to return to HomeView, and the possibil-
ity of updating a task, employing a button that allows it to go from undone to done, and vice versa.
So, let’s start by seeing what code we have to introduce in the TaskListView's bindToModel method to bind the
UITableView element with the TaskListViewModel, keeping in mind that we are working with inputs and outputs (Listing
4-23).
tableView.rx
.setDelegate(self)
.disposed(by: disposeBag)
tableView.rx.itemDeleted
.bind(to: viewModel.input.deleteRow)
.disposed(by: disposeBag)
viewModel.output.tasks
.drive(tableView.rx.items(cellIdentifier: TaskCell.reuseId, cellType: TaskCell.self)) { (index, task, cell) in
cell.setParametersForTask(task, at: index)
cell.checkButton.rx.tap
.map({ IndexPath(row: cell.cellIndex, section: 0) })
.bind(to: viewModel.input.updateRow)
.disposed(by: cell.disposeBag)
}
.disposed(by: disposeBag)
Listing 4-23 Binding between the UITableView component and the TaskListViewModel
Surely all this code is familiar to you. The only addition is the button that allows us to update the status of the task
(checkButton), and that we configure for each of the cells of the table.
Next, we have the code that allows us to associate a target to each of the two buttons on this screen (Listing 4-24).
addTaskButton.rx.tap
.asDriver()
.drive(onNext: { [self] in
delegate?.addTask()
})
.disposed(by: disposeBag)
backButton.rx.tap
.asDriver()
.drive(onNext: { [self] in
delegate?.navigateBack()
})
.disposed(by: disposeBag)
Listing 4-24 Configuration of the targets of the addTaskButton and backButton buttons
Pressing these buttons will call the corresponding methods of the TaskListViewController via the delegate.
Finally, we have the link with two parameters of type output of the TaskListViewModel, and that will allow us, on the
one hand, to show the title of the task list and, on the other, to hide or show the EmptyState depending on whether or not
there are tasks in the list (Listing 4-25).
viewModel.output.pageTitle
.drive(pageTitle.rx.text)
.disposed(by: disposeBag)
viewModel.output.hideEmptyState
.drive(emptyState.rx.isHidden)
.disposed(by: disposeBag)
Listing 4-25 Binding of the pageTitle and emptyState elements to the pageTitle and hideEmptyState outputs of the TaskListViewModel
As we have done in the HomeView, at the end of the bindToViewModel method, what we do is make a call to reload the
table which, as we will see now in the ViewModel, calls the database to obtain the created task lists.
viewModel.input.reload.accept(())
TasksListViewModel
In the TasksListViewModel, we will have the logic that will manage this screen of the application and in which we will
establish the inputs and outputs to which we will bind from the TaskListView (Listing 4-26).
class TaskListViewModel {
var output: Output!
var input: Input!
struct Input {
let reload: PublishRelay<Void>
let deleteRow: PublishRelay<IndexPath>
let updateRow: PublishRelay<IndexPath>
}
struct Output {
let hideEmptyState: Driver<Bool>
let tasks: Driver<[TaskModel]>
let pageTitle: Driver<String>
}
...
}
Listing 4-26 Setting the inputs and outputs of the TaskListViewModel
Now, as in the previous cases, we set them in the initialization of the TaskListViewModel (Listing 4-27).
class TaskListViewModel {
...
init(tasksListModel: TasksListModel,
taskService: TaskServiceProtocol,
tasksListService: TasksListServiceProtocol) {
self.tasksListModel = tasksListModel
self.taskService = taskService
self.tasksListService = tasksListService
// Inputs
let reload = PublishRelay<Void>()
_ = reload.subscribe(onNext: { [self] _ in
fetchTasks()
})
let deleteRow = PublishRelay<IndexPath>()
_ = deleteRow.subscribe(onNext: { [self] indexPath in
deleteTaskAt(indexPath: indexPath)
})
let updateRow = PublishRelay<IndexPath>()
_ = updateRow.subscribe(onNext: { [self] indexPath in
updateTaskAt(indexPath: indexPath)
})
input = Input(reload: reload, deleteRow: deleteRow, updateRow: updateRow)
// Outputs
let items = tasks
.asDriver(onErrorJustReturn: [])
let hideEmptyState = tasks
.map({ items in
return !items.isEmpty
})
.asDriver(onErrorJustReturn: false)
let pageTitle = pageTitle
.asDriver(onErrorJustReturn: "")
output = Output(hideEmptyState: hideEmptyState, tasks: items, pageTitle: pageTitle)
...
}
...
}
Listing 4-27 Establishment of the behavior of the parameters associated with the inputs and outputs
The input.reload parameter will execute the method that fetches the tasks recorded in the database.
The input.deleteRow parameter will receive the position in the table of the cell we want to delete (as IndexPath) and will
execute the corresponding TaskService method.
The input.updateRow parameter will receive the position in the table of the cell that we want to update (as IndexPath) and
will execute the corresponding TaskService method.
The parameter output.hideEmptyState will emit a boolean that will be true if there are tasks in the list and false if there are
not.
The output.tasks parameter will emit an array with the tasks that form a list (as a TaskModel), each time it receives them
from the database.
The output.pageTitle parameter is in charge of passing the title of the task list.
Finally, we establish the methods that allow us to access the model (the database), as shown in Listing 4-28.
class TaskListViewModel {
...
private var tasksListModel: TasksListModel!
private var taskService: TaskServiceProtocol!
private var tasksListService: TasksListServiceProtocol!
let tasks = BehaviorRelay<[TaskModel]>(value: [])
let pageTitle = BehaviorRelay<String>(value: "")
init(tasksListModel: TasksListModel,
taskService: TaskServiceProtocol,
tasksListService: TasksListServiceProtocol) {
...
NotificationCenter.default.addObserver(self,
selector: #selector(contextObjectsDidChange),
name: NSNotification.Name.NSManagedObjectContextObjectsDidChange,
object: CoreDataManager.shared.mainContext)
}
@objc func contextObjectsDidChange() {
fetchTasks()
}
func fetchTasks() {
guard let list = tasksListService.fetchListWithId(tasksListModel.id) else { return }
let orderedTasks = list.tasks.sorted(by: { $0.createdAt.compare($1.createdAt) == .orderedDescending })
tasks.accept(orderedTasks)
pageTitle.accept(list.title)
}
func deleteTaskAt(indexPath: IndexPath) {
taskService.deleteTask(tasks.value[indexPath.row])
}
func updateTaskAt(indexPath: IndexPath) {
var taskToUpdate = tasks.value[indexPath.row]
taskToUpdate.done.toggle()
taskService.updateTask(taskToUpdate)
}
}
Listing 4-28 Relationship between the ViewModel and the Model
As you can see, by initializing this class we set the database change watcher. In this way, every time there is a change, the
fetchTasks function will be called, which will be in charge of calling the database (through the TaskListService), it will order
the tasks by creation date, and then, it will pass the corresponding values to the Observables that we have created so that
the View is updated according to the new data.
This screen is responsible for adding tasks to a given list and the communication between its components is shown in
Figure 4-14.
AddTaskViewController
In this case, AddTaskViewController is in charge of receiving the taskListModel object in its initialization (with the list of
tasks in which we want to add new tasks) and of instantiating the AddTaskViewModel (passing it the list of tasks and an
instance of the TaskService), and later passing it to the view (Listing 4-29).
AddTaskView
The code for the AddTaskView is very similar to what we developed for the AddListView, as it contains a UITextField
element, an IconsSelectorView element, and a UIButton element. Therefore, the code of the bindViewToModel method will
be familiar to you (Listing 4-30).
AddTaskViewModel
The AddTaskViewModel class will be in charge of creating and recording a task in the database. In the same way as in the
ViewModel earlier, we start by setting the inputs and outputs (Listing 4-31).
class AddTaskViewModel {
var output: Output!
var input: Input!
struct Input {
let icon: PublishRelay<String>
let title: PublishRelay<String>
let addTask: PublishRelay<Void>
}
struct Output {
let dismiss: Driver<Void>
}
...
}
Listing 4-31 Setting the inputs and outputs of the AddTaskViewModel
In this case, we only have three inputs (the icon, the task title, and the action to register the task) and one output (the ac-
tion to dismiss the view).
Then we just have to set them in the initialization of the AddTaskViewModel (Listing 4-32).
class AddTaskViewModel {
...
private var tasksListModel: TasksListModel!
private var taskService: TaskServiceProtocol!
private(set) var task: TaskModel!
let dismiss = BehaviorRelay<Void>(value: ())
init(tasksListModel: TasksListModel,
taskService: TaskServiceProtocol) {
self.tasksListModel = tasksListModel
self.taskService = taskService
self.task = TaskModel(id: ProcessInfo().globallyUniqueString,
icon: "checkmark.seal.fill",
done: false,
createdAt: Date())
// Inputs
let icon = PublishRelay<String>()
_ = icon.subscribe(onNext: { [self] newIcon in
task.icon = newIcon
})
let title = PublishRelay<String>()
_ = title.subscribe(onNext: { [self] newTitle in
task.title = newTitle
})
let addTask = PublishRelay<Void>()
_ = addTask.subscribe(onNext: { [self] _ in
taskService.saveTask(task, in: tasksListModel)
dismiss.accept(())
})
input = Input(icon: icon, title: title, addTask: addTask)
// Outputs
let dismissView = dismiss.asDriver()
output = Output(dismiss: dismissView)
}
}
Listing 4-32 Establishment of the behavior of the parameters associated with the inputs and outputs
The input.icon and input.title parameters will receive the corresponding values for the selected icon and the entered title.
For the input.addTask parameter, it will execute the method that adds the new task to the database and then call the
dismiss method (which we’ve seen is bound to the delegate’s navigateBack method on the AddTaskView).
MVVM-MyToDos Testing
In the MVVM architecture that we have just studied, the link between the View and the Model has been done through a
new component, the ViewModel. But, in addition to the link between the View and the ViewModel, we have done it
through a process called Data Binding, for which we have used a specific library: RxSWift. In this way, we get the View to
be aware of the changes that occur in the ViewModel, and update accordingly.
Therefore, the question that arises is, how can we test a system that works with events and data streams (i.e., values that
change over time)? How do we test code developed with RxSwift? For this, we will use a library that accompanies RxSwift:
RxTest.
NoteAs you will remember, at the beginning of this chapter, when we installed RxSwift with the Swift Package Manager,
together with RxSwift, RxCocoa, and RxRelay, we also included RxTest (which we added to the MVVM_MyToDosTests tar-
get).
RxTest Introduction
As we have seen, using RxSwift, we go from working with individual values to working with streams that emit values over
time. And RxTest is going to help us test these streams.
Therefore, to carry out a test, we must first create an instance of TestScheduler (with an initial argument “initialClock”,
which will define the start of the transmission), an instance of DisposeBag to have the subscriptions of previous tests, and
also an instance of the ViewModel (Listing 4-33).
Now we just have to apply these elements to a test. For example, suppose we want to test if the data has been loaded
from a server after activating the redial button (Listing 4-34).
func testLoading_whenThereIsNoList_shouldShowEmptyState() {
// We create an observer that will be what we want to observe and that will be the result of the ViewModel output.
let isLoaded = testScheduler.createObserver(Bool.self)
// We link the output we want to test to the observable.
viewModel.output.isLoadedData
.drive(isLoaded)
.disposed(by: disposeBag)
// We send to the ViewModel the action of calling the server through a next event at time 10.
testScheduler.createColdObservable([.next(10, ())])
.bind(to: viewModel.input.callServer)
.disposed(by: disposeBag)
// Start the scheduler
testScheduler.start()
// Finally, we test using XCAssert.
XCTAssertEqual(isLoaded.events, [.next(0, false), .next(10, true)])
}
Listing 4-34 Test example with TestScheduler
HomeViewModel Tests
Now we are going to apply what we have just seen about RxTest and the testing of RxSwift-based ViewModels in the
HomeViewModel component.
NoteRemember that you can find the complete project code, including the tests, in the repository associated with this
book.
First, we create instances of TestScheduler, DisposeBag, and HomeViewModel. Also, since to instantiate the
HomeViewModel we have to pass a reference to the TaskListService, we also create an instance of it and an additional
TaskList object (Listing 4-35).
import XCTest
import RxSwift
import RxTest
@testable import MVVM_MyToDos
@testable import MVVM_MyToDos
var disposeBag: DisposeBag!
var viewModel: HomeViewModel!
var testScheduler: TestScheduler!
let tasksListService = TasksListService(coreDataManager: InMemoryCoreDataManager.shared)
let taskList = TasksListModel(id: "12345-67890",
title: "Test List",
icon: "test.icon",
tasks: [TaskModel](),
createdAt: Date())
override func setUpWithError() throws {
disposeBag = DisposeBag()
testScheduler = TestScheduler(initialClock: 0)
tasksListService.fetchLists().forEach { tasksListService.deleteList($0) }
viewModel = HomeViewModel(tasksListService: tasksListService)
}
override func tearDownWithError() throws {
disposeBag = nil
viewModel = nil
testScheduler = nil
tasksListService.fetchLists().forEach { tasksListService.deleteList($0) }
super.tearDown()
}
...
}
Listing 4-35 Instantiation of the different components of the HomeViewModelTests
Unlike what we did in the MVC and MVP architectures, in which we used TasksListSevice and TaskService mocks, here we
will use the application’s database (Core Data), but we will configure it to work its persistence in memory and not in the
memory of the device (InMemoryCoreDataManager).
To work with a clean database, we have introduced some calls that allow us to delete all the lists created in each test,
both before and after performing the test:
tasksListService.fetchLists().forEach { tasksListService.deleteList($0) }
Now we just have to create the different tests for the HomeViewModel.
EmptyState Test
As we can see in Listing 4-36, in the first test we check that, with the database empty, the EmptyState will be displayed.
For this reason, the last event (after reloading the table) must return false (.next(10, false)), since according to the
HomeViewModel, the output.hideEmptyState returns the boolean !items.isEmpty.
In the second test, since we first added a list, now the value of !items.isEmpty will be true, so the last event is .next(10,
true).
func testEmptyState_whenThereIsNoList_shouldShowEmptyState() {
let hideEmptyState = testScheduler.createObserver(Bool.self)
viewModel.output.hideEmptyState
.drive(hideEmptyState)
.disposed(by: disposeBag)
testScheduler.createColdObservable([.next(10, ())])
.bind(to: viewModel.input.reload)
.disposed(by: disposeBag)
testScheduler.start()
XCTAssertEqual(hideEmptyState.events, [.next(0, false), .next(10, false)])
}
func testEmptyState_whenAddOneList_shouldHideEmptyState() {
let hideEmptyState = testScheduler.createObserver(Bool.self)
tasksListService.saveTasksList(taskList)
viewModel.output.hideEmptyState
.drive(hideEmptyState)
.disposed(by: disposeBag)
testScheduler.createColdObservable([.next(10, ())])
testScheduler.createColdObservable([.next(10, ())])
.bind(to: viewModel.input.reload)
.disposed(by: disposeBag)
testScheduler.start()
XCTAssertEqual(hideEmptyState.events, [.next(0, false), .next(10, true)])
}
Listing 4-36 EmptyState test methods
NoteNote that every time the HomeViewModel has a change in the output.hideEmptyState, we’ll have to handle it in the
XCTAssertEqual.
In this test, we will prove that after the action of deleting a cell from the table, the table is empty (Listing 4-37). The
steps to follow will be to add a list to the database, reload the table, launch the delete action of the cell, reload the list
again, and verify that there are no lists in the database.
func testRemoveListAtIndex_whenAddedOneList_shouldBeEmptyModelOnDeleteList() {
let lists = testScheduler.createObserver([TasksListModel].self)
tasksListService.saveTasksList(taskList)
viewModel.output.lists
.drive(lists)
.disposed(by: disposeBag)
testScheduler.createColdObservable([.next(10, ())])
.bind(to: viewModel.input.reload)
.disposed(by: disposeBag)
testScheduler.createColdObservable([.next(20, IndexPath(row: 0, section: 0))])
.bind(to: viewModel.input.deleteRow)
.disposed(by: disposeBag)
testScheduler.createColdObservable([.next(30, ())])
.bind(to: viewModel.input.reload)
.disposed(by: disposeBag)
testScheduler.start()
XCTAssertEqual(lists.events, [.next(0, []), .next(10, [taskList]), .next(30, []), .next(30, [])])
}
Listing 4-37 Method for testing cell deletion
In the last test, we will verify that after adding a list to the database, we can select it in the table and that the model
returns that list (Listing 4-38).
func testSelectListAtIndex_whenSelectAList_shouldBeReturnOneList() {
let selectedList = testScheduler.createObserver(TasksListModel.self)
tasksListService.saveTasksList(taskList)
viewModel.output.selectedList
.drive(selectedList)
.disposed(by: disposeBag)
testScheduler.createColdObservable([.next(10, ())])
.bind(to: viewModel.input.reload)
.disposed(by: disposeBag)
testScheduler.createColdObservable([.next(20, IndexPath(row: 0, section: 0))])
.bind(to: viewModel.input.selectRow)
.disposed(by: disposeBag)
testScheduler.start()
XCTAssertEqual(selectedList.events, [.next(0, TasksListModel()), .next(20, taskList)])
}
Listing 4-38 Method for testing the selection of lists
MVVM-C: Model–View–ViewModel–Coordinator
Until now, in the different architectures studied, we have seen how the navigation between the different screens, or what
is the same between the different view controllers, occurred within said controllers.
is the same between the different view controllers, occurred within said controllers.
This made it difficult, on the one hand, to test them and, on the other, that they could be reused in other parts of the ap-
plication.
What Is a Coordinator?
A Coordinator is a class that handles navigation outside of view controllers. This way we extract the navigation code from
the view controllers, making them simpler and more reusable. Therefore, a Coordinator will have the tasks of loading the
view controller that we are going to access and managing the navigation flow from that view controller to others (Figure
4-15).
The first thing we will do is create a protocol for all of our coordinators to adhere to. Since the application is simple,
this protocol will also be simple (Listing 4-39).
protocol Coordinator {
var navigationController: UINavigationController { get set }
func start()
}
Listing 4-39 Coordinator protocol
All coordinators that implement this protocol must define a navigationController variable (to which we will assign the ap-
plication’s navigationController) and a start() function, which will be in charge of calling the view controller.
NoteIn large applications, with more complex navigation flows, it is a good strategy to break them into simpler parts,
managed by a ParentCoordinator on which an array of ChildCoordinator depends.
As most of the application code is the same as when we have seen the MVVM architecture, we are going to see only those
things that we have added new.
things that we have added new.
NoteRemember that all the code of the project can be found in the repository of this book.
SceneDelegate
In the SceneDelegate, what we do now is load the HomeCoordinator, passing it the navigationController. Next, we call the
start() method.
Home Screen
The class that will manage the navigation, and which we have called from the SceneDelegate, is the HomeCoordinator. As
shown in Listing 4-40, in addition to the start method, which is given by the protocol, it presents a couple of methods plus
the showSelectedList method (which loads the Coordinator of the screen that shows the tasks of a list) and addList (which
loads the Coordinator from the create a new list screen).
protocol HomeCoordinatorProtocol {
func showSelectedList(_ list: TasksListModel)
func gotoAddList()
}
class HomeCoordinator: Coordinator, HomeCoordinatorProtocol {
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let viewModel = HomeViewModel(tasksListService: TasksListService(), coordinator: self)
navigationController.pushViewController(HomeViewController(viewModel: viewModel), animated: true)
}
func showSelectedList(_ list: TasksListModel) {
let taskListCoordinator = TaskListCoordinator(navigationController: navigationController, taskList: list)
taskListCoordinator.start()
}
func gotoAddList() {
let addListCoordinator = AddListCoordinator(navigationController: navigationController)
addListCoordinator.start()
}
}
Listing 4-40 HomeCoordinatorProtocol and HomeCoordinator class
As you can see, when passing the HomeViewController to the navigation controller, we are also passing an instance of the
HomeViewModel, which includes a reference to the Coordinator itself to be able to manage navigation calls from
said ViewModel, without the need for it to go through the View and the Controller.
Therefore, if we call, for example, the gotAddList method of the HomeCoordinator, it will take care of instantiating the