Handling SwiftUI Views Using Protocol-Oriented Programming
Write scalable and testable code
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:
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 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 nowuser
list populates both the user model and also agroup
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.
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.
- Displayable models are stored in an array and marked as
Published
- When views appear, or when it’s needed the method
fetch
is invoked - Within it the
ViewModel
asynchronously fetch two different models:Group
andUser
- 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.