VIPER Architecture, Swift, XCode, Non-technical.

Understand VIPER Architecture in Swift

Let’s go through VIPER from a non-technical side

Aaron Cleveland
Geek Culture
Published in
6 min readMar 1, 2023

--

Understanding VIPER in Swift

In the world of iOS development, there are many architectural patterns to choose from. One such pattern that has gained popularity in recent years is VIPER architecture. VIPER stands for View, Interactor, Presenter, Entity, and Router, and it is a modular approach to building iOS applications.

VIPER is a clean architecture that helps in building an application in a modular, scalable, and maintainable way. It’s ideal for developing applications where the logic can be complex and the team size is large. The VIPER architecture is suitable for developing medium to large scale applications.

In this article, we’ll dive deep into VIPER architecture in Swift, covering each component in detail, and how they work together to make the application modular, testable, and maintainable.

Components of VIPER Architecture

View

The View is responsible for presenting data to the user and handling user interactions. It doesn’t have any logic, but instead delegates the user’s actions to the Presenter for processing. The View can be a UIViewController, UIView, or even a UITableViewCell. The View is passive, which means that it doesn’t know anything about the data, logic, or architecture of the application.

protocol ExampleView: AnyObject {
func displayData(_ data: [String])
}

class ExampleViewController: UIViewController, ExampleView {
var presenter: ExamplePresenter?
func displayData(_ data: [String]) {
// Update the UI with the data
}
@IBAction func buttonPressed(_ sender: UIButton) {
presenter?.buttonPressed()
}
}

Interactor

The Interactor is responsible for business logic and data management. It contains the use cases of the application and provides data to the Presenter. The Interactor communicates with the entities to perform operations like CRUD (Create, Read, Update, and Delete) on the data.

protocol ExampleInteractorInput {
func fetchData()
}

protocol ExampleInteractorOutput: AnyObject {
func dataFetched(_ data: [String])
}
class ExampleInteractor: ExampleInteractorInput {
var output: ExampleInteractorOutput?
var dataService: ExampleDataService?
func fetchData() {
dataService?.fetchData(completion: { [weak self] data in
self?.output?.dataFetched(data)
})
}
}

Presenter

The Presenter is responsible for presentation logic. It receives input from the View and Interactor, processes it, and then updates the View. The Presenter knows about the View, but not the other way around. The Presenter also converts the data from the Interactor into a format that can be presented by the View.

protocol ExamplePresenter {
func buttonPressed()
}

class ExamplePresenterImpl: ExamplePresenter {
weak var view: ExampleView?
var interactor: ExampleInteractorInput?
func buttonPressed() {
interactor?.fetchData()
}
}
extension ExamplePresenterImpl: ExampleInteractorOutput {
func dataFetched(_ data: [String]) {
view?.displayData(data)
}
}

Entity

The Entity represents the application’s data. It can be a database, network service, or any other data source. The Entity is responsible for CRUD operations on the data and communicates with the Interactor.

struct ExampleData {
let title: String
let subtitle: String
}

protocol ExampleDataService {
func fetchData(completion: @escaping ([ExampleData]) -> Void)
}
class ExampleDataServiceImpl: ExampleDataService {
func fetchData(completion: @escaping ([ExampleData]) -> Void) {
// Fetch the data and call the completion handler with the data
}
}

Router

The Router is responsible for navigation and routing between different modules of the application. It knows about all the modules of the application and is responsible for transitioning between them.

protocol ExampleRouter {
func navigateToDetail()
}

class ExampleRouterImpl: ExampleRouter {
weak var viewController: UIViewController?
func navigateToDetail() {
let detailViewController = DetailViewController()
viewController?.navigationController?.pushViewController(detailViewController, animated: true)
}
}

Working of VIPER Architecture

Each component of VIPER architecture has a clear and defined role. The View sends user input to the Presenter, which in turn communicates with the Interactor to perform operations on the data. The Interactor then returns the result to the Presenter, which converts it into a format that can be presented by the View. The Router is responsible for transitioning between different modules of the application.

The communication between each component is done through protocols. This makes it easy to write unit tests for each component and ensures that they can be replaced or modified without affecting the other components.

Advantages of VIPER Architecture

Modular Design

VIPER architecture follows a modular design, which makes it easy to maintain and scale. Each component has a clear and defined role, which makes it easy to add or remove functionality without affecting the other components.

// Before adding new functionality
class ExampleInteractor: ExampleInteractorInput {
var output: ExampleInteractorOutput?
var dataService: ExampleDataService?

func fetchData() {
dataService?.fetchData(completion: { [weak self] data in
self?.output?.dataFetched(data)
})
}
}
// After adding new functionality
class ExampleInteractor: ExampleInteractorInput {
var output: ExampleInteractorOutput?
var dataService: ExampleDataService?
var analyticsService: ExampleAnalyticsService?
func fetchData() {
dataService?.fetchData(completion: { [weak self] data in
self?.output?.dataFetched(data)
analyticsService?.trackEvent("Data Fetched")
})
}
}

Testability

VIPER architecture is highly testable as each component is separated and communicates through protocols. This makes it easy to write unit tests for each component, ensuring that the application is bug-free and reliable.

class ExamplePresenterTests: XCTestCase {
var sut: ExamplePresenterImpl!
var mockView: ExampleViewMock!
var mockInteractor: ExampleInteractorInputMock!

override func setUp() {
super.setUp()
sut = ExamplePresenterImpl()
mockView = ExampleViewMock()
mockInteractor = ExampleInteractorInputMock()
sut.view = mockView
sut.interactor = mockInteractor
}
func test_buttonPressed() {
sut.buttonPressed()
XCTAssertTrue(mockInteractor.fetchDataCalled)
}
}

Separation of Concerns

VIPER architecture follows the principle of separation of concerns. Each component has a clear and defined role, which makes it easy to maintain and modify the application.

// Before modifying the View
class ExampleViewController: UIViewController, ExampleView {
var presenter: ExamplePresenter?

func displayData(_ data: [String]) {
// Update the UI with the data
}
}
// After modifying the View
class ExampleViewController: UIViewController, ExampleView {
var presenter: ExamplePresenter?
var data: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
presenter?.viewDidLoad()
}
func updateUI() {
// Update the UI with the data
}
}

Disadvantages of VIPER Architecture

Complexity

VIPER architecture can be complex, especially for small applications. It requires a lot of boilerplate code and can be time-consuming to set up.

protocol ExampleView: AnyObject {
func displayData(_ data: [String])
}

class ExampleViewController: UIViewController, ExampleView {
var presenter: ExamplePresenter?
func displayData(_ data: [String]) {
// Update the UI with the data
}
@IBAction func buttonPressed(_ sender: UIButton) {
presenter?.buttonPressed()
}
}

protocol ExampleInteractorInput {
func fetchData()
}

protocol ExampleInteractorOutput: AnyObject {
func dataFetched(_ data: [String])
}

class ExampleInteractor: ExampleInteractorInput {
var output: ExampleInteractorOutput?
var dataService: ExampleDataService?
func fetchData() {
dataService?.fetchData(completion: { [weak self] data in
self?.output?.dataFetched(data)
})
}
}

protocol ExamplePresenter {
func viewDidLoad()
func buttonPressed()
}

class ExamplePresenterImpl: ExamplePresenter {
weak var view: ExampleView?
var interactor: ExampleInteractorInput?
func viewDidLoad() {
interactor?.fetchData()
}
func buttonPressed() {
interactor?.fetchData()
}
}

extension ExamplePresenterImpl: ExampleInteractorOutput {
func dataFetched(_ data: [String]) {
view?.displayData(data)
}
}

protocol ExampleDataService {
func fetchData(completion: @escaping ([String]) -> Void)
}

class ExampleDataServiceImpl: ExampleDataService {
func fetchData(completion: @escaping ([String]) -> Void) {
// Fetch the data and call the completion handler with the data
}
}

Steep Learning Curve

VIPER architecture can have a steep learning curve for developers who are new to iOS development. It requires an understanding of protocols,

protocol ExampleView: AnyObject {
func displayData(_ data: [String])
}

protocol ExampleInteractorInput {
func fetchData()
}

protocol ExampleInteractorOutput: AnyObject {
func dataFetched(_ data: [String])
}

protocol ExamplePresenter {
func viewDidLoad()
func buttonPressed()
}

protocol ExampleRouter {
func navigateToDetail()
}

class ExampleViewController: UIViewController, ExampleView {
var presenter: ExamplePresenter?
func displayData(_ data: [String]) {
// Update the UI with the data
}

@IBAction func buttonPressed(_ sender: UIButton) {
presenter?.buttonPressed()
}
}

class ExampleInteractor: ExampleInteractorInput {
var output: ExampleInteractorOutput?
var dataService: ExampleDataService?
func fetchData() {
dataService?.fetchData(completion: { [weak self] data in
self?.output?.dataFetched(data)
})
}
}

class ExamplePresenterImpl: ExamplePresenter {
weak var view: ExampleView?
var interactor: ExampleInteractorInput?
var router: ExampleRouter?
func viewDidLoad() {
interactor?.fetchData()
}

func buttonPressed() {
interactor?.fetchData()
router?.navigateToDetail()
}
}

extension ExamplePresenterImpl: ExampleInteractorOutput {
func dataFetched(_ data: [String]) {
view?.displayData(data)
}
}

class ExampleRouterImpl: ExampleRouter {
weak var viewController: UIViewController?
func navigateToDetail() {
let detailViewController = DetailViewController()
viewController?.navigationController?.pushViewController(detailViewController, animated: true)
}
}

protocol ExampleDataService {
func fetchData(completion: @escaping ([String]) -> Void)
}

class ExampleDataServiceImpl: ExampleDataService {
func fetchData(completion: @escaping ([String]) -> Void) {
// Fetch the data and call the completion handler with the data
}
}

Conclusion

In conclusion, VIPER architecture provides a clear and defined structure that makes it easier to build complex iOS applications. It encourages modularity, testability, and separation of concerns, making the codebase more maintainable, scalable, and easier to work with. Additionally, it’s ideal for large development teams as it reduces the risk of conflicts and encourages collaboration.

However, VIPER architecture can be complex and time-consuming to set up, which can be challenging for small or simple applications. The use of protocols can also be challenging for developers who are new to iOS development or are unfamiliar with object-oriented programming. Furthermore, VIPER architecture can be over-engineered for small or simple applications, making it unnecessary.

Despite these drawbacks, VIPER architecture remains a popular choice for building complex and scalable iOS applications. By carefully considering the requirements and complexity of their application, developers can choose a design pattern that best suits their needs.

--

--

Aaron Cleveland
Geek Culture

iOS Developer @HomeDepot | Father | Inspiring Game Developer