Introducing VIPER with Reducer and State

Daniel
Blockchain.com Engineering
9 min readMar 6, 2020

Having been an iOS engineer for nearly 7 years, I have always been looking for exciting new patterns and architectures to try out.

A few dominant architectures are MVC, MVVM-C, and VIPER. Every pattern offers a different level of abstraction:

  • MVC is the simplest one. There are almost no abstractions, and everything is pretty much straight forward.
  • In MVVM-C, the view’s presentation logic is outsourced into a specialized construct called View-Model.
  • VIPER, for example, abstracts even further by adding another layer for data handling — the interaction layer.

In this article, I am going to introduce you to a state-driven architecture built on top of VIPER, that we have been successfully using in our wallet app.

VIPER(RS) stands for View, Interactor, Presenter, Entity, Router, (Reducer, State).

Before we dive into the (RS) part, let’s just start by explaining the building blocks of VIPER.

If you already feel comfortable using VIPER you may want to skip straight to the part explaining Reducer and State.

Safety comes first; Put on your helmets before entering construction mode

*This part of the tutorial does not contain a full implementation of the VIPER(RS) suite, as I’m leaving this for the next article.

View

A view is a component domain layer that is mostly passive.

It may contain other views (subviews), and receive user interaction and lifecycle events.

Among the view’s responsibilities:

  • Displaying information to the end-user.
  • Receiving input and user interaction events, and passing them to the presenter.
  • Receiving output from the presenter and displaying it to the end-user.

Some ground rules about the view layer:

  • A view should be a UIView or a UIViewController object. duh! 🎮
  • A view that is a part of the navigation stack may be created by the routing layer (which will be explained shortly) or another designated object.
  • A view that is not a part of the navigation stack should be created and added to the layout hierarchy by the parent view.
A standard view-presenter relationship: presenter hierarchy should match view’s

This article refers to views and view-controllers simply as views, as both represent the view layer.

Presenter

A component domain layer which is responsible for the presentation logic.

Its job is to mediate between the view and the interaction layer, processing data retrieved from the interactor into user-readable data (e.g Strings, Colors, Fonts), and feeding it to the view.

Therefore, the presenter must sit between the view and the interactor.

Among the presenter’s responsibilities:

  • Receiving input from the view and passing it to the interactor for further processing.
  • Receiving data from the interactor, mapping it into presentation data and passing it back to the view.
The screen view interacts with the screen presenter. Subviews directly interact with their corresponding presenters.

Some ground rules about the presentation layer:

  • A presenter is typically a reference type, but can also be a struct (the more complex the presentation logic is, the more reasons to choose a reference over a value type).
  • A presenter is a dependency of the view, and as such, it should be injected into the view.
  • The presentation hierarchy should reflect the view hierarchy: a presenter should be created only by a parent presenter, a router (in case of a screen), or a specialized object such as a factory.
  • A presenter could have more than one child-presenters.
The main presenter initializes its child-presenters. Each presenter depends on a specialized interactor

Separate concerns, but simplify when possible

In a real app, there is no need to always over-engineer things.

Therefore, in really small components, we would rather omit the interaction layer and have only a view and presenter.

In that sense, presenters function similarly to view-models in MVVM.

Interactor

A component domain layer that interacts with external services, translating their data output into interaction data (entities).

Its job is to mediate between the service and the presentation layers.

As such, the interactor sits right between the presenter and the service layer.

Among the interactor’s responsibilities:

  1. Receiving input from the presenter and passing it to the service layer for further processing.
  2. Receiving service data back from the service layer, mapping it into interaction data, and passing the processed data to the presenter.
Presenter-Interactor relationship: each view is coupled to a presenter

Some ground rules about the interaction layer:

  • An interactor is typically a reference type.
  • The interactor is a dependency of the presenter.
  • The interactor is typically injected into the presenter.
  • Interactors may be created by other interactors higher in the hierarchy.
  • Similarly to presenters, an interactor could have multiple sub-interactors.
Interaction-service layer relationship

About Services

Often this is a source of confusion, so although the service layer is not a part of the VIPER stack, I thought it deserves a short explanation.

A service typically executes asynchronous operations in a background queue.

It may be a database, a REST endpoint client or even a service that aggregates multiple other services, combining their logic and providing data to the interactor.

Some ground rules about services:

  • The interaction layer is the only layer allowed to directly access the service layer.
  • The service should be injected into the interactor.
  • It’s important to stress that services do not belong to the component domain, and therefore may be reused by multiple clients.
Component building blocks: comprise a view, presenter & interactor

Entity

An entity is a “data bag holder” — any data construct that lives in the interaction layer and is manipulated by it.

Other architectures, like MVC and MVVM, refer to Entity as Model, and unsurprisingly, Entities have exactly the same job in VIPER.

One rule of thumb about entities — should contain only data.

  • It does not contain complicated logic.
  • It does not contain class decoupling patterns like KVO or delegation.
  • It is (typically) a value type.

Router

The router's responsibility is to wire different screens together, creating a logical flow.

Some of you who are experienced in MVVM-C know this layer as Coordinator.

An app may have single or multiple routers, it depends on the number of possible flows.

Presenter-Router relationship: The presenter uses the Router to navigate to adjacent screens

Some ground rules about the routing layer:

  • The router may be created by another router, which can be the main application router.
  • The router may have a reference to a navigation controller (or to a proxy object that exposes a navigation API) — That means that the router has some degree of control over the navigation stack.
  • The router may instantiate screens and inject presenters in them, or use a proxy object for that.
A routing layer that further abstracts the navigation by using a proxy for controlling the view-controller stack

State

A state is a data representation of a component at a given point in time.

A state may typically be implemented as an enum. For example, if we needed a ternary-value representation of a data-fetching interactor, we could define something like the following:

enum State<Value> {    /// Data was already fetched and is currently available
case fetched(Value)
/// Data is being fetched and currently unavailable
case fetching
/// Some failure occurred while fetching the data
case failure(Error)
}
final class Interactor { private(set) state = State.fetching
private let service: ServiceAPI
func fetch(completion: @escaping () -> Void) {
// Fetch Result<Data, Error> type
service.fetchData { [weak self] result in
switch result {
case .success(let data):
self?.state = .fetched(data)
case .failure(let error):
self?.state = .failure(error)
}
}
}
}

Why would we want to use states at all?

  • States are a readable representation of data. As such, they are easy to read and debug.
  • Swift is a type-safe language. Using an enum as a state allows us to differentiate between possible states using the type alone, which is pretty powerful in itself.
  • What you see is what you get — translating data into a state limits our scope of thought to a predictable spectrum of values.

Reducer

A reducer/reduce is a concept that exists in lots of programming languages, as well as in vanilla Swift as a higher-order function operating on Sequence type.

React and Redux, for example, use reducer and reducer-hook to reduce a bunch of data into a state.

Reducer in VIPER(RS)

In VIPER(RS), a reducer deterministically operates on a sequence of states by reducing it into a single state.

In other words, a reducer receives input representing multiple states (of multiple sub-components), calculates and returns an output, which is a single enum.

A super-simple reducer will return a valid state if all the states in the input sequence are valid, and an invalid state if there is at least one invalid state in the input sequence.

final class Reducer {
func reduce(states: Set<State>) -> State
return states.contains(.invalid) ? .invalid : .valid
}
}

Pretty simple, hah?

But of course, reducers may have more complex heuristics to reflect the state of things.

Reducers can come in handy in particularly complex objects. For example, an interactor can contain multiple other small interactors, where each child interactor potentially has a state of its own. In such a use-case, the reducer will sit inside the main interactor, reducing the child-interactor states into a single interaction state.

Reducer receives a sequence of states, mapping them into a single state result

Applying state and reducer to the interaction layer

In TextFieldInteractor add State as follows:

Each text field interactor has a property attesting its current state

In ScreenInteractor add State and Reducer as described:

The main interactor reduces the smaller interactors’ states into a unified state that represents the entire input.

This is how multipleTextFieldInteractor.State values are reduced into ScreenInteractor.State.

The problem that reducers solve

So far, I was showing pretty simple examples, and clearly, you must be thinking that we don’t have to use a reducer as a proxy only to map / reduce an array of states with a single line of code.

However, imagine we had a typical form, with 5–8 text fields, each with different validation rules. Our folding logic would have been fatter in the least.

Taking that logic out of the VIPER stack, and placing it somewhere else may save us some precious time implementing it all over, for another fancy future component. It will also allow us to further delegate responsibilities among objects.

Communicating the states across VIPER(RS)

After understanding the building blocks of the VIPER(RS) stack, it’s time to choose a communication pattern that will allow us to create a feedback loop across all levels.

This is the time to say that VIPER(RS) goes hand in hand with reactive programming, and in the next article, we will see why they perfectly match each other. 💑

Getting started with Rx:

There are multiple wonderful libraries that you can use to write reactive Swift. We are going to choose RxSwift, RxRelay, and RxCocoa, but ReactiveSwift and Apple’s shiny brand new Combine are no lesser tools for the task.

I would highly recommend incorporating one of them into your codebase.

Of course, if you would rather not use reactive functionality at all, you can always use other patterns like good ole’ delegation, notification center, KVO or simple closures. That will work too.

Sssssummary 🐍

VIPER(RS) adds another two building blocks to the familiar VIPER architecture:

  • State — represents the state of a single layer.
  • Reducerfolds a sequence of states (originated in the sub-components) into one state representing the entire layer.

Each piece gets abstracted by exposing only the relevant data, as well as by knowing exactly what it needs to know in order to play its part.

This level of modularization allows us to:

  • Make a clear distinction between the different layers
  • Avoid bloated objects with a lot of responsibilities.
  • Have flexibility in testing each building block, or the entire stack, simply by replacing one piece with a mock that implements fake functionality.
One object. One job.

You’ve reached the end. Congratulations! 🎉🎈

What’s next? 💡

In the next article, I’m going to show you how to apply what you learned in a real-world application using RxSwift, RxRelay, and RxCocoa.

--

--

Daniel
Blockchain.com Engineering

♥️ Open Source ⚡️ Owner of SwiftEntryKit and others