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

Model-View-Controller iOS Architecture Patterns

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)
14 views

Model-View-Controller iOS Architecture Patterns

Uploaded by

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

NSNotification.Name.

NSManagedObjectContextObjectsDidChange is the name of the notification the controller


subscribes to, context is the object we’re looking at, and methodToExecute is the method to run when a change is
detected, for example:

@objc func methodToExecute() {


view.updateView()
}

Core Data

In this folder, we will have the CoreDataManager.swift file (which we created in Chapter 1), along with the four files cre-
ated by Xcode automatically for the database entities.

Models

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 (Listing 2-1).

protocol EntityModelMapProtocol {
associatedtype EntityType: NSManagedObject
func mapToEntityInContext(_ context: NSManagedObjectContext) -> EntityType
static func mapFromEntity(_ entity: EntityType) -> Self
}
Listing 2-1 EntityModelMapProtocol code

In our application, we will define two models (TaskModel and TasksListModel), one for each entity in the database.

Each of these models will also conform to the EntityModelMapProtocol protocol so that we can go from model to entity
(NSManagedObject subclass) and vice versa (Listing 2-2 and Listing 2-3).

struct TasksListModel {
var id: String!
var title: String!
var icon: String!
var tasks: [TaskModel]!
var createdAt: Date!
}
Listing 2-2 TasksListModel.swift file content

struct TaskModel {
var id: String!
var title: String!
var icon: String!
var done: Bool!
var createdAt: Date!
}
Listing 2-3 TaskModel.swift file content

Services

Here we will have the classes that allow us to send information to the database (create, update, or delete it) or retrieve
information from the database and transform it into models.

In the case of the class that will manage the task lists, we will find the necessary methods to add, retrieve, or delete
task lists. We will define these methods in the TasksListServiceProtocol protocol that we will then implement in the
TasksListService class (Listing 2-4).

protocol TasksListServiceProtocol: AnyObject {


init(coreDataManager: CoreDataManager)
func saveTasksList(_ list: TasksListModel)
func fetchLists() -> [TasksListModel]
func fetchListWithId(_ id: String) -> TasksListModel?
func deleteList(_ list: TasksListModel)
}
class TasksListService: TasksListServiceProtocol {
let context: NSManagedObjectContext
let coreDataManager: CoreDataManager
required init(coreDataManager: CoreDataManager = CoreDataManager.shared) {
self.context = coreDataManager.mainContext
self.coreDataManager = coreDataManager
}
func saveTasksList(_ list: TasksListModel) {
_ = list.mapToEntityInContext(context)
coreDataManager.saveContext(context)
}
func fetchLists() -> [TasksListModel] {
var lists = [TasksListModel]()
do {
let fetchRequest: NSFetchRequest<TasksList> = TasksList.fetchRequest()
let value = try context.fetch(fetchRequest)
lists = value.map({ TasksListModel.mapFromEntity($0) })
lists = lists.sorted(by: { $0.createdAt.compare($1.createdAt) == .orderedDescending })
} catch {
debugPrint("CoreData Error")
}
return lists
}
func fetchListWithId(_ id: String) -> TasksListModel? {
do {
let fetchRequest: NSFetchRequest<TasksList> = TasksList.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "id = %@", id)
let listEntities = try context.fetch(fetchRequest)
guard let list = listEntities.first else {
return nil
}
return TasksListModel.mapFromEntity(list)
} catch {
debugPrint("CoreData Error")
return nil
}
}
func deleteList(_ list: TasksListModel) {
do {
let fetchRequest: NSFetchRequest<TasksList> = TasksList.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "id = %@", list.id)
let listEntities = try context.fetch(fetchRequest)
for listEntity in listEntities {
context.delete(listEntity)
}
coreDataManager.saveContext(context)
} catch {
debugPrint("CoreData Error")
}
}
}
Listing 2-4 TasksListServiceProtocol and TasksListService structure and methods

In the case of the class that will manage the tasks, TaskService, we will find the necessary methods to add, retrieve,
update, or delete tasks (Listing 2-5). In this case, as in the previous ones, we will define the methods to be implemented
in a protocol.

protocol TaskServiceProtocol: AnyObject {


init(coreDataManager: CoreDataManager)
func saveTask(_ task: TaskModel, in taskList: TasksListModel)
func fetchTasksForList(_ taskList: TasksListModel) -> [TaskModel]
func updateTask(_ task: TaskModel)
func deleteTask(_ task: TaskModel)
}
class TaskService: TaskServiceProtocol {
let context: NSManagedObjectContext
let coreDataManager: CoreDataManager
required init(coreDataManager: CoreDataManager = CoreDataManager.shared) {
self.context = coreDataManager.mainContext
self.coreDataManager = coreDataManager
}
func saveTask(_ task: TaskModel, in taskList: TasksListModel) {
do {
let fetchRequest: NSFetchRequest<TasksList> = TasksList.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "id = %@", taskList.id)
guard let list = try context.fetch(fetchRequest).first else {
return
}
let taskEntity = task.mapToEntityInContext(context)
list.addToTasks(taskEntity)
coreDataManager.saveContext(context)
} catch {
debugPrint("CoreData Error")
debugPrint("CoreData Error")
}
}
func fetchTasksForList(_ taskList: TasksListModel) -> [TaskModel] {
var tasks = [TaskModel]()
do {
let fetchRequest: NSFetchRequest<TasksList> = TasksList.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "id = %@", taskList.id)
guard let list = try context.fetch(fetchRequest).first,
let taskEntities = list.tasks else {
return tasks
}
tasks = taskEntities.map({ TaskModel.mapFromEntity($0 as! Task) })
} catch {
debugPrint("CoreData Error")
}
return tasks
}
func updateTask(_ task: TaskModel) {
do {
let fetchRequest: NSFetchRequest<Task> = Task.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "id = %@", task.id)
guard let taskEntity = try context.fetch(fetchRequest).first else {
return
}
taskEntity.done = task.done
coreDataManager.saveContext(context)
} catch {
debugPrint("CoreData Error")
}
}
func deleteTask(_ task: TaskModel) {
do {
let fetchRequest: NSFetchRequest<Task> = Task.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "id = %@", task.id)
let taskEntities = try context.fetch(fetchRequest)
for taskEntity in taskEntities {
context.delete(taskEntity)
}
coreDataManager.saveContext(context)
} catch {
debugPrint("CoreData Error")
}
}
}
Listing 2-5 TaskServiceProtocol and TaskService structure and methods

Extensions

In this case, we have created a UIColor extension to be able to easily access the colors created especially for this applica-
tion, which are found in the Assets file.

We will also add 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. In this case, it is a list with the names of the
icons that the user can choose when creating tasks and task lists.

View

This layer contains all those elements that the user can see and that make up the graphical interface and those with which
the user can interact (Figure 2-6).

These views can be simple, like a button or a label, or complex like the entire view of a page that contains buttons, labels,
images, etc.

In the case of simple views, they are usually graphic elements that are reused throughout the application, such as a but-
ton, for example.

The most complex views are formed by the composition of simpler views. Since we are not working with storyboards or
xib files, we will define the characteristics of each component, such as its position or size, using constraints.
Figure 2-6 View layer files

Controller

The controllers (subclasses of UIViewController) are the main part of the application and the ones in charge of connecting
the model with the view. Each of the screens in our application is a view controller (Figure 2-7).
Figure 2-7 Controller layer files

MyToDos Application Screens

In Chapter 1, we described how the application that we would use to implement each of the architecture patterns that we
will work on in this book would be.

As you may remember, this application has four screens, each one represented by a UIViewController, which will be related
to the view and the model, managing the passage of information from one to the other.

Information Flow

But how will we pass this information?

The controller is the central part of the MVC model, and it will be the one that contains references (instances) to the view
and the model. Therefore, the controller will be able to pass information by directly calling public methods of both the
view and the model.

For example, if we have an instance of the TasksListService class (model) in our controller, we can retrieve the task lists
by calling its fetchList method:

let tasksLists = tasksListService.fetchLists()

And then pass this information to the view:

let view = HomeView()


view.setLists(taskLists)

Delegate Pattern

And, the user interactions in the view, how do we pass them to the controller?

Since the view doesn’t have a controller instance to call its methods, we can do that by using the Delegate pattern.

This design pattern allows a class to delegate some of its responsibilities to an instance of another class. In our case, the
behavior against user interactions in the view will be implemented by the controller.

How to Implement Delegate Pattern

To implement the Delegate pattern, first, we create a protocol that will contain the methods we want to delegate. For
example, let’s create an example protocol with two methods:

protocol ExampleDelegate: AnyObject {


func methodA()
func methodB(value: String)
}

The next step is to create a property of the type of the protocol that we have created, which we will call delegate, in
the class that delegates (which in our case would be the view):

class ExampleView {
...
weak var delegate: ExampleDelegate?
...
}

Now, depending on what happens in the view (pressing a button, writing in a text field, etc.), we can call the different
methods of the protocol:

delegate?.methodA()
delegate?.methodB(value: "Input text")

Now, in the controller, so that it can implement the protocol methods, we must configure the delegate property of the
exampleView instance to “self,” indicating that it will be the controller that implements the protocol methods:

class ExampleViewController {
...
exampleView.delegate = self
...
}

And, finally, we have to make the controller adopt the protocol and its methods (we can do this in an extension to
improve code readability):

extension ExampleViewController: ExampleDelegate {


func methodA() { ... }
func methodB(value: String) { ... }
}

Now we are going to see how to implement all this in the development of our application. At the beginning of each
screen, we will show a diagram of how the different components (the main ones) communicate with each other.

AppDelegate and SceneDelegate

Since the release of iOS 13, some of the responsibilities that the AppDelegate had in previous versions were transferred to
the SceneDelegate. Thus, now, while the AppDelegate is in charge of the life cycle of the application and its setup, the
SceneDelegate is responsible for what is displayed on the screen and how it is displayed.

In the example application that we are going to develop, we will not modify the AppDelegate that Xcode generates
when creating the application. What we will do is modify the SceneDelegate, specifically the
scene(_:willConnectTo:options:) method, which is the first one called in the UISceneSession life cycle (Listing 2-6).

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(tasksListService: TasksListService(), taskService: TaskService()))
navigationController.interactivePopGestureRecognizer?.isEnabled = false
window.backgroundColor = .white
window.rootViewController = navigationController
self.window = window
window.makeKeyAndVisible()
}
}
Listing 2-6 Modification in the SceneDelegate to call HomeViewController

As you can see, what we do in this method is create a new UIWindow, we set the application’s root view controller (which
is a UINavigationController component, whose first controller will be the HomeViewController class), and finally, we make
the window we have created be the key window that should be displayed.

Home Screen

On the Home screen, the main component is the HomeViewController class and we can see how it communicates with the
rest of the components in Figure 2-8.

Figure 2-8 Home screen components communication schema

HomeViewController

The Controller (HomeViewController) is the core of the MVC model and must have references to both the view (HomeView)
and the model (TasksListServiceProtocol and TaskServiceProtocol), as seen in Listing 2-7.

The fact of passing instances of the classes that we need in the initializer (this is what is known as dependency
injection) allows us a greater decoupling of the components and facilitates the implementation of unit tests (using, for
example, mock objects).
example, mock objects).

class HomeViewController: UIViewController {


private var homeView = HomeView()
private var tasksListService: TasksListServiceProtocol!
private var taskService: TaskServiceProtocol!
init(tasksListService: TasksListServiceProtocol,
taskService: TaskServiceProtocol) {
super.init(nibName: nil, bundle: nil)
self.tasksListService = tasksListService
self.taskService = taskService
}
...
}
Listing 2-7 HomeViewController initialization

As we have seen for the MVC architecture, the Controller will pass the information to the View for display. We do this
with the fetchTasksLists method, whose function is to retrieve the information from the database and pass it to the view
(Listing 2-8).

func fetchTasksLists() {
let lists = tasksListService.fetchLists()
homeView.setTasksLists(lists)
}
Listing 2-8 The fetchTasksLists method calls the Model to fetch the lists and then passes them to the View

On the other hand, the Controller will receive user interactions (through the Delegate pattern) and act accordingly.

These interactions are as follows:

• Access a tasks list.


• Add a tasks list.
• Delete a tasks list.

To do this, we first define a protocol with the methods related to these interactions (Listing 2-9).

protocol HomeViewDelegate: AnyObject {


func addListAction()
func selectedList(_ list: TasksListModel)
func deleteList(_ list: TasksListModel)
}
Listing 2-9 HomeViewDelegate protocol

And then, we make the HomeViewController adopt this protocol and implement its methods (Listing 2-10).

extension HomeViewController: HomeViewDelegate {


func addListAction() {
let addListViewController = AddListViewController(tasksListModel: list, taskService: taskService, tasksListService: tasksListService)
navigationController?.pushViewController(addListViewController, animated: true)
}
func selectedList(_ list: TasksListModel) {
let taskViewController = TaskListViewController(tasksListModel: list)
navigationController?.pushViewController(taskViewController, animated: true)
}
func deleteList(_ list: TasksListModel) {
tasksListService.deleteList(list)
}
}
Listing 2-10 HomeViewController extension that implements the HomeViewDelegate protocol methods

The addListAction method is executed when the user clicks the “Add list” button and navigates the application to the
AddListViewController screen.

The selectedList method navigates the application to the TaskListViewController screen (passing it the selected list infor-
mation).

Finally, the deleteList method is in charge of communicating to the model that it must delete the selected list from the
database and then reloads the view.

In addition, we have to implement the observation of the model to know when task lists are added or deleted and thus
update the view (Listing 2-11).
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self,
selector: #selector(contextObjectsDidChange),
name: NSNotification.Name.NSManagedObjectContextObjectsDidChange,
object: CoreDataManager.shared.mainContext)
}
@objc func contextObjectsDidChange() {
fetchTasksLists()
}
Listing 2-11 Model observer implementation

HomeView

Basically, the HomeView is made up of a UITableView element (to display the information) and a UIButton element (to be
able to add a new list). We have seen how the Controller will pass the information that the View should display through
the setTasksList method of the View (Listing 2-12).

func setTasksLists(_ lists: [TasksListModel]) {


tasksList = lists
tableView.reloadData()
emptyState.isHidden = tasksList.count > 0
}
Listing 2-12 Upon receiving the information from HomeViewController, the HomeView is updated with the new data

What we do in this function is take the “lists” parameter and assign it to the taskList variable of our class (which is the one
we will use to fill the table), reload the table, and hide or show an “Empty state” depending on whether or not the list
contains values.

On the other hand, the View will also need to pass user interactions to the controller via delegation.

We will do this by adding a delegate property of the type HomeViewDelegate to the top of our HomeView:

class HomeView: UIView {


...
weak var delegate: HomeViewDelegate?
...
}

Also, in the HomeViewController, we must configure the delegate property of the HomeView instance to “self,”
indicating that it will be the HomeViewController that implements the protocol methods (Listing 2-13).

class HomeViewController: UIViewController {


...
override func loadView() {
super.loadView()
setupHomeView()
}
private func setupHomeView() {
homeView.delegate = self
self.view = homeView
}
...
}
Listing 2-13 Setting HomeView delegate on HomeViewController

Once the delegate is defined, we can use it to implement it in our code and call each of the protocol functions.

Thus, the addListAction method will be called from the function associated with the add list button (Listing 2-14).

extension HomeView {
...
func configureAddListButton() {
addListButton.addTarget(self, action: #selector(addListAction), for: .touchUpInside)
...
}
@objc func addListAction() {
delegate?.addListAction()
}
...
}
Listing 2-14 Configure the addListButton target
Listing 2-14 Configure the addListButton target

The selectList method will be called when the user selects a cell in the table (Listing 2-15).

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {


delegate?.selectedList(tasksList[indexPath.row])
}
Listing 2-15 HomeView delegates the selectedList method implementation to the HomeViewController

And the method deleteList will be called on swipe a cell (Listing 2-16).

func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let list = tasksList[indexPath.row]
tasksList.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
delegate?.deleteList(list)
}
}
Listing 2-16 On swipe a cell, the delete method is called (removing cell from the table, and delegating the deletion from the Model to the HomeViewController)

Add List Screen

This screen is responsible for adding task lists and the communication between its components is shown in Figure 2-9.

Figure 2-9 Add list screen components communication schema

AddListViewController

In the AddListViewController (the screen where we can add task lists), we have a reference to the View (AddListView) and
another to the Model (TasksListService); although in this case, we will not pass information from the Controller to the
View, we will only receive user interactions via delegation (Listing 2-17).

class AddListViewController: UIViewController {


private var tasksListService: TasksListService!
init(tasksListService: TasksListService) {
super.init(nibName: nil, bundle: nil)
self.tasksListService = tasksListService
}
...
private func setupAddListView() {
addListView.delegate = self
self.view = addListView
}
private func backToHome() {
navigationController?.popViewController(animated: true)
}
}
extension AddListViewController: AddListViewDelegate {
func addList(_ list: TasksListModel) {
tasksListService.saveTasksList(list)
backToHome()
}
}
extension AddListViewController: BackButtonDelegate {
func navigateBack() {
backToHome()
backToHome()
}
}
Listing 2-17 AddListViewController implementation

As you can see, we have set a single delegate parameter on the AddListView class, but the AddListViewController is adopt-
ing two protocols: AddListViewDelegate and BackButtonDelegate. This is because, as we will now see, we can set multiple
types for the delegate.

AddListView

AddListView contains a UITextField element to enter the name of the task list, a UICollectionView element to choose an
icon for the list, a button to create the list, and another button to return to the Home screen.

In this view, we can see both sides of the Delegate pattern at the same time.

On the one hand, it delegates the implementation of a series of methods to the controller that references it.

Thus, AddListView delegates to AddListViewController the implementation of the methods referring to the
BackButtonDelegate and AddListViewDelegate protocols (Listing 2-18).

protocol BackButtonDelegate: AnyObject {


func navigateBack()
}
protocol AddListViewDelegate: AnyObject {
func addList(_ list: TasksListModel)
}
Listing 2-18 BackButtonDelegate and AddListViewDelegate implementation

The first serves to indicate that the user has selected the button to navigate back without having created any list. The
second allows us to pass the data of the created list to the Controller (which will be in charge of asking the Model to save
it in the database).

Since we want to implement all these protocols in our controller, in the View we can create a delegate that conforms
to all of them (in case we don’t want to implement any of them, we should create independent delegates with different
names):

weak var delegate: (AddListViewDelegate & BackButtonDelegate)?

Now we can call the different methods via the delegate (Listing 2-19).

@objc func backAction() {


delegate?.navigateBack()
}
@objc func addListAction() {
guard titleTextfield.hasText else { return }
listModel.title = titleTextfield.text
listModel.id = ProcessInfo().globallyUniqueString
listModel.icon = listModel.icon ?? "checkmark.seal.fill"
listModel.createdAt = Date()
delegate?.addList(listModel)
}
Listing 2-19 Calling methods on AddListViewController via delegate

But AddListView not only delegates the implementation of methods but also implements others. Thus, the icon selector of
icons that we have incorporated in this view, IconSelectorView, introduces the IconSelectorViewDelegate protocol.

This protocol makes us implement the method that returns the icon selected by the user (Listing 2-20).

protocol IconSelectorViewDelegate: AnyObject {


func selectedIcon(_ icon: String)
}
func configureCollectionView() {
...
iconSelectorView.delegate = self
...
}
extension AddListView: IconSelectorViewDelegate {
func selectedIcon(_ icon: String) {
listModel.icon = icon
}
}
}
Listing 2-20 IconSelectorViewDelegate implementation

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 2-10.

Figure 2-10 Tasks list screen components communication schema

TaskListViewController

The TaskListViewController, which controls the screen in which we are shown the tasks that make up a list, has a reference
to the view (TaskListView) and the model (TaskServiceProtocol and TasksListServiceProtocol).

Also, as this screen will show the tasks that make up a list, when we start it by calling it from the HomeViewController,
we will have to pass it an object with the list we want to show (TaskListModel), as shown in Listing 2-21.

class TaskListViewController: UIViewController {


private var taskListView = TaskListView()
private var tasksListModel: TasksListModel!
private var taskService: TaskServiceProtocol!
private var tasksListService: TasksListServiceProtocol!
init(tasksListModel: TasksListModel,
taskService: TaskServiceProtocol,
tasksListService: TasksListServiceProtocol) {
super.init(nibName: nil, bundle: nil)
self.tasksListModel = tasksListModel
self.taskService = taskService
self.tasksListService = tasksListService
} ...
}
Listing 2-21 TaskListViewController initialization

As the view is the one that shows the tasks, we have to pass our object of type TaskListModel and also establish the
delegate of the view to be able to receive the interactions of the user (along with the methods associated with the
delegate protocols: TaskListViewDelegate and BackButtonDelegate), as shown in Listing 2-22.

class TaskListViewController: UIViewController {


...
override func loadView() {
super.loadView()
navigationController?.navigationBar.isHidden = true
setupTaskListView()
}
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self,
selector: #selector(contextObjectsDidChange),
name: NSNotification.Name.NSManagedObjectContextObjectsDidChange,
object: CoreDataManager.shared.mainContext)
taskListView.setTasksList(tasksListModel)
}
private func setupTaskListView() {
taskListView.delegate = self
self.view = taskListView
}
private func updateTasksList() {
guard let list = tasksListService.fetchListWithId(tasksListModel.id) else { return }
guard let list = tasksListService.fetchListWithId(tasksListModel.id) else { return }
tasksListModel = list
taskListView.setTasksList(tasksListModel)
}
@objc func contextObjectsDidChange() {
updateTasksList()
}
}
extension TaskListViewController: TaskListViewDelegate {
func addTaskAction() {
let addTaskViewController = AddTaskViewController(tasksListModel: tasksListModel, taskService: taskService)
addTaskViewController.modalPresentationStyle = .pageSheet
present(addTaskViewController, animated: true)
}
func updateTask(_ task: TaskModel) {
taskService.updateTask(task)
}
func deleteTask(_ task: TaskModel) {
taskService.deleteTask(task)
}
}
extension TaskListViewController: BackButtonDelegate {
func navigateBack() {
navigationController?.popViewController(animated: true)
}
}
Listing 2-22 TaskListViewController implementation

If you notice, in the addTaskAction function, which is executed when we select the “Add Task” button, we show as a modal
the screen of creating a new task.

TaskListView

This view will show us the tasks that make up a list, which, as we have just seen, we pass from the controller with the
setTasksLists method (Listing 2-23).

func setTasksLists(_ tasksList: TasksListModel) {


tasks = tasksList.tasks.sorted(by: { $0.createdAt.compare($1.createdAt) == .orderedDescending })
pageTitle.setTitle(tasksList.title)
tableView.reloadData()
emptyState.isHidden = tasks.count > 0
}
Listing 2-23 Upon receiving the information from TaskListViewController, the TaskListView is updated with the new data

This View contains as user interaction elements a UITableView element that shows the tasks in the list and two buttons:
the one to go back (to the Home screen) and the “Add Task” button (Listing 2-24).

As we have seen in previous cases, the actions on these elements are delegated to the controller.

class TaskListView: UIView {


...
weak var delegate: (TaskListViewDelegate & BackButtonDelegate)?
...
}
private extension TaskListView {
...
@objc func backAction() {
delegate?.navigateBack()
}
...
@objc func addTaskAction() {
delegate?.addTaskAction()
}
}
TaskListView extension: UITableViewDelegate, UITableViewDataSource {
...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: TaskCell.reuseId, for: indexPath) as! TaskCell
cell.setParametersForTask(tasksList[indexPath.row])
cell.delegate = self
return cell
}
...
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
let task = tasksList[indexPath.row]
list.remove(at: indexPath.row tasks)
tableView.deleteRows(at: [indexPath], with: .automatic)
delegate?.deleteTask(task)
}
}
}
extension TaskListView: TaskCellDelegate {
func updateTask(_ task: TaskModel) {
delegate?.updateTask(task)
}
}
Listing 2-24 TaskListView implementation

But, as we can see in that code, the cells that are displayed in the table delegate (TaskCellDelegate) to it to implement the
update of the cells when they are made to go to the Done state by pressing the circle to the right of each cell.

Since the fact of updating the database is called from the Controller, we will have to pass the update call of the tasks
from this View (TaskListView) to the Controller (TaskListViewController).

Add Task Screen

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

Figure 2-11 Add task screen components communication schema

AddTaskViewController

The last screen, controlled by AddTaskViewController, is the one that will allow us to add new tasks to a list. This
Controller presents references to the View, AddTaskView, and the model. The information entered by the user in the View
will reach the Controller (as in the previous cases) by delegation (Listing 2-25).

class AddTaskViewController: UIViewController {


private var taskService: TaskServiceProtocol!
init(tasksListModel: TasksListModel,
taskService: TaskServiceProtocol) {
super.init(nibName: nil, bundle: nil)
self.tasksListModel = tasksListModel
self.taskService = taskService
}
...
private func setupAddTaskView() {
addTaskView.delegate = self
self.view = addTaskView
}
}
extension AddTaskViewController: AddTaskViewDelegate {
func addTask(_ task: TaskModel) {
taskService.saveTask(task, in: tasksListModel)
dismiss(animated: true)
}
}
Listing 2-25 AddTaskViewController initialization

AddTaskView

This View has a structure similar to the one we use to create task lists, with a UITextField element, an IconSelectorView ele-
ment, and a UIButton element.

This View will both delegate task creation to the Controller via the protocol, as well as implement the method
associated with the icon selector (Listing 2-26).

protocol AddTaskViewDelegate: AnyObject {


func addTask(_ task: TaskModel)
}
class AddTaskView: UIView {
...
weak var delegate: AddTaskViewDelegate?
...
}
private extension AddTaskView {
...
@objc func addTaskAction() {
guard titleTextfield.hasText else { return }
taskModel.title = titleTextfield.text
taskModel.icon = taskModel.icon ?? "checkmark.seal.fill"
taskModel.done = false
taskModel.id = ProcessInfo().globallyUniqueString
taskModel.createdAt = Date()
delegate?.addTask(taskModel)
}
func configureCollectionView() {
...
iconSelectorView.delegate = self
...
}
}
extension AddTaskView: IconSelectorViewDelegate {
func selectedIcon(_ icon: String) {
taskModel.icon = icon
}
}
Listing 2-26 AddTaskViewDelegate and AddTaskView implementation

Testing

In Chapter 1 we saw that one of the important points of a good architecture is that it is testable. Now we are going to
write a few tests for the application that we have developed with the MVC architecture.

For this, we will use Apple’s framework, XCTest, to write our tests. As an introduction to the development of tests, and
that will serve us for the next chapters, what we will do is test the main functionalities, such as the services that work
with the database, the user interactions, and the main navigation flows.

How Should the Tests Be?

In addition, when writing the tests, we have to take into account a series of criteria, known by the acronym FIRST:

• Fast: The tests must be fast.


• Independent: The tests must be independent of each other and not pass information so that they can be executed in
any order.
• Repeatable: The result of the tests must be the same each time they are executed and in any environment.
• Self-validating: The tests must be self-validating, that is, whether they pass or fail must not depend on any external
intervention (such as having to check a log).
• Timely: Tests should be written before the production code is written. This is what is known as test-driven develop-
ment.

Let’s Create the First Test

In Chapter 1, we saw how to create our application project and how to activate the “Include Tests” option when creating
it. In this way, Xcode creates our test target, so that we only have to add our tests.

When initially creating our project, for the MVC architecture, with the name MVC-MyToDos, we will see that two
folders have been created, MVC-MyToDosTests and MVC-MyToDosUITests, although we will only focus on the first one,
which will be the one that contains the unit tests that we have mentioned before (Figure 2-12).
Figure 2-12 MVC-MyToDos test files

Now, let’s create our first test. Suppose we first create the Home view, HomeView.swift. We select the MVC-
MyToDosTests folder and add a new file. From the different file options, we choose the “Unit Test Case Class” and give it
the name HomeViewTest (Figure 2-13 and Figure 2-14).

Figure 2-13 Unit Test Case Class template selection


Figure 2-14 HomeViewTest class creation

Doing this, Xcode will create a file with some initial code (Listing 2-27).

import XCTest
class HomeViewTestd: XCTestCase {
override func setUpWithError() throws {}
override func tearDownWithError() throws {}
func testExample() throws {}
func testPerformanceExample() throws {}
}
Listing 2-27 Initial HomeViewTest code

In the setUpWithError() function, we will put the code that is executed before each test and in tearDownWithError(), the
one that should be executed after each test.

The other two functions are examples, which tell us that all the tests we write (the functions) must start with the word
“test.”

Once we know this, we are going to write the first test (Listing 2-28).

import XCTest
@testable import MVC_MyToDos
class HomeViewTest: XCTestCase {
var sut: HomeView!
override func setUpWithError() throws {
sut = HomeView()
}
func testViewLoaded_whenViewIsInstantiated_shouldBeComponents() {
XCTAssertNotNil(sut.pageTitle)
XCTAssertNotNil(sut.addListButton)
XCTAssertNotNil(sut.tableView)
XCTAssertNotNil(sut.emptyState)
}
}
Listing 2-28 Test code for checking HomeView components

In order to test the HomeView, we must first make it visible to our MVC_MyTodosTests target. To do this, we have
imported our project using the command:

@testable import MVC_MyToDos

The next step has been to create a variable of type HomeView:

var sut: HomeView!

The name of the parameter, sut, comes from “system under test.” By this, we mean which class we are testing.

Finally, we have created the test:

testViewLoaded_whenViewIsInstantiated_shouldBeComponents

For the Apple framework to detect that it is a test to run, the function must begin with the word “test.” Also, it is a good
practice to define in the name of the function that you want to test (ViewLoaded), when (whenViewIsInstantiated), and
what we should get as a result (shouldBeComponents).

When creating the different components of the HomeView, we have defined them as private(set), which allows us to
access them from outside the class to read them but not modify them (Figure 2-15).
access them from outside the class to read them but not modify them (Figure 2-15).

Figure 2-15 First test passed

In this case, we have used a type of XCAssert function, XCTAssertNotNil; what it does is to validate that what is inside the
parentheses is not nil (different functions depend on what we want to test: XCAssertEqual, XCAssertTrue, etc.).

Helper Classes

To facilitate the testing of our code, it is necessary to create some classes that will help us.

The first of them, InMemoryCoreDataManager, has the same functionalities as the application’s CoreDataManager file,
but the database is generated in memory and does not persist when the tests are finished (Listing 2-29).

class InMemoryCoreDataManager: CoreDataManager {


override init() {
super.init()
let persistentStoreDescription = NSPersistentStoreDescription()
persistentStoreDescription.type = NSInMemoryStoreType
let container = NSPersistentContainer(name: "ToDoList")
container.persistentStoreDescriptions = [persistentStoreDescription]
container.loadPersistentStores { _, error in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
persistentContainer = container
}
}
Listing 2-29 InMemoryCoreDataManager help class

The second file, MockNavigationController, allows us to identify when navigation calls (push and pop) have been made
in the application. This is achieved by making a mock of the UINavigationController class in which some variables are
introduced that allow us to know if a push call or a pop call has occurred (Listing 2-30).

class MockNavigationController: UINavigationController {


var vcIsPushed: Bool = false
var vcIsPopped: Bool = false
override func pushViewController(_ viewController: UIViewController,
animated: Bool) {
super.pushViewController(viewController,
animated: animated)
vcIsPushed = true
}
override func popViewController(animated: Bool) -> UIViewController? {
vcIsPopped = true
return viewControllers.first
}
}
Listing 2-30 MockNavigationController to test navigation in app

MVC-MyToDos Testing

With all this in mind, we continue to develop our tests and code. We have a test file for each of the services (access to the
database), two files per screen (for each controller and each view), and two more files that will help when performing the
tests.

We are not going to show on these pages all the tests carried out for the MVC-MyToDos project since you can find them in
its repository. However, we are going to show those that may be more relevant.

NoteFrom the pedagogical point of view when working with the different architectures of an application, we will first see
how the code is structured, and then, with ease or difficulty from the point of view of its testing, it is recommended that
in our day-to-day work as developers, we work with the TDD (test-driven development) methodology. This methodology
is based on first writing the tests (generally unit tests), then writing the code that allows the tests to pass, and, finally,
refactoring said code.2
refactoring said code.
TasksListServiceTest

This class contains the tests for the TasksListService class (we won’t put the tests for the TaskService class because they
are very similar), as shown in Listing 2-31.

class TasksListServiceTest: XCTestCase {


var sut:TasksListServiceProtocol!
var list: TasksListModel!
override func setUpWithError() throws {
sut = TasksListService(coreDataManager: InMemoryCoreDataManager())
list = TasksListModel(id: "12345-67890",
title: "Test List",
icon: "test.icon",
tasks: [TaskModel](),
createdAt: Date())
}
override func tearDownWithError() throws { ... }
func testSaveOnDB_whenSavesAList_shouldBeOneOnDatabase() {
sut.saveTasksList(list)
XCTAssertEqual(sut.fetchLists().count, 1)
}
func testSaveOnDB_whenSavesAList_shouldBeOneWithGivenIdOnDatabase() { ... }
func testDeleteOnDB_whenSavesAListAndThenDeleted_shouldBeNoneOnDatabase() {
sut.saveTasksList(list)
XCTAssertNotNil(sut.fetchListWithId("12345-67890"))
sut.deleteList(list)
XCTAssertEqual(sut.fetchLists().count, 0)
}
}
Listing 2-31 TaskListServiceTest file code tests

In the test

testSaveOnDB_whenSavesAList_shouldBeOneOnDatabase

we first save a list to the database and then check that a list exists in the database (we use the XCTAssertEqual function to
do this).

In the test

testDeleteOnDB_whenSavesAListAndThenDeleted_shouldBeNoneOnDatabase

we first save a list to the database, check that the list exists, then delete it, and finally check that it no longer exists in the
database. In this case, we have combined two XCTAssert functions (XCTAssertNotNil and XCTAssertEqual).

Mocking Services

To facilitate the testing of those classes that depend on access to the Model layer, we can use “mocks” of said model,
which will allow us to simulate the behavior of real classes and, at the same time, more easily verify the results.

In our case, we will set two such classes to “impersonate” the services: MockTaskListService and MockTaskService
(Listing 2-32 and Listing 2-33).

class MockTaskListService: TasksListServiceProtocol {


private var lists: [TasksListModel] = [TasksListModel]()
required init(coreDataManager: CoreDataManager) {}
convenience init(lists: [TasksListModel]) {
self.init(coreDataManager: CoreDataManager.shared)
self.lists = lists
}
override func saveTasksList(_ list: TasksListModel) {
lists.append(list)
}
override func fetchLists() -> [TasksListModel] {
return lists
}
override func fetchListWithId(_ id: String) -> TasksListModel? {
return lists.filter({ $0.id == id }).first
}
override func deleteList(_ list: TasksListModel) {
lists = lists.filter({ $0.id != list.id })
}

You might also like