0% found this document useful (0 votes)
37 views

MVVM Model-View-ViewModel iOS

Uploaded by

champsdload
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
37 views

MVVM Model-View-ViewModel iOS

Uploaded by

champsdload
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 26

They contain the constant parameters that we will use in the application.

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.

Figure 4-4 View layer files

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

MyToDos Data Binding

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 reactive programming library for iOS application development.

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.

Observables and Observers

When using this library, we will have two types of elements:

• Observable: Issue notifications when a change occurs.


• Observer: An Observer subscribe to an Observable to receive its notifications. An Observable can have one or more ob-
servers.

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.

To install RxSwift in our project, we will follow these steps:

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.

Figure 4-8 Search for the RxSwift package

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

It is a functional approach in which the concept of Input/Output is used.

• 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.

First, we can see the PublishRelay<String> and Driver<String> elements:

• 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

Here we can see two blocks of code:

• 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.

AppDelegate and SceneDelegate

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).

Figure 4-11 Home screen components communication schema


HomeViewController

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).

class HomeViewController: UIViewController {


private var homeView: HomeView!
...
override func loadView() {
super.loadView()
setupHomeView()
}
private func setupHomeView() {
let viewModel = HomeViewModel(tasksListService: TasksListService())
homeView = HomeView(viewModel: viewModel)
homeView.delegate = self
self.view = homeView
}
}
Listing 4-5 HomeViewModel instantiation in HomeViewController

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.

extension HomeViewController: HomeViewControllerDelegate {


func addList() {
navigationController?.pushViewController(AddListViewController(), animated: true)
}
func selectedList(_ list: TasksListModel) {
let taskViewController = TaskListViewController(tasksListModel: list)
navigationController?.pushViewController(taskViewController, animated: true)
}
}
Listing 4-6 Implementation of the methods of the HomeViewControllerDelegate

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).

class HomeView: UIView {


...
private let viewModel: HomeViewModel!
private let disposeBag = DisposeBag()
init(frame: CGRect = .zero, viewModel: HomeViewModel) {
self.viewModel = viewModel
...
bindViewToModel(viewModel)
}
}
private extension HomeView {
...
func bindViewToModel(_ viewModel: HomeViewModel) {
...
}
}
Listing 4-7 Setting ViewModel in HomeView

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

Add List Screen

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.

Figure 4-12 Add list screen components communication schema

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).

class AddListViewController: UIViewController {


private var addListView: AddListView!
...
private function setupAddListView() {
let viewModel = AddListViewModel(tasksListService: TasksListService())
addListView = AddListView(viewmodel: viewmodel)
addListView.delegate = self
self.view = addListView
}
}
AddListViewController extension: BackButtonDelegate {
func navigateBack() {
navigationController?.popViewController(animated: true)
}
}
Listing 4-17 Setting AddListViewController code

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.

var selectedIcon = BehaviorRelay<String>(value: "checkmark.seal.fill")

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:

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {


selectedIcon.accept(Constants.icons[indexPath.item])
selectedIcon.accept(Constants.icons[indexPath.item])
}

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.

Definition of Input and Output for 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).

Tasks List Screen

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.

Figure 4-13 Tasks list screen components communication schema

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).

class TaskListViewController: UIViewController {


private var taskListView: TaskListView!
private var tasksListModel: TasksListModel!
init(tasksListModel: TasksListModel) {
self.tasksListModel = tasksListModel
super.init(nibName: nil, bundle: nil)
}
...
private func setupTaskListView() {
let viewModel = TaskListViewModel(tasksListModel: tasksListModel, taskService: TaskService(), tasksListService: TasksListService())
taskListView = TaskListView(viewModel: viewModel)
taskListView.delegate = self
self.view = taskListView
}
}
extension TaskListViewController: TaskListViewControllerDelegate {
func addTask() {
let addTaskViewController = AddTaskViewController(tasksListModel: tasksListModel)
addTaskViewController.modalPresentationStyle = .pageSheet
present(addTaskViewController, animated: true)
}
}
extension TaskListViewController: BackButtonDelegate {
func navigateBack() {
navigationController?.popViewController(animated: true)
}
}
Listing 4-22 Setting TaskListViewController code

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.

Add Task Screen

This screen is responsible for adding tasks to a given list and the communication between its components is shown in
Figure 4-14.

Figure 4-14 Add task screen components communication schema

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).

class AddTaskViewController: UIViewController {


private var addTaskView: AddTaskView!
private var tasksListModel: TasksListModel!
init(tasksListModel: TasksListModel) {
super.init(nibName: nil, bundle: nil)
self.tasksListModel = tasksListModel
}
...
...
private func setupAddTaskView() {
let viewModel = AddTaskViewModel(tasksListModel: tasksListModel, taskService: TaskService())
addTaskView = AddTaskView(viewModel: viewModel)
addTaskView.delegate = self
self.view = addTaskView
}
}
Listing 4-29 Setting AddTaskViewController code

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).

func bindViewToModel(_ viewModel: AddTaskViewModel) {


titleTextfield.rx.text
.map({!($0?.isEmpty)!})
.bind(to: addTaskButton.rx.isEnabled)
.disposed(by: disposeBag)
titleTextfield.rx.text
.map({ $0! })
.bind(to: viewModel.input.title )
.disposed(by: disposeBag)
addTaskButton.rx.tap
.bind(to: viewModel.input.addTask)
.disposed(by: disposeBag)
iconSelectorView.selectedIcon
.bind(to: viewModel.input.icon)
.disposed(by: disposeBag)
viewModel.output.dismiss
.skip(1)
.drive(onNext: { [self] in
delegate?.addedTask()
})
.disposed(by: disposeBag)
}
Listing 4-30 Binding elements between AddTaskView and AddTaskViewModel

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.

For this, we will use the main component of RxTest: TestScheduler.


This component allows us to create Observables and Observers that we can bind and intercept so that we can see what
goes in and what goes out of our ViewModel. We can do this in certain “virtual times”; therefore, we can create events at
specific times.

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).

class ExampleTests: XCTestCase {


var testScheduler: TestScheduler!
var disposeBag: DisposeBag!
var viewModel: ViewModel!
override func setUpWithError() throws {
testScheduler = TestScheduler(initialClock: 0)
disposeBag = DisposeBag()
vieModel = ViewModel()
}
override func tearDownWithError() throws {
testScheduler = nil
disposeBag = nil
viewModel = nil
super.tearDown()
}
}
Listing 4-33 Creating instances of TestScheduler, DisposeBag, and ViewModel for our tests

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.

Testing the Deletion of Lists

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

List Selection Testing

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.

To solve this situation, we are going to use the Coordinator pattern.

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).

Figure 4-15 Model–View–ViewModel–Coordinator schema

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.

Using MVVM-C in MyToDos

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.

class SceneDelegate: UIResponder, UIWindowSceneDelegate {


var window: UIWindow?
var homeCoordinator: HomeCoordinator?
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.init()
navigationController.navigationBar.isHidden = true
navigationController.interactivePopGestureRecognizer?.isEnabled = false
homeCoordinator = HomeCoordinator(navigationController: navigationController)
homeCoordinator?.start()
window.backgroundColor = .white
window.rootViewController = navigationController
self.window = window
window.makeKeyAndVisible()
}
}
...
}

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

You might also like