Scalable modern architecture for iOS projects — Part 1

Alejandro Barros Cuetos
7 min readJun 13, 2021

--

A modern architecture is composed of many different parts

Many articles and books describe different flavours of clean architectures for iOS projects in detail, but, unfortunately, real-life projects with growing and sizeable mobile teams are not usually that straightforward.

While researching to define a “real world” architecture, I wasn’t able to find a source that provided a fully defined architecture (User Interface, deep links management, communication between layers, routing etc.) to use as a reference.

This is the objective of this series of articles, not a statement on how to define and architect your application, but to reference and interpret several sources, patterns, and approaches and how they could work together. Every project is different and has its requirements; this is just a series of examples that worked well for several projects, with variations and changes to adapt the architecture to the project and team, never the other way around. The idea is not to provide a template to use all the time, but a series of tools and references that will allow you and your team to define the architecture, tools and ways of working that best suit you and your team.

What will be covered in the series?

This series of articles aims to cover almost all aspects of building and maintaining an enterprise-level iOS application. When deciding on the tools and technologies, there is always many options (CoreData, Realm, etc.) and choices to make, sometimes based on requirements and other times based on the team’s previous experiences. This series of articles, like any project, make several assumptions in technology and tools but always trying to abstract those decisions as much as reasonable, allowing for easier replacement later down the line.

General architecture definition and implementation

The next article will introduce the general definition of the architecture based on the principles defined later in this article. It will also start creating the abstractions needed to implement the main layers of the architecture. In this section we’ll cover ViewModels, ApplicationContext, Coordinators, etc.

Definition of the different layers in the architecture

We will expand on several of the critical layers of the architecture, both from the definition and implementation point of view. We’ll cover StorageContext, Managers, etc.

Environments and secrets management

A vital element of any enterprise project that is often an afterthought is setting up and managing different environments (not only Debug and Release but with varying back-end environments) and keeping the application’s secrets secure . At the same time, maintaining a pleasant development environment for your team.

Analytics, Deep-links, Logging, UI, etc.

Many of these requirements are added to architecture only when needed in a less than optimal way. In reality, for almost any enterprise project, they are complex requirements that should be considered since the beginning to provide a clean and testable approach that scales as your team and project do.

Modularisation

This is a controversial one; unfortunately, I don’t have a magical recipe for the right balance of modularisation without falling into dependency hell. Each team will need to find the right approach that works for them and the project. I’ll show some basic guidelines from a minimal point of view, but the good news is that some of the principles followed by the architecture simplify migrating to modules when needed.

Testing approaches and implementation

We’ve all worked in projects that testing wasn’t a priority for the company or that testing was complex because of the codebase structure, hence not worthwhile. Fortunately, this is changing; most companies now realise the vast benefits of having a good testing strategy that will reassure a fast release cycle and make the developer’s life easier. The architecture, as stated later in this article, should be defined with testing at the forefront. Thus, facilitating the implementation of the testing pyramid. I’ll cover Unit Testing, Snapshot testing and UI Testing.

Setting up Continuous Integration & Continuous Deployment

Any enterprise-level project will need an effective and automated CI / CD integration that doesn’t require constant involvement from the team. Using tools like Firebase, GitHub and Bitrise I’ll go through setting up a complete CI / CD system that executes our test plans, distributes development builds to our stakeholders and uploads them to the Apple Store when we’re ready for release to our customers.

Open source full project

The last part of the series will be a complete project, developed using the techniques discussed, available entirely in GitHub.

Assumptions / Requirements

All the projects need some assumptions from the start based on an initial set of requirements. With all the changes that Apple is introducing to the development environment, we have some controversial decisions.

UKIT vs Swift UI

This topic will give, on its own, several articles and discussions. What follows is my personal opinion at the moment of writing this article. SwiftUI is the future of iOS and macOS development; I don’t think there is any discussion possible on that, but, in general, I follow this decisssion flow:

  • Any personal project, go with SwiftUI, is much more fun and new; I can use all the practice with it that I can get.
  • Professional projects, unless it will only support iOS 14 forward (SwiftUI in iOS 13 is a source of constant headaches) and is not expected to be developed by a big team, use UIKit.

Most companies still need support from iOS 12 (sometimes even lower) to reach the majority of their users. However, the articles aim for projects with a sizeable number of developers, so I’ll use UIKit for it.

At the end of the series, an extra article will show how to adapt the architecture (what is possible and/or makes sense) to a SwiftUI environment.

RxSwift vs Combine

Even since iOS 13, Combine is mature and friendly to use, so if your project only needs to support iOS 13 forward, I would certainly consider using Combine instead of RxSwift. My personal opinion, though, is that Combine excels itself when used with SwiftUI as the integration between them is fantastic.

For these articles, I’ll use RxSwift, and its companion RxCocoa, which massively simplifies FRP (Functional Reactive Programming) with UIKit, which uses the Target-Action and Delegate patterns.

The good news is that RxSwift and Combine are pretty similar (they use identical principles at the end), so anything unrelated to UI will be relatively easy to migrate later down the line.

Architecture principles

Any architecture, or probably better named architectural design pattern should aim to solve and standardise the solution for a series of challenges and issues in a project.

Not a single architecture pattern suits any project

Real-world, large-scale mobile projects have some common challenges with web projects, but, contrary to web development, iOS doesn’t impose the patterns and architecture to use; it gives more freedom to the developers. Although this is great from the point of view of development and probably from an engineering perspective, one of its main attractions, in large-scale projects, can cause many issues; a set of principles that the selected architecture should address is an essential first step.

Single Responsibility Principle SRP

Each architecture component (being a ViewModel, Repository, etc.) should only fulfil one function. All its parts should only contribute to that responsibility. This simplifies the reusability of components and facilitates testing.

This principle applies to all level of the architecture/application; one that is usually overlooked is when using modularisation, a specific module should only address one high-level function; for example, a module that provides UI Elements should only do that, provide UI Elements, any data transformation should be happening somewhere else.

Decoupling

Making the different components of the architecture decoupled to each other will allow us to be far more efficient in testing (Protocols, Mocking, etc.) and increase reusability. For example, a decoupled ViewModel that does not have any awareness of the view layer could be reused for iPhone, iPad and Watch views.

Inversion Of Control IoC

This principle is the key one to provide scalability to the architecture. By inverting the control flow, we provide “independence” of the rest of the application to a “module/section” of the program, allowing different teams to work in different parts of the application without stepping into each other toes.

Each section will provide a “public” set of routes that is how that section interacts and communicated with the rest of the application.

Modularisation

It’s a powerful tool, but as Uncle Ben said:

with great power comes great responsibility

Finding the right balance is complicated and easy to fall into dependency hell; some basics ideally should be followed like User Interface elements, Network layer, etc. But with IoC and in a modern architecture increasing, the modularisation of an application is supported if needed.

Standardised User Interface

To avoid reinventing the wheel and different approaches for UI and interactions, the architecture should provide a UI layer that can be reused easily through the program and allow for easy changes to be rolled out across the app, like new devices, dark mode support, etc.

Multi-device support

The reality is that most companies develop their applications for iPhone, which is understandable as it provides, by far, the biggest pool of potential users. Still, as applications and projects evolve, it might become essential to have custom interfaces for iPad or even support TV OS and Watch OS. Therefore, the architecture should allow for an easy way of supporting new devices/platforms without having to refactor the entire project.

Testability

In modern applications, testing is critical, and mainly automated tests. Therefore, any modern architecture should facilitate the simple implementation of at least Unit Tests, but ideally Unit, Snapshot and UI testing to get the most reassurance.

The following article will define the architecture based on these principles and implement the essential components and protocols needed.

--

--

Responses (1)