KEMBAR78
model View Presenter | PDF | Databases | Model–View–Controller
0% found this document useful (0 votes)
35 views13 pages

model View Presenter

Uploaded by

champsdload
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
35 views13 pages

model View Presenter

Uploaded by

champsdload
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 13

updateView()

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

Figure 3-5 Presenter layer files

MyToDos Application Screens


MyToDos Application Screens

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.

AppDelegate and SceneDelegate

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

class HomeViewController: UIViewController {


private var homeView = HomeView()
...
override func loadView() {
super.loadView()
setupHomeView()
}
private func setupHomeView() {
let presenter = HomePresenter(homeView: homeView, tasksListService: TasksListService())
homeView.delegate = self
homeView.presenter = presenter
homeView.setupView()
self.view = homeView
}
}
Listing 3-4 HomePresenter instantiation in HomeViewController

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.

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 3-5 Implementation of the methods of the HomeViewControllerDelegate

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

extension HomeView: UITableViewDataSource {


...
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return presenter.numberOfTaskLists
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: ToDoListCell.reuseId, for: indexPath) as! ToDoListCell
cell.setCellParametersForList(presenter.listAtIndex(indexPath.row))
return cell
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
presenter.removeListAtIndex(indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
}
}
}
extension HomeView: HomeViewDelegate {
func reloadData() {
tableView.reloadData()
emptyState.isHidden = presenter.numberOfTaskLists > 0
}
}
Listing 3-6 Setup of the tasks lists table in the HomeView

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

extension HomeView: UITableViewDelegate {


...
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
delegate?.selectedList(presenter.listAtIndex(indexPath.row))
}
}
private extension HomeView {
...
@objc func addListAction() {
delegate?.addList()
}
...
}
Listing 3-7 HomeView delegates navigation methods to HomeViewController

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

Add List Screen

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.

Figure 3-7 Add list screen components communication schema

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

extension AddListViewController: BackButtonDelegate {


func navigateBack() {
backToHome()
}
}
Listing 3-11 BackButtonDelegate implementation in AddListViewController

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

extension AddListView: AddListViewDelegate {


func backToHome() {
delegate?.navigateBack()
}
}
Listing 3-13 AddListView implements the AddListViewDelegate protocol

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

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 3-8.

Figure 3-8 Tasks list screen components communication schema

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

private func setupTaskListView() {


let presenter = TasksListPresenter(taskListView: taskListView,
tasksListModel: tasksListModel,
taskService: TaskService(),
tasksListService: TasksListService())
taskListView.delegate = self
taskListView.presenter = presenter
taskListView.setupView()
self.view = taskListView
}
Listing 3-15 TaskListPresenter instantiation in TaskListViewController

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

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 3-16 Implementation of TaskListViewControllerDelegate and BackButtonDelegate methods

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

extension TaskListView: UITableViewDelegate, UITableViewDataSource {


...
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return presenter.numberOfTasks
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: TaskCell.reuseId, for: indexPath) as! TaskCell
cell.setParametersForTask(presenter.taskAtIndex(indexPath.row))
cell.delegate = self
return cell
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
presenter.removeTaskAtIndex(indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
}
}
}
Listing 3-17 UITableViewDelegate and UITableViewDatasource implementation in TaskListView

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

extension TaskListView: TaskListViewDelegate {


func setPageTitle(_ title: String) {
pageTitle.text = title
pageTitle.text = title
}
func reloadData() {
tableView.reloadData()
emptyState.isHidden = presenter.numberOfTasks > 0
}
}
Listing 3-18 TaskListViewDelegate methods implementation in TaskListView

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

extension TaskListView: TaskCellDelegate {


func updateTask(_ task: TaskModel) {
presenter.updateTask(task)
}
}
Listing 3-19 TaskCellDelegate method implementation in TaskListView

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

Add Task Screen

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

Figure 3-9 Add task screen components communication schema

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

private func setupAddTaskView() {


let presenter = AddTaskPresenter(addTaskView: addTaskView,
tasksListModel: tasksListModel,
taskService: TaskService())
addTaskView.delegate = self
addTaskView.presenter = presenter
self.view = addTaskView
}
Listing 3-22 AddTaskPresenter instantiation in AddTaskViewController
From a navigation point of view, this Controller is only responsible for dismissing the View once a task has been
added (since this screen is presented as a modal), as shown in Listing 3-23.

extension AddTaskViewController: AddedTaskViewControllerDelegate {


func addedTask() {
dismiss(animated: true)
}
}
Listing 3-23 AddedTaskViewControllerDelegate implementation in AddTaskViewController

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

private extension AddTaskView {


@objc func addTaskAction() {
guard titleTextfield.hasText else { return }
presenter.addTaskWithTitle(titleTextfield.text!)
}
}
extension AddTaskView: IconSelectorViewDelegate {
func selectedIcon(_ icon: String) {
presenter.setTaskIcon(icon)
}
}
Listing 3-24 Presenter gets info from the view

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

extension AddTaskView: AddTaskViewDelegate {


func addedTask() {
delegate?.addedTask()
}
}
Listing 3-25 AddTaskViewDelegate implementation in AddTaskView

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

class AddListViewControllerTest: XCTestCase {


var sut: AddListViewController!
var navigationController: MockNavigationController!
override func setUpWithError() throws {
sut = AddListViewController()
navigationController = MockNavigationController(rootViewController: UIViewController())
navigationController.pushViewController(sut, animated: false)
navigationController.vcIsPushed = false
}
override func tearDownWithError() throws {
sut = nil
navigationController = nil
super.tearDown()
}
func testPopVC_whenBackActionIsCalled_thenPopHomeCalled() {
sut.navigateBack()
XCTAssertTrue(navigationController.vcIsPopped)
}
}
Listing 3-27 AddListViewControllerTest code

You might also like