Think And Build

iOS

VIPER-S: writing your own architecture to understand its importance (part 1)

Posted on .

VIPER-S: writing your own architecture to understand its importance (part 1)

Introduction

After some months using VIPER for my apps, I started working on my own architecture: I wanted to create something better for my own needs. I then started sharing thoughts with my colleague Marco. He is on the Android side of things, but we needed to discuss to find common ground and both get to a consistent result.

We “kind of” failed and ended up with something really similar to VIPER, but! This revisited version of VIPER is what I’m currently using in my applications, so I wouldn’t consider it a failed attempt. More like a custom version of VIPER.

Along this path I learned so many things about architectures that I decided to share the experience with a series of articles. There are two things I’d like to focus on:

• the decisions taken to get to a complete architecture, highlighting rationale and doubts (some of which are still there)
• the architecture I ended up with, showing code and some practical examples.
From now on let’s call this structure VIPER-S. The S here stands for Semantic, since I tried to obtain a clearer way to name things, giving more significance to roles and communication, and adding some rules that improve code readability and writing.

Here you can download the final code of VIPER-S.

LET’S ARCHITECT

Let’s start this journey with a question. Why do we need an architecture? This question has many different answers but the most relevant are:
• to have a clear understanding of our code (and simplify its maintenance)
• To easily distribute responsibilities (and simplify team working)
• to improve testability (and simplify your life)

With these answers in mind, and moved by a profound sense of purpose, we can start planning our architecture.

I’m a big fan of “divide-et-impera”: to me it’s a way of life. That’s why I’d start by identifying all the domains and roles, the actors that are going to work on these domains and how those actors communicate with each other. Those elements are going to define the pillars of our architecture so it’s really important to have a clear understanding of what they are.

A domain is a big set that contains all the logic for an area of responsibility. A role is a little part of this set, which is more specific and identifies a precise need. An actor is a code element that implements all the functions to satisfy a role.

Let’s list and describe the domains and roles that I’ve identified to build VIPER-S.

ARCHITECTURE DOMAINS: USER INTERFACE

With the User Interface domain we show information to the users and we interact with them. Let’s see the roles for this domain.

ROLE: DISPLAY UI INFORMATION

This is a really “dumb” role. The data reaching this domain is ready to use and there’s no need to work on it any further. It only needs to be sent down to the final UI elements, with functions like this:


func display (date:String){ 
    label.text = date 
}

As you can see, the date property has probably been converted from Date to String in a previous step. We only display the ready-to-use information here. A label displays a String, so we expect to receive a String.

ROLE: HANDLE UI EVENTS

This is another not-too-pro-active role, in fact we only intercept user interactions or application lifetime events here. The actor responsible for this role is generally called within a UI target-action:


@IBAction func save(){ 
    eventsHandler.onSave()
}

ARCHITECTURE DOMAINS: DATA

The Data domain is where we obtain information from a source and we transform it to be presented later, or, alternatively, where we process a user action into something that can be stored somewhere or used somehow. Here are the roles for the Data domain.

ROLE: MANAGE DATA

Let’s imagine this role as a set of one or more workers that are responsible of handling specific jobs. They only know how to get their jobs done and they notify someone else when they have completed or failed an operation.

Here is a simple (unsafe) example of a function for this role:


func fetchItems() { 
    networkManager.get(at: itemsPath){ 
        (completed, items) in 
        if (completed){
            presenter.present(items: items)
        } else {
            presenter.presentItemFetchError()
        }
    }
}

A worker is fetching items using a network manager. It knows exactly how to use the manager, but it doesn’t work with any value coming from the network, it just passes the value to an object that in turn knows how to present it.

ROLE: PRESENT DATA

Let’s remind ourselves not to confuse presenting with displaying: when we present the information we transform it into something that will be displayed through the UI later. The object that implements this role, is often called from the Manage Data role. Returning to the previous example for the user interface, we are not setting the text value of the label here. Instead, we are transforming a Date into a readable String.


func present(date:Date){ 
    let dateString = date.localizedString(“YYYY-mm-dd”)
    ui.display(date:dateString)
}

ARCHITECTURE DOMAIN: NAVIGATION

This domain has a single role: handling the navigation for the App. The logic behind how to display the “next view” is entirely handled within this role and the same is true for its initialization and dismissal. We then need to use UIKit to work with Storyboards and call all the needed default iOS navigation functions.


func navigateToDetail(‘for’ item:Item) { 
    let itemDetail = ItemDetailNavigator.makeModule(with: item)
        push(nextViewController: itemDetail)
}

In this example the navigator is building the module (more on this term later — just look at it as a ViewController for now) that we are going to present and it pushes it to the current navigation stack.

COMMUNICATION BETWEEN DOMAINS

Let’s now introduce the “director”, the first actor for the architecture. We are going to see its code in detail later. For now let’s just talk about it as the way to build a bridge between the domains we just saw.

The director is responsible of driving the flow of information from UI events to data handling and from data handling back to the UI. It is also responsible of defining when navigation has to take place. Each operation starts from the director and each result of the operation, at some point, passes through it.

Let’s check the overview of the architecture discussed so far to better understand how communication happens:

All those arrows… but trust me, the flow is easier than it looks. Here is a real-life example: when a user taps the save button, the application has to save some information and display a dialog with a success message (or an error in case something goes wrong).

The flow will start from the left of the previous image from “handle events”. The user tap is intercepted and passed to the director. The director sends the information to the object responsible for the role “manage data”. When this object completes the operation it’s ready to present the result to the director which, at this point, is sending the information back to the UI domain which in turn knows how to display it. Now let’s say that at the end of the saving operation, instead of presenting a popup we’d rather go to another page. Easy. The director, instead of moving the flow to the UI domain, can just drive it to the Navigation.

LET’S CODE

It’s now finally time to translate the architecture logic into code!

Before starting this process we need to identify the required modules for the example we are going to implement. What’s a module, though? The architecture considers a module what we can simply call a “Page” or a “View” of the application. This means that for an application where you can list, display and add items you have 3 modules. With the MVC architecture, for instance, each module would be a view controller.

Let’s introduce the example of code that we will implement with these tutorials. We are writing an application to handle generic “Items” that can be enabled or disabled. An Item has a name, a creation date and a state (enabled or disabled). We are going to implement 3 modules to handle items: “Items List”, “Add Item” and “Item Detail”. Adding to the above the welcome pages, we have a total of 4 modules divided in 2 groups: Items and General.

ORGANIZE YOUR PROJECT

I’m a messy guy, so I need a strict schema to follow when I’m working on a big project. For VIPER-S I decided to have a very clear folder structure. This is part of defining the architecture, after all.

Each module group has a root folder. In this case “General” and “Items” (I’d rather create real folders for the module groups, not just the Xcode project folders). Each module has its own folder. For Items we have “Add”, “List” and “Detail” and for “General” just “Welcome”.

This is the current folder structure for the project:

Each file and class follows a simple naming convention: prefixed using the folder structure that contains it, and then named after its specialization. For example, the director of the List module for Items is called “ItemsListDirector.swift” and the class name is “ItemsListDirector”. This will be really useful when used with autocomplete. When you start writing “List…” you’ll get all the classes for this group. Then “…Add” to get only classes for ListAdd module. It’s a really handy convention! 🙂

We’ll discuss other naming conventions later. This is just a simple rule that creates a shared logic for name definition and project organization. It’s a life-saver if you, like me, are not really good at keeping your naming style unchanged over the course of very long projects.

THE CONTRACT AND PROTOCOLS DEFINITION

Let’s begin by writing a contract that describes the architecture for each module through protocols. A contract is the part of the architecture where you can define precisely what a module does. It’s a sort of documentation for the module.

We’ll start from the “Items List” module, translating the roles previously described into protocols. For this module we know that it shows the list of “items” through a table and it has a “delete all” button to flush all the available items.

The “Display UI” role has to display items, errors and success messages. A good protocol to describe this role would be:


protocol ItemList_DisplayUI {
    func display(items: [ItemUI])
    func display(error:String)
    func displaySuccess()
}

The itemUI is a base object defined by simple types like String, UIImage or Bool. We’ll discuss it later.
All the functions that update UI elements with a UI model are prefixed with the “display” word. Being really strict in the naming convention is important, because I don’t want to have doubts like “should I call this function ‘show’, ‘display’, ‘update’ WAT?!”. All the protocols have a predefined set of verbs/keywords to use.

Note: here is another little naming convention that I’m using. Considering we will end up with a considerable number of files and classes for a single module, I found it useful to differentiate protocols from classes. That’s why I’m putting an underscore between the module name and the role name, obtaining the protocol name (ItemList_DisplayUI). Trust me, you’ll love this little trick later, when you write your own code and you want to autocomplete a class or a protocol name quickly.

The “Handle UI events” role has 3 functions: it has to say when the UI is ready (i.e. when viewDidLoad is called), it has to trigger an event when the user taps the “Delete All” button, and another event when an item is selected from the table.


protocol ItemsList_HandleUIEvents {
    func onUIReady()
    func onDeleteAll()
    func onItemSelected(index:Int) 
}

In general, the functions for this role start with the prefix “on” followed by the name of the handled event.

Let’s move on to the Data domain. The first role is “Manage Data” and it has 2 functions: fetch the items and delete all the items.


protocol ItemsList_ManageData{ 
    func fetchItems()
    func deleteAllItems()
}

The second role is “Present Data”. With this role we want to present items when available and we could also present generic success or error messages.


protocol ItemsList_PresentData{ 
    func present(items:[Item])
    func presentSuccess()
    func presentError()
}

Personally I love this notation and and I find it extremely readable. The verb “present” is prefix to all the protocol functions.

The Navigation domain’s only role is “Navigate”. From the ItemsList module we know that we can select an item and see its detail in a dedicate view. We can also go back to the Welcome view, or more generically, we can just go back to the previous view.


protocol ItemsList_Navigate{ 
    func gotoDetail(‘for’ item:Item)
        func goBack()
}

Functions for the navigate protocol are prefixed with “go/goto”.


protocol ItemsList_Navigate{
    func gotoDetail(`for` item:Item)
    func goBack()
}

This is the full code of the ItemsListProtocol.swift file. As you can see, if you know each role’s functionality you can easily understand what this module does:


protocol ItemList_DisplayUI {
    func display(items: [ItemUIModel])
    func displayError()
    func displaySuccess()
}

protocol ItemsList_HandleUIEvents {
    func onUIReady()
    func onDeleteAll()
    func onItemSelected(index: Int)
}

protocol ItemsList_ManageData{ 
    func fetchItems()
    func deleteAllItems()
}

protocol ItemsList_PresentData{ 
    func present(items:[Item])
    func presentSuccess()
    func presentError()
}

protocol ItemsList_Navigate{
    func gotoDetail(`for` item:Item)
    func goBack()
}

This concludes part one of the series. In the coming parts we’ll dive deeper into the architecture’s code, writing all the actors involved. We’ll complete the ItemList module and we’ll talk about how to handle some specific patterns like passing information to another module (i.e. when you select an Item and you navigate to the detail page) and getting information from another module (i.e. when you add a new Item in the ItemsAdd module and you need to notify the ItemsList module to refresh the list).

Thanks for reading this far and stay tuned for the next installments in the series. Ciao!

Yari D'areglia

Yari D'areglia

https://www.thinkandbuild.it

Senior iOS developer @ Neato Robotics by day, game developer and wannabe artist @ Black Robot Games by night.

Navigation