Composable Contexts Architecture

Mykola Fiantsev
5 min readOct 7, 2021

--

Let’s talk about app architecture and the approach I apply as an iOS software engineer in a few companies. My team and I were trying to build something solid without slipping into a dense swamp where following the rules distracts you from actual business domain code. As a result, we got something that works for us and good enough to be told from my point of view.

Previously I wrote a short article on how iOS engineers can adopt Clean Architecture in SwiftUI world. After that, I realized that not many developers understood the ideas and abstractions I was talking about. So I decided to write this as a prequel to explain how I apply Clean Architecture in my everyday UIKit world.

The architecture of an entire application is the most important thing you should care about if you are building a solid, reliable, scalable product. Nowadays, you will be asked about MVC, MVVM in almost every job interview or VIPER, and RIBs if an interviewer added some creativity to a century-old checklist. However, here’s the problem — most of these cover the presentation layer and leave us alone when we step out from “presentation” boundaries. But can we build some abstractions that will work on every layer and across multiple applications no matter what?

My answer is yes. This recipe includes Clean Architecture, MVVM, RxSwift, and Dependency Injection. The glue how we put it together is the Composition. Composition is one of the essential things that help developers deal with difficulty and requirements mutation. As long as you make things composable, your abstractions will face all bumps and turns.

Let’s take a look at a simple example — the user screen. This screen should display the current user name and have a “Sign Out” button. On the domain level, we have an entity — User, which contains all user info.

struct User {
let id: String
let firstName: String
let lastName: String
let permissions: [Permission]
}

As I mentioned before, we already have a bunch of well-known patterns for the presentation layer. I consider myself a big FRP (Functional Reactive Programming) fan, so my choice was quite obvious — MVVM with RxSwift.

struct UserScreenViewModel {
let name: Observable<String>
let signOut: () -> Void
...
}

It was the easiest part. That is why I don’t want to talk about a presentation based on MVVM-RxSwift. There are a lot of good articles about it, while I prefer to stay focused on the upcoming part. This is the part I’d like to really talk about and explain it as clearly as possible. Sorry for my drawings.

Thus we have ViewModel on the one side and some source of our User on the other. How will we connect them? The nasty dirty hack that comes to mind is putting all Stores, Managers, and Repositories we need into the initializer. Oo

init(userStore: SomeUserStore, locationManager: SomeLocationManager, …, justAnotherManager: BullshitManager)

But how it makes things composable? Where should I put a clean User’s domain code? How to provide the granularity of operations what this particular object can do? How to reuse it in some other place?

Instead, we aimed to decompose our application into multiple “pieces” and abstract them one from another with low coupling inspired by Clean Architecture idea. Clean Architecture, someone calls it Onion Architecture, is a decomposition of your application into layers and unidirectional data flow. In this article, I am talking about my real-life implementation and our example. So we put away digging deep into Clean Architecture itself.

We are going to abstract all the things our ViewModel needs into small pieces called UseCases. UseCase is a protocol with one particular function. It will provide us granularity for operations we are about to give our ViewModel and precise semantics because we can easily understand what this specific model can do at one glance.

You can see that you can compose as many UseCases as you like and anywhere you need. The initializer, just waiting for the object confirming them. We will return to that object a little bit later. Now I’d like to focus on our business domain code. In UseCases, we deal with a tiny decomposed piece of our program and don’t care about the rest of the world. In other words, we can do some Protocol Oriented Programming and define the following statement: “Any object which has Repository A can do something.”

It’s cool. We don’t care where the data are coming from or what purpose they serve. We are just dealing with a very tiny problem that can easily fit in our minds. That’s a UseCase.

Let’s transform the diagram above into something closer to our app.

And here it comes, the Context. Context is the “bridge” between all the stuff that our ViewModels require and managers, services, repositories, etc. Usually, each screen has its Context, which constructs from a Dependency Injection container. The primary idea here is that Context doesn’t have any code. It holds the repository properties and gets all the code it needs via the composition of UseCases.

That’s it. We achieved very low coupling and made things composable we are prepared for new requirements by adding new repositories mechanism and UseCase composition. We can write UnitTests with mocked UseCases or Repositories. We can even rearrange our ViewControllers, and as long as we have the required objects for its initialization in Dependency Container, they will be instantiated.

Furthermore, to put everything together, I have a straightforward repository which I am using from time to time on accidentally workshops for newcomers or some other occasions. The “master” branch is a starting point from where I explain everything I said above. After that, we connect the new repository, and the result is “workshops/ComposableContextsArchitecture” branch. I hope it helps.

Thank you for your time!

--

--

Mykola Fiantsev
Mykola Fiantsev

Written by Mykola Fiantsev

iOS developer, engineering enthusiast, biker. 👀

No responses yet