The Magic of Generating an Xcode Project

Planet Earth vs. Jupiter. There is the same order of magnitude between the ratio of these two planets and the amount of code that defined the SoundCloud iOS project before — Jupiter — and after — Earth — generating it using Tuist.
In this blog post, we will discuss the problems of maintaining a complex iOS project, how we managed to simplify the process by writing less and more consistent code, and how we ended up with lower build times as an unexpected result.
What’s the Problem with Xcode Projects?
Xcode is the official tool for developing apps on Apple platforms. Every time you make a change to an Xcode project, such as adding a new source file or changing a setting, the IDE generates some code into a file called project.pbxproj.
For example, here is an extract of changes that are automatically generated by Xcode when creating a new file:

The first thing you may notice is that the generated code is not easy to read or change, as it uses unique random identifiers to refer to every entry of the project. Furthermore, a single file addition creates changes in different sections of the project file, which needs to respect a specific format and particular conventions, or else it would result in an invalid project that can’t be opened.
Project Files Are for Computers, Not for Humans
Now let’s try to imagine how large this file would be in a project of the size of the SoundCloud iOS app, which is composed of thousands of source files and split across dozens of frameworks with multiple targets, hundreds of build settings, and complex linking rules. To give you a better understanding of this, in December 2019, we had more than 80,000 lines of code representing our project. That’s Jupiter!
Maintaining project files has always been a problem for iOS developers, especially as the number of contributors working on the same codebase grows. As an example of one possible issue, if multiple engineers are working in parallel and trigger changes on the same project file, there is a high chance that this will result in painful git conflicts.
Project Generation
To solve most of these problems, some engineers in the iOS community came up with the idea of automatically generating the project files. In this way, the projects can be redefined using a “user friendly” format composed of more concise and readable rules. A tool will then run to create a pbxproj file that adheres to Apple’s standards.
The main benefits of this process are:

Consistency — one rule applies to all modules
Simplicity — it’s easier to understand how your project is defined
Microprojects — you can generate a subset of your project on demand, in order to work on a feature in isolation and get lower build times
No conflicts — you’ll avoid having to solve difficult conflicts on project files
Extensibility — you can easily add new modules

Introducing Tuist
When we decided to find a new way of generating projects, we opted to use Tuist, an open source project started by one of our former employees, Pedro Piñera. Tuist generates Xcode projects based on some rules expressed on a Swift manifest file named Project.swift and on the current state of the filesystem.

Before using Tuist, creating a framework for a new μFeature at SoundCloud was manual, error-prone work. Those days of work are now behind us, as it takes just one line of code and a couple of seconds to run the tuist generate command.
For example, this is the project definition of our μFeature Search:

The result of running Tuist is the creation of a non-trivial project all set up with our common dependencies, build settings, multiple targets, an example app, etc. Everything is generated by a single line of code:

With Tuist, We Finally Got Control of Our Project
By using Tuist, we didn’t just improve our productivity, but we also upped the consistency of our projects. All the modules are now generated using the same rules, expressed in Swift, using Tuist project helpers.
If we want to change a build setting or a linking rule across all our frameworks, we can simply do it in a single place and regenerate all our projects. It is a faster and more robust approach.
This is an example of a project helper function that defines the generation of the main target of our μFeatures:

There are also other popular open source tools for generating Xcode projects, such as XcodeGen. But we decided to use Tuist because of its ability to express rules in a powerful language like Swift, as opposed to the more verbose approach of using a data interchange format like YAML or JSON. In addition, our developers prefer to edit projects in Swift on Xcode to benefit from code completion and all the features the IDE provides.
Serendipity
On our way to generate the SoundCloud project, two unplanned yet fortunate discoveries occurred. The first one is that we got better compilation times for our μFeatures. This is because, when using the tuist generate command, the tool also generates a separate workspace on which developers can work in isolation.
So for example, when we want to make some changes on the Search feature, we can quickly generate a workspace that contains only the Search framework and all the modules needed to compile it:

Surprisingly, a smaller workspace resulted in faster compilation times, probably because Xcode and its compiler have an easier job understanding which source files should be indexed and recompiled and which ones don’t need to be.
The second unplanned advantage was that we cleaned up our project. While writing the rules to generate SoundCloud, we got rid of: files that were not part of any targets, duplicate resources, and dependencies that should not have been linked. Also, the generated project looks tidier: for example, all of our dependencies are now ordered both by type and alphabetically.
Conclusion
Generating the project was not an easy journey, as the process of writing the manifest files can be tedious and error-prone. Debugging problems at the project level is not always easy, nor is it easy to validate if the generated project is semantically equal to the previously existing one, even with the support of a great open source tool like xcdiff.
However, the benefits clearly overcame the costs for us. Being able to quickly change the setup of all our projects is a huge step toward further modularization of the SoundCloud app. It enables our iOS engineers to quickly create more modules while we are still able to keep the architecture under control.
Looking to the future, we are planning on using Tuist to continue our process of extracting code from the main application to feature modules, since it’s increasing the productivity of our engineers. We are proud to contribute to open source projects like Tuist, and we are very interested in the further development of the tool that has already proven to be a game changer!