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.
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 VIPER Architecture Pattern
30 mins
- Getting Started
- What is VIPER?
- Comparing Architectures
- Defining an Entity
- Adding an Interactor
- Setting Up the Presenter
- Building a View
- Creating a View with a Presenter
- Modifying the Model from the View
- Seeing It In Action
- Deleting a Trip
- Routing to the Detail View
- Setting Up the Trip Detail Screens
- Routing
- Finishing Up the Detail View
- Using a Second Presenter for the Map
- Considering the Map View
- Editing Waypoints
- Making Modules
- Where to Go From Here?
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.
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.
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.
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!
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.
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!”