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.

See course reviews 4.4 (10) · 7 Reviews

Download materials
Save for later
Share

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.

The VIP architectural pattern is one of the least known patterns in iOS development compared to MVVM, MVC or VIPER. You’re reading that correctly; VIP isn’t VIPER.

They’re different implementations of the Uncle Bob’s Clean Architecture pattern.

VIPER’s creators wanted to name it VIP. But they didn’t want developers to read it as Very Important Architecture, so they named it VIPER instead.

What does VIP stand for then? ViewInteractorPresenter.

The VIP architecture for writing clean Swift code was introduced by Raymond Law. He created a clean-swift website where you can read about how to use VIP pattern in UIKit.

In this tutorial, you’ll build an ice cream maker app called Scoops&Scones using the VIP architecture in SwiftUI. The UI is already set up, so you can focus on building the logic and making the app work.

By the end of this tutorial, your app should look like this:

Animated gif showing working Scoops&Scones app in the simulator

Follow along and you’ll start making ice cream in no time!

NOTE: You’ll write unit tests when you finish building the app so you should be familiar with unit testing. If you want to brush up before you begin, check out the iOS Unit Testing and UI Testing Tutorial.

Getting Started

Download the project materials by clicking Download Materials at the top or bottom of the tutorial. Open the project inside the starter folder. Then, build and run.

Scoops&Scones app showing a UI to choose a cone or cup, flavor of ice cream and a topping. There's a disabled DONE button and a text field with the word Preparing

There’s not much you can do with the app at the moment. Tapping “Select a cone or cup” navigates to an empty view, and the other fields are inactive until you choose one. You’ll add the necessary logic and start making ice cream by the end of this tutorial. :]

In Xcode, look at the files already set up:

  • icecream.json contains your app’s data.
  • ContentView launches your app’s main view CreateIceCreamView.
  • In the Models group, you have two files: IceCream will hold the data parsed from the json and IceCreamDataStore will hold the data for displaying on the view.
  • CreateIceCreamView is your app’s main View, and it’s where the VIP cycle starts.
  • The Extensions and UI Components groups contain helper files. You won’t be directly working with those files.

Before you start making ice cream, you’ll learn more about the VIP pattern.

What Is VIP?

VIP is an architectural pattern like MVC, MVP or MVVM. Its main difference is the clear logic separation between its components. VIP was created to fix MVC, or Massive View Controllers, a common problem in UIKit apps where View Controllers become the main object where code is kept and they become Massive in that most if not all of your logic is kept within them. This can also cause problems when testing your code.

VIPER already existed, but its implementation was a bit too complex and didn’t allow you to use segues.

The VIP pattern is a unidirectional architectural pattern. You might have already heard of some others, such as Redux, Flux or Model-View-Intent (MVI). These patterns focus on reactive UIs and state management.

Unidirectional patterns share one property: Their components are all interconnected and aren’t designed to mix. Each has its own clear responsibility.

Look at this diagram:

Diagram showing VIP architecture's unidirectional cycle that goes from View to Interactor, Interactor to Presenter and from Presenter back to View

Each letter in VIP stands for one component: View, Interactor and Presenter.

  • The View is your app’s UI. This corresponds to a SwiftUI View. It sends requests to the interactor every time the user interacts with the UI.
  • The Interactor handles your app’s business logic, such as fetching data from the network or saving data in a database. When the view requests something, the interactor does the processing and sends the result as a response to the presenter.
  • The Presenter handles the presentation logic. It formats the received data into view models suitable for display and then passes it to the view.

You’ll see how following this approach leads your code to fall into place logically. Each component has a single responsibility, and there’s no confusion regarding what goes where.

You might even know where to look when you review your code six months later. :]

VIP vs VIPER

VIP and VIPER have the same basic components, but the data flow is different. Although VIP follows a unidirectional approach, VIPER has a bidirectional flow that starts with the presenter.

Two diagrams side by side showing the flow of data of VIP and VIPER patterns to highlight their differences

In VIPER, the presenter directs data between the view and the interactor. The view and interactor don’t talk with each other.

NOTE: To learn more about VIPER architecture check out Getting Started with the VIPER Architecture Pattern tutorial.

Now that you know how VIP works, you’ll put that knowledge into practice and build an app. But before you start writing any code, look at how to structure your files and groups in Xcode.

Code organization is one of the features of VIP architecture.

Structuring Your Files in Xcode

In Xcode, look at your project’s structure:

ScoopsAndScones project's group structure in Xcode, highlighting the project files in a purple rectangle

ScoopsAndScones is the root group of your project. Inside, you have a json file, AppMain, ContentView, other resources and a Domain group:

ScoopsAndScones project's group structure in Xcode, highlighting the project files in a purple rectangle and domain group in blue box

Domain contains all your app’s domains or use-cases. In this case, IceCream is the only domain you have. If you wanted to expand the app by adding Cookies, for example, it would become another domain of your app.

The IceCream group comprises Models and Scenes:

ScoopsAndScones project's group structure in Xcode, highlighting the project files in a purple rectangle, domain group in a blue box, the IceCream domain in a red box, Models group in a pink box and the IceCream scene in a green box

Models contains your domain’s model that you use across the scenes and isn’t tied to any specific scene. Each scene will have its scene-specific model that you’ll add later.

Scenes contains all the scenes for your domain. A scene normally is a single screen or collection of screens for a feature. ScoopsAndScones has a single scene called CreateIceCream.

Now it’s time to start building your app’s logic.

Building Your App Logic

As already mentioned, your app has a single scene called CreateIceCream. You’ll start building your app logic by adding Model, View, Interactor and Presenter. After you’ve added the components, you’ll see your app in action.

The first step is to learn about and create the data model.

Model in VIP

Data Models in VIP are decoupled data abstractions. They pass the data between components and consist of only primitive types such as Int, Double or String.

You could create Struct, Class or Enum, but if the business logic changes, the underlying data models change. Using primitive types makes it easier to change the components without needing to update the entire codebase.

Look at this diagram:

Diagram of VIP pattern showing the request, response and view model data models between the components

The typical user interaction goes like this: The cycle starts in the view when the user taps a button in the app’s UI.

The view creates a Request object and sends it to the interactor. The interactor takes the request object, performs work and sends the result as a Response to the presenter. The presenter then takes the response, formats the data into primitive types and sends the result as a ViewModel back to the view.

Then, finally, the view displays results to the user.

These three payloads make the data model:

  • Request
  • Response
  • ViewModel

Next, you’ll create your data models.

Creating a Model

In Xcode, create a Swift File named CreateIceCreamModel.swift in the CreateIceCream group.

Add the following code to the file:

enum CreateIceCream {
  enum LoadIceCream {
    struct Request {}

    struct Response {
      var iceCreamData: IceCream
    }

    struct ViewModel {
      var cones: [String]
      var flavors: [String]
      var toppings: [String]
    }
  }
}

Here, you create Request, Response and ViewModel data models.

In this case, your request is empty. The view knows it needs to request something but doesn’t know the type of data. When the interactor receives the request, it loads the data and sends IceCream as a response. When the presenter receives the response, it formats the data for the view model and sends it to the view as an Array of strings.

LoadIceCream wraps the data model and represents a single functionality of your code, loading the ice cream and showing it in the view.

If the app had extra logic such as saving or deleting the data, you’d create separate enums called SaveIceCream and DeleteIceCream. Both of them would then have their own data models.

The advantage of nesting your data models like this is readability. For instance, if you wanted to create a request for LoadIceCream, you’d do it like this:

let request = CreateIceCream.LoadIceCream.Request()

It’s easy to understand which scene and functionality this request belongs to.

Next, you’ll set up the view.

Setting Up the View

The view is already created for you and contains your app’s UI. You’re going to add the display logic and call the interactor to load the data.

First, open CreateIceCreamView.swift. At the top of the file is an extension with a helper method for displaying the image. You’ll implement the display logic methods in the extension.

But before you do that, your view needs to know about the interactor.

At the top of CreateIceCreamView, replace // TODO: Add interactor with:

var interactor: CreateIceCreamBusinessLogic?

This links the interactor with the view through the CreateIceCreamBusinessLogic protocol.

Components communicate with each other through protocols. This makes each component of a scene less tightly tied to other components. The view asks the interactor to perform the business logic but doesn’t know how it’s done.

The compiler will complain it cannot find the type in scope. Don’t worry; you’ll fix this in the following section, where you’ll create the protocol in the interactor. Having the protocol in the same file with the component is a typical feature of using the VIP pattern.

Next, replace // TODO: Call interactor to fetch data with:

func fetchIceCream() {
  let request = CreateIceCream.LoadIceCream.Request()
  interactor?.loadIceCream(request: request)
}

fetchIceCream() creates a request and calls the interactor’s loadIceCream(request:), passing the request inside.

It might seem redundant to create and pass an empty request. You’ll create it in this example to understand how passing the data works between components. But it’s a good idea to have it in case your logic changes.

Finally, scroll down to the end of the file and, after navigationTitle("Scoops&Scones"), add:

.onAppear {
    fetchIceCream()
}

Now, when your app’s view appears, it’ll call the added fetchIceCream().

Next, you’ll set up the interactor.

Setting Up the Interactor

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

Add the following to the file:

protocol CreateIceCreamBusinessLogic {
  func loadIceCream(request: CreateIceCream.LoadIceCream.Request)
}

class CreateIceCreamInteractor {
  var presenter: CreateIceCreamPresentationLogic?
}

You’ve fixed the previous compiler error, but now it’s complaining again. You’ll create a presenter in the following section that fixes the error.

In the code above, you create a new CreateIceCreamInteractor class to handle the scene’s business logic. Then, you define the presenter so the interactor can pass the response to it.

As already mentioned, protocols allow you to decouple your components. The interactor passes the response to the presenter but doesn’t know who’s presenting the data or how.

Next, add the following extension to the bottom of the file:

extension CreateIceCreamInteractor: CreateIceCreamBusinessLogic {
  func loadIceCream(request: CreateIceCream.LoadIceCream.Request) {
    // TODO
  }
}

CreateIceCreamInteractor conforms to CreateIceCreamBusinessLogic protocol and implements loadIceCream(request:).

Next, replace // TODO with:

let iceCream = Bundle.main.decode(IceCream.self, from: "icecream.json")
let response = CreateIceCream.LoadIceCream.Response(iceCreamData: iceCream)
presenter?.presentIceCream(response: response)

In the code above, you decode the json file into IceCream domain model and store it inside iceCream.

NOTE: Bundle.decode(_:from:) is an extension of Bundle. You can find it in the project’s Extensions group.

You create a new response with the decoded IceCream data. Then, you call the presenter’s presentIceCream(response:) and pass the response to the presenter.

The interactor is your app’s brain and handles all the business logic such as loading, deleting or saving the data. But there’s another component you could add to the interactor, called Worker.

Look at this diagram:

Diagram showing the VIP pattern cycle and how workers communicate with the interactor bidirectionally

You can have multiple workers for the interactor, with each handling a specific logic. If your app fetched the data from an API, you’d create a NetworkWorker and have all the networking logic inside. If your app saved the data using CoreData, you’d add a CoreDataWorker and so on.

Now, you need to create and set up the presenter, the final component.

Setting Up the Presenter

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

Add the following code to the file:

protocol CreateIceCreamPresentationLogic {
  func presentIceCream(response: CreateIceCream.LoadIceCream.Response)
}

class CreateIceCreamPresenter {
  var view: CreateIceCreamDisplayLogic?
}

The compiler will complain once more about the unknown type in scope. You’ll finally fix all the errors in the following section, where you’ll create the CreateIceCreamDisplayLogic protocol inside the view.

In the code above, you create a new CreateIceCreamPresenter class to handle the scene’s presentation logic. Then, you define the view so the presenter can format the data from the interactor and pass it to the view.

Next, add the following extension at the bottom of the file:

extension CreateIceCreamPresenter: CreateIceCreamPresentationLogic {
  func presentIceCream(response: CreateIceCream.LoadIceCream.Response) {
    // TODO
  }
}

The presenter conforms to CreateIceCreamPresentationLogic protocol and implements presentIceCream(response:).

Next, replace // TODO with:

let viewModel = CreateIceCream.LoadIceCream.ViewModel(
  cones: response.iceCreamData.cones,
  flavors: response.iceCreamData.flavors,
  toppings: response.iceCreamData.toppings
)
view?.displayIceCream(viewModel: viewModel)

The presenter formats the response into three arrays of strings and passes it to the ViewModel. The view model is then passed to displayIceCream(viewModel:) on the view to display the formatted data.

Now, you need to add the display logic protocol to your view.

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?

Implementing Navigation Using Router

In UIKit, you use segues to navigate and pass data between views. But, in SwiftUI, you use NavigationLink inside the View.

Do you see an issue here? The view ends up handling both the navigation and data passing logic. This isn’t the way of clean architecture.

Check out this diagram:

Diagram of VIP pattern with added router component that talks to the view in a bidirectional way

You could add Router to extract the navigation logic out of the View. It seems a bit counterintuitive when it comes to SwiftUI, but it’s doable.

The router component has two protocols: RoutingLogic and DataStore.

RoutingLogic contains navigation methods. If your view navigates to multiple views, the router handles all.

DataStore contains the data you need to pass to the destination view. You create it inside the interactor. That way, the router can get the data it needs to pass to another view and doesn’t know about the interactor.

NOTE: If you’d like to learn more about the router, check the Routing to the Detail View section of Getting Started with the VIPER Architecture Pattern tutorial.

Where to Go From Here?

You can download the completed project files by clicking Download Materials at the bottom or top of this tutorial.

VIP was created as a solution to massive view controllers in UIKit. It’s meant to make building larger apps and adding new features easier on long-term projects. All you need to do is add a new scene, create your components and start adding the logic.

You’ve seen how it forces you to write shorter methods with single responsibility, making your code easier to unit test. But the architecture relies on protocols with often-complicated naming and responsibilities.

It’s hard to maintain the separation between view and presenter. Sometimes, the presenter calls the view without preparing the UI and can feel useless and create boilerplate code.

But like every other architecture out there, it has its advantages and disadvantages. It’s up to the developer to decide what architecture is best for the project and the team.

To learn more about MVVM, the most popular architecture for SwiftUI apps, check out MVVM with Combine Tutorial for iOS.

If you want to learn more about implementing modern clean architectures in your iOS apps, see the Advanced iOS App Architecture book and take your skills to the next level.

We hope you enjoyed this tutorial. If you have any questions or comments, feel free to drop them in the discussion below.