Infrastructure as Code Best Practices with Terraform for DevOps
João Victor Alhadas | Dec 17, 2024
The dev team at Cheesecake Labs has been using VIPER for iOS and Android mobile app development for over one year and we just love this clean architecture!
This article summarizes our best practices on the VIPER architecture, using code examples from our VIPER boilerplate. The code samples used here are in Swift, but all concepts mentioned may be applied to an Android project developed with VIPER, either using Java or Kotlin.
If you still need to get familiar with basic VIPER concepts and how this architecture can make your code more organized and scalable, I suggest you read the following articles:
So, are you ready to learn our best practices on how to build an app with VIPER?
You can see that we keep all VIPER’s modules as decoupled as possible, saving us from future headaches when the project grows or specification changes. If you delete any of them, you should get several errors just on the Routers that reference that model – but not on Views, Presenters, Interactors, Data Managers or Entity classes.
One key point to help you truly decouple your modules is to keep all entities on a separate folder, linking them to the project itself and not to any specific module.
Also, using Data Managers to perform API requests and manipulate local database is an excellent way of increasing the project organization, but requires some attention:
class MainSearchInteractor {
// Properties
weak var output: MainSearchInteractorOutput?
var apiDataManager = ProfileApiDataManager()
var localDataManager = ProfileLocalDataManager()
}
extension MainSearchInteractor: MainSearchUseCase {
// Code below show how interactor get data from API and then saves it on local DB with separate data managers
func searchProducts(with searchTerm: String, onPage page: Int) {
self.apiDataManager.searchProducts(with: searchTerm, forPage: page) { (products) in
if let products = products {
self.localDataManager.updateSearchResultFavorites(products) { (products) in
self.output?.onFetchProductsSuccess(Array(products), shouldAppend: page != 1)
}
} else {
self.output?.onFetchProductsSuccess(nil, shouldAppend: page != 1)
}
}
}
}
If you’ve already developed using VIPER, you’ve had the bad experience of creating all the 20+ Swift files needed for a simple screen with three tabs on the navigation bar. But there’s a light at the end of the tunnel: this amazing Xcode plugin which automates the generation of all VIPER files for one module with three clicks.
If you think that that’s too much, meet Generamba: a code generator designed to create VIPER modules from the terminal, quite easy to customize for any other classes.
Just like for us humans, contracts on VIPER are a voluntary agreement between two parties (module components) concerning the rights (methods) and duties (arguments) that arise from agreements. At Cheesecake Labs, we use protocols to define the methods that a module component can call from other components on the same module.
However, before start writing the code for a new View or Presenter, for example, think about the information flow between both components and declare their methods on the Contract first.
// MainSearchContract.swift
import Foundation
protocol MainSearchView: BaseView {
func showCustomError(_ message: String?)
func updateVisibility(onSearchController willBeActive: Bool)
func showSearchResult(_ products: [Product]?, shouldAppend: Bool)
}
protocol MainSearchPresentation: class {
func onViewDidLoad()
func onWillPresentSearchController()
func onSearchTermChanged(to searchTerm: String)
func onProductFavoriteChanged(_ product: Product, to isFavorite: Bool)
func onProductSelected(_ product: Product)
func onInfiniteScrollTriggered()
}
protocol MainSearchUseCase: class {
func searchProducts(with searchTerm: String, onPage page: Int)
func updateProductFavorited(_ product: Product, to newValue: Bool)
}
protocol MainSearchInteractorOutput: class {
func onFetchProductsSuccess(_ products: [Product]?, shouldAppend: Bool)
func onFetchProductsFailure(message: String)
}
protocol MainSearchWireframe: class {
func showProductScreen(delegate: ProductScreenDelegate, product: Product?)
}
The Xcode plugin mentioned before will also create a ModuleNameContract.swift file with all protocols, waiting for your declaration of the necessary methods. Once those protocols are defined, you have complete control of the information flow between the components of a VIPER module.
Before presenting the View of a VIPER module, you need to make sure all components have been properly initialized. I can think of at least 3 very different ways of doing it, but the flow below is the best option we’ve came up with. The ace in the hole here is to have a static function on each Router to initialize its own module together with some UIViewController
and UIStoryboard
extensions. Then, if module A wants to present Module B:
As simple as that. Having the module initialization code on its own Router will eliminate a bunch of code repetition, specially for huge projects.
You need to create these extensions once:
// ReusableView.swift
protocol ReusableView: class {}
extension ReusableView {
static var reuseIdentifier: String {
return String(describing: self)
}
}
// UIViewController.swift
extension UIViewController: ReusableView { }
// UIStoryboard.swift
extension UIStoryboard {
func instantiateViewController() -> T where T: ReusableView {
return instantiateViewController(withIdentifier: T.reuseIdentifier) as! T
}
}
And then, leave initialization code on the router of each VIPER module:
// MainSearchRouter.swift
class MainSearchRouter {
// MARK: Properties
weak var view: UIViewController?
// MARK: Static methods
static func setupModule() -> MainSearchViewController {
let viewController = UIStoryboard(name: MainSearchViewController.storyboardName, bundle: nil).instantiateViewController() as MainSearchViewController
let presenter = MainSearchPresenter()
let router = MainSearchRouter()
let interactor = MainSearchInteractor()
viewController.presenter = presenter
presenter.view = viewController
presenter.router = router
presenter.interactor = interactor
router.view = viewController
interactor.output = presenter
return viewController
}
}
It might seem like a lot of steps, but good news: the aforementioned plugin automates that for us as well! 🙂
However, you’ll need to take some additional steps if you want to fit a UITabBarController
or a UIPageViewController
into the VIPER architecture. If you need any help, just drop a comment on this post and I’ll prepare a specific Gist for you.
If you’ve came up this far, dear reader, you’re really avid for knowledge. So I’ll give you 3 advices to make sure you’ve fully understood the responsibilities of the Router:
UIApplication
.shared
.openURL(url)
on the Router because you’re navigating (i.e. routing) out of your current module;UIActivityViewController
from the Router because iOS will send the user to a View or app out of your current module;UIAlertController
.You’ve probably faced a situation where a field on Module A is filled with the selected item of the Module B. So Module A calls Module B when the user clicks the field, and Module B returns the selected item to the existing Module A through the delegate.
Delegates are an awesome approach to send information back and forth between VIPER modules:
// 1. Declare which messages can be sent to the delegate
// ProductScreenDelegate.swift
protocol ProductScreenDelegate {
//Add arguments if you need to send some information
func onProductScreenDismissed()
func onProductSelected(_ product: Product?)
}
// 2. Call the delegate when you need to send him a message
// ProductPresenter.swift
class ProductPresenter {
// MARK: Properties
weak var view: ProductView?
var router: ProductWireframe?
var interactor: ProductUseCase?
var delegate: ProductScreenDelegate?
}
extension ProductPresenter: ProductPresentation {
//View tells Presenter that view disappeared
func onViewDidDisappear() {
//Presenter tells its delegate that the screen was dismissed
delegate?.onProductScreenDismissed()
}
}
// 3. Implement the delegate protocol to do something when you receive the message
// ScannerPresenter.swift
class ScannerPresenter: ProductScreenDelegate {
//Presenter receives the message from the sender
func onProductScreenDismissed() {
//Presenter tells view what to do once product screen was dismissed
view?.startScanning()
}
...
}
// 4. Link the delegate from the Product presenter in order to proper initialize it
// File ScannerRouter.swift
class ProductRouter {
static func setupModule(delegate: ProductScreenDelegate?) -> ProductViewController {
...
let presenter = ScannerPresenter()
presenter.view = view
presenter.interactor = interactor
presenter.router = router
presenter.delegate = delegate // Add this line to link the delegate
...
}
}
Using a POSO (Plain Old Swift Object) to send information between VIPER’s components is the best approach if you want to be 100% compliant with the VIPER architecture. But sending the Entity itself between VIPER components works fine and removes the overhead of creating POSOs.
Anyway, avoid sending this data using dictionaries if you don’t want to get lost with key names when your project starts growing and changing.
If you want to take the most of this architecture, it’s important to keep your team completely in sync with the responsibilities of each component of a VIPER module.
Even after understanding the role of each specific component, our team still faced some doubts, mostly influenced by previous experience with MVC.
UIKit
and implements all logic regarding UI elements from its module. TableView logics, for example, are implemented on the View. If you want to make your code more readable, split TableView logics on extensions. If you want to make your project even more concise, use a TableViewDataManager;UIKit
and does not handle UI elements, but it does prepare the data in the format required by the view and take decisions based on UI events from the view. Do not manipulate any UI element on the presenter, it shouldn’t handle them;The product team decided to drop out a feature from your project? Or your small project started growing huge? Use proper VIPER architecture and avoid future headaches!
Automating VIPER files creation and modules initialization will eliminate the overhead of working with this – complex at first sight – but clear and awesome architecture. Android developers can also use it as well.
We’ve seen that our approach to VIPER architecture is actually composed of VIPRC modules (View-Interactor-Presenter-Router-Contract), while Entities are decoupled from the modules, along with Data Managers. I know the name VIPRC is not sexy at all, but it allows you to build an app like a boss.
Do you have any other tips for using VIPER architecture on iOS and Android app development? Feel free to share your experience!
Jumped drillships to join great friends on their amazing mission, exploring his developer/entrepreneur skills. Loves traveling and can cook a lasagna better than his grandmother.