“Unlocking VIPER: Simplify Your iOS Development Journey”

TheJuniorDeveloper
The Junior Developer
8 min readJun 10, 2024

Ahoy, matey! Pour yerself a cof o’ grog or tea, as this is a long ‘un!

VIPER Architecture: Implementing a Listing Page with Combine

In this discussion, we will cover the fundamentals of the VIPER architecture and compare it to the MVVM architecture by developing a simple listing page that displays names with their corresponding IDs

The VIPER architecture (View, Interactor, Presenter, Entity, Router) is a design pattern that enhances code maintainability and organization in iOS development.

In the VIPER architecture,

VIEW represents the user interface elements that users interact with, such as labels, text views, and buttons.

Now, the ViewModel is divided into three distinct parts:

  1. PRESENTER: This component contains the business logic responsible for determining what data should be presented on the UI and when. It acts as an intermediary between the View and the Interactor, processing data and making decisions about how it should be displayed to the user.
  2. INTERACTOR: The Interactor handles tasks related to fetching data, whether it's from a network call or from local storage. It encapsulates the data retrieval process, abstracting away the implementation details from the Presenter. This separation allows for better organization and testability of the codebase.
  3. ROUTER: The Router is responsible for managing the navigation flow within the application. It handles transitions between different screens or pages, determining which view should be presented next based on user interactions or other events. By centralizing navigation logic in the Router, the codebase remains modular and easier to maintain.

ENTITY: The Entity represents the data model or object that encapsulates the application’s business logic. It defines the structure of the data being manipulated within the application. The Entity component is typically platform-independent and does not contain any presentation logic. Instead, it focuses on representing the core data and behaviors of the application domain.

When to Use VIPER

VIPER is particularly suitable for projects with the following characteristics:

  1. Complex Business Logic: When the app has complex business rules and interactions, VIPER helps in managing the complexity by separating concerns.
  2. Large Teams: In large development teams, VIPER’s modularity allows multiple developers to work on different layers simultaneously without causing merge conflicts.
  3. Long-term Projects: For projects that require long-term maintenance and scaling, VIPER provides a clear structure that makes future modifications and additions easier.
  4. Testability: When unit testing is a priority, VIPER’s separation of concerns makes it easier to write tests for each component in isolation.

Why Use VIPER

VIPER offers several advantages:

  1. Separation of Concerns: Each component in VIPER has a distinct responsibility, making the codebase more organized and maintainable.
  2. Modularity: VIPER’s structure allows for reusable and replaceable components, which is beneficial for scaling and extending applications.
  3. Testability: The clear separation between components makes it easier to write unit tests, improving the reliability of the application.
  4. Collaboration: In team environments, VIPER enables better collaboration by allowing team members to work on different parts of the architecture simultaneously.

Benefits of VIPER over MVVM

While MVVM (Model-View-ViewModel) is a popular architecture in iOS development, VIPER provides distinct advantages:

  1. Single Responsibility Principle: VIPER adheres more strictly to the single responsibility principle than MVVM, reducing the chances of “god classes” that do too much.
  2. Clearer Boundaries: VIPER’s components have well-defined roles, whereas in MVVM, the ViewModel can sometimes become bloated with business logic and UI state management.
  3. Enhanced Testability: VIPER’s clear separation of the business logic (Interactor) from the presentation logic (Presenter) makes it easier to write isolated tests for each part of the architecture.

Implementing the listing page using Viper and Combine

  1. Entity: Create a folder named Entity and add a Swift file named Item Model encapsulating class given below.
//We’ll create a simple Item struct that work as the model.
struct Item: Decodable {
let id: Int
let name: String
}

2. Protocols : Defining protocols for communication between the Presenter and the View, as well as the Interactor and the Presenter, is a crucial part of the VIPER architecture. These protocols ensure a clear and structured way for components to interact, maintaining the separation of concerns and enhancing testability and modularity.

Protocols for Communication

a. Presenter to View (PtoV) Protocol: Defines the methods that the Presenter can call on the View.

b. Interactor to Presenter (ItoP) Protocol: Defines the methods that the Interactor can call on the Presenter.

c. View to Presenter (VtoP) Protocol: Defines the methods that the View can call on the Presenter.

d.Presenter to Interactor (PtoI) Protocol: Defines the methods that the Presenter can call on the Interactor.

a. Presenter to View (PtoV) Protocol

This protocol will define the methods that the Presenter will use to instruct the View to update the UI.

protocol ListingViewProtocol: AnyObject {
func showItems(_ items: [Item])
func showError(_ message: String)
}

b. Interactor to Presenter (ItoP) Protocol

This protocol will define the methods that the Interactor will use to communicate results back to the Presenter.

protocol ListingInteractorOutputProtocol: AnyObject {
func didFetchItems(_ items: [Item])
func didFailToFetchItems(with error: Error)
}

c. View to Presenter (VtoP) Protocol

This protocol will define the methods that the View will use to communicate user actions to the Presenter.

protocol ListingPresenterProtocol: AnyObject {
func loadItems()
}

d. Presenter to Interactor (PtoI) Protocol

This protocol will define the methods that the Presenter will use to instruct the Interactor to perform tasks.

protocol ListingInteractorInputProtocol: AnyObject {
func fetchItems()
}

We’ll dive into the reasons behind these protocols shortly. Hang tight and grab your snacks! 🍺

3. Interactor: Create the folder named Interactor and add a ListingViewInteractor file.This class manages data retrieval from either the API or local storage, as instructed. It facilitates reading data from the designated source, ensuring flexibility in data retrieval mechanisms.

class ListingInteractor: ListingInteractorInputProtocol {
var output: ListingInteractorOutputProtocol?
private var cancellables = Set<AnyCancellable>()

func fetchItems() {
let url = URL(string: "https://jsonplaceholder.typicode.com/users")!
URLSession.shared.dataTaskPublisher(for: url)
.tryMap { result -> Data in
guard let httpResponse = result.response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return result.data
}
.decode(type: [Item].self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
switch completion {
case .failure(let error):
self.output?.didFailToFetchItems(with: error)
case .finished:
break
}
}, receiveValue: { items in
self.output?.didFetchItems(items)
})
.store(in: &cancellables)
}
}

Combine Walkthrough

  • URLSession.shared.dataTaskPublisher(for: url): Initiates a data task and returns a publisher.
  • .tryMap { result -> Data in … }: Transforms received data, ensuring successful server response.
  • .decode(type: [Item].self, decoder: JSONDecoder()): Decodes data into an array of Items.
  • .receive(on: DispatchQueue.main): Ensures subsequent operations occur on the main thread.
  • .sink(receiveCompletion: { completion in … }, receiveValue: { items in … }): Handles completion and emitted values, triggering appropriate callbacks.
  • .store(in: &cancellables): Stores the subscription for later cancellation, preventing memory leaks.

In this scenario, we utilize the Combine Framework with UrlSession. If there’s a failure in fetching items, the “didFailToFetchItems” protocol communication method is triggered. Conversely, when items are successfully fetched and updated, the “didFetchItems(items)” method is invoked.

The interactor will fetch data and use the ListingInteractorOutputProtocol to communicate results to the presenter.

4. PRESENTER: The presenter will handle user actions from the view and interact with the interactor. It will use the ListingViewProtocol to update the view.

import Foundation
import Combine

class ListingPresenter: ListingPresenterProtocol, ListingInteractorOutputProtocol {
private let interactor: ListingInteractorInputProtocol
private let router: ListingRouterProtocol
private weak var view: ListingViewProtocol?
private var cancellables = Set<AnyCancellable>()

init(interactor: ListingInteractorInputProtocol, router: ListingRouterProtocol, view: ListingViewProtocol) {
self.interactor = interactor
self.router = router
self.view = view
}

func loadItems() {
interactor.fetchItems()
}


func didFetchItems(_ items: [Item]) {
dump(items)
DispatchQueue.main.async {
self.view?.showItems(items)
}
}

func didFailToFetchItems(with error: Error) {
DispatchQueue.main.async {
self.view?.showError(error.localizedDescription)
}
}
}

The Presenter utilizes the “fetchItems” method to retrieve data from the Interactor. Upon data retrieval, the Presenter confirms it through either “didFetchItems” or “didFailToFetchItems”. Through the delegate pattern, the Interactor notifies the Presenter of data updates, facilitating UI updates accordingly.

Now “didFetchItems” calls the view’s method “showItems” which reloads the tableView which forces the viewController to callibrate and recalculate the datasource again and refreshes the tableView.

On Error “didFailToFetchItems” method calls the “showError” which does nothing but we can implement a way to show a No Data View (Future Scope) if needed.

5. ROUTER: The router handles navigation logic. In this simple example, we assume no navigation is needed.

protocol ListingRouterProtocol {}

class ListingRouter: ListingRouterProtocol {}

6. VIEW: The view will display the data and handle user interactions. It will conform to a protocol defined by the presenter.

import Foundation
import UIKit

protocol ListingViewProtocol: AnyObject {
func showItems(_ items: [Item])
func showError(_ message: String)
}

class ListingViewController: UIViewController, ListingViewProtocol {
var presenter: ListingPresenterProtocol!
var tableView = UITableView()
private var items: [Item] = []

override func viewDidLoad() {
super.viewDidLoad()
presenter.loadItems()
tableView.register(TitleTableViewCell.nib, forCellReuseIdentifier: TitleTableViewCell.identifier)
tableView.dataSource = self
tableView.delegate = self
self.view.addSubview(tableView)
self.tableView.frame = self.view.bounds
self.tableView.reloadData()
}

func showItems(_ items: [Item]) {
self.items = items
// Update UI with the items
tableView.reloadData()
}

func showError(_ message: String) {
// Show error message to the user
}
}

extension ListingViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: TitleTableViewCell.identifier, for: indexPath) as? TitleTableViewCell {
let item = items[indexPath.row]
let titleVm = TitleTableViewCellVM(title: item.name, id: String(describing: item.id))
cell.showData(viewModel: titleVm)
return cell
}
return UITableViewCell()
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
}

This code initializes a ListingViewController with a Presenter for loading items. In viewDidLoad(), the TableView is configured with a custom cell (TitleTableViewCell), and its dataSource and delegate are set. The Presenter communicates with the view through showItems(_:) and showError(_:) methods. When items are fetched, showItems(_:) updates the UI. The TableView’s dataSource methods populate the TableView with fetched items using TitleTableViewCell and TitleTableViewCellVM.

7. ASSEMBLY: Finally, we’ll set up the dependencies and initialize the VIPER components.

class ListingModuleBuilder {
static func build() -> UIViewController {
let interactor = ListingInteractor()
let router = ListingRouter()
let view = ListingViewController()
let presenter = ListingPresenter(interactor: interactor, router: router, view: view)
view.presenter = presenter
return view
}
}

CELL CONFIGURATION: The TableView cell is registered with the TableView using the provided guidelines. Here’s the Title Cell ViewModel and View setup:

TITLE CELL VIEWMODEL:

import Foundation
class TitleTableViewCellVM {
var title:String?
var id: String?

init(title: String? ,id: String?) {
self.title = title
self.id = id

}
}

TITLE CELL VIEW:


import UIKit

class TitleTableViewCell: UITableViewCell {

@IBOutlet weak var idLabel: UILabel!
@IBOutlet weak var labelTitle: UILabel!
var viewModel: TitleTableViewCellVM? {
didSet{
self.labelTitle.text = viewModel?.title
self.idLabel.text = viewModel?.id
}
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}

override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)

// Configure the view for the selected state
}

func showData(viewModel: TitleTableViewCellVM) {

self.viewModel = viewModel
}

}
extension TitleTableViewCell:CellIdentifiable {

}

Conclusion

By implementing the VIPER architecture with Combine, we achieve a clear separation of concerns in our Listing Page example. The view handles the UI, the interactor fetches data, and the presenter mediates between them. This modular approach not only makes the codebase more maintainable but also enhances testability, allowing each component to be tested in isolation. The VIPER architecture, combined with Combine, provides a robust framework for building scalable and maintainable iOS applications.

And when you’re feeling tired and parched, remember, even the best developers need a break. So take a moment, grab a drink, and return refreshed for your next coding adventure! Until then, keep in mind that VIPER’s clear separation of concerns and emphasis on modularity can help ease the burden and keep your app sailing smoothly.

Arr, apologies matey, but it seems me drink be runnin’ dry. Time to hoist anchor and set sail. I’ll be back with a fresh mug o’ ale for our next adventure, delvin’ into testin’ within the realm of VIPER and its impact on modularity.🏴‍☠️

--

--