Writing an app with VIPER architecture - Part 1
Last week I came across one of the many awesome frameworks on iOS. Having recently worked with MVVM and MVC, I was bit hesitant to learn VIPER, since more frameworks you learn, there is good chance you will forget the previous ones. However, I tried to keep my previous teaching as intact as possible while learning this new architecture.
Below are basic building blocks for VIPER. Every component has its own role in an architecture. The main goal of VIPER is single responsibility principle where it tries to keep responsibility for each module as minimal as possible and confined. This has two advantages
- Change is easier
- Unit testing is better and simple that heavy and mixed-up MVC approach
Let's look at the individual components now,
-
View - Refers to ViewController or any subclass which houses
UIView
and/orUIViewController
subclasses. This is strictly dumb part associated with VIPER. It does not know about complex logic or data fetching happening under the hood. Its job is to merely use the data sent by Presenter (We will see shortly below) and display it -
Presenter - Interacts directly with
View
andInteractor
. Responsible for taking inputs fromView
and transfer it toInteractor
.Interactor
part then performs complex logic of fetching a data based on user input.Presenter
can also perform type conversion ifinteractor
requires data in specific format.
Presenter also receives a data, converts that into appropriate format and feeds to view. View is then responsible for displaying it to user. One of the examples of such case is when Interactor provides data models and presenter converts them to view models which are then fed to view
Presenter also performs an additional task of routing since it has direct access to view.
- Interactor - Interacts with Presenter bidirectionally to receive inputs, fetch data, perform complex calculations and push it back to view through presenter. Interactor is the main brain of architecture where logic happens. In ideal case interactor and presenter are the one that should be tested since view is merely a placeholder in this architecture.
Interactor also communicates directly with DataManager
- A component responsible for fetching data from the network.
-
Entity - It is also called a model. It is a representation of data downloaded or received from cache. Entity is an output after converting JSON to local struct or class. Fo example, entity could be an
User
object after login or list of products after user search action -
Router - Responsible for routing an app to appropriate screen. This is mainly based on the user action such as button tap or selection of specific option or simply going back in Navigation stack hierarchy. Since router has to have direct reference to
ViewController
associated with architecture, presenter acts as an intermediary between view and router -
DataManager - Communicates directly to interactor. Interactor, upon receiving input from view through presenter then contacts DataManager to fetch necessary data. When downloaded, DataManager passes the data back to interactor either through delegate callback or reactive binding
Enough with the theory, let's now play with a real example. An example I am going to demo is very simple login screen. It has following moving parts
- User enters
username
andpassword
- User presses
Login
button - App sends pseudo request to server which takes second or two to get response back
- Upon the receipt of response, app updates an UI and make transition to next screen
First off, we will create a LoginController
with an UI
class LoginViewController: UIViewController {
let presenter: LoginPresentorProtocol
var loginViewModel: LoginViewModel?
let activityIndicatorView: UIActivityIndicatorView
let loginSuccessfulLabel: UILabel
let button: UIButton
let usernameTextField: UITextField
let passwordTextField: UITextField
init(presenter: LoginPresentorProtocol) {
self.presenter = presenter
activityIndicatorView = UIActivityIndicatorView()
activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
activityIndicatorView.hidesWhenStopped = true
loginSuccessfulLabel = UILabel()
loginSuccessfulLabel.translatesAutoresizingMaskIntoConstraints = false
loginSuccessfulLabel.numberOfLines = 0
usernameTextField = UITextField()
passwordTextField = UITextField()
usernameTextField.translatesAutoresizingMaskIntoConstraints = false
passwordTextField.translatesAutoresizingMaskIntoConstraints = false
button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Login", for: .normal)
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .red
self.activityIndicatorView.activityIndicatorViewStyle = .whiteLarge
self.view.addSubview(activityIndicatorView)
self.view.addSubview(loginSuccessfulLabel)
self.view.addSubview(button)
self.view.addSubview(usernameTextField)
self.view.addSubview(passwordTextField)
usernameTextField.text = "foo"
passwordTextField.text = "password"
self.button.addTarget(self, action: #selector(performLogin), for: .touchUpInside)
self.view.addConstraint(NSLayoutConstraint(item: activityIndicatorView, attribute: .centerX, relatedBy: .equal, toItem: self.view, attribute: .centerX, multiplier: 1.0, constant: 0))
self.view.addConstraint(NSLayoutConstraint(item: activityIndicatorView, attribute: .centerY, relatedBy: .equal, toItem: self.view, attribute: .centerY, multiplier: 1.0, constant: 0))
let views = ["label": loginSuccessfulLabel, "button": button, "username": usernameTextField, "password": passwordTextField]
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[label]-|", options: [], metrics: nil, views: views))
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[button]-|", options: [], metrics: nil, views: views))
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[username]-|", options: [], metrics: nil, views: views))
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[password]-|", options: [], metrics: nil, views: views))
self.view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-100-[username(>=0)]-[password(>=0)]-[label(>=0)]-[button(44)]", options: [], metrics: nil, views: views))
}
@objc private func performLogin() {
self.presenter.executeFindItems(name: usernameTextField.text ?? "", password: passwordTextField.text ?? "")
}
private func goToNextScreen() {
self.presenter.showList()
}
}
Next comes the protocol named LoginViewProtocol
which this LoginViewController
class conforms to. These methods will be used as a callback from presenter to view for an update of any kind of data.
On the next line we will create a protocol and make our viewController to conform to it
protocol LoginViewProtocol: class {
func showLoadingSpinner()
func hideLoadingSpinner()
func showUserWithSuccess(viewModel: LoginViewModel)
func showUserWithError(_ error: String)
}
// Protocol conformence
extension LoginViewController: LoginViewProtocol {
func showLoadingSpinner() {
self.activityIndicatorView.startAnimating()
}
func hideLoadingSpinner() {
self.activityIndicatorView.stopAnimating()
}
func showUserWithSuccess(viewModel: LoginViewModel) {
self.loginViewModel = viewModel
self.loginSuccessfulLabel.text = viewModel.successMessage
self.title = viewModel.titleMessage
let when = DispatchTime.now() + 1
DispatchQueue.main.asyncAfter(deadline: when) {
self.goToNextScreen()
}
}
func showUserWithError(_ error: String) {
self.loginSuccessfulLabel.text = error
self.title = ""
self.loginViewModel = nil
}
}
Please note that viewController only has an access to viewModel which is a superset of model thus passed back from the presenter to view. Presenter and interactor never have access to UI elements and view never has access to dataManager or model object. This helps to separate out elements of concern
Since we have used two structures, User
and LoginViewModel
in the above example, below is their actual structures,
// Simple model
struct User {
let username: String
let password: String
}
// ViewModel
struct LoginViewModel {
let user: User
let successMessage: String
let titleMessage: String
}
Please note that the difference between two. We get model from the interactor. Presenter's job is to convert this model into viewModel. Thus viewModel derives some of its values from model it receives from interactor. ViewModel is an object which is eventually communicated to view
Let's move on to Presenter now. Similar to view, presenter will also use the protocol named LoginPresentorProtocol
to which it conforms to
// Protocol definition
protocol LoginPresentorProtocol: class {
var view: LoginViewProtocol? { get set }
var user: User? { get set }
func executeFindItems(name: String, password: String)
func showList()
func itemDownloaded(user: User?)
}
// Protocol conformance
class LoginPresenter: LoginPresentorProtocol {
weak var view: LoginViewProtocol?
var user: User?
let interactor: LoginInteractorProtocol
let wireframe: LoginWireframeProtocol
init(interactor: LoginInteractorProtocol, wireframe: LoginWireframeProtocol) {
self.interactor = interactor
self.wireframe = wireframe
}
}
extension LoginPresenter {
func executeFindItems(name: String, password: String) {
self.view?.showLoadingSpinner()
self.interactor.findUpcomingItems(name: name, password: password)
}
func showList() {
guard let user = self.user else {
self.view?.showUserWithError("User selected is nil, cannot proceed")
return
}
self.wireframe.presentPosts(view: view!, user: user)
}
func itemDownloaded(user: User?) {
self.user = user
self.view?.hideLoadingSpinner()
if let user = user {
self.view?.showUserWithSuccess(viewModel: LoginViewModel(user: user, successMessage: "Login Successful", titleMessage: "Welcome Back"))
} else {
self.view?.showUserWithError("Failed to get user object")
}
}
}
Delegate methods used in the presenter will be used as a communication channel between interactor and view. As it is clear from above example, presenter is also responsible for calling delegate methods in router/wireframe which performs the task of routing from one screen to another
Since discussion about wireframe (I am calling it a wireframe based on the online tutorials I followed, but really it is just a router), We will make a protocol that Wireframe class will conform to and will be responsible for app routing
protocol LoginWireframeProtocol {
func presentPosts(view: LoginViewProtocol, user: User)
}
class LoginWireframe: LoginWireframeProtocol {
func presentPosts(view: LoginViewProtocol, user: User) {
let dataManager: DataDownloader = DataManager()
let interactor: DetailsInteractorProtocol = DetailsInteractor(dataManager: dataManager)
let wireframe: DetailWireframeProtocol = DetailWireframe()
let detailsPresenter: DetailsPresenterProtocol = DetailsPresenter(interactor: interactor, wireframeProtocol: wireframe, user: user)
let detailsVC: DetailViewProtocol = DetailViewController(detailsPresenter: detailsPresenter)
interactor.presenter = detailsPresenter
detailsPresenter.view = detailsVC
dataManager.itemsDetailsInteractor = interactor
if let view = view as? UIViewController, let detailsVC = detailsVC as? UIViewController {
view.navigationController?.pushViewController(detailsVC, animated: true)
}
}
}
In the example above,
detailsVC
is the destination viewController to which we want to make transition to. As it is clear from type information, it also conforms toDetailViewProtocol
Let's move on to interactor now. This is a piece which communicates with our low level component DataManager
and pushes the data back to view. As with previous components, it will also involve a protocol which our interactor class will conform to
protocol LoginInteractorProtocol: class {
var presenter: LoginPresentorProtocol? { get set }
func findUpcomingItems(name: String, password: String)
func itemDownloaded(user: User?)
}
class LoginInteractor: LoginInteractorProtocol {
let dataManager: DataDownloader
weak var presenter: LoginPresentorProtocol?
init(dataManager: DataDownloader) {
self.dataManager = dataManager
}
func findUpcomingItems(name: String, password: String) {
self.dataManager.downloadItem(name: name, password: password)
}
func itemDownloaded(user: User?) {
self.presenter?.itemDownloaded(user: user)
}
}
Last, but not least - Let's outline our last and low level component DataManager
. It will have direct contact only with Interactor
part of VIPER and will be responsible for fetching a data and sending it over.
protocol DataDownloader: class {
func downloadItem(name: String, password: String)
var listItemsInteractor: LoginInteractorProtocol? { get set }
}
class DataManager: DataDownloader {
weak var listItemsInteractor: LoginInteractorProtocol?
func downloadItem(name: String, password: String) {
let when = DispatchTime.now() + 0.5
DispatchQueue.main.asyncAfter(deadline: when) {
if name == "foo" && password == "password" {
self.listItemsInteractor?.itemDownloaded(user: User(username: name, password: "password"))
} else {
self.listItemsInteractor?.itemDownloaded(user: nil)
}
}
}
}
Now, since we have all the code let's put it together. All in all, the components will have references to each other. We will also declare some of variable with
weak
keyword to avoid retain cycles. Components won't have direct reference to each other, but they will talk to each other in the form of protocol and are completely agnostic of underlying datatype
// Initialize the DataManager object which conforms to DataDownloader protocol
let dataManager: DataDownloader = DataManager()
// Initialize an interactor named LoginInteractor which conforms to LoginInteractorProtocol
let interactor: LoginInteractorProtocol =
LoginInteractor(dataManager: dataManager)
// Initiate a wireframe object of type LoginWireframe which conforms to LoginWireframeProtocol and has access to view part of VIPER architecture.
let wireFrame: LoginWireframeProtocol = LoginWireframe()
// Initialize the presenter object of type LoginPresenter which conforms to LoginPresenterProtocol and access to interactor and wireframe objects.
let presenter: LoginPresentorProtocol = LoginPresenter(interactor: interactor, wireframe: wireFrame)
// Initialize a viewController of type LoginViewController which conforms to LoginViewProtocol and has reference to presenter object.
let vc: LoginViewProtocol & LoginViewController = LoginViewController(presenter: presenter)
// Presenter should have reference back to view
presenter.view = vc
// Interactor will maintain reference to presenter
interactor.presenter = presenter
// DataManager object will have reference to interactor
dataManager.listItemsInteractor = interactor
// Once our viewController is initialized with necessary components, we are ready to use it in our application
self.window?.rootViewController = UINavigationController(rootViewController: vc)
After we put it all together, below are app states on how it looks like before and after login is completed with error and success states.
Initial
Login Success State
Login Error State
I have hosted a code on GitHub if you want to play with it
You can find the second part of VIPER architecture series here
References:
objc.io - VIPER architecture
Mindorks Blog