Model-View-Controller iOS Architecture Patterns
Model-View-Controller iOS Architecture Patterns
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).
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.
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
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
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:
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.
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:
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):
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.
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.
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).
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.
To do this, we first define a protocol with the methods related to these interactions (Listing 2-9).
And then, we make the HomeViewController adopt this protocol and implement its methods (Listing 2-10).
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).
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:
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).
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).
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)
This screen is responsible for adding task lists and the communication between its components is shown in Figure 2-9.
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).
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).
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):
Now we can call the different methods via the delegate (Listing 2-19).
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).
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.
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.
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.
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).
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.
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).
This screen is responsible for adding tasks to a given list and the communication between its components is shown in
Figure 2-11.
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).
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).
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.
In addition, when writing the tests, we have to take into account a series of criteria, known by the acronym FIRST:
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).
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:
The name of the parameter, sut, comes from “system under test.” By this, we mean which class we are testing.
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).
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).
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).
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.
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).