Handling SwiftUI Views Using Protocol-Oriented Programming

Write scalable and testable code

Arek Pituła
Better Programming

--

Photo by Dmitry Osipenko on Unsplash

SwiftUI has shown a newer, faster, and more efficient way to build views. Declarative programming is an amazing technology, and SwiftUI alongside Jetpack Compose on Android and Flutter’s Widget is making view building more enjoyable than ever.

However, building a view is not the only part of a mobile developers’ life. Designing a good, scalable, and effective UI that works well with any kind of model in a highly changing environment is quite a challenge.

How is it handled commonly?

In most common scenarios, and simple cases, there are two layers:

  • Model: This is fetched from the repository, can be data from API, a record from a database or other kinds of data from multiple services. Models usually exposed fields publicly, very rarely methods.
  • View: Struct that implements View protocol — in best case scenario has only one dependency.

The dependencies diagram looks like the following:

View — Model dependency

And the code:

Inject model directly into view is an idea that… works. Don’t get me wrong! I’m not saying that it’s an incorrect approach. Those solutions work well when a model is a raw type like:

  • String
  • Boolean
  • Numbers

Injecting the above types into the views allows us to avoid redundant boilerplate code — like additional abstraction layer or ViewModel class created only to fulfill superfluous requirements.

Moreover, when one big view is separated into multiple smaller views, changing a particular view is easy to do, especially when they aren’t dependent on the model, but instead on the raw type.

The second approach is MVVM architecture — which is well described here. In this architecture, most views have their own ViewModel.

MVVM dependencies

MVVM enforces an additional ViewModel layer between the Model and View. The model and view object are not different than in the previous example, with the important difference being how the data to be displayed is passed to the view.

This is usually a very good way to resolve a dependency problem between view and model layers — and widely used. The biggest advantages of this approach are:

  • Layer separation: it’s clearly visible what abstraction is in charge of, and there is no dependency between domain models and UI
  • Testability: view models are usually easy to test
  • View models communication: publishers or delegates could be passed from parent to child view model and handled on the upper level

But I also see a few disadvantages:

  • Different kinds of sources: only one model is accepted by ViewModel. This means that a class is not open for extensibility and if a new business case appears it has to be rewritten. For example, an additional requirement appears, and now user list populates both the user model and also a group model — which is a completely different model.
  • Boilerplate code and ViewModel that actually is nothing more that wrapper around a model — only exposes more fields to view.
  • In most cases creating new view models for every view seems like an over-engineering

Knowing these problems we can smoothly move on to:

Protocol oriented approach

I recommend something between MVVM and raw model — view dependency in order to avoid:

  • A lot of view model files — that only describe how to translate the model into view.

And ensure:

  • Good level of testability
  • Easily adopt different kinds of sources
  • Easy transform from protocol into view model when needed

The main objective of the POP approach is to create an adaptable and flexible environment that fits into any kind of business requirement. The key part of it is a protocol (generally called DisplayableModel) which describes what needs to be displayed in a View.

protocol oriented model

It may look like this:

The Displayable protocol defined all data required by view. Here, it’s a name variable, describing that every user row view has a name text element, and an ImageSource to display the user avatar on the screen.

The displayable model does not hold any kind of business logic. Properties that are in it are essential to display a view properly. So, the goal of DisplayableModel is to be as clear and small as possible.

Views would then accept DisplayableModel protocol as an entry point:

Every model, handled by UserRow, has to implement DisplayableModel protocol.

Next, every model that has to be displayed on UserView has to implement a DisplayableModel protocol. Example:

The rest is up to you. The displayable models could come from ViewModel, or from the Observable store, or from any kind of source.

In all, using this technique we can build fast, scalable, and adaptable views.

ViewModel example

Now, I’ll show how the above approach works in combination which is MVVM. Remember, you have to display not only the user but also group in the same list view.

  1. Displayable models are stored in an array and marked as Published
  2. When views appear, or when it’s needed the method fetch is invoked
  3. Within it the ViewModel asynchronously fetch two different models: Group and User
  4. Because both are implement a UserRowDisplayableModel, it could be easily passed to published elements.

Protocols are by far one of the best features that Swift has. It allows writing a simple, well-described, and clear code that has one and only one easily understandable purpose.

I strongly recommend wrapping relevant code into a protocol for better readability and easier testing.

--

--