Our approach to building modular iOS apps

profile picture

Yannick Engineer

17 Nov 2016  ·  11 min read


Modular applications are a powerful setup – with a lot of caveats. In this post, we walk you through our setup and learnings from the past!

Lately, we’ve noticed a lot of interest in the iOS development sphere surrounding modular applications. Since we’ve built quite a few of those over the years, we’d love to share our experiences. We’ve built modular apps for very diverse projects to meet equally diverse needs, and we’ve learned a lot in the process… In this blog post we discuss our current “state-of-the-art” approach, and, equally important, when not to use modularity.

Where it all started

To walk you through our current setup for building modular applications, it’s useful to dive into our history with them first. We first chose to go with a modular setup because of something we’ve encountered while defining the strategy for the original version of Medialaan’s VTM app. VTM didn’t have a mobile app at the time, but they were building small, temporary companion applications for their TV shows. These apps were temporary because they were only relevant while that particular TV show was on air, usually only for a few weeks.

As a solution, we decided to build a kind of ecosystem, containing both the VTM app’s core features (the TV guide, videos, …), and the temporary integrations for TV shows. This seemed like a much more relevant approach, and got rid of the overkill of having to manage multiple applications (including the extra hassle of certification and provisioning ;) ). To properly accommodate these temporary integrations, we introduced our very first modular setup.

There were a number of goals we were looking to achieve with this setup. Firstly, we wanted to be able to easily add the code for these integrations before the start of the TV season and remove it when the season was over. Secondly, one of the most important requirements was that these integrations automatically had to show up in the hamburger menu – yeah, we know, super old-fashioned, but it really was all the rage back then. We really wanted to be able to add and remove an entire integration - or module - with 1 line of code.

diagram 1

This first version of our modular setup had a reasonably straightforward architecture. The application core contained a Module Owner class that managed and owned all modules. The module owner keeps an array of all active modules; the hamburger menu then uses this list to display them. These modules would be called by their class names, so if no class existed for these modules, they wouldn’t be added. The modules each had a configuration for their hamburger menu representation. They also functioned kind of like an independent app, except for the fact that they had references to certain parts of the parent application, for example databases, user info, etc. Finally, each module was responsible for showing its own ViewControllers.

Evolution and overkill

Not long after building the VTM application, we used the same setup for another project: the My BASE app and its rebrands. We chose to use the same modular setup because the client indicated they’d like to be able to easily include or exclude functionality based on differences between their different brands (Base, JIM, Allo RTL, Turk Telekom, …). In reality, more functionality was only ever added, never removed. Nevertheless, we extended the features of the modules, which increased the possibilities of how they could appear throughout the app. They could now add cards to a feed and settings to a settings screen, all from within the module’s code – without touching our “one line of code to include the module” principle.

In hindsight, our modular setup for the My BASE app (and its rebrands) wasn’t really necessary, although the app functions quite nicely with them. We did, however, have to account for the fact that some of the branded versions don’t require the same functionality as the main My BASE app. To be able to solve this specific issue in later products, we introduced another concept, called ‘feature flags’, which are configured on the rebrand level.

There seem to be a number of different definitions of feature flags out there; for us, a feature flag is a centrally defined (usually in some kind of config file) boolean, which can enable and disable a particular feature. The feature itself is responsible for monitoring this boolean, which means it enables or disables itself when necessary. Unlike a module, with a feature flag the code is always contained in the project. The advantage over modules, of course, is that it allows features to be switched on or off on the fly.

The next steps

Our next modular project was also created together with BASE Company: the Mobile Workplace. This is BASE Company’s internal utility application, and it was the very first POC for what today is Spencer (you can read all about Spencer on getspencer.com).

At the time, we were already toying with the idea of introducing the Mobile Workplace concept in different companies, which led us to create it as a modular app. For Mobile Workplace, we didn’t make drastic changes to the architecture as we defined it for VTM, although a few things were added. Mobile Workplace needed multiple menu items, action menu items, and, exactly like the My BASE app, a number of cards in a feed and settings on a settings screen.

In the end, the Mobile Workplace app remained an internal tool at BASE Company, which meant the modular setup was, much like for the MyBASE app, a bit of overkill. It did, however, create a nice basis for what would later become Spencer.

While one team was building Mobile Workplace, another was constructing the new version of the Appmiral framework.

Our festival app framework was also (you guessed it) built to be entirely modular, because different music festivals required different functionality, depending on the package they ordered from us.

For Appmiral we faced an additional challenge. Modules were no longer only represented in a menu, like a hamburger menu or a tab bar, but instead could also have a visual representation in the view controller of another module. There’s no better example of this than our artist detail page, which is owned by the Artist module. The Spotify player that is shown on these detail pages is owned by the Spotify Playback module. Why? Because some festivals are sponsored by Spotify, and others by its competitor Deezer, so this is a functionality that we need to be able to add or remove per project. This unfortunately means that our artist detail page has to know about the player and has to know what type it is.

This actually goes against one of our basic principles for modular apps: we normally never make modules reference each other, because that undermines the clean distinctions that make modularity so useful.

Appmiral is a second example of a product where, looking back, a setup with feature flags would have been the better option. But of course, there’s no need to be glum: we took our modularity learnings from each of these projects, and kept fine-tuning our modular app setup and the best use cases to put it to practice.

Our state-of-the-art modular apps

And we’re happy to say that we now have two brand new modular apps in continuous development – and they incorporate the best of our modular development learning curve.

The first is the VTM app we already mentioned earlier. This year, we completely rebuilt the original app, in order to modernise its UI and UX and add a number of new features VTM has added to their repertoire. The second is Spencer, an in-house product that aims to combat over-tooling and needless complexity in the workplace.

Let’s start with the new VTM app: how would it implement this modern modular setup? VTM now has a new section called apps. This section is in fact an overview of the temporary integrations we mentioned earlier, with a small preview of each integration. These integrations are the modules in our modular approach.

screen new vtm app

These integrations make use of the Medialaan libraries we have as part of our own internal libraries, but they don’t have a reference to the VTM app itself. The integrations don’t reference each other either. The VTM app itself, in addition to containing build settings, branding, etc, also makes use of the Medialaan libraries. This way we can still share the database, UI components and user info between the the app itself and its integrations. Integrations would now register themselves to the module owner.

diagram 2

With this setup, we can now split our code into multiple git-repositories. In addition, we can use private Cocoapods, a Swift and Objective C dependency manager, in order to manage the Medialaan libraries and integrations. This means that the cocoapods configuration file is now where it’s decided which integrations will be used. Enabling or disabling an integration, then, just means adding or removing it from the Cocoapods configuration file.

Another huge advantage of this setup is that because an integration only uses the Medialaan libraries turning an integration into a standalone application only takes a few clicks.

To build our Spencer app, we used a similar, but slightly extended approach.

Spencer needs its modular setup because it needs to have a different feature set for each client company. Like we explained for the Mobile Workplace, the features in Spencer are also much more visible: they would need to add a card to the today feed, a set of actions in the action menu, some overview screens referenced from a consult menu and some settings.

We decided to make every feature a module, not referencing each other, but using components from the Spencer core. This way, we could also split the source code into different repositories, as well as using Cocoapods for managing the feature set and – in contrast to VTM – also Spencer’s core functionality. This means that the Cocoapods configuration file is responsible for defining which modules are added. Additionally, we could also create modules that are only used for a single client.

diagram 3

This is shown more clearly with an example. Say we build two applications for two different clients with different requirements. Both applications would make use of the core of Spencer, which is a ‘library’. The Cocoapods configuration file for application 1 would include module 1 and 3, and the configuration file for application 2 would include module 1 and 2. The modules would then, simply because of the fact they’re included, register themselves to the module owner. This then results in two different apps with two different feature sets.

Now let’s translate this to source code, a simple implementation of a module. We would like to build a modular application where modules must be shown in a menu structure. We need a ‘Module’ Class that the entire application knows the interface of:

@interface Module

@property (nonatomic, readonly) NSString* moduleIdentifier;

-(NSArray<NSDictionary<NSString *,NSString *> *> *)menuItems;
-(BOOL)didSelectMenuItemWithIdentifier:(NSString *)menuItemIdentifier;

@end

This means we need a unique identifier to identify the module. Furthermore, we need a way to represent the module in the menu structure, and a method that should be called when a menu item is pressed. We can then make a subclass of ‘Module’ that implements exactly what we need.

@implementation TestModule

+(void)load
{
    TestModule* module = [[TestModule alloc] init];
    [ModuleOwner registerFunctionalityModule:module];
}

-(NSString *)moduleIdentifier
{
    return @"test_module";
}

-(NSArray<NSDictionary<NSString *,NSString *> *> *)menuItems
{
    NSDictionary* testMenuItem = @{@"menuItemIdentifier": @"menu_item_test",
                                   @"menuItemDisplayName": @"Test"};

    return @[testMenuItem];
}

-(BOOL)didSelectMenuItemWithIdentifier:(NSString *)menuItemIdentifier
}
    if ([consultItemIdentifier isEqualToString:@"menu_item_test"])
    {
        // TODO: show ViewController for test menu item here

        result = YES;
    }

    return result;
}

@end

With the +load method, you should register your module to the ModuleOwner class. If the menu is opened, it asks the ModuleOwner to return all modules and their menu items. When a menu item is pressed, a ‘for’-loop goes through all modules and calls ‘didSelectMenuItemWithIdentifier:‘. If this call returns YES, the menu knows an action was triggered.

As you can see from these examples, the setups we created for VTM and Spencer were the right choice for their purpose. In these two situations a modular setup really comes in handy. For VTM, it allowed us to easily include and exclude source code, using Cocoapods. Furthermore, we could turn an integration into a standalone app using only project setup and just a few lines of code. For Spencer, it lets us make different apps containing an entirely different feature set within seconds.

In conclusion, modularity is great, when you use it wisely.

We learned through experience that there are a number of guidelines to follow in order to use modularity to its full effect.

The most important thing, call it the golden rule of modularity, is to consider using a completely modular approach versus using feature flags. Using modules, no matter how good your approach is, makes your code a lot more complex; it’s important to make sure the resulting benefits are large enough.

And finally, once you’ve decided the modular approach is the way to go, make sure your setup is sturdy and your code is as clean and standalone as possible…