VIPER offers an alternative to this scenario and can be used in conjunction with SwiftUI and Combine to help build apps with a clean architecture that effectively separates the different functions and responsibilities required, such as the user interface, business logic, data storage and networking. These are then easier to test, maintain and expand.
What is VIPER?
VIPER is an architectural pattern like MVC or MVVM, but it separates the code further by single responsibility. Apple-style MVC motivates developers to put all logic into a UIViewController subclass. VIPER, like MVVM before it, seeks to fix this problem.
Each of the letters in VIPER stand for a component of the architecture: View, Interactor, Presenter, Entity and Router.
The View is the user interface. This corresponds to a SwiftUI View.
The Interactor is a class that mediates between the presenter and the data. It takes direction from the presenter.
The Presenter is the “traffic cop” of the architecture, directing data between the view and interactor, taking user actions and calling to router to move the user between views.
An Entity represents application data.
The Router handles navigation between screens. That’s different than it is in SwiftUI, where the view shows any new views.
VIPER goes a step further by separating the view logic from the data model logic. Only the presenter talks to the view, and only the interactor talks to the model (entity). The presenter and interactor coordinate with each other. The presenter is concerned with display and user action, and the interactor is concerned with manipulating the data.
Defining an Entity
The fastest way to get something on screen is to start with the entity. The entity is the data object(s) for the project. In this case, the main entities are Trip, which contains a list of Waypoints, which are the stops in the trip.
Adding an Interactor
class TripListInteractor {
let model: DataModel
init (model: DataModel) {
self.model = model
}
}
Setting Up the Presenter
The presenter cares about providing data to the UI and mediating user actions.
import SwiftUI
import Combine
class TripListPresenter: ObservableObject {
private let interactor: TripListInteractor
init(interactor: TripListInteractor) {
self.interactor = 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.
Building a View
let model = DataModel.sample
let interactor = TripListInteractor(model: model)
let presenter = TripListPresenter(interactor: interactor)
return TripListView(presenter: presenter)
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.
Add the following to the class in TripListInteractor.swift:
func addNewTrip() {
model.pushNewTrip()
}
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.
TripListView(presenter:
TripListPresenter(interactor:
TripListInteractor(model: model)))
Routing to the Detail View
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.
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
}
}
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
}
}
Routing
Create a new Swift File named TripListRouter.swift
.
Set its contents to:
import SwiftUI
class TripListRouter {
func makeDetailView(for trip: Trip, model: DataModel) -> some View {
let presenter = TripDetailPresenter(interactor:
TripDetailInteractor(
trip: trip,
model: model,
mapInfoProvider: RealMapDataProvider()))
return TripDetailView(presenter: presenter)
}
}
In an imperative UI paradigm — in other words, with UIKit — a router would be responsible for presenting view controllers or activating segues.
Reference
https://www.raywenderlich.com/8440907-getting-started-with-the-viper-architecture-pattern