How to control your iOS dependencies

Measuring Cocoapod based Projects complexity

Oswaldo Rubio
New Work Development
8 min readSep 15, 2022

--

In large-scale projects (we have one of these at XING) with more than 30 iOS engineers collaborating in different feature-oriented teams, we have the requirement to build, test, and run our iOS App in isolation in a single mobile monolithic repository (monorepo). That’s the reason we divided the app in Cocoapods development pods trying to minimize the dependencies between them as much as possible.

The way you decide to divide and connect all these pods requires you to have a good strategy and discipline about which dependencies are allowed or in which targets as there are no boundaries or rules that prevent you to create what you want.

That’s normally easy when you only have a few pods, but this can be really hard when you have hundreds of them and the impact this can have is not always easy to measure.

a guy with a computer measuring things

This article wants to explain how we as an iOS Platform Team at XING are solving the dependency issues and share with you the tools we use for that.

What’s the problem?

The problem we want to solve is to avoid taking modularization and architectural decisions, that most of the times are small changes, without being conscious of the global impact these changes have on the full project:

  • Increased build time
  • Reduction of module isolation
  • Creation of a more coupled dependency graph

So, these are the solutions we want to provide to solve this problem:

  • to have a scalable solution for project modularization that can support increasing the number of modules, but maintain a stable complexity along the time
  • to provide the iOS community the tools to analyse, compare and decide about good modularity designs based on numbers, not only personal perceptions or beliefs in a simple and consistent way

First approach

We already have some internal tools to generate the graph dependency using the .podspec.json format that Cocopods has, which allows us to see those relations in Graphviz format, so our first idea was to only count the number of modules we have in every graph version.

Let’s see one easy example with a hypothetical app with 3 feature modules and a single library that could contain code needed in some of these 3 modules.

comparison between 2 graphs with different complexity

This can be applied also at the feature level, giving us a rough idea about how many modules or dependencies are needed to build that feature module. The problem with this approach is that it doesn’t express only one of the 2 graphs has a better design.

The one on the left could resolve the dependency between the features and the library through dependency injection that the main app provides or bridge solutions for inter-feature dependency. You can read more about some of the Dependency Injection solutions we have used in our own project in this article.

Second approach

We wanted to have a better representation of this complexity change, and this is how we discover Predicting Subsystem Failures using Dependency Graph Complexities research document from Microsoft which identifies that complexity relation with the failure rate in really large systems. There are also other studies like this one for Google Chrome that also relates the vulnerabilities with the architecture complexity.

They mention multiple metrics there, but all of them are based on the one from Thomas McCabe Cyclomatic Complexity metric, which can be defined in the following way.

“Mathematically, the cyclomatic complexity of a structured program is defined with reference to the control-flow graph of the program, a directed graph containing the basic blocks of the program, with an edge between two basic blocks if control may pass from the first to the second.” (“Cyclomatic complexity — Wikipedia”)

Although normally that’s something used for measuring the complexity of source code flows, as you can see in the previous research studies, can be applied also to component and system graphs like our iOS project.

We compare our complexity using the following two variants or the formula where |E| is the number of edges, |V| is the number of nodes, and |P| is the number of weakly connected components (1 for our case).

  • Simple complexity:
formula: edges minus number of nodes plus weakly connected components
  • MultiEdge complexity
formula: multi-edges minus number of nodes plus weakly connected components

In our case, we decided to use multi-edge complexity because this allows us to see the transitive dependencies that, although you’re not declaring in the .podspec file, you can potentially use.

Jungle; How did we measure our dependency complexity?

We want to share with you Jungle; a tool we at XING have created to parse our dependency graph, and also report the complexity to the developer or push it into our Grafana dashboard for historical analysis.

You can clone it from https://github.com/xing/jungle and collaborate to provide missing features and also if you’re already using Mint, you can install it directly with:

Now let’s see a practical usage and how we can use jungle to decide where it should be the best place to introduce a new third-party dependency in our XING iOS Xcode project.

Depending on our particular project, this could be a list of possible options:

  • APIClient: the library used for all the network-related stuff
  • LibraryBridge: library without dependencies but used abroad all the project
  • FoundationModule: few dependencies, but also used in most of the libraries
  • Feature: the feature module that needs that dependency

The most basic command jungle support is compare, which compares our current complexity against the one in the main branch or HEAD reference.

Also, we show the moduleCount, which is as you can suspect, the number of modules used at that point in the history.

Let’s iterate on each of the libraries and see the increment of complexity against the main branch for every changed graph. These are the numbers using the following formula:

(current.complexity / main.complexity -1) * 100

  • APIClient: 22026/21066 -1 = +4.5%
  • LibraryBridge: 23360/21066 -1 = +10.8%
  • FoundationModule: 23022/21066–1 = +9.3%
  • Feature: 21127/21066–1 = 0.3%

So, it seems that at least for this case, introducing this dependency only in the feature module has a lower impact on the project complexity. I’m not saying this should be a general rule, because each case could be different, that dependency could be required in more modules, or maybe that feature module is a dependency of other modules. But at least, we can compare and choose which one is the best option.

Creating the graphs

We love the graphs! And although for bigger changes a number is more pragmatic than this, you’ll want to have a more visual representation of your dependencies. That’s something you can get only with this command and graphviz tool:

This is an example of one of our features in our XING iOS App with zero complexity.

Showing historical data

Another command you can use to retrieve previous history metrics is history. That’s really fast and this only retrieves the commits where Podfile.lock file has been changed. Every Podspec change modifies this file and this is our source of truth for the graph dependency generation.

Focusing on a particular Pod

Checking the impact in the global App is nice, but also you can focus on a particular module and filter this historic or current complexity information only for a particular pod (author and commit information has been removed):

As you can see in the last column, this has been a really intensive 4 months period for this module where we have reduced the complexity from 1844 to 304.

Applying Jungle on other open-source iOS Apps

We want to share with you also how this tool works also with other open source projects or even with your projects. The only condition we need (at this moment) is to have a Podfile.lock in your git repository.

This is the Complexity vs. ModuleCount graph for the popular Wordpress App (you can check the source code here):

last 2 years graph for the Wordpress project

Seems something weird happened on 2021.06… 🤔 but we can use this command to check the details and see this commit increased not only the number of pod dependencies but also the complexity.

a detailed image with the commit that increase the complexity in the Wordpress project

Our experience

At this point, and looking at the numbers for the last 3 years in the iOS monorepo repository, we can say that even though the number of modules (on the bottom) has been always increasing, the complexity has been under control in the last year. And now, using these tools and having these numbers in our Grafana dashboard we can be sure this will be in the future too.

a graph for the dependency complexity and the number of modules in the last 3 years

This reduction in the complexity was something we did intentionally because we detected build times were increasing lately but now with jungle we can confirm which actions had a real impact and we can quantify it and see even the commit that produced changes in the project complexity and take decisions based on that.

That being said that we want to continue focusing on including these checks at Pull Request level, giving the developer relevant information to continue improving the modularisation design and also, improving the tool to support other package managers like the Swift Package Manager. If you want to contribute, please check the project and give us feedback.

Also, I want to say really thank you to Shammi Didla and Andreas Koslowski for all the support, knowledge, code, and hours expended on these complexity concepts and tools that I’m showing you here. ❤️

--

--