Getting Started with the VIPER Architecture Pattern

In this tutorial, you’ll learn about using the VIPER architecture pattern with SwiftUI and Combine, while building an iOS app that lets users create road trips. By Michael Katz.

4.6 (42) · 1 Review

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

Setting Up the Presenter

Now, create a new Swift File named TripListPresenter.swift. This will be for the presenter class. The presenter cares about providing data to the UI and mediating user actions.

Add this code to the file:

import SwiftUI
import Combine

class TripListPresenter: ObservableObject {
  private let interactor: TripListInteractor

  init(interactor: TripListInteractor) {
    self.interactor = interactor
  }
}

This creates a presenter class that has reference to the interactor.

Since it’s the presenter’s job to fill the view with data, you want to expose the list of trips from the data model.

Add a new variable to the class:

@Published var trips: [Trip] = []

This is the list of trips the user will see in the view. By declaring it with the @Published property wrapper, the view will be able to listen to changes to the property and update itself automatically.

The next step is to synchronize this list with the data model from the interactor. First, add the following helper property:

private var cancellables = Set<AnyCancellable>()

This set is a place to store Combine subscriptions so their lifetime is tied to the class’s. That way, any subscriptions will stay active as long as the presenter is around.

Add the following code to the end of init(interactor:):

interactor.model.$trips
  .assign(to: \.trips, on: self)
  .store(in: &cancellables)

interactor.model.$trips creates a publisher that tracks changes to the data model’s trips collection. Its values are assigned to this class’s own trips collection, creating a link that keeps the presenter’s trips updated when the data model changes.

Finally, this subscription is stored in cancellables so you can clean it up later.

Building a View

You now need to build out the first View: the trip list view.

Creating a View with a Presenter

Create a new file from the SwiftUI View template and name it TripListView.swift.

Add the following property to TripListView:

@ObservedObject var presenter: TripListPresenter

This links the presenter to the view. Next, fix the previews by changing the body of TripListView_Previews.previews to:

let model = DataModel.sample
let interactor = TripListInteractor(model: model)
let presenter = TripListPresenter(interactor: interactor)
return TripListView(presenter: presenter)

Now, replace the content of TripListView.body with:

List {
  ForEach (presenter.trips, id: \.id) { item in
    TripListCell(trip: item)
      .frame(height: 240)
  }
}

This creates a List where the presenter’s trips are enumerated, and it generates a pre-supplied TripListCell for each.

Preview window of the trip list view

Modifying the Model from the View

So far, you’ve seen data flow from the entity to the interactor through the presenter to populate the view. The VIPER pattern is even more useful when sending user actions back down to manipulate the data model.

To see that, you’ll add a button to create a new trip.

First, add the following to the class in TripListInteractor.swift:

func addNewTrip() {
  model.pushNewTrip()
}

This wraps the model’s pushNewTrip(), which creates a new Trip at the top of the trips list.

Then, in TripListPresenter.swift, add this to the class:

func makeAddNewButton() -> some View {
  Button(action: addNewTrip) {
    Image(systemName: "plus")
  }
}

func addNewTrip() {
  interactor.addNewTrip()
}

This creates a button with the system + image with an action that calls addNewTrip(). This forwards the action to the interactor, which manipulates the data model.

Go back to TripListView.swift and add the following after the List closing brace:

.navigationBarTitle("Roadtrips", displayMode: .inline)
.navigationBarItems(trailing: presenter.makeAddNewButton())

This adds the button and a title to the navigation bar. Now modify the return in TripListView_Previews as follows:

return NavigationView {
  TripListView(presenter: presenter)
}

This allows you to see the navigation bar in preview mode.

Resume the live preview to see the button.

Trip List with Button in Live Preview

Seeing It In Action

Now’s a good time to go back and wire up TripListView to the rest of the application.

Open ContentView.swift. In the body of view, replace the VStack with:

TripListView(presenter:
  TripListPresenter(interactor:
    TripListInteractor(model: model)))

This creates the view along with its presenter and interactor. Now build and run.

Tapping the + button will add a New Trip to the list.

Trip List with a New Trip added

Deleting a Trip

Users who create trips will probably also want to be able to delete them in case they make a mistake or when the trip is over. Now that you’ve created the data path, adding additional actions to the screen is straightforward.

In TripListInteractor, add:

func deleteTrip(_ index: IndexSet) {
  model.trips.remove(atOffsets: index)
}

This removes items from the trips collection in the data model. Because it’s an @Published property, the UI will automatically update because of its subscription to the changes.

In TripListPresenter, add:

func deleteTrip(_ index: IndexSet) {
  interactor.deleteTrip(index)
}

This forwards the delete command on to the interactor.

Finally, in TripListView, add the following after the end brace of the ForEach:

.onDelete(perform: presenter.deleteTrip)

Adding an .onDelete to an item in a SwiftUI List automatically enables the swipe to delete behavior. The action is then sent to the presenter, kicking off the whole chain.

Build and run, and you’ll now be able to remove trips!

With onDelete, the Delete action is enabled.

Routing to the Detail View

Now’s the time to add in the Router part of VIPER.

A router will allow the user to navigate from the trip list view to the trip detail view. The trip detail view will show a list of the waypoints along with a map of the route.

The user will be able to edit the list of waypoints and the trip name from this screen.

Yay Router!

Setting Up the Trip Detail Screens

Before showing the detail screen, you’ll need to create it.

Following the previous example, create two new Swift Files: TripDetailPresenter.swift and TripDetailInteractor.swift and a SwiftUI View named TripDetailView.swift.

Set the contents of TripDetailInteractor to:

import Combine
import MapKit

class TripDetailInteractor {
  private let trip: Trip
  private let model: DataModel
  let mapInfoProvider: MapDataProvider

  private var cancellables = Set<AnyCancellable>()

  init (trip: Trip, model: DataModel, mapInfoProvider: MapDataProvider) {
    self.trip = trip
    self.mapInfoProvider = mapInfoProvider
    self.model = model
  }
}

This creates a new class for the interactor of the trip detail screen. This interacts with two data sources: an individual Trip and Map information from MapKit. There’s also a set for the cancellable subscriptions that you’ll add later.

Then, in TripDetailPresenter, set its contents to:

import SwiftUI
import Combine

class TripDetailPresenter: ObservableObject {
  private let interactor: TripDetailInteractor

  private var cancellables = Set<AnyCancellable>()

  init(interactor: TripDetailInteractor) {
    self.interactor = interactor
  }
}

This creates a stub presenter with a reference for interactor and cancellable set. You’ll build this out in a bit.

In TripDetailView, add the following property:

@ObservedObject var presenter: TripDetailPresenter

This adds a reference to the presenter in the view.

To get the previews building again, change that stub to:

static var previews: some View {
    let model = DataModel.sample
    let trip = model.trips[1]
    let mapProvider = RealMapDataProvider()
    let presenter = TripDetailPresenter(interactor:
      TripDetailInteractor(
        trip: trip,
        model: model,
        mapInfoProvider: mapProvider))
    return NavigationView {
      TripDetailView(presenter: presenter)
    }
  }

Now the view will build, but the preview is still just “Hello, World!”

Just the default view preview