How did we start Vivid Money for iOS

Ilya Kharabet
Life at Vivid
Published in
7 min readMay 28, 2021

Hi everyone. My name is Ilya and I’m an iOS TechLead at Vivid Money. We have been developing our fintech product for more than a year and we are now ready to share our experience and knowledge with the community.

This is an introductory article in which we’ll tell you superficially about several technical decisions that we made at the beginning of the development and then we’ll post detailed articles about the most interesting of them.

Architecture

First of all, we decided on the project architecture. I mean not only an architecture of some screen (or module), but all other things related to architecture. I can’t put all these things in this article, so I’ll tell you about three: project architecture, screen architecture and dependency injection.

Project architecture

Since the project was supposed to be large and divided into products, we decided to divide it into several modules. This helps to better structure the code and also makes it easier to develop in product teams. The module architecture looks like this:

There are 4 layers in the diagram:

  • Core. These are projects that are either not associated with the application and can be used anywhere, or those that all higher layers depend on.
  • Platform. This layer contains projects that are only related to the application. For example, DesignKit contains all things related to the app UI and our design system.
  • Features. Projects in which individual features or entire products are developed. These projects are not related to each other, which makes them faster to develop, easier to test, and makes it easier not to mix code from different products.
  • App. This brings all projects together, configures 3rd-party frameworks and contains all routing.

Screen architecture

We decided to find something that would satisfy our needs and does not contain anything superfluous. As a result, we came to VIP (View, Interactor, Presenter). We kept the VIPER basics, but removed the Router and Entity. This separation of the module helps to better test it. Also, the separation came in handy in some places where it was necessary to use different implementations of the view or interactor (yes, this is a possibility).

We replaced Router with Coordinator. This is a good pattern that allows you to make modules independent of each other and concentrate all the transition logic of a user story within one class.

Dependency injection

We don’t use a library for dependency injection. I could end here, but maybe it’s better to explain why.

Firstly, we do not really like to connect third-party frameworks, especially those that can be easily replaced with your own solution. Secondly, we have not found any practical benefit from DI frameworks.

In our project, dependency injection is simple and has two parts:

  • Dependencies definition:
    We have the DI class that contains a few functions to manage dependencies. To define a new dependency we create an extension for DI (or use an existing one) and put the dependency definition there. Each project has only one extension for DI with all dependencies.
  • Dependencies resolving:
    Usually, we resolve dependencies in a special class called Assembly. This class exists in each VIP module and creates all its components. Dependencies are injected into the components through the initializer (except cases when 2 components have references to each other).

Example:

extension DI: PlatformDI {
static let platform: PlatformDI { shared() }
var contactsService: ContactsServiceProtocol {
stored(by: #function)
}
}
let service = DI.platform.contactsService

Managing third party dependencies

In terms of dependency management, everything was like in most projects — we used CocoaPods. It’s a proven dependency manager and almost all open-source libraries support it.

After a while we had relatively many dependencies that we could not do without (Firebase, Amplitude, AWS and other similar frameworks), and their rebuilding took a lot of time. Then we decided to try Carthage.

In the first iteration, we made a symbiosis of these two managers, since not all libraries supported Carthage. But after a while, we completely switched to Carthage and, in addition, implemented Rome, a utility for caching libraries.

Now we use SPM for our internal frameworks. We are going to get rid of Carthage in the near future, but we are waiting for a build cache support like in Carthage.

Testing

At the very beginning of development, we dreamed of the absence of manual testers, which would allow us to make short release cycles and get rid of the human factor when testing functionality. Unfortunately, there were circumstances that did not allow us to achieve this at the start, but we are confidently moving in this direction. In any case, we had to think about organizing product testing. As a result, we came to the conclusion that we will write Unit, UI and snapshot tests.

Unit tests

For Unit tests, we had been using the SwiftyMocky framework, but at some moment decided to delete it due to rare use. In an already established paradigm, we test using a Given-When-Then structure so that all tests look the same and are logically structured.

Unit tests are mainly written for common components (utilities, services, etc.) and classes with complex business logic (in most cases, Presenter and Interactor), which will be quite problematic to verify in UI tests.

UI tests

UI tests appeared much later. In them, we also did not invent anything special and made several auxiliary classes to implement the Page object pattern.

UI tests in our project are divided into 2 types: component and end-to-end. Component tests check the operation of an individual screen or part of it using mocks, and end-to-end tests check some chain of screens and use a real API, but on the dev environment.

Snapshot tests

We implemented snapshot tests for UI components. It’s more helpful at the beginning, because it’s cheap and gives a big coverage per cent (if you use the same components across the app).

API clients generation

Our backend is divided into microservices, which means that in the application we also access several APIs. Keeping track of each one and updating the code manually is a difficult task, and we decided to automate it.

Each microservice has a different specification with which we generate frameworks using Swagger Codegen. We slightly changed the templates for generation to suit our requirements, and automated the process of updating frameworks on CI.

Each generated API client is in a separate repository and added to the project using SPM.

There are improvements that can be made in terms of generating client APIs, but this approach has already given a huge increase in development speed and eliminated the need to manually update the API.

Code conventions

We needed to take some steps to ensure that the code was understandable to everyone anywhere in the application and developed quickly. Several such steps have been taken.

The most basic is writing code conventions. All rules and syntax guidelines are concentrated there. Almost all of them are checked using SwiftLint, and for some of them there are custom rules. Code style conventions help us to review code faster and make code easier to read.

The following were described:

  • Rules for working with the repository: how to name branches, how to write messages to commits, and so on;
  • The process of completing a task: what tasks can be taken, task priorities, task statuses, how to create a pull request;
  • Patterns and mechanisms used: how to solve typical tasks (cache data, create services, etc.);
  • Terminology: Typical names for methods or business definitions in code.

All of this helps us to design in the same way and not deal with problems that have already been solved.

We also use Danger CI to validate pull requests. The list of our rules is small at the moment: checking for filling in the required fields in the pull request, checking for the number of changes made, searching for TODOs, notes from Swiftlint and a couple of recommendation messages. This helps to keep important details in mind and to remind you of things that can be easily forgotten.

Automation

In a project with a large code base and a growing number of developers, automation is indispensable. Therefore, the right decision would be to pay attention to this at the very beginning of the project.

Scripts

We’ve written several of our own scripts that automate work. We divide them on the groups:

  • Code generation scripts (screen modules, projects, etc)
  • Project setup scripts (updating dependencies, setting up an environment, etc)
  • Scripts for downloading resources (localization, feature toggles, etc)

To avoid merge conflicts in project files, we use XcodeGen, which generates project files according to yaml specifications.

With so many scripts, the question of ease of use arose, because remembering their names, arguments, and order of invocation is not the most interesting task. So we created a Mac OS application which has a convenient UI and other features that make our life easier.

Project setup

Since our project is not the easiest to configure, and requires some utilities and calls to various scripts, we decided to simplify its configuration.

At first it was an executable file that had to be run in order to download all dependencies (ruby, brew, python, and so on) and perform the necessary settings. But later we implemented project customization via Ansible. This allowed us to keep everything in one place and configure not only employees’ computers, but also build agents.

Conclusion

We talked briefly about those things and the experience that we have gained when we just started taking the first steps in the project.

We plan to continue to share our development experience as we consider this an important part of the development of the community. To help us do it better, write comments and constructive criticism. We would be very grateful for any feedback.

Thanks to all!

--

--