Modular iOS Part 4: Sharing Configuration Between Modules

Sam Dods
Kin + Carta Created
6 min readMay 25, 2018

--

If you haven’t read the previous parts in this series, they are:

Here at TAB we promote modularisation with everything we do. I described the benefits of a modular architecture in the Part 1 of this series: Strangling the iOS Monolith. But one of the implementation challenges is managing how configuration is passed between modules. This is because configuration is typically loaded into the main app bundle, which the dependent modules are unable to access (because it would form a circular dependency).

This article will show how we overcome this challenge at TAB for a modular Xcode workspace; and how a protocol oriented approach could benefit third-party libraries too.

What do I mean by a modular Xcode workspace?

We break our codebases into feature modules, each its own Xcode project within the Xcode workspace.

The image below shows how we might break up the workspace for a grocery shopping app:

Modular Xcode workspace structure

Each feature module is completely independent, guaranteeing separation of concerns. There are no dependencies between the feature modules, and the feature modules cannot depend on the main Grocery project.

The challenge of configuring each module

A codebase will typically have some kind of compile-time configuration. With the modular workspace, we now have several modules that must be configured, but we don’t want to duplicate any configuration. (Duplication is unreliable and fragile, because it would mean making updates in multiple places should any configuration parameters change.)

The app’s target is built from the Grocery project. This includes AppDelegate.swift and the main configuration file Config.swift. We use a tool built internally called configen to build the configuration into code at compile-time (see related article). But whatever you use, you will be faced with the challenge of how you pass your configuration between Xcode projects.

A naive approach would be to create a corresponding Config object in each feature module and set the properties on app launch, like so:

But this approach is clearly flawed:

  • the feature module’s Config properties need to be mutable (i.e. var) in order to configure them
  • the feature module’s Config properties must be declared public in order to set them from the main app
  • you need to ensure that all parameters are setup appropriately
  • any parameter setup could easily be removed by mistake in future
  • when adding a new parameter, you have to make sure you pass the value through on app launch, which is unnecessary overhead
  • it’s not intuitive to newcomers to the codebase

A robust solution

The solution I propose, which we are using in several apps at TAB, is designed to meet the following criteria:

  • intuitive, compiler-enforced setup for new parameters
  • immutable Config in each feature module (i.e. using let properties)
    Config properties are internal to the feature module (no need for public)
  • single line setup for each feature module
  • same usage of feature module’s configuration as we’re used to in the main app

The solution is simple. It requires a bit of additional setup in the feature module, but once that is done, extending is simple and robust.

Using the Analytics module as an example, we define the type of our configuration, in the Config.swift file in the Analytics module:

We need to define an initialiser for this type. If we allow this type to be instantiated from another object conforming to a protocol, then we can simply make our main app’s Config type conform to this protocol. So we define the initialiser as follows:

The protocol AnalyticsConfig is defined as follows:

This configuration is internal to the module. But we wish to set it up from the containing app’s main project. We want to enforce setup using some object that conforms to our protocol, so we define a module-scope public setup method like so:

The final step is to allow the configuration properties to be accessed in the same way, whether we’re accessing them in the main app, or from within the feature module.

The main app has its own configuration provided by a Config struct with static properties. (It’s created using Configen, described in this blog post.) The main app references its configuration like so: Config.analyticsKey. The feature module’s ConfigType class currently serves no purpose, because its initialiser is private to the Config.swift file. And its static shared property is private too. So we make a module-scope, internal computed property named Config to make usage the same as in the main app (note that it’s Config with a capital C to make it the same usage at the call site as the struct in the main app):

Note, we use Realm’s SwiftLint across all of our projects, so we need to disable the check for properties that start with an uppercase letter.

This property means we can access the properties throughout our codebase in the same way, i.e. Config.analyticsKey.

Configuring on app launch

We need to make our main app’s Config type conform to the feature modules’ protocols. We do this in a Config+Additions.swift file, in which we also define some runtime configuration parameters, like so:

We keep the property names the same in the feature modules’ configurations, so that conformance is automatic.

Now setting up the feature modules’ configurations on app launch is a simple one-liner for each module

If this line is removed, then no configuration is passed through to the feature module. And as soon as the feature module’s configuration is accessed, the app will crash with an informative fatal error message.

Adding a new parameter to the feature module

We will in future need to add a new parameter to the future module, for example, if we were to add a buildVersion parameter to the Analytics module’s configuration.

We start by adding it to the ConfigType class, because this is the type we’re accessing when we need to use the configured value.

What’s elegant about this approach is that the compiler will not allow us to make a mistake:

  1. By adding the instance property, we’re force to update the initialiser
  2. By updating the initialiser, we’re forced to update the protocol
  3. By updating the protocol, we’re forced to update any conforming type, i.e. the main app’s Config type
  4. By updating the main app’s Config type, we’re forced to declare from where this configuration parameter comes (e.g. build time static property, or runtime)

For this example, we would end up extending our main app’s Config type as follows:

Configuring for unit tests

We need to ensure our module is configured when unit testing, because otherwise we’d see the fatal error. The code in the main app’s AppDelegate.swift file is not going to execute for each of our feature modules’ test targets.

So we need another way to setup the module’s Config that will only be executed for unit tests. And we want to avoid having to do this in every test class.

Introducing the TestInitializer

In the test target’s Info.plist file, we set for the NSPrincipalClass key the value AnalyticsTests.TestInitializer. This is super simple: the “Principal class” option can be selected from the dropdown in the Plist editor.

We define the TestInitializer class as follows (we have one in each test target, where necessary):

Conclusion

I’ve shared the full implementation of the feature module’s Config.swift.

The technique outlined in this article is just another way we can all build robustness into our apps––something we’re really hot on at TAB. It makes extending intuitive for newcomers; and it removes all possibility of making mistakes.

This is a robust technique for dependencies that are internal to a single workspace, but there’s no reason it has to stop there. If third parties used this protocol-oriented approach to configuring their libraries, it would make setup trivial and robust. So we should consider this ourselves next time we make a library, whether open source or closed.

The iOS team at TAB are continually improving our processes to streamline development, so we have more time for the fun stuff. We’re also continually expanding our team.

And you can follow me on Twitter for my rare insights. 😊

--

--

Sam Dods
Kin + Carta Created

Tech Lead and Mobile Evangelist based in Edinburgh, Scotland