model View Presenter
model View Presenter
}
Listing 3-3 Setting the observer on Presenter initialization
Core Data
In this folder, we will have the CoreDataManager.swift file (which we created in Chapter 1), along with the four files
created by Xcode automatically for the database entities.
Models
As described in Chapter 2, here we have the models into which we can transform the database entities. In addition, we
will create a protocol that the models must comply with, to transform from model to entity and vice versa.
Services
Here we will have the classes that allow us to send information to the database (create, update, or delete it) or re-
trieve information from the database and transform it into models.
Extensions
In this case, we have created a UIColor extension to be able to easily access the colors created especially for this appli-
cation and an extension to the NSManagedObject class that will prevent us from conflicting with the contexts when we
do the testing part.
Constants
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 3-4.
Figure 3-4 View layer files
Remember that in MVP, Controllers usually only have coordination/routing functions (to navigate between screens)
and, in some cases, pass information (via a Delegate pattern, for example).
Presenter
This folder only contains the Presenters, which, as we have seen, connect the Model to the View (Figure 3-5).
As we have just commented, in the MVP architecture, unlike the MVC, it is the Presenter that is in charge of the busi-
ness logic (leaving the navigation tasks to the Controller).
Next, we will see, for each of the different screens of our app, how we connect the different layers with each other:
Model, View(+UIViewController), and Presenter.
As we saw in Chapter 2, the AppDelegate and the SceneDelegate are in charge of managing the life cycle of the appli-
cation and what is displayed and how it is displayed on the screen.
In our case, we will only modify the SceneDelegate, which will be in charge of creating a new UIWindow, configuring
the root view controller of the application, and, finally, that the created UIWindow is the key window.
Unlike when we studied the MVC, in which we passed an instance of the HomeViewController to the
UINavigationController component, which in its initialization required that the dependencies be passed to the
TasksListService and TaskService services, now the HomeViewController does not require them, since as we will see in a
moment, these dependencies will be held by the HomePresenter component.
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()
}
}
Home Screen
On the Home screen, the main component is the Presenter, which is in charge of passing information to the Model and
subscribing to notifications of its changes. In addition, it is also responsible for receiving user interactions in the View
and updating it with the information it receives from the Model.
The View is separate from the Controller, and some of its actions, such as navigating to another screen, are
delegated to the Controller (Figure 3-6).
Figure 3-6 Home screen components communication schema
HomeController
Regarding the MVC architecture, in the MVP architecture, the HomeController has lost its relationship with the Model
layer. Now, it is in charge, on the one hand, of instantiating the HomeView and passing it to the HomePresenter (which,
as we have seen, we will pass as a protocol with the methods it must use) (Listing 3-4).
On the other hand, HomeViewController also takes care of the routing between screens (in the 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 3-5.
HomeView
Now the HomeView receives from the HomePresenter the information it should display. This is seen when filling the
table with the task lists that are created or the HomeViewDelegate implementation (Listing 3-6).
On the other hand, it delegates to the Controller those actions that require navigation within the application, such
as when selecting and accessing a list of tasks or creating a new one (Listing 3-7).
HomePresenter
The Presenter is in charge of acting as an intermediary between the View and the Model. For this reason, in the initial-
ization of the HomePresenter, we pass references to both the View (HomeView, in protocol form) and the Model
(TaskListService). Access to the Model through the TaskListService will allow us both to retrieve the information that
we want to show (task lists) and to delete those lists that we select.
To know when there was a change in the database and, thus, update the view, we also introduce an observer in the
initialization of the HomePresenter, as shown in Listing 3-8.
class HomePresenter {
private weak var homeView: HomeViewDelegate?
private var tasksListService: TasksListService!
private var lists: [TasksListModel] = [TasksListModel]()
init(homeView: HomeViewDelegate? = nil,
tasksListService: TasksListService) {
self.homeView = homeView
self.tasksListService = tasksListService
NotificationCenter.default.addObserver(self,
selector: #selector(contextObjectsDidChange),
name: NSNotification.Name.NSManagedObjectContextObjectsDidChange,
object: CoreDataManager.shared.mainContext)
}
@objc func contextObjectsDidChange() {
fetchTasksLists()
}
func fetchTasksLists() {
lists = tasksListService.fetchLists()
homeView?.reloadData()
}
var numberOfTaskLists: Int {
return lists.count
}
func listAtIndex(_ index: Int) -> TasksListModel {
return lists[index]
}
...
}
Listing 3-8 HomePresenter initialization
When the observer receives a notification that the database has changed, the contextObjectsDidChange event is fired
(which calls the fetchTasksLists event), with which the information that the HomePresenter has about the task lists to
be displayed is updated, to then call to the reloadData method of the HomeView.
If the user wants to delete a task list, the HomeView will pass the action to the HomePresenter and this will be in
charge of communicating it to the Model (Listing 3-9).
class HomePresenter {
...
func removeListAtIndex(_ index: Int) {
let list = listAtIndex(index)
tasksListService.deleteList(list)
lists.remove(at: index)
}
}
Listing 3-9 HomePresenter communicates with Model to delete lists
This screen is responsible for adding task lists and the communication between its components. The connection
between the different components is shown in Figure 3-7.
AddListViewController
In the same way that we did in the HomeViewController, the AddListViewController class is in charge of initializing the
Presenter and passing it to the view (Listing 3-10).
private func setupAddListView() {
let presenter = AddListPresenter(addListView: addListView, tasksListService: TasksListService())
addListView.delegate = self
addListView.presenter = presenter
self.view = addListView
}
Listing 3-10 AddListPresenter instantiation in AddListViewController
Once the list is created, or also by user action on the back button, it navigates back to the Home screen (which the
AddListView has delegated, as shown in Listing 3-11).
AddListView
In AddListView the user can create a task list by selecting an icon and adding a title. All this information is passed to
the AddListPresenter through the reference that the view maintains with it (Listing 3-12).
extension AddListView {
@objc func addListAction() {
guard titleTextfield.hasText else { return }
presenter.addListWithTitle(titleTextfield.text!)
}
}
extension AddListView: IconSelectorViewDelegate {
func selectedIcon(_ icon: String) {
presenter.setListIcon(icon)
}
}
Listing 3-12 Presenter gets info from the view
The user can also exit this screen without creating any task lists. Simply select the navigate back button (the
HomePresenter can call this method because when instantiating it we have passed it to the AddListViewDelegate
protocol that it must comply with). In this case, the View delegates to the Controller to implement this action (Listing
3-13).
AddListPresenter
The AddListPresenter is in charge of managing the creation of lists on this screen. In its initialization, we will pass both
the AddListView (as a protocol) and the TaskListService that allows it to connect to the Model.
In addition, we will initialize a list that will be completed with the information of the title and the icon that comes
from the View (Listing 3-14).
class AddListPresenter {
class AddListPresenter {
private weak var addListView: AddListViewDelegate?
private var tasksListService: TasksListService!
private var list: TasksListModel!
init(addListView: AddListViewDelegate? = nil,
tasksListService: TasksListService) {
self.addListView = addListView
self.tasksListService = tasksListService
self.list = TasksListModel(id: ProcessInfo().globallyUniqueString,
icon: "checkmark.seal.fill",
createdAt: Date())
}
func setListIcon(_ icon: String) {
list.icon = icon
}
func addListWithTitle(_ title: String) {
list.title = title
tasksListService.saveTasksList(list)
addListView?.backToHome()
}
}
Listing 3-14 AddListPresenter code
As you can see at the end of the addListWithTitle method, once the order to create the list has been sent to the ser-
vice, the Presenter tells the view to execute the backToHome method to return to the Home screen (which, as we just
saw, it does by delegation).
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 3-8.
TaskListViewController
Unlike the MVC architecture, in which the TaskListViewController had communication functions with the Model, here it
is only in charge of instantiating the TaskListPresenter (Listing 3-15).
And to manage navigation, either to the add tasks screen (AddTaskViewController) or to return to the Home screen,
we'll implement the methods associated with the TaskListViewControllerDelegate and BackButtonDelegate protocols
(Listing 3-16).
TaskListView
In the TaskListView, similar to what we did in the HomeView, we have a UITableView element that shows us the
information it gets from the TaskListPresenter (Listing 3-17).
On the other hand, you have to implement the methods of the TaskListViewDelegate protocol that will allow the
TaskListPresenter to update the view (Listing 3-18).
Finally, we must also implement the TaskCellDelegate protocol method associated with the TaskCell component,
and that is the one that will allow us to modify the state of a task in the database via the TaskListPresenter (Listing
3-19).
TaskListPresenter
The TaskListPresenter is the most complex of the four since it has to display the tasks that make up a list, manage the
deletion or update of a task, as well as observe the addition of new tasks.
Therefore, in the initialization of this class, we must pass not only the view but also the TaskListModel object with the
tasks to be displayed, and instances to the TaskListService and TaskService services, which will allow us to interact with
the Model.
We’ll use the TaskListService to retrieve the list of tasks we’re working with from the database and the TaskService to
update or delete a given task.
In addition, we will need to add an observer for changes in the database (Listing 3-20).
class TasksListPresenter {
...
init(taskListView: TaskListViewDelegate? = nil,
tasksListModel: TasksListModel,
taskService: TaskService,
tasksListService: TasksListService) {
self.taskListView = taskListView
self.tasksListModel = tasksListModel
self.taskService = taskService
self.tasksListService = tasksListService
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 }
tasksListModel = list
tasks = tasksListModel.tasks.sorted(by: { $0.createdAt.compare($1.createdAt) == .orderedDescending })
taskListView?.reloadData()
}
...
}
Listing 3-20 TaskListPresenter initialization code
In Listing 3-21, you can also see the code for the interaction of the TaskListPresenter with the Model. In this case,
TaskListView will pass the delete task action to the TaskListPresenter, which will be in charge of communicating it to
the Model.
extension TasksListPresenter {
func updateTask(_ task: TaskModel) {
taskService.updateTask(task)
}
func removeTaskAtIndex(_ index: Int) {
let task = taskAtIndex(index)
taskService.deleteTask(task)
tasks.remove(at: index)
}
}
Listing 3-21 TaskListPresenter code for interaction with TaskService
This screen is responsible for adding tasks to a given list and the communication between its components is shown in
Figure 3-9.
AddTaskViewController
As in the case of the previous controllers, in the AddTaskViewController the AddTaskPresenter is initialized, to which
the view is passed, the object of the list of tasks to which we want to add a new task, and the service with which to
access the Model (TaskService) (Listing 3-22).
AddTaskView
In AddTaskView, the user can create a task by selecting an icon and adding a title. All of this information is passed to
the AddTaskPresenter through the reference the View maintains to it (Listing 3-24).
On the other hand, once the user performs the action of adding the task, the AddTaskPresenter will call the
addedTask method (defined in the AddTaskViewDelegate protocol, and which AddTaskView should implement) (Listing
3-25).
In turn, the action related to the addedTask method is delegated to the AddTaskViewController which, as we just saw,
will remove the screen.
AddTaskPresenter
The AddTaskPresenter is in charge of creating and adding tasks to a list. In its initialization, we will pass the
AddTaskView (as a protocol), the object (TaskListModel) that contains the list to which we will add the new task, and
the TaskService, which will be the connection with the Model layer, and that will allow us to add the task in the data-
base.
Also, similar to how we did when adding a new task list, we will initialize a task that we only have to complete
later with the title and the selected icon (Listing 3-26).
class AddTaskPresenter {
private var addTaskView: AddTaskViewDelegate?
private var tasksListModel: TasksListModel!
private var taskService: TaskService!
private var task: TaskModel!
init(addTaskView: AddTaskViewDelegate? = nil,
tasksListModel: TasksListModel,
taskService: TaskService) {
self.addTaskView = addTaskView
self.tasksListModel = tasksListModel
self.taskService = taskService
self.task = TaskModel(id: ProcessInfo().globallyUniqueString,
icon: "checkmark.seal.fill",
done: false,
createdAt: Date())
}
func setTaskIcon(_ icon: String) {
task.icon = icon
}
func addTaskWithTitle(_ title: String) {
task.title = title
taskService.saveTask(task, in: tasksListModel)
addTaskView?.addedTask()
}
Listing 3-26 AddTaskPresenter code
MVP-MyToDos Testing
If we compare the MVC architecture with the MVP, we can see that in the MVP the business logic has passed from the
Controller to the Presenter, leaving only the navigation in the Controller. Therefore, all the testing of the business
logic will have to be transferred to the Presenter.
We are now going to see how the testing of the components belonging to the screen that allows the user to add a
new list would be.
NoteRemember, as we indicated in Chapter 2, that although we show the code and the tests separately for pedagogi-
cal reasons, it is recommended that when developing our applications, we work according to the TDD (test-driven de-
velopment) methodology.
AddListViewController
As we just discussed, the Controller only handles navigation. In this case, we will have the navigation back to the
Home screen (Listing 3-27).