Modular iOS Part 1: Strangling the Monolith
Most iOS engineers are familiar with the iOS Monolith. I’m referring to a codebase that is either a single project itself, or consists of a workspace that contains only one project (plus the possibility of a Pods
project).
Does your workspace look like this?
In the example above, that FunkySocial project will become pretty big, pretty quickly, if other popular social media apps are to go by!
The project might include features such as a news feed, user profiles, friend lists, events, groups, photos, videos, third party content views, messages, stickers. The list goes on.
When starting to develop any app, it’s rare that we can foresee the scale that our app will grow to. So it’s very common to build everything in a single project. Bravo to those that plan ahead with a modular workspace from the beginning! 👏
For the rest of us, our single project will grow out of control, at which point we should consider “strangling” our monolith, splitting it into individual feature modules. This is similar to the micro-service architecture our fellow platform engineers refer to.
“Strangling” in this sense is a term that Martin Fowler uses to describe code migration, gradually creating a new system around an old system, until the old system is strangled of functionality and therefore becomes redundant.
I use it in a similar sense, to describe migrating our iOS monolith into feature modules. Gradually extracting functionality from the monolith, creating one feature module at a time, until the the main app (previously a monolith) is suffocated of all responsibility.
The diagram above shows how we organise our code into vertical slices of functionality.
Even with a single project codebase, you might already have architectural separation similar to the diagram above. But there are advantages to making this separation explicit with separate Xcode projects.
Not a new concept, but…
There’s nothing new about this concept in iOS, but the introduction of Swift certainly makes it simpler. In Objective-C it was always a bit of a hassle to separate internal
scope from public
scope (requiring separate header files for each).
With Swift, it’s super simple to expose internal
functionality across the module. And all our module code is internal
by default, so we’re forced to limit what we expose to other modules and to our main app.
Layers in detail
Main App
The Main App contains the App Delegate, and that’s about it! It might also contain some compile-time configuration that is later injected into the feature modules. The main app may also contain some kind of root view controller, perhaps a tab bar controller, depending on the type of app.
Feature
Each vertical Feature module is completely independent. Your feature modules should expose as little public functionality as possible. For example, if your app is based around tabs, you would expose a “Router” object that your Main App can embed. And that’s it. Sometimes your user journey involves navigating between views in a different modules. In this case, you would delegate the navigation to a “Router” object in the Main App project.
Shared
There may be some modules that need Shared functionality. For example, perhaps you have a NewsFeed
module and a Profile
module, which both rely on a Videos
module. But you might have an Auth
module that doesn’t require this dependency, so you limit the feature modules that depend on Shared functionality.
You might have model objects that need to be shared between modules. You might consider breaking down your modular codebase into separate UI and model modules, such as VideosUI
and Videos
(as seen in the iOS SDK, for example Photos
and PhotosUI
, Contacts
and ContactsUI
, etc.).
Common
And there will likely be Common functionality exposed to the entire codebase. This is where you expose common helper objects and extensions.
Benefits
The benefits are aplenty. It enforces separation of concerns between features, because by default everything has internal
access level, and you have to explicitly add modules as dependencies.
Individual modules can be written using different architectures. For example, you might follow MVVM in one module, but VIPER in another. This is good if you’re trying out a new architectural approach. Modules can even be written in different languages. If you have a large Objective-C codebase, you might consider strangling it and migrating individual modules to Swift.
By having a modular codebase, you can also migrate modules to the next version of Swift on an individual basis. You don’t have to migrate the entire codebase all at once.
The most valuable benefit for me is that it makes it easier to reuse modules. Splitting your workspace into feature modules might be a step towards splitting out these features into separate repositories to be used by other apps. Or you might even build a secondary app from the same repository. See the image below.
In our social media app example, we might build a messaging app from the same codebase. The Secondary App only consists of the App Delegate and little else, but it can depend on whichever Feature modules it requires.
The result!
The result is that you end up moving away from the monolith shown on the left below, towards the beautiful modular workspace shown on the right:
Note you might have model objects within the feature modules. It’s only those model objects that need to be shared across feature modules that need to exist in separate modules themselves.
Coming up next
In the next article in this series, I’ll go into the implementation detail of splitting up your codebase. In subsequent articles I will cover testing across multiple modules, setting up dependency management and sharing configuration between modules.
If you’re interested in solving problems like this, and want to join the most passionate, collaborative iOS team in London, head to our careers page! 😎