XcodeGen & Sourcery - A Case Study
Jan Borowski
November 28, 2023
XcodeGen & Sourcery: A Case Study
Introduction
In this article, I would like to share how we introduced Sourcery and XcodeGen in our VOD project. I will explain the pros and cons of Sourcery and XcodeGen, why we decided to give them a chance, and what problems they are solving. Let’s get started!
VOD project
First, let’s say a few words about our VOD project. One of our flag VOD products is a project dedicated to iOS and tvOS. This single project holds many brands like BET+, Smithsonian, MTV, and more. The project contains many modules, and each module has many targets. Some modules are feature-oriented, like search or analytics; others contain design systems or shared functionality.
Having such a large project brings a lot of challenges. I want to focus on challenges on two levels: project level and test level.
Project level
- Conflicts in pbxproj
- When two developers edit the same pbxproj and want to commit to master, there can be conflicts between the revisions
- Resolving conflicts in huge pbxproj is challenging
- Pbxproj are hard to read
- It is not clear what is the configuration of the project due to the sheer complexity of project files
- It means that the code review of pbxproj is complex, and developers can easily miss essential changes
- No unified directory structure
- Xcode group hierarchy out of sync with directories
- Xcode group hierarchy doesn’t always match the directory hierarchy
- By design, the Xcode group hierarchy doesn’t have to follow the directory hierarchy
- Orphaned files
- Special cases of mismatch are files that are in no target but still are present in the repository
- Other cases are references to files that are present in pbxproj, but files are no longer there
Test level
- It is hard to maintain a unified naming style
- Some mock names start with Mock or Test
- Others end with Mock or Stub
- All these mocks serve the same purpose
- Different mock conventions
- It was up to the developer to manually implement a mock
- We ended up with multiple mock styles
- When checking unit test implementation, we had to learn particular mock convention
- Different functionalities between mocks
- Some count calls of methods
- Some capture method parameters
- Other enable developers to stub methods
- Some contain logic, while others are basic
- Developers must write their mocks and fixtures manually
- Whenever there is a change in some protocol, developers need to edit mocks manually
Solutions
To solve these problems, we picked two tools: XcodeGen and Sourcery.
XcodeGen
It is a tool to generate Xcode projects based on project specs and directory structure. Project spec contains information about required dependencies, targets, and configuration of specific targets. XcodeGen uses a directory structure to map group hierarchy in the Xcode project. This approach ensures that files and directories are always in sync with the Xcode project.
Sourcery
Tool for code generation in Swift. Sourcery uses templates written in Stencil language to generate code. Developers around the world use Sourcery for generating mocks, decorators, and others. Any repetitive, boilerplate code can be a candidate for Sourcery migration.
How we did it
Not all in
We did it one step at a time. XcodeGen and Sourcery share an essential feature: they are not all-in tools. You can decide what modules you want to use them in and when. We didn’t have to migrate all modules at once. Gradual adoption helped us polish configuration files and quickly deliver features to the whole team.
Keep things manageable
We had two distinct epics in JIRA, one for XcodeGen migration and the other for Sourcery. For all modules eligible for migration to XcodeGen, we created one task. For Sourcery, we started with one task per module. We quickly discovered that some test targets contain so many mocks to migrate that it would be overwhelming for one person to complete. We decided to split one task per directory. Sometimes, we even added tasks for sub-directories, too! We started with one task for the biggest test target and divided it into more than 30 smaller stories!
Do not track generated code
Sourcery and XcodeGen help in code generation. Yet, we wanted to avoid committing generated code to the repository. We would have to deal with conflicts in git again! We would have to deal with conflicts in git again! We only kept the definition of what we want to generate. We added generated code and generated pbxproj to gitignore and, as a result, excluded them from history.
The neat thing is anytime we can decide to stop using any of these tools. In such a situation, we can add generated files to git. There is no danger of entangled dependencies that we would need to maintain for years.
Git hooks to the rescue
One thing is to migrate mocks/modules on the machine of the initial developer. But how did we manage to make sure that migration is happening on other workstations? The answer is a set of tools: Brew, git hooks, and in-house scripts.
We use Brew to install external tools. Brew uses Brewfile to keep track of tools to install. We added “XcodeGen”, “Sourcery”, and “pre-commit” - git hooks tool.
We created simple, in-house scripts in bash to run XcodeGen and Sourcery. Those scripts fetched a list of modules to work on from the configuration file. There is a separate file for XcodeGen and Sourcery. Thanks to this approach, these tools work only in ready modules and do not perform any work for frameworks we haven’t migrated yet.
Git hooks enabled us to run in-house scripts whenever the developer commits changes, checkouts, or merges branches. In the pre-commit configuration file, we specified what and when it should be executed. For example, when a user checkouts the branch, pre-commit runs XcodeGen generation script. Most of the time, developers don’t have to worry about running scripts manually, since it is automatically done by pre-commit.
Moreover, we added Sourcery generation to build phases for Xcode projects. That made working with mocks super easy. Every time the developer runs unit tests, Sourcery generates all mocks. There is no need to run scripts by hand!
Avoid competence silos
We wanted to avoid a competence silo, and we encouraged team members to take on migration tasks. Thanks to this, everyone gained experience with at least one of the technologies. Doing tasks in parallel by different team members encouraged knowledge transfer. We also avoided dumping the whole responsibility on a single dev.
What we gained
Thanks to introducing XcodeGen and Sourcery, we were able to deliver the following improvements:
XcodeGen
- Unified directory structure
- Files and directories and in sync with Xcode projects
- No more discrepancies between Xcode group hierarchy and directory hierarchy
- Clear module configuration
- project.yml is much more readable than pbxproj
- No more conflicts in pbxproj
- pbxproj files are now in gitignore. XcodeGen creates them locally, so there are no conflicts between branches.
- Shared configuration between modules
- Templates help keep consistent naming conventions or directory structure
- Shared configuration between iOS and tvOS targets
- We couldn’t achieve this with vanilla Xcode projects
- Previously we had to copy & paste configuration between platforms
- Shared configuration makes integration of shared tools easier
- When we added Sourcery to the shared configuration in XcodeGen, it made introducing Sourcery easier to new modules.
- And more manageable.
- Cleaned configuration of some modules
- This was a bit of a surprise for us. During migration modules to XcodeGen, we simplified the configuration and eliminated unnecessary abstractions.
- It improved the stability of unit tests and removed cryptic errors for some targets.
- We managed to write a simple CLI tool to create a new framework
- It helps in the automation of the tedious process of creating a new framework
- It took us just 3 days to create it, thanks to XcodeGen
Sourcery
- Unified naming style of mocks
- Common template for mocks enforces naming convention
- It applies not only to type names but also call counters, capturing parameters, etc.
- Existing mocks got a lot of new functionalities
- Thanks to Sourcery, we were able to add a lot of “testing-related” functionality like call counters, capturing of parameters, etc.
- Much more predictable behavior of mocks
- Removed a lot of boilerplate code
- Reducing the future cost of maintaining the project
- If someone updates protocol in the future, there will be no need for manual updating of mocks.
- Removing not-used code
- This is an unexpected by-product: When we reviewed what mocks are eligible for migration, we discovered that some implementations are no longer used! We deleted them with a smile 😎
Every rose has its thorns
Although XcodeGen and Sourcery are improvements to our VOD project, they come with its quirks:
- Time-consuming migration
- The most obvious one
- Migrating whole swats of code requires a lot of effort, and it took us a couple of sprints to complete
- Updating stencil templates is not easy
- It is not always obvious how to do things
- If you want to go beyond existing templates, you must go through a lot of trial and error to find the best working solution
- There are limits to migration
- It is technically possible to migrate all manual mocks to Sourcery
- Sometimes, it is not the right call
- It is especially true for
- Very simple mocks
- Protocol with generics
- Mocks with only default values
- Mocks with custom logic
- There are limits to modularization
- If we have a complex module with lots of targets and we want to migrate it to XcodeGen, it will not be easy. There is little you can do to split it into smaller parts.
- The best approach here is to leave such tasks at the end and make sure you can simplify the project as much as possible
- Sometimes, generated code is far from perfect
- It is especially true if we want to use RxSwift
- If there are overloaded methods in mocks, there can be conflicts in the generated code
- You can either refrain from generating code for this mock
- Or encode all parameter names into generated code, which will end with ridiculously long property names
- Neither is perfect
- Beware if you are migrating all
- We experienced a problem when we wanted to migrate the analytics module
- It turned out that we missed one of the parameters in pbxproj. Luckily, we caught this issue before pushing the app to AppStore. If we hadn’t, we might have compromised the analytics.
Summary
Both Sourcery and XcodeGen are exciting tools to use. They can improve project quality and make day-to-day work more pleasant. After playing with Sourcery a bit, code generation feels like a superpower to me! I encourage you to look at all the tools. See for yourself if they fit into your project.