This post is part 2 of the tutorial.
→ Check part 1 for an introduction on VIPER.
→ Check part 3 to learn how to do unit tests with VIPER.
To better explain how VIPER can be applied, I’ll use a simple todo list App developed as an example. A link for the code will be available at the end of this post.
I’ll start with a simple screen with a very simple interaction, the Splash screen. It’s the first screen that you see when you open the app, and after 2 seconds, it routes to the following screen.
Presenter
Let’s begin by implementing the Presenter. Remember, the Presenter it’s the crossroad that connects all the other components.
First, we must define the protocols that will handle the communication between the components:
-- CODE language-js line-numbers --
protocol SplashPresenterType {<br>
func onSplashPresented(on splashView: SplashViewControllerType)<br>
}
<br><br>protocol SplashPresenterRouterDelegate: class {<br>
func routeToHome(from splashView: SplashViewControllerType)<br>
}
The SplashPresenterType is used as View-to-Presenter communication. In this case, the View will report when it’s presented.
SplashPresenterRouterDelegate is Presenter-to-Router communication. We’ll use this to route to the Home screen. Notice that it’s a subclass of class. We need this to use the delegate as a variable later.
Next up, let’s define the class itself:
-- CODE language-js line-numbers --
final class SplashPresenter {<br>
private let interactor: SplashInteractorType<br>
private weak var routerDelegate: SplashPresenterRouterDelegate?<br>
<br>
weak var splashView: SplashViewControllerType?<br>
<br>
init(interactor: SplashInteractorType, routerDelegate: SplashPresenterRouterDelegate) {<br>
self.interactor = interactor<br>
self.routerDelegate = routerDelegate<br>
}<br>
}
As we can see, on the Presenter we have one strong and one weak private references holding the interactor and the routerDelegate. The first will be used to call the interactor to handle some operation, while the second is the Presenter-to-Router delegate.
Additionally, it also holds weak references to the views, so that we can use them after executing some asynchronous task and dispose of them when we don’t need them anymore.
Finally, we define the necessary extensions to connect everything:
-- CODE language-js line-numbers --
extension SplashPresenter: SplashPresenterType {<br>
func onSplashPresented(on splashView: SplashViewControllerType) {<br>
self.splashView = splashView<br>
self.interactor.performTimeout()<br>
}<br>
}
<br>extension SplashPresenter: SplashInteractorDelegate {<br>
func onTimeoutPerformed() {<br>
guard let splashView = self.splashView else {<br>
assertionFailure("splashView should be available on SplashPresenter")<br>
return<br>
}<br>
self.routerDelegate?.routeToHome(from: splashView)<br>
}<br>
}
The first extension is the Presenter’s protocol, used for View-To-Presenter communication. When the View is presented, it notifies the Presenter. The Presenter then holds a reference to that View and calls the Interactor to perform some action. In this case, I’ll only do a simple timeout.
The second extension is the Interactor-To-Presenter delegate. After the timeout is done, the Interactor notifies the Presenter. In our case, when the timeout is complete, we first check if the view is still available using a simple guard let case, and then call the routerDelegate to move us to the Home screen.
The combination of these two extensions creates asynchronous communication between the Presenter and the Interactor.
Interactor
Our next step will be implementing the Interactor. It’s where the logic is processed and then returned to the Presenter.
First off, the protocols:
-- CODE language-js line-numbers --
protocol SplashInteractorType { <br>
var interactorDelegate: SplashInteractorDelegate? { get set }<br>
<br>
func performTimeout()<br>
}
<br>protocol SplashInteractorDelegate: class {<br>
func onTimeoutPerformed()<br>
}
The SplashInteractorType will be used as Presenter-To-Interactor communication. In our case, we’ll use it to expose the interactorDelegate and to perform the timeout for our Splash screen.
Now let us define the class:
-- CODE language-js line-numbers --
final class SplashInteractor {<br>
weak var interactorDelegate: SplashInteractorDelegate?<br>
}
Yup, that’s it. In this specific case, it’s as simple as that, but I’ll show a more complex example further in this tutorial.
Not let’s define the extensions:
-- CODE language-js line-numbers --
extension SplashInteractor: SplashInteractorType {<br>
func performTimeout() {<br>
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {<br>
self.interactorDelegate?.onTimeoutPerformed()<br>
}<br>
}<br>
}
The extension makes it possible for the Presenter to call the action on the Interactor. After that, the Interactor calls the delegate notifying the Presenter that the action has finished.
Usually, the Interactor would communicate with the Entity here, but in this scenario that is not necessary. But think about the Entity as a Database, an API Service using an HTTP client, or a value stored on the UserDefaults. The Entity is anything where you can retrieve information from.
And that’s it for the Interactor. 🎉
View
Before jumping to the Router, let’s take a glance at the View. Just like all the other components, let’s start by defining the protocols:
-- CODE language-js line-numbers --
protocol SplashViewControllerType: class {<br>
var presenter: SplashPresenterType { get }<br>
}
In this case, it’s pretty simple. This will be used as Presenter-To-View communication. In this case, it’s pretty simple, I only pass a reference for the Presenter since I don’t need any sort of interaction from the Presenter to the View.
Now let’s create the View. I’ll omit the creation of the UI components, but you can check them out at the repository following the link I provide at the end of this tutorial. The View goes as following:
-- CODE language-js line-numbers --
class SplashViewController: ViewController {<br>
var presenter: SplashPresenterType<br>
<br>
...<br>
<br>
init(presenter: SplashPresenterType) {<br>
self.presenter = presenter<br>
super.init(nibName: nil, bundle: nil)<br>
}<br>
<br>
required init?(coder: NSCoder) {<br>
fatalError("init(coder:) has not been implemented")<br>
}
<br>
override func viewDidLoad() {<br>
super.viewDidLoad()<br>
self.presenter.onSplashPresented(on: self)<br>
}
<br><br> ...<br>
}
The init function is replaced by a custom initialization method where we send the Presenter so that the view holds a reference to it.
After that, on the viewDidLoad lifecycle function, we tell the Presenter that the Splash screen is presented. This will trigger the Presenter to notify the Interactor to perform the timeout!
Finally, we add the extension:
extension SplashViewController: SplashViewControllerType { }
Since there are no Presenter-To-View interactions, this will remain empty.
Router
Finally let’s go to the final component of VIPER architecture, the Router. It’s responsible for routing between screens either by switching from one ViewController to another, pushing and popping ViewControllers on a NavigationController, or presenting and dismissing modal ViewControllers. All of that logic resides in the Router component.
Just like we did previously, let’s declare our communication protocols:
-- CODE language-js line-numbers --
protocol SplashRouterType {<br>
func startModule()<br>
}
<br>protocol SplashRouterDelegate: class {<br>
func routeToHome()
}
The first one, the SplashRouterType is used to initialize the module. I won’t enter in much detail here, as this is some extra configuration that you must do to get the ViewControllers up and running after the application start. Feel free to check the repository to get more information about that.
The last protocol is used as Presenter-To-Router communication. It’s how the Presenter notifies to the Router where should we go next. In this case, we’ll use it to route to the Home module.
Now let’s define the SplashRouter:
-- CODE language-js line-numbers --
class SplashRouter {<br>
private weak var appViewController: AppViewControllerType?<br>
private weak var routerDelegate: SplashRouterDelegate?<br>
<br>
init(appViewController: AppViewControllerType, routerDelegate: SplashRouterDelegate) {<br>
self.appViewController = appViewController<br>
self.routerDelegate = routerDelegate<br>
}<br>
}
Here you can see a very interesting component, the AppViewController. This is the root ViewController of the whole application. The main purpose of the AppViewController is to present the current ViewController and to handle transitions to the next ViewController. This is mainly used when transitioning between modules where the main intention is to change the root ViewController. Here is a snippet showcasing how the AppViewController does that transition:
-- CODE language-js line-numbers --
extension AppViewController: AppViewControllerType {<br>
func updateCurrent(to viewController: UIViewController) {<br>
self.addChild(viewController)<br>
viewController.view.frame = self.view.bounds<br>
self.view.addSubview(viewController.view)<br>
viewController.didMove(toParent: self)<br>
self.current?.willMove(toParent: nil)<br>
self.current?.view.removeFromSuperview()<br>
self.current?.removeFromParent()<br>
self.current = viewController<br>
}<br>
}
This is a very standard operation that you can find with a simple search if you want to learn more about it.
Continuing with our SplashRouter, we have the routerDelegate, and our first thought is that this is some sort of communication inside of the Splash module, but that’s not the case. In reality, we also need to have a higher level Router to handle navigation between modules called the AppRouter. The routerDelegate is used as Router-To-AppRouter communication. In this case, it’s used to notify the AppRouter to move to another module, the Home module. Feel free to check the repository to get more context on it.
Now we have the extensions:
-- CODE language-js line-numbers --
extension SplashRouter: SplashRouterType {<br>
func startModule() {<br>
let interactor = SplashInteractor()<br>
let presenter = SplashPresenter(interactor: interactor, routerDelegate: self)<br>
interactor.interactorDelegate = presenter<br>
let viewController = SplashViewController(presenter: presenter)<br>
self.appViewController?.updateCurrent(to: viewController)<br>
}<br>
}
<br>extension SplashRouter: SplashPresenterRouterDelegate {<br>
func routeToHome(from splashView: SplashViewControllerType) {<br>
self.routerDelegate?.routeToHome()<br>
}<br>
}
So the first one is used to start the Splash module and it’s composed by some interesting twists and turns to get all the delegates up and running, but after working a bit with it you’ll just do this automatically. Let’s break this down:
- First, we create the Interactor. In this case, it’s empty, but if you have any sort of HTTP Client or some information to later be processed that you need on the Splash module, this is where you would send it to since the Interactor is responsible for handling the logic part of the module.
- Then we create the Presenter. It receives the Interactor and the Presenter-To-Router delegate.
- Remember this piece of code inside of the SplashInteractorType?
var interactorDelegate: SplashInteractorDelegate? { get set }
This is where it’s used as a way of setting the Interactor-To-Presenter communication. - Now we initialize the SplashViewController sending the Presenter through our custom initialization method.
- Lastly, we tell the AppViewController to update the current ViewController to our new SplashViewController, causing the switch from one module to another.
The last extension is Presenter-To-Router communication. Used to notify the Router to navigate to another View or module. In this specific case, it’s telling the AppRouter to transition to the Home module using the Router-To-AppRouter delegate.
And that’s it for the basics of VIPER! We just successfully built our first module, although simple, already gives a lot of insight into the pattern. 🙌
Protocols
A very common trend here is the usage of protocols to refer to classes. For example:
-- CODE language-js line-numbers --
/// Good, using the protocol<br>
private let interactor: SplashInteractorType
<br><br>
/// Not so good, using the class<br>
private let interactor: SplashInteractor<br>
This is not something related to VIPER but more with security. By using the protocol, we limit access to the class, using only what the protocol provides. This is also a very good way to keep everything clean and tidy.
VIPER in action
To showcase some more advanced usages of the VIPER architecture, let’s take a quick look at the Home module. In this module, we have a list of todos with some simple actions, and a button to go to a new screen and create a new todo. Let’s go through the full flow of showing the todos on the View.
Let go through each step since the presentation of the view until the list is populated.
1. View is presenter
When the View is presented, it must notify the Presenter that it’s ready to receive the todos.
-- CODE language-js line-numbers --
override func viewDidLoad() {<br>
super.viewDidLoad()<br>
self.presenter.onHomePresenter(on: self)<br>
<br>
...
}
2. Presenter notifies Interactor to fetch todos
After receiving the notification from the View, the Presenter tells the Interactor to retrieve the todos. Furthermore, a reference for the View is stored for later use.
-- CODE language-js line-numbers --
func onHomePresenter(on homeView: HomeViewControllerType) {<br>
self.homeView = homeView<br>
self.interactor.fetchTodos()<br>
}
3. Interactor fetches todos and reports back to Presenter
The Interactor then fetches the todos from CoreData and returns them back to the Presenter via a delegate. Since we need to fetch the todos from some Entity, that Entity is the StoreService, responsible for holding a reference to the container.
-- CODE language-js line-numbers --
func fetchTodos() {<br>
let context = self.storeService.getContext()<br>
let todos = Todo.fetchAll(from: context)<br>
self.interactorDelegate?.onTodosFetched(todos: todos)<br>
}
4. Presenter sends the todos back to the View
Now the Presenter must send the todos to the View. Since we are always holding weak references of Views, we must verify if that reference still exists.
-- CODE language-js line-numbers --
func onTodosFetched(todos: [Todo]) {<br>
guard let homeView = self.homeView else {<br>
assertionFailure("homeView should be present on HomePresenter")<br>
return<br>
}<br>
homeView.onTodosFetched(todos: todos)<br>
}
5. The View receives the todos and updates accordingly
After the View receives the todos, it promptly updates the list and refreshes the UI.
-- CODE language-js line-numbers --
func onTodosFetched(todos: [Todo]) {<br>
self.todos = todos<br>
self.tableView.reloadData()<br>
}
And there you have it, a simple interaction using VIPER. It indeed requires a lot of steps to accomplish a simple action, but the fact that everything is so isolated and tightly coupled makes it easy to maintain and scale. And when you get to the complex stuff, you’ll see that it will not be any harder than this since the flow will always be the same.
You can check the full code here.
Check part 3 for some insight on how to take advantage of VIPER and XCode built-in testing framework to keep your functionalities tested and avoid unwanted errors.
If you enjoyed reading this post, please give us some Claps below! ❤️👇
Also, don’t forget to follow Pixelmatters on Medium, Twitter, Facebook, LinkedIn, and Instagram.