VIP design pattern (or Clean Swift)

Artun Erol
6 min readJun 29, 2022

--

VIP design pattern (or Clean Swift) was first mentioned by Raymond Law, whose ultimate goal is to write clean and testable code. We can safely say that Raymond successfully found a way to implement Uncle Bob’s Clean Architecture to Swift.

There are 3 main components in the VIP Design model: ViewController, Interactor and Presenter. As seen in the picture below, they communicate with each other in a unidirectional manner.

Let’s dive into the components.

Every component has different tasks and variables but all components have references for their output.

Also in order to conform SOLID’s Dependency inversion principle, every output, viewController, or router reference should be in the type of related protocols.

An example of Dependency Inversion is as below:

Presenter class holds a reference of “PresenterOutput” which is a protocol that has been bounded to typealias called ViewControllerInput. That means that ViewControllerInput is the new name for “PresenterOutput” protocol.

Then this protocol has been conformed by ViewController. Basically, class holding reference of a protocol that has been conformed by another class is called Dependency Inversion and this concept is pretty useful for VIP design pattern.

protocol PresenterOutput {
func displayResult(with model: ViewModel)
}
class Presenter {
var output: PresenterOutput!
}
typealias ViewControllerInput = PresenterOutputclass ViewController: ViewControllerInput {
var output: ViewControllerOutput!
}

Main Components:

ViewController

  • Listening to UI Events
  • Passing view models to related Views
  • Don’t care about the logic. Only gets the data and displays it using Views.
  • Should hold a reference to a router in order to navigate to another ViewController
  • Uses Configurator to set up the VIP Chain. (There are no configurator in VIPER)

Interactor

  • Holds most of the logic
  • Fetching and networking can be done here by using workers
  • Getting some results for the Presenter

Presenter

  • Getting the responses of interactor and organizing them to create View Models.
  • It’s output should lead those View Models back to View Controller.

Helpers:

Router

  • Holds a weak reference of viewController which uses that router
  • When user wants to navigate to next ViewController, navigation function of Router should trigger.
  • Passing data to other ViewController should also be done by Router. Ex: navigateNextVC(with data: Data)
  • Should hold the viewController as weak variable(or can be done by routing configuration)

Worker

  • Basically a class which mostly has functions that do some specific tasks.
  • You can break down tasks into many workers.
  • Each worker ideally should do one work at a time. (Ex: Fetching data from a specific endpoint)
  • Each worker can be used elsewhere.

Configurator

  • Initiates the VIP Cycle

In my very simple example project, the user enters a value in textFields, when the user didFinishEditing, a network request initiates and checks the values. If these values are the same as the values of the network response, they are displayed in the labels.

Also, the user can navigate to a viewController named TargetViewController by pressing one of the 3 color buttons. The background of TargetViewController will change with the color of the button name.

In order to make more sense, I have a mock json data as below. Name and surname texfield inputs are checked and compared with those values.

{
"name": "James",
"surname": "Hetfield"
}

Deep Dive:

ViewController

Created the ViewController with related IBoutlets. As we mentioned earlier, every component of VIP should conform to their Input protocol and should have a reference for their output.

In ViewController case; ViewControllerInput is PresenterOutput. So presenter is passing data (or viewModel) to ViewController.

ViewControllerOutput has a function called checkIfTextValid. As you can guess it out, ViewControllerOutput is Interactor’s input so that interactor will conform to that protocol.

ViewController also has a reference for router, so that we can navigate to other ViewControllers.

import UIKittypealias ViewControllerInput = PresenterOutputenum TextFieldType {
case nameTextField
case surnameTextField
}
// MARK: - ViewController Outputprotocol ViewControllerOutput {
func checkIfTextValid(text: String, for textFieldType: TextFieldType)
}
// MARK: - ViewControllerclass ViewController: UIViewController, ViewControllerInput {
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var surnameLabel: UILabel!

@IBOutlet weak var blueButton: UIButton!
@IBOutlet weak var redButton: UIButton!
@IBOutlet weak var yellowButton: UIButton!

@IBOutlet weak var nameTextField: UITextField!
@IBOutlet weak var surnameTextField: UITextField!

var router: Router?
var output: ViewControllerOutput!

// MARK: - Life Cycle

override func viewDidLoad() {
super.viewDidLoad()
Configurator.shared.configure(viewController: self)
nameTextField.delegate = self
surnameTextField.delegate = self
}

We also call Configurator on viewDidLoad so that it initializes VIP cycle before the views load. Will come back to Configurator.

We have called displayResult function, which is a function that comes from the conformed protocol “ViewControllerInput”.

This function is used by presenter(aka viewController’s output) to pass ViewModel to ViewController and present related text on labels.

func displayResult(with model: ViewModel) {
if model.name == "" {
self.surnameLabel.text = model.surname
}

else {
self.nameLabel.text = model.name
}
}

IBActions are basically navigating to TargetViewController(using router) while also passing backgroundColors to it.

// MARK: - Buttons
@IBAction func blueButtonPressed(_ sender: Any) {
router?.pushToTargetViewController()
router?.passBackgroundColor(color: .blue)
}
@IBAction func redButtonPressed(_ sender: Any) {
router?.pushToTargetViewController()
router?.passBackgroundColor(color: .red)
}
@IBAction func yellowButtonPressed(_ sender: Any) {
router?.pushToTargetViewController()
router?.passBackgroundColor(color: .yellow)
}

When the user did end editing, the extension below is checking if the written text is valid(interactor is responsible for this validation).

// MARK: - Extensionextension ViewController: UITextFieldDelegate {
func textFieldDidEndEditing(_ textField: UITextField) {
if textField == nameTextField {
self.output.checkIfTextValid(text: textField.text?.uppercased() ?? "", for: .nameTextField)
}
else { //Case where textField == surnameTextField
self.output.checkIfTextValid(text: textField.text?.uppercased() ?? "", for: .surnameTextField)
}
}
}

Interactor

The Interactor is responsible for managing the logic using “Workers”. I will not re-explain the Output reference and the Conformance of the Input protocol. In addition to the viewController, the Interactor has a reference to a corresponding worker. In the example here it is NetworkWorkerLogic.

checkIfTextValid function is triggered when user didEndEditing, and the operation of that function is done in Interactor as below. Interactor request a response from the network using NetworkWorker (in our case it requests a mock json data), then checks if the text written in textfield is equal to the response of the data.

After getting a result from this validation, Interactor passes the result to presenter

typealias InteractorInput = ViewControllerOutputprotocol InteractorOutput {
func nameResult(result: String)
func surnameResult(result: String)
}
// MARK: -class Interactor {
var output: InteractorOutput?
var networkWorker: NetworkWorkerLogic?
}
// MARK: -extension Interactor: InteractorInput {
func checkIfTextValid(text: String, for textFieldType: TextFieldType) {
networkWorker = NetworkWorker()
networkWorker!
.decodeJSON(
completion: { result in
switch textFieldType {

case .nameTextField:
if result.name.uppercased() == text.uppercased() {
self.output?.nameResult(result: result.name.uppercased())
}
else {
self.output?.nameResult(result: "Please enter a valid Name")
}

case .surnameTextField:
if result.surname.uppercased() == text.uppercased() {
self.output?.surnameResult(result: result.surname.uppercased())
}
else {
self.output?.surnameResult(result: "Please enter a valid Surname")
}
}
})
}
}

Presenter

After presenter gets the result of the interactor, it converts it to a ViewModel and passes that to ViewController again using the output protocol’s function.

import Foundationtypealias PresenterInput = InteractorOutputprotocol PresenterOutput {
func displayResult(with model: ViewModel)
}
class Presenter: PresenterInput {
var output: PresenterOutput?
func nameResult(result: String) {
let viewModel = ViewModel(name: result, surname: "")
output?.displayResult(with: viewModel)
}

func surnameResult(result: String) {
let viewModel = ViewModel(name: "", surname: result)
output?.displayResult(with: viewModel)
}
}

displayResult function was conformed by ViewController as below: Here we get the model from the presenter and use it to display on Labels.

func displayResult(with model: ViewModel) {
if model.name == "" {
self.surnameLabel.text = model.surname
}

else {
self.nameLabel.text = model.name
}
}

Those were the main components of VIP cycle. Now jump into helpers.

Worker

NetworkWorker is just fetching the data from the mock json file.

typealias DecodedJSONClosure = ((JSONModel) -> Void)protocol NetworkWorkerLogic {
func decodeJSON(completion: @escaping DecodedJSONClosure)
}
class NetworkWorker: NetworkWorkerLogic {

func decodeJSON(completion: @escaping DecodedJSONClosure) {
guard let path = Bundle.main.path(forResource: "jsonData", ofType: "json") else { return }
let url = URL(fileURLWithPath: path)
let decoder = JSONDecoder()

do {
let data = try Data(contentsOf: url)
let decodedData = try decoder.decode(JSONModel.self, from: data)
completion(decodedData)
}
catch {
print("Can not decode the JSON")
}
}
}

Router

Router holds weak references for both the initial view controller and the target view controller.

protocol RouterInput {
func pushToTargetViewController()
}
class Router: RouterInput {
weak var viewController: ViewController!
weak var targetViewController: TargetViewController!

func pushToTargetViewController() {
let targetViewController = TargetViewController(nibName: TargetViewController.getNibName(), bundle: nil)
self.targetViewController = targetViewController
viewController.navigationController?.pushViewController(targetViewController, animated: true)
}

func passBackgroundColor(color: UIColor) {
targetViewController.backgroundColor = color
}
}

Configurator

Configurator initiates the VIP cycle by initializing the output and router references.

class Configurator {
static let shared = Configurator()

func configure(viewController: ViewController) {
let interactor = Interactor()
let presenter = Presenter()
let router = Router()

viewController.output = interactor
interactor.output = presenter
presenter.output = viewController

viewController.router = router
router.viewController = viewController
}
}

Configurator is called on viewDidLoad of ViewController in our case.

override func viewDidLoad() {
super.viewDidLoad()
Configurator.shared.configure(viewController: self)
}

Thanks for reading.

Raymond Law’s blog: https://clean-swift.com/clean-swift-ios-architecture/

--

--