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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Getting Started With the VIP Clean Architecture Pattern
30 mins
- Getting Started
- What Is VIP?
- VIP vs VIPER
- Structuring Your Files in Xcode
- Building Your App Logic
- Model in VIP
- Creating a Model
- Setting Up the View
- Setting Up the Interactor
- Setting Up the Presenter
- Creating a Display Logic Protocol
- Adding a Configurator
- Unit Testing
- Testing Display Logic in View
- Testing Business Logic in the Interactor
- Implementing Navigation Using Router
- Where to Go From Here?
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.
But there’s nothing in there. Where’s your ice cream?
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.
Good work! Your app should be up and running, and you can finally make some ice cream.
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:
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:
Click Finish and you should have the files added to your project in Xcode:
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.
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
whenfetchIceCream()
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?