VISCS architecture for SwiftUI project

Aleksei Pichukov
Mac O’Clock
Published in
7 min readApr 13, 2020

Why do we need one more architecture? The main reason is to have some that aggregates practices from other solutions that we already have, make it work for SwiftUI in enterprise applications and create a list of rules and conventions of how to use it that will cover most cases we could encounter.

Many examples we find on the internet just operate with a simple HelloWorld-like application. Let's be honest, every architecture works great with a simple app. If you have just one screen in your application, you can use any form of architecture you can imagine and it will work.

However, when your application begins to grow, that’s when you will start to feel the consequences of your choice. So, let’s take a look at what we want to see from the architecture perspective in the enterprise application:

  • We want to keep a maximum possible code coverage (Unit and UI tests)
  • We want to split the app into some Components
  • Components shouldn’t be coupled to each other
  • The architecture should allow us to painlessly add Components and change it
  • We want to use Dependency Injection
  • We want to avoid using global objects, singletons and other similar practices as much as possible
  • We want to follow SOLID principles

I will not explain what SwiftUI is, how it works and how it affects the architecture level; you can find this description elsewhere. The only thing that I want to mention is that SwiftUI a declarative state-driven framework, so we have a State and we have a View that reacts to any State update and redraws the UI respectively. We also need to have someplace to operate with business logic for this View; let's have an Interactor for it.

Now we have the VIS part covered from our VISCS architecture. We can call it Component. Let's take a look at our Component and describe a list of rules it should follow.

Components

  1. Component is a separate instance of the functionality in our application
  2. The simple component can have only one single view inside
For some screens that don't have business logic (Error Message View, Success View, Information View, etc.) it doesn't make any sense to make things overly complex (we are not making VIPER). This simple view will have it's own @State property wrappers and, for example, can have a Completion Block closure for letting the outside world know that something happened

3. All other components should have a View, State and Interactor

4. The State and Interactor objects should be injected into the View from outside

5. Interactor should have all dependencies it needs already injected

6. Interactor and all its dependencies should be covered by protocols (we want the app to be testable). But don't take it too far. It doesn't mean that if Interactor needs some simple Service (VISCS) that doesn't keep any state inside, we need to keep this dependency through the whole app. We can simply inject it like this:

7. Only Interactor can update State

8. View tells Interactor about its events and reacts to State updates

Here you can see the simple Component

Component

Features

As you can see above, View is getting all dependencies from the outside. We also need to have some object that will be responsible for cross-components communication and navigation. Let's call it Coordinator (VISCS). It's not the same coordinator that we had in MVVM + Coordinators architecture. It's similar, but a bit different. The main difference is we want to keep SwiftUI native navigation (I hope Apple will improve it and fix all these bugs it has soon), so our Coordinator is not responsible for the type of navigation. it's a View's responsibility, but only Coordinator can provide a new View that should be opened next.

View in SwiftUI is a value type, so in our case, the Coordinator is kind of a view factory because it can't keep a reference to the View(it's a value type). It can just create the View, inject all dependencies View needs and also keep some of these dependencies.

We already have a Component and now we have a Coordinator on top of one or several components; together let's call it Feature.

  1. Feature should have only one single Coordinator
  2. Feature should have one or more Component
  3. Feature can have sub-features
  4. If Feature has sub-features, it means that features Coordinator is a holder of sub-features coordinators and responsible for its creation and injecting all dependencies they need

You can find the Feature example below

Feature with two components

Coordinator

It’s time to talk more about Coordinators.

Our application should have one RootCoordinator that will be the entry point to our UI and business logic for the application. Basically, this RootCoordinator (the entry point to our applications feature tree) is a part of our main RootFeature.

RootFeature has its own component (we follow the rules that have been described before). This component is just a container for the app.

This is how the SceneDelegate can look like in our case:

As you can see from the example above the RootCoordinator confirms the ViewProvidable protocol:

NOTE: All Coordinators should confirm a ViewProvidable protocol

Also, we have all dependencies injected to the RootCoordinator.

Here is how the Coordinators tree can look (this tree also represents the Features tree in the app)

Coordinators tree

This is an example of Coordinator:

The only question we have here is what is coordinatorDelegate that our FeatureView expects as a dependency. And the answer takes us to the next topic.

Cross-component navigation

Let’s imagine that our Feature has several components inside. We need to provide some mechanism to navigate between our components. We can add this responsibility to the Coordinator which is covered by CoordinatorDelegate protocol

That’s how it can look like

Cross-component navigation

In the View of the current component we just call the CoordinatorDelegate method that provides a View for the destination component.

We need to keep a reference to the sub-feature Coordinator in case we have a sub-feature we want to show:

and now the extension for CoordinatorDelegate implementation will look like this:

NOTE: We need to check to see if subFeatureCoordinator is not nil because of the way SwiftUI creates views even before we actually want to show them

You can see above that we also have an instance of our coordinator injected as NavigationDelegate to the child Coordinator. It takes us to the next topic.

Cross-feature navigation

We introduce the NavigationDelegate to make a cross-feature navigation be possible. Every Coordinator that can provide a navigation functionality should confirm it's own NavigationDelegate protocol and be injected to the child Coordinator

Cross-feature navigation

Cross-component communication

For any cross-component communication, we need to inject the State (covered by the StateShareable protocol) of the first component to the second component Interactor as it is shown below.

Cross-component communication

Now Interactor from the second component can call any available and shareable method from the State of the first component.

Services

Service is any kind of injected functionality. It can be a DataProvider, NeetworkService, AnalyticsSystem or any other object that will provide some sort of service to our Component.

  1. Service is a separate instance of functionality in the app
  2. Service should be covered by the protocol
  3. It is better to have a Service as a separate swift Package, Pod or Framework
  4. This package should be covered by tests
  5. Service can have sub-services that should follow the same rules

Summary

Now we have our VISCS architecture described.

Component layer

  • View - just a UI representation based on the State
  • Interactor - the holder of business logic and the one who change the State
  • State - the current state of the component

Feature layer

  • Component + Coordinator that responsible for DI, View creation and application navigation

Service layer

  • Service - a functionality provider

Of course, it’s not perfect; nothing is perfect. It looks similar to many other architectures, but the goal of having it is to create a list of rules for SwiftUI-based projects that will work for more than just simple applications. I try to find a balance between the old fashioned UIKitapproach and the current declarative state-driven approach that SwiftUI has. Let's be honest - SwiftUI has a lot of bugs and a lot of missing functionality; it's in beta and not yet production-ready (though it will be soon) without mixing it with UIKit elements. I didn't cover much regarding the architecture, but I hope that this article will help someone to find his/her own architecture balance in this new iOS SwiftUI-based world.

--

--