Getting Started With the VIP Clean Architecture Pattern

In this tutorial, you’ll learn how to utilize the VIP clean architecture pattern to develop apps for Apple platforms while building a SwiftUI for ordering an ice cream. By Danijela Vrzan.

4.3 (12) · 9 Reviews

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Creating a Display Logic Protocol

Open CreateIceCreamView.swift and add the following above the extension:

protocol CreateIceCreamDisplayLogic {
  func displayIceCream(viewModel: CreateIceCream.LoadIceCream.ViewModel)
}

This creates the display logic protocol and defines displayIceCream(viewModel:).

Next, replace extension CreateIceCreamView { with:

extension CreateIceCreamView: CreateIceCreamDisplayLogic {

The compiler will raise an error saying your view doesn’t conform to protocol and ask if you want to add the protocol stubs. Click Fix to add the method at the top of the extension.

Finally, replace code with:

iceCream.displayedCones = viewModel.cones
iceCream.displayedFlavors = viewModel.flavors
iceCream.displayedToppings = viewModel.toppings

This adds the data from the ViewModel to the iceCream @ObservedObject that’s used to update the UI.

You’ve added all components. It’s about time you get rewarded for your hard work.

Build and run the project.

Animated gif showing Scoops&Scones app running in the simulator without showing any data

But there’s nothing in there. Where’s your ice cream?

Image of a confused yeti holding a white paper with a big question mark on it

Worry not! You’ve created all the components, but you need to create instances of the presenter and interactor and connect them using a Configurator.

Adding a Configurator

The configurator’s job is to instantiate and connect the components of the VIP cycle. This is where you create the unidirectional cycle between the VIP components. There’s only one configurator for every scene and you need to call it only once, so you’ll create it in a separate file.

The view loads when the app starts, but you need to create presenter and interactor instances manually.

Create a Swift File named CreateIceCreamConfigurator.swift in the CreateIceCream group.

The first step is to replace import Foundation with:

import SwiftUI

Because you’ll reference SwiftUI’s View, you need to import SwiftUI to the file.

Next, add the following extension to the file:

extension CreateIceCreamView {
  func configureView() -> some View {
    var view = self
    let interactor = CreateIceCreamInteractor()
    let presenter = CreateIceCreamPresenter()
    view.interactor = interactor
    interactor.presenter = presenter
    presenter.view = view

    return view
  }
}

In the code above, you create an extension of CreateIceCreamView with a single method configureView() that returns some View.

configureView() creates instances of the interactor and presenter and assigns the corresponding references.

Now, all you have to do is call the function on the view.

Open ContentView.swift and replace CreateIceCreamView() with:

CreateIceCreamView().configureView()

Now, you can finally see your app in action. Build and run.

Animated gif showing working Scoops&Scones app in the simulator

Good work! Your app should be up and running, and you can finally make some ice cream.

Happy iPhone caricature raising its hands in the air with medals around it

In the next part of the tutorial, you’ll focus on adding simple unit tests.

Unit Testing

One of the advantages of the logic separation in VIP is better testability. You’ll see how clear separation of logic and shorter methods help you write better unit tests.

Before you begin writing tests, you need to import the Unit Test Case Classes that are already created for you. In your project’s starter folder in Finder, open Test Classes and you’ll see three files:

Finder window showing test classes folder in the projects starter folder with three unit test case class files

Select and drag them to the IceCream group in Xcode under ScoopsAndSconesTests. Make sure Copy items if needed is checked and all other settings are as follows:

Xcode window showing the settings needed to import external files into the project

Click Finish and you should have the files added to your project in Xcode:

Project files in Xcode showing the three files that were added in the previous step

These are the Unit Test Case Classes for your view, the presenter and the interactor. They contain the basic setup code so you can focus on writing the unit tests. There’s also a Seeds file added under ScoopsAndSconesTests that contains the mock data you’ll use for testing.

First, you’ll test your view’s display logic.

Testing Display Logic in View

Open CreateIceCreamViewTests. setUpWithError() and tearDownWithError() are already set up.

The System under test (sut) is CreateIceCreamView. The view sends requests to the interactor. You want to isolate the component’s dependency by creating the interactor spy test double. It’s going to conform to the CreateIceCreamBusinessLogic protocol so you can test the defined methods.

NOTE: A Spy is a specific type of mock object, or test double, used in testing. It’s used to record the output or effect produced by the system under test so you can verify it behaves as you’d expect. Martin Fowler describes other test double types.

Under //MARK: - Test doubles, replace CreateIceCreamInteractorSpy {} with the following:

class CreateIceCreamInteractorSpy: CreateIceCreamBusinessLogic {
  var loadIceCreamCalled = false

  func loadIceCream(request: CreateIceCream.LoadIceCream.Request) {
    loadIceCreamCalled = true
  }
}

This creates the interactor spy test double and conforms it to the CreateIceCreamBusinessLogic protocol. loadIceCreamCalled is your method’s call expectation, and its initial state is false. After loadIceCream(request:) gets called, the expectation is set to true.

You’ll add one unit test to test if loadIceCream(request:) gets called when the view appears.

Add the following code below //Mark: - Tests:

func testShouldLoadIceCreamOnViewAppear() {
  // Given
  sut.interactor = interactorSpy
  // When
  sut.fetchIceCream()
  // Then
  XCTAssertTrue(
    interactorSpy.loadIceCreamCalled,
    "fetchIceCream() should ask the interactor to load the ice cream"
  )
}

It’s a good practice to separate your tests into “given”, “when” and “then” sections, and VIP makes it easier to follow that practice:

  • Given: First, you assign the interactor spy to your interactor.
  • When: Then, you execute the code you’re testing. Call fetchIceCream().
  • Then: And finally, you assert the expectation with a message that shows if the test fails. In this case, you assert the loadIceCreamCalled expectation to be true when fetchIceCream() gets called.

This test might look redundant, but it’s good to test whether your methods are being called.

You won’t add more tests for the view in this tutorial because that would make it too long, but you’re more than welcome to add them yourself.

Run the test suite with Command-U. Your test should pass. If it fails, go back to your view and make sure you called fetchIceCream() in onAppear(perform:) and run your tests again.

Now, you’ll test the business logic in your interactor.

Testing Business Logic in the Interactor

The system under test is CreateIceCreamInteractor. You’ll write one test to see if loadIceCream(request:) sends the same data it loaded from the json to the presenter.

You’ll add the test double. But, in this case, it’ll be the presenter spy that conforms to the CreateIceCreamPresentationLogic protocol.

Open CreateIceCreamInteractorTests. setUpWithError() and tearDownWithError() are already set up.

Under //MARK: - Test doubles, replace CreateIceCreamPresenterSpy {} with:

class CreateIceCreamPresenterSpy: CreateIceCreamPresentationLogic {
  var iceCream: IceCream?
  var presentIceCreamCalled = false

  func presentIceCream(response: CreateIceCream.LoadIceCream.Response) {
    presentIceCreamCalled = true
    iceCream = response.iceCreamData
  }
}

This creates the presenter spy test double and conforms it to the CreateIceCreamPresentationLogic protocol. presentIceCreamCalled is your method’s call expectation, and its initial state is false. After presentIceCream(request:) gets called, the expectation is set to true.

You also define iceCream data and populate it with the data sent from the interactor.

Add the following code below //Mark: - Tests:

func testLoadIceCreamCallsPresenterToPresentIceCream() {
  // Given
  sut.presenter = presenterSpy
  let iceCream = Seeds.iceCream
  // When
  let request = CreateIceCream.LoadIceCream.Request()
  sut.loadIceCream(request: request)
  // Then
  XCTAssertEqual(
    presenterSpy.iceCream,
    iceCream,
    "loadIceCream(request:) should ask the presenter to present the same ice cream data it loaded"
  )
}

Here’s what’s happening in the code above:

  • Given: In this section, you assign the presenter spy to your presenter and create iceCream data using the seed data.
  • When: Here, you create a request and execute loadIceCream(request:).
  • Then: Finally, you assert that data decoded from the json is the same data you’re sending to the presenter.

Run the test suite with Command-U. Your tests should all pass.

You’ve learned how to set up and write a few simple unit tests for the view and the interactor. Try setting up the presenter and testing the presentation logic as a challenge.

You’ve seen how VIP architecture helps you write readable code and how it separates your business and presentation logic from the view.

But most of your apps have more than one view, so what about navigation?