DI in a Modularized Project in Swift

+10 years project, Objective-C + Swift, UIKit + SwiftUI…

Juantri Jiménez
New Work Development
8 min readSep 16, 2022

--

Photo by jesse orrico on Unsplash

More than two years ago the new version of the Xing app was released to a small percentage of users and every month the percentage was increasing until a few months ago when the app was 100% rolled out. Now is the time to clean old code, refactor, improve compiling times, improve the dependency injection, etc.

More than 30 developers spread across approximately 20 teams are constantly working on our project. Teams are divided into Platform team, Feature teams and Design team:

  • Platform team is in charge of the common parts of the project, architectural decisions, managing the crashes of the app, etc.
  • Feature teams create the features of the app and maintain them.
  • Design team creates the UI components of the app.

Each feature team has as many frameworks to develop the feature as it needs, so counting the common frameworks, the feature ones and the external dependencies, our project contains more than 100 dependencies.

Because of that, the dependency graph of the project is huge and the compiling time for a clean build is extremely high, so we decided that after this big rollout it was time to clean the project.

What do we want to achieve?

We have 3 main objectives:

  • Improve compilation time. A clean and build takes over 8 min.
  • Improve dependency injection. With over 100 development frameworks, there are multiple dependencies between them. The average of dependencies that a framework has is 24.
  • Once we have a clear dependency graph and a better DI we want to move to Swift Package Manager instead of CocoaPods

Steps to achieve it

1. Identify common frameworks

The first step was to identify the common frameworks (the ones that have common logic, components, etc) that should be cleaned or refactored. Once we have these frameworks, we have to organize code (avoid helpers frameworks), check if code belongs to this framework or should be moved to another one or a new one, check if some code should be converted to Swift…

We have some internal tools to generate the graph dependency of the project using the .podspec.json format that CocoaPods has, which allows us to see those relations in Graphviz format, so our first idea was to only count the number of frameworks we have in every graph version. We listed the number of dependencies we have in each framework and we obtained something like this:

We realized that some common frameworks that contain a huge number of dependencies were also a dependency of some feature frameworks, so the feature ones were importing also the dependencies of the common frameworks. Because of that, we decided to focus on these common frameworks and try to refactor them.

2. Improve the DI

Normally, the first step when the app starts is initialising the third-party frameworks with their tokens and properties, and then using them in the project. When the project is big and you have many feature frameworks, things start to become more complex because maybe you need to access any feature from any other framework so you have to inject the third-party frameworks into them and you need a way to communicate your feature frameworks.

To solve this issue and be able to access any framework from them, we use a framework called Bridge.

Framework Bridge is a singleton with a weak reference of each feature framework of our project and it works with protocols. So it does not depend on any framework and can be added as a dependency.

We can differentiate between two steps for managing the DI of the project: the DI of third-party / services frameworks and the DI of the feature frameworks. The idea is to initialize all these third-party / services frameworks and create an “Environment” for the app. Then, with this environment, we initialize all the feature frameworks to set up the “Bridge”, so everything is accessible from all parts of the app, and finally, the app runs.

Process of initializing the dependencies and the app

DI for external dependencies

Let’s start with the code.

Managing the external dependencies can be done in many ways but we will going to manage them with a wrapper framework. So each third-party framework will have its own wrapper framework.

By doing this, we force that the third-party framework is just the dependency of this wrapper framework and is not spread through the project. And as we mentioned before, we are going to create an environment for the app that manages all the external dependencies, so, using SPM, the ExternalDependencies.package could be something like this:

Package.swift
  • Environment framework

As we have one dependency, the struct is easy:

AppEnvironment.swift

What is this AppboyTracker?

This is the public struct of the environment that is accessible by the app and is the wrapper of the third-party framework.

AppboyTracker.swift

The AppboyProtocol is the one that Braze will conform to, so we can use Braze in our wrapper.

  • Appboy framework
AppBoyConnection.swift

First, Appboy implements our protocol so we can use it in the tracker.
Second, one static func to initialize the tracker with Appboy.
We have created mocks because testing and launching the app are useful.

And this is how we use it:

AppDelegate+AppEnvironmentSetup.swift
AppDelegate.swift

DI for internal dependencies

We are going to try to create an example of how a complex app could be: some feature frameworks, the bridge to communicate between the features and a component framework. So the package is something like this:

Package.swift

For managing the dependencies we are going to create protocols with which you can work inside the framework but are implemented out of it. With this step, you will remove dependencies and improve compilation times in the framework as well as the main project. Also, you will be able to test everything because you will be able to mock things.

  • FeatureA framework

This is how our FeatureA framework entry point looks like:

FeatureAEntryPoint.swift

What is this FeatureAProtocol?

This is the public protocol that contains the methods that are available to access of FeatureA framework and it is located in the bridge framework.

FeatureAProtocol.swift

So in the main target, we initialize the FeatureA:

FeatureAAdapter.swift

We do the same process for the rest of the feature frameworks. We inject them the dependencies they need and use them to set up the bridge:

BridgeConnections.swift

Once the bridge is set, we can access the features like this:

Example of how to use the Bridge

One step more

If you can inject all dependencies of a framework, you will be able even to create an “example” app of just your framework. You would launch your framework mocking everything and you will not need to launch the complete app. This step will allow you to save a lot of time because you will not need to compile your project every time.

3. The transition from CocoaPods to SPM

This step could be the most difficult one because you need to have a clear dependency graph in your project, avoid dependencies between third-party frameworks and development frameworks, not have objc and swift code in the same framework…

But, since you have cleaned the dependencies between frameworks, the transition to SPM will be easy. If you are able to remove the dependencies of your feature frameworks to the third-party ones or common ones, you could work with both framework managers at the same time because development frameworks and third-party ones are linked by the main target.

How it worked on our project

Going back to the list obtained in the first step, we focused on one common framework that contains a lot of dependencies and was used by many feature frameworks:

  • First, we organized the basic code that could be in different frameworks and we made it accessible through the bridge.
  • Then, we removed some dependencies that could be accessed through the bridge.
  • Finally, we improved the DI of the feature frameworks that were using this framework as a dependency.

This is the dependency graph of one of these feature frameworks that had the dependency of the common one before the changes:

FeatureA dependency graph before refactor

After some refactors and improvements, the final result is this:

FeatureA dependency graph after refactor

Conclusions

The result was great: we reduced 1/3 of the compiling times of the project. Also, we realized that the complexity of the project improved and was reduced one half. If you want to know more about how to measure the complexity of your project, here you have the post.

This work was done just in one common framework, we are still working on this because there are much more. Also, some teams are improving the DI of their frameworks so the project is improving and we hope that in the next months we will start moving from CocoaPods to SPM

You can find the project of this post here

--

--

Spanish iOS software engineer. I write about features of Swift and iOS development practices