Flexible modular VIPER architecture for iOS with code generators

As the app grows, the familiar layer scheme begins to slow down builds and release updates. We design and implement a turnkey modular VIPER architecture for iOS: we break the project into independent modules, remove coupling, and accelerate builds multiple times over. The team gets a framework that withstands product growth and developer turnover.

Development and support of all types of mobile applications:

Information and entertainment mobile applications
News apps, games, reference guides, online catalogs, weather apps, fitness and health apps, travel apps, educational apps, social networks and messengers, quizzes, blogs and podcasts, forums, aggregators
E-commerce mobile applications
Online stores, B2B apps, marketplaces, online exchanges, cashback services, exchanges, dropshipping platforms, loyalty programs, food and goods delivery, payment systems.
Business process management mobile applications
CRM systems, ERP systems, project management, sales team tools, financial management, production management, logistics and delivery management, HR management, data monitoring systems
Electronic services mobile applications
Classified ads platforms, online schools, online cinemas, electronic service platforms, cashback platforms, video hosting, thematic portals, online booking and scheduling platforms, online trading platforms

These are just some of the types of mobile applications we work with, and each of them may have its own specific features and functionality, tailored to the specific needs and goals of the client.

Our competencies:

Frequently Asked Questions

Latest works

  • Development of a mobile application for FEEDME
    Development of a mobile application for FEEDME
    971
  • Development of a mobile application for XOOMER
    Development of a mobile application for XOOMER
    869
  • Development of a mobile application for RHL
    Development of a mobile application for RHL
    1289
  • Development of a mobile application for ZIPPY
    Development of a mobile application for ZIPPY
    1143
  • Development of a mobile application for Affhome
    Development of a mobile application for Affhome
    1076
  • Development of a mobile application for the FLAVORS company
    Development of a mobile application for the FLAVORS company
    677

What a modular framework provides: build speed and predictability

A modular framework based on the VIPER pattern splits an application into independent modules, where a screen is a set of isolated layers: view, logic, data handling.

The layers are connected only through contracts, so a change in one screen does not affect neighboring ones, and the build rebuilds one module instead of the entire project — project build time stops growing with the number of screens.

We don't write identical layers by hand: a module generator creates stubs from a template with unified entry points and already connected dependencies, and the developer immediately writes logic.

The structure remains the same across all sections — a new engineer reads an unfamiliar screen by the same rules as their own. We keep the framework within Apple conventions for the current platform branch.

Screen reuse here does not work as code copying: typical scenarios — authorization, list, card, filters — are extracted into standalone modules and connected to a new section without edits inside.

The amount of new code per screen drops several times over, and the behavior of typical blocks does not diverge between sections.

Isolated layers provide predictable checks: we test logic separately from the interface and without launching the entire application.

The share of paths covered by checks grows, and defects at module boundaries — the most expensive category by cost of fixing — stop reaching release.

Ways to organize VIPER layers: comparison of approaches

VIPER layers can be assembled in different ways, and the difference is visible not on the first screen but on the twentieth — when a change to one scenario pulls in the View, Router, and the entire test suite.

The key question: where module boundaries run and who owns the contracts — the protocols of the Presenter, Interactor, and Router.

Option When it fits Limitations
Manual contracts within a single module 2–5 screens, short deadline Manual copying, layers diverge, tests are duplicated
Template-based generation Dozens of similar screens The template dictates the structure, the generator must be updated on a live project
Separately built modules Multiple teams, reuse More expensive start, strict rules for contract versions
Shared layer of contracts and modules Large product with flows Without discipline, the shared layer grows

The choice is along two axes: number of screens and number of teams. While there are few screens, template-based generation removes manual copying and locks the contract in one place, but a shared layer is not needed yet.

When independent flows and reuse appear, we move modules outside the build boundary and version the contracts.

The transition is staged, without rewriting: first we introduce a template, then extract a ready module — layers stay in place, only the contract address changes. This path lets you compare options on your own code, not in a presentation.

Stages of migrating an application to a modular scheme

Migrating a running application to a modular scheme is not a one-off refactoring but a sequence of steps, each ending in a verifiable result. We implement the modular scheme in waves: first we record the current state, then change one area and make sure behavior hasn't shifted. Development does not stop — releases continue to ship.

The order of work is as follows.

  1. Dependency audit. We build a connection map: where logic is placed in the View layer, which classes pull half the project with them, where cycles and hidden references to shared state are. Output: a list of nodes and the order in which to split them.
  2. Module scheme and migration plan. We define boundaries: what becomes a standalone module, what goes into the shared layer, which interfaces are public. The migration plan is split into waves — one or two modules per release, so changes remain manageable.
  3. Setting up code generators. We set up module templates — View, Interactor, Presenter, Entity, Router — with unified names and folder structure. A new module is created with a command, not by copying a neighboring one, so inconsistency between areas does not accumulate.
  4. Pilot module. We migrate one independent area entirely to the new scheme and integrate it into the application without changing behavior. The pilot shows the real cost of the step and how much time generators save.
  5. Regression check. We run automated tests for Interactors and Presenters, manually go through critical scenarios and compare behavior before and after. We separately record build time and artifact size — they show that modularity works, not just is described.
  6. Documentation handover and team training. We hand over module schemes, boundary rules, code review checklists and generator instructions. We go through everything on live code so the team can further migrate modules on its own.

What remains with the team after project handover

Project handover is not an archive with source code but a set of artifacts the client's team uses to continue work without the authors. Everything listed lies in the repository next to the code and in regulations: an engineer reads them and takes a task to work independently.

  • Module documentation — purpose, inputs and outputs, connections with VIPER module layers; it's clear what can be changed in isolation without affecting the rest of the application.
  • Templates and generation settings — module stubs and configuration of tools that create new code according to the adopted standard; the template is edited in one place, and subsequent modules are born consistent.
  • Dependency map — diagram of module connections so new functionality doesn't turn the architecture into a tangle.
  • Code review rules — reviewer's checklist: layer boundaries, error handling, test coverage, naming agreements.
  • Release procedure — path from branch to publication in app stores: who approves, how the changelog is prepared, how a failed release is rolled back.
  • Instructions for new engineers — environment, entry points, typical first-week tasks; becoming able to make independent changes takes days, not months.

We separately record who on the team owns each artifact and when it is updated — otherwise documentation diverges from the code within a couple of iterations.

Case study: reduced build time and removed a class of failures

Case study of a mobile application in Swift: at the start of work, it was built as a single build where any screen pulled all code with it. A change in one place triggered a rebuild of the entire project, and the verification cycle took minutes the team didn't have.

The more costly part was not the delays themselves but errors: most failures surfaced not inside screens but at the boundaries between layers, where one layer called another directly.

Migration to modules

We decomposed the application into layers according to VIPER — View, Interactor, Entity, Presenter, Router — and fixed the contracts between them.

Code generators create the skeleton of a new module and test stubs, so the developer immediately writes logic instead of reproducing identical files. The build became incremental: a change in one module does not pull the others along.

Numbers before and after

Full build was reduced threefold, build after a typical change — from tens of seconds to single digits. Share of failures at module boundaries fell from 30% to 6%: contracts are checked at build time, not during testing or release.

Effort to add a new screen — 2 days instead of 1.5 weeks, because structure, navigation and tests are already generated. Rolling back an individual module now also does not require rebuilding the entire application.

What to do if the monolith is already deployed and cannot be rewritten

Rewriting the monolith entirely is not necessary and is almost always harmful.

The question is not whether modular layers can be introduced, but how to do it in parts: new iOS code is built on the VIPER architecture, while the old core continues to work alongside and gradually loses weight.

Coexistence of old and new code rests on module boundaries. Each new screen gets its own module, where view, logic, navigation and data handling are separated and know nothing about each other, and the monolith calls it through a thin interface. Boundaries are drawn where changes happen most often — these are the areas that pay off first.

Release process does not stop. One release adds one or two modules and migrates one or two legacy areas, build and automated tests cover both layers simultaneously, and rollback is possible at the level of an individual area, not the entire application. Already the first deliveries reduce the share of changes that affect unrelated parts of the code.

Gradual introduction of modules wins over complete rewriting in terms of risk.

With rewriting, parity with current behavior has to be restored from indirect signs, timelines grow, and the old code all this time continues to change and diverge from the new branch — release is postponed until everything is ready at once.

Therefore phased rework is planned by releases: the share of code under modules grows, the share of changes in the monolith falls, and the scope of the first stage is calculated by the number of areas with the highest frequency of changes.

Technical doubts: tests, dependencies, hiring for the scheme

Objections to a strict layered scheme repeat from project to project: won't it complicate support of old code, won't it require a dedicated engineer, and how much time will team training take. We answer to the point.

How to test layers separately?

Presenter is tested without the interface, Interactor — without network calls: dependencies are replaced with stubs at protocol boundaries.

Scenario logic is covered by fast checks, while heavy end-to-end runs remain for assembling the entire screen — the test suite stops growing along with the project.

What to do with dependencies between modules?

Modules communicate through declared contracts and a factory, not direct calls, and dependency assembly is moved to a separate layer.

Replacing or removing a module doesn't pull a chain of changes across the entire project, and the same screen is reused in another scenario without copying code.

Is a dedicated engineer needed for support?

As a rule, no. A change is localized to one layer, so the task goes to whoever is free, not to whoever "wrote this screen." Support effort also falls because you don't need to keep connections across the entire codebase in your head. Old code is migrated to the scheme gradually, as changes are made, without stopping releases.

How quickly does a new developer get into the project?

First change in a working version — within the first week: each layer has one area of responsibility, and code generators assemble a new module from a ready template. Rules and examples are fixed in a short internal guide, so training doesn't depend on one person.

Let's discuss the task and come back with a work plan

To make the conversation specific rather than general, it's useful to attach what you already have to your inquiry. You don't need to prepare presentations — working context that shows the real scope of work is enough.

  • product description and key user scenarios — two or three paragraphs are enough;
  • current state of the code: whether there is a project, at what stage, what prevents developing it further;
  • list of screens and transitions between them — the module scheme for the project grows from it;
  • integration requirements: authorization, payments, analytics, notifications, offline mode;
  • constraints on timelines and platforms, access to existing repositories and accounts;
  • how release currently works: build, checks, publication in app stores.

After your inquiry, we analyze the situation and come back with an estimate and work plan.

The plan includes introduction of a modular scheme: each module covers one scenario and doesn't pull neighboring ones along, code generators remove routine from typical screens, and mobile app support follows a clear release procedure.

Describe the task in any convenient form — we'll come back with an estimate and work plan. If there is no code yet, that's not a hindrance: we'll build the module scheme for the project from scratch, for your product and your team.