VIPER with SwiftUI: SwuiPWER, A New Architecture Evolved from VIPER

VIPER with SwiftUI? Hell yes!

Martin M
12 min readAug 25, 2024

What is VIPER architecture?

VIPER is an architecture introduced in UIKit era of iOS development world. It is known as iOS architecture with most number of layers. It is often misunderstood to be too complex(but is really not) and difficult to understand. I’d say those who properly understand and have used this architecture love is(which I really do love it).

VIPER stands for names of layers it defines like below.

  • View
  • Interactor
  • Presenter
  • Entity
  • Router

It is a fantastic architecture but people have hard time adopting it to SwiftUI. Some even believes VIPER cannot be used with SwiftUI and thinks trying to do so is really a bad idea.

Will I agree to this opinion? NO.

You can totally use VIPER with SwiftUI

Firs of all, let’s understand what VIPER achieves.

Some say VIPER is a architecture for UIKit thus it cannot be adopted to SwiftUI but is it?

  • Separating Ideas into layers: necessary layers
  • Clean Architecture DIP

both needed in swift ui so we are good to go.

Problem of VIPER with SwiftUI: Router cannot route SwiftUI view

VIPER was a architecture designed when there was no SwiftUI so it is designed to be adopted with UIKit. There are some problems adopting SwiftUI to VIPER.

In VIPER with UIKit, to navigate from one view to another, you send ViewController’s self to Presenter and it send ViewController to Router. Router then creates destination’s ViewController(with VIPER module components attached to it) and calls push or present method of source VC with newly created VC as destination.

In SwiftUI, you cannot really pass View’s instance to presenter and also navigation must be done within navigation source view. This is the reason many people are having hard time trying to adopt VIPER to SwiftUI.

Router’s primary task is not to Route, it is a Wireframe

Let’s talk about what VIPER’s Router is before adopting navigation system to SwiftUI.

Router actually had two roles.

  1. To navigate view controllers(to Route views)
  2. Construct VIPER modules: Injecting View, Presenter, Interactor to each other

Common VIPER routing flow would be

  1. User taps some row in List of Models(let’s say human tapped Post row)
  2. id of post is sent to Presenter
  3. Presenter asks Interactor to get Post with given ID
  4. Presenter receives a Post with the ID
  5. Presenter asks Router to show PostDetailView with the Post just received from Interactor
  6. Router creates destination View with VIPER modules set up with the Post received using destination Router’s static createModule method
  7. Router commands navigation from self stored view to the destination, ex. self.viewController.push(destinationViewController)

Router may look unnecessary and some omits Router from VIPER when using VIPER architecture (which by the way is a really bad idea since it breaks the most important Clean Architecture rule).

Presenter needed to posses Router to avoid the followings.

View(ViewController) must receive Entity model for navigation

Without Router a VC itself has to create destination VC inside it class and to do so a VC has to receive Entity model which is necessary exposure of model to view(it gives view a chance to change model itself which it really should not).

View(ViewController) will know everything

When VC do the navigation works it first need to create destination VC but to do so it has to reference all the layers of VIPER like Presenter, Interactor and Entity. And sometimes (it is not defined in VIPER but) it needs to reference network requests. This really breaks rule of DIP which is core rule of Clean Architecture and VIPER has aspect of way to implement Clean Architecture in iOS world.

Yes, without following Clean Architecture rule and just to use VIPER for separating concern into layers is OK but if Clean Architecture is practiced properly, it will increase project productivity greatly.

VIPER is a good and easy way of achieving Clean Architecture so it really should follow Clean Architecture rules but just to have its layers.

Presenter will reference View

If you did’t create destination inside source VC, you might create it in Presenter and send it to navigation source VC and it also Clan Architecture’s DIP violation.

Routers were not really routers

What I can tell from the reasons above, Router’s primary concern was not to route but to separate VIPER module building process from View or Presenter and make View or Presenter not to reference items violating Clean Architecture’s rule.

So a Router was actually Wirerame and some people actually call VIPER’s Router Wireframe.

SwuiPWER: A new architecture evolved from VIPER

SwuiPWER is a new architecture which converted VIPER system into SwiftUI compatible style. I’ll explain differences and similarities.

SwuiPWER: Solving Router problem

First of all, Router will not do routing but it will only do wireframing in SwuiPWER so it is called Wireframe.

In SwuiPWER, view possess wireframe. On navigation triggering even, view asks wireframe to create the destination view. Wireframe then creates destination view with VIPER module set up and returns it to the view. View itself then navigates to the destination.

This way we can avoid view directly referencing all the VIPERR module components but rather just sees a destination view.

import View
import SwiftUI
import Presenter
import UseCase
import Gateway
import Entity

class HomeCheckListsWireframe {
private let interactor: HomeCheckListsInteractorInput

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

static func createModule() -> some View {
...
}
}

extension HomeCheckListsWireframe: HomeCheckListsViewDestinationCreatable {

// func which view calls
@ViewBuilder
func destination(forCheckListWithId checkListId: Int) -> some View {
if let checkList = interactor.checkList(withId: checkListId) {
CheckListItemsWireframe.createModule(with: checkList)
} else {
Text("Error CheckList for CheckListID:\(checkListId) not fopunc")
}
}
}

Some important thing need to be explained in the code above is that Wireframe possesses interactor.

Inside func destination, in receives info needed to create destination module which in this case is an id of CheckList model. Wireframe passes this id to Interactor and interactor returns CheckList associated to the id and then passes it to destination module's Wireframe to create the destination module.

And View calls uses Wireframe like below.

import SwiftUI
import Presenter

public struct HomeCheckListsView<
ViewModel: HomeCheckListsViewModelProtocol,
EventHandler: HomeCheckListsViewEventHandling,
DestinationViewFactory: HomeCheckListsViewDestinationCreatable>: View {

// These two will be explained later in this article
@StateObject var viewModel: ViewModel
@StateObject var eventHandler: EventHandler

// Possesses Wireframe via protocol DestinationViewFactory
@StateObject var destinationViewFactory: DestinationViewFactory

public init(viewModel: ViewModel, eventHandler: EventHandler, destinationFactory: DestinationViewFactory) {
_viewModel = StateObject(wrappedValue: viewModel)
_eventHandler = StateObject(wrappedValue: eventHandler)
_destinationViewFactory = StateObject(wrappedValue: destinationFactory)
}

public var body: some View {
NavigationStack {
List(viewModel.checkLists) {
NavigationLink($0.name, value: $0.id)
}
.navigationDestination(for: Int.self) { checkListId in
// This is called on Row tap event.
// self.destinationViewFactory returns destination view.
//
// Wireframe is inserted in self.destinationViewFactory
// but view should not know that it is Wireframe but the view
// only needs to know that it is something returns destination view.
self.destinationViewFactory.destination(forCheckListWithId: checkListId)
}
.navigationTitle("All Check List")
.navigationBarTitleDisplayMode(.inline)
}
.onAppear() {
eventHandler.onAppear()
}
}
}

Wireframe is located on outside of view layer so we need to define a protocol for View to communicate with Wireframe.

import SwiftUI

public protocol HomeCheckListsViewDestinationCreatable: ObservableObject {
associatedtype DestinationView: View
func destination(forCheckListWithId checkListId: Int) -> DestinationView
}

Wireframe possessing interactor may seem to very nice but it was necessary

SwuiPWER: Presenter and View communication

SwiftUI is a declarative system while old UIKit is imperative so we cannot give what to display on SwiftUI view like we did using Presenter.

In SwuiPWER, we use View, ViewModel and Presenter here.

View possesses ViewModel and Presenter. Presenter possesses ViewModel(the same instance as the view possesses).

When something happened to View like user tapped a button, View sends the event to Presenter: ex. ‘self.presenter.didTapUpdateUserButton’.

Then presenter asks use case to process business logic which is same as older VIPER system.

After presenter received the response from UseCase, Presenter updates ViewModel value to represent new application’s state.

View observes ViewModel thus changes made to the view model by the presenter is immediately applied to the display.

From view perspective, presenter is only used to send view event to inner layer and ViewModel is only used to know what the view should display.

To achieve this code will be like below.

View

import SwiftUI
import Presenter

// <<< Important! >>> : There is some generics tick to make View possess ViewModel, Presenter and Wireframe via protocols
public struct HomeCheckListsView<
ViewModel: HomeCheckListsViewModelProtocol,
EventHandler: HomeCheckListsViewEventHandling,
DestinationViewFactory: HomeCheckListsViewDestinationCreatable>: View {

// Possesses ViewModel
@StateObject var viewModel: ViewModel

// Possesses Presenter via protocol HomeCheckListsViewEventHandling
// HomeCheckListsViewEventHandling hides idea of Presenter from View since
// View does not need to know there exists something like Presenter.
// View only should know that there is a thing handles view's event.
@StateObject var eventHandler: EventHandler

@StateObject var destinationViewFactory: DestinationViewFactory

public init(viewModel: ViewModel, eventHandler: EventHandler, destinationFactory: DestinationViewFactory) {
_viewModel = StateObject(wrappedValue: viewModel)
_eventHandler = StateObject(wrappedValue: eventHandler)
_destinationViewFactory = StateObject(wrappedValue: destinationFactory)
}

public var body: some View {
NavigationStack {
List(viewModel.checkLists) {
NavigationLink($0.name, value: $0.id)
}
.navigationDestination(for: Int.self) { checkListId in
self.destinationViewFactory.destination(forCheckListWithId: checkListId)
}
.navigationTitle("All Check List")
.navigationBarTitleDisplayMode(.inline)
}
.onAppear() {
eventHandler.onAppear()
}
}
}

Presenter

import Entity
import UseCase
import Combine

// --------------------------------------------------------
// MARK: - Presenter
// --------------------------------------------------------

public class HomeCheckListsPresenter {
public var interactor: HomeCheckListsInteractorInput
public let viewModel: HomeCheckListsViewModel = .init()

public init(interactor: HomeCheckListsInteractorInput) {
self.interactor = interactor
}
}

// --------------------------------------------------------
// MARK: - from View
// --------------------------------------------------------

extension HomeCheckListsPresenter: HomeCheckListsViewEventHandling {

public func onAppear() {
interactor.loadCheckLists()
}
}


// --------------------------------------------------------
// MARK: - from Interactor
// --------------------------------------------------------

extension HomeCheckListsPresenter: HomeCheckListsInteractorOutput {

public func didUpdate(models: [CheckList]) {
let listImtes = models.map {
return HomeCheckListsViewModel.ListItemViewModel(id: $0.id, name: $0.name)
}

viewModel.checkLists = listImtes
}
}


// --------------------------------------------------------
// MARK: - View Model
// --------------------------------------------------------

// ViewModel is defines in the same file as Presenter to make its property
// only editable from the presenter
public class HomeCheckListsViewModel: ObservableObject, HomeCheckListsViewModelProtocol {
// It is set as fileprivate(set) so HomeCheckListsPresenter is the only class
// can update ViewModel data
@Published public fileprivate(set) var title: String = ""
@Published public fileprivate(set) var checkLists: [ListItemViewModel] = []
}

extension HomeCheckListsViewModel {
public struct ListItemViewModel: Identifiable {
public let id: Int
public let name: String
}
}

Protocols

import Entity

// --------------------------------------------------------
// MARK: - View -> Presenter
// --------------------------------------------------------

public protocol HomeCheckListsViewEventHandling: ObservableObject {
func onAppear()
}


// --------------------------------------------------------
// MARK: - Presenter -> View
// --------------------------------------------------------

public protocol HomeCheckListsViewModelProtocol: ObservableObject {
var checkLists: [HomeCheckListsViewModel.ListItemViewModel] { get }
}

Why separate ViewModel and Presenter

You can just use ViewModel and make it work as both ViewModel and Presenter.

I’d say it is still OK and in some cases it may be a better choice.

The reason I separated these is that those ideas can be clearly separated.

ViewModel only acts as an object representing how the view’s display states and nothing else.

Presenter in other hand acts as a mediator of View and UseCase. It receives view event and tells UseCase what to do upon the event. Then after receiving UseCase response it converts the result into view understandable and display ready form and updates ViewModel’s value.

It will be a lot more obvious to know what values in view can be changed if ViewModel was a separate class since it only lists properties that can be changed in View. If it was mixed in Presenter, there will be list of properties for View and other properties used in Presenter.

I like it in this way but you do not have to, it’s your choice.

How Wireframe works

Each Wirerame has static func createModule to create its SwuiPWER module and this is exactly the same as old VIPER.

What new is that when View asks Wireframe to create destination view, it may receive necessary info for destination view like id of a model which will be displayed in the destination view.

In old VIPER this id was received in Presenter and it asks UseCase for actual model corresponding the id and then Presenter send it to Router(Wireframe) and the router inject the model to destination module.

In SwuiPWER this work has to be done by Wireframe. When wireframe receives destination’s info like id of a model, it asks UseCase(yes Wireframe possesses UseCase) to get actual model for given id and then it injects it calls destination Wireframe’s func something like static func craeteModule(with model …) giving the just received model. Then Wireframe returns newly created view with SwuiPWER module set up.

This is not pretty but Wireframe is a component which knows everything and does dirty job. It also follows its original responsibility which is to construct VIPER modules.

import View
import SwiftUI
import Presenter
import UseCase
import Gateway
import Entity

class HomeCheckListsWireframe {
private let interactor: HomeCheckListsInteractorInput

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

static func createModule() -> some View {
// Setup presenter and interactor relations
let interactor = HomeCheckListsInteractor()
let presenter = HomeCheckListsPresenter(interactor: interactor)
interactor.output = presenter

// Setup view
return HomeCheckListsView(
viewModel: presenter.viewModel,
eventHandler: presenter,
destinationFactory: HomeCheckListsWireframe(interactor: interactor))
}
}

extension HomeCheckListsWireframe: HomeCheckListsViewDestinationCreatable {

@ViewBuilder
func destination(forCheckListWithId checkListId: Int) -> some View {
if let checkList = interactor.checkList(withId: checkListId) {
CheckListItemsWireframe.createModule(with: checkList)
} else {
Text("Error CheckList for CheckListID:\(checkListId) not fopunc")
}
}
}

Presenter and UseCase communications

Nothing different in SwuiPWER compared to old VIPER so I will not go deeper into it.

Complete sample code

This code will just work so in you want to just try SwuiPWER just use this code.

I separated modules for each layers so the code below contains module imports and access controls since separating modules for layers is highly recommended in SwuiPWER but you can also place all the file into single module.

Directory structure will be like this.

SceneDelegate

Set HomeCheckLists as root view.

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let _ = (scene as? UIWindowScene) else { return }

if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)

window.rootViewController = UIHostingController(
// Setting HomeCheckLists as Root
rootView: HomeCheckListsWireframe.createModule()
)

self.window = window
window.makeKeyAndVisible()
}
}

Wireframe

import View
import SwiftUI
import Presenter
import UseCase
import Gateway
import Entity

class HomeCheckListsWireframe {
private let interactor: HomeCheckListsInteractorInput

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

static func createModule() -> some View {
let interactor = HomeCheckListsInteractor()
let presenter = HomeCheckListsPresenter(interactor: interactor)
interactor.output = presenter

return HomeCheckListsView(
viewModel: presenter.viewModel,
eventHandler: presenter,
destinationFactory: HomeCheckListsWireframe(interactor: interactor))
}
}

extension HomeCheckListsWireframe: HomeCheckListsViewDestinationCreatable {

@ViewBuilder
func destination(forCheckListWithId checkListId: Int) -> some View {
if let checkList = interactor.checkList(withId: checkListId) {
CheckListItemsWireframe.createModule(with: checkList)
} else {
Text("Error CheckList for CheckListID:\(checkListId) not fopunc")
}
}
}

View

import SwiftUI
import Presenter

public struct HomeCheckListsView<
ViewModel: HomeCheckListsViewModelProtocol,
EventHandler: HomeCheckListsViewEventHandling,
DestinationViewFactory: HomeCheckListsViewDestinationCreatable>: View {

@StateObject var viewModel: ViewModel
@StateObject var eventHandler: EventHandler
@StateObject var destinationViewFactory: DestinationViewFactory

public init(viewModel: ViewModel, eventHandler: EventHandler, destinationFactory: DestinationViewFactory) {
_viewModel = StateObject(wrappedValue: viewModel)
_eventHandler = StateObject(wrappedValue: eventHandler)
_destinationViewFactory = StateObject(wrappedValue: destinationFactory)
}

public var body: some View {
NavigationStack {
List(viewModel.checkLists) {
NavigationLink($0.name, value: $0.id)
}
.navigationDestination(for: Int.self) { checkListId in
self.destinationViewFactory.destination(forCheckListWithId: checkListId)
}
.navigationTitle("All Check List")
.navigationBarTitleDisplayMode(.inline)
}
.onAppear() {
eventHandler.onAppear()
}
}
}
import SwiftUI

public protocol HomeCheckListsViewDestinationCreatable: ObservableObject {
associatedtype DestinationView: View
func destination(forCheckListWithId checkListId: Int) -> DestinationView
}

Presenter

import Entity
import UseCase
import Combine

// --------------------------------------------------------
// MARK: - Presenter
// --------------------------------------------------------

public class HomeCheckListsPresenter {
public var interactor: HomeCheckListsInteractorInput
public let viewModel: HomeCheckListsViewModel = .init()

public init(interactor: HomeCheckListsInteractorInput) {
self.interactor = interactor
}
}

// --------------------------------------------------------
// MARK: - from View
// --------------------------------------------------------

extension HomeCheckListsPresenter: HomeCheckListsViewEventHandling {

public func onAppear() {
interactor.loadCheckLists()
}
}

// --------------------------------------------------------
// MARK: - from Interactor
// --------------------------------------------------------

extension HomeCheckListsPresenter: HomeCheckListsInteractorOutput {

public func didUpdate(models: [CheckList]) {
let listImtes = models.map {
return HomeCheckListsViewModel.ListItemViewModel(id: $0.id, name: $0.name)
}

viewModel.checkLists = listImtes
}
}


// --------------------------------------------------------
// MARK: - View Model
// --------------------------------------------------------

public class HomeCheckListsViewModel: ObservableObject, HomeCheckListsViewModelProtocol {
@Published public fileprivate(set) var title: String = ""
@Published public fileprivate(set) var checkLists: [ListItemViewModel] = []
}

extension HomeCheckListsViewModel {
public struct ListItemViewModel: Identifiable {
public let id: Int
public let name: String
}
}
import Entity

// --------------------------------------------------------
// MARK: - View -> Presenter
// --------------------------------------------------------

public protocol HomeCheckListsViewEventHandling: ObservableObject {
func onAppear()
}


// --------------------------------------------------------
// MARK: - Presenter -> View
// --------------------------------------------------------

public protocol HomeCheckListsViewModelProtocol: ObservableObject {
var title: String { get }
var checkLists: [HomeCheckListsViewModel.ListItemViewModel] { get }
}

Interactor

import Entity

public class HomeCheckListsInteractor {
public weak var output: HomeCheckListsInteractorOutput?

private var checkLists: [CheckList] = []

var checkListId: Int = -1
var checkItemId: Int = -1

public init() {}
}

// --------------------------------------------------------
// MARK: - from Presenter
// --------------------------------------------------------

extension HomeCheckListsInteractor: HomeCheckListsInteractorInput {

public func loadCheckLists() {
self.checkLists = [
// Add your own sample items
]
self.output?.didUpdate(models: self.checkLists)
}

public func checkList(withId id: Int) -> CheckList? {
return self.checkLists.first(where: { $0.id == id })
}
}
import Entity

// --------------------------------------------------------
// MARK: - Presenter -> Interractor
// --------------------------------------------------------

public protocol HomeCheckListsInteractorInput {
func loadCheckLists()
func checkList(withId id: Int) -> CheckList?
}


// --------------------------------------------------------
// MARK: - Interactor -> Presenter
// --------------------------------------------------------

public protocol HomeCheckListsInteractorOutput: AnyObject {
func didUpdate(models: [CheckList])
}

Entity

public class CheckList: Codable {
public let id: Int
public var name: String
}

HomeCheckList’s destination

It is just empty view to make HomeCheckList navigation work. You may want to try implementing SwuiPWER here.

import View

class CheckListItemsWireframe {
static func createModule(with checkList: CheckList) -> some View {
return CheckListItemsView()
}
}
public struct CheckListItemsView {
public init() {}

public var body: some View {
Text("Hello CheckListItemsView")
}
}

Wrap up

SwuiPWER is a new powerful architecture so if you were interested in it just use the full sample code above to try it out.

If you want to translate this article into another language, see my profile for the guideline(it basically says yes but please check it before you do).

If you liked this article please hit the like button and subscribe button to not to miss articles like this.

Please also see [SwuiPWER visually explained](https://medium.com/@martin.mj/swuipwer-visually-explained-cc5d21e7a964) for more understandings of SwuiPWER. In this article I’m explaining SwuiPWER with various charts.

--

--

Martin M

A professional iOS developer with more than 10 years of experience.