Warming Up To Havana: Accelerate iOS Builds With Binary Caching Using Tuist 2.x

Ronan O Ciosoig
7 min readFeb 14, 2022

Tuist 2.x brings binary conversion of SPM frameworks that significantly reduce build times.

Havana, Cuba, by Spencer Everett October 2019.

For any iOS developer working on a large project, one common complaint is how long it takes to run a clean build, or for the CI to finish a build with checks for every PR. Typical large projects would contain dozens of external libraries, most of which are open source projects that need to be compiled along side the main project, and all of its modules, and then run all the tests can take over an hour. Now that Xcode has built-in support for Swift Package Manager, many developers have switched their dependencies over from CocoaPods. But since Xcode resolves those dependencies on opening the project, the developer must wait even more time (especially when the resolver fails and you are forced to re-open the project repeatedly). Surely, you have asked your self, there has to be a better way. And there is — A tuist way.

When I started writing about Tusit in April 2021, the latest version at that time was 1.35. It has evolved significantly since then and in today’s article I will update the project to the January 2022 release: 2.6.0 Havana (BTW: each release is given a name). But first, I will compare the old and the new approaches, provide some in-depth details of what is going on, and how to get the most out of the tuist 2.x features.

Dependencies

In Tuist 1.x, external dependences are defined as packages in the Project.swift manifest, in a syntax that is not too far away from how Swift packages are defined.

Using this syntax, tuist utilises SPM to download the source repo and link it to the Xcode project, much the same way as it would be done in Xcode it self.

But there is a downside to this approach: It doesn’t always work. In some cases the compile can fail, which can be very frustrating. It is also slow. Thus a better solution was needed.

In tuist 2.x Dependencies.swift has been added as the new approach and currently supports both Carthage and Swift Packages, with support for CocoaPods still a work in progress.

This file must be placed in the Tuist folder, and then run

tuist dependencies fetch

This will checkout the repo into a relative path under Tuist/Dependencies/ into sub-folders Carthage and SwiftPackageManager respectively.

If the project is opened now using tuist generate and tuist focus App the project will open with the dependencies referenced as source frameworks. Note that in linked frameworks, the dependencies are represented by light yellow icons, as opposed to the more solid yellow for core SDK frameworks.

Warming Up The Engine

And now for the magic… Run tuist cache warm You will see in the terminal that the dependencies are compiled. Open the project with focus again and see that now they are referenced as compiled frameworks (solid yellow icons).

When running a clean build will now run much faster as add the dependencies are pre-compiled.

Close the project, and re-run the cache warm, but this time add --xcframeworks to the command:

tuist cache warm --xcframeworks

Next run generate again and then focus the project but this time with the flag:

tuist focus --xcframeworks App

Now the project loads all the dependencies as compiled XCFrameworks which contain both simulator and ARM64 binaries.

Comparison Fixtures

For the sake of having repeatable tests that everyone can validate I created an open source repo here. Within the Fixtures folder are a number of sub-folders. I will run the clean builds on these 3:

  • AppWithSwiftPackages
  • AppWithSPMDependencies
  • AppWithCarthageDependencies

Case 1: The first fixture had the 9 dependencies defined using the tuist 1.x approach.

The second fixture defines the dependencies using Dependencies.swift using Swift Packages. I ran tests on 3 cases in this fixture:

  • Case 2: Only fetch, generate, and focus
  • Case 3: Fetch, cache warm, generate and focus
  • Case 4: Fetch, cache warm using XCFrameworks, generate and focus with XCFrameworks

Case 5: The last fixture is using Dependencies.swift with Carthage dependencies which once fetched are compiled directly into XCFramewokrs.

Comparison Results

Each case was run 5 times with the average build time calculated.

Build times for the 5 cases using the 3 fixtures defined above.

There is a clear difference in build times once the dependencies are compiled into binary frameworks, but there is little difference between the output from Swift Packages and Carthage.

Note that more more detailed information is provided in the README of the repo.

Pokedex Project Comparisons

When I started working on this series of articles in March 2021, the starting point was a simple 4 scene app with dependencies Moya and JGProgressHUD loaded using CocoaPods. Over the series I refactored the scenes into individual modules, and removed the Moya dependency by rewriting the code to use Combine and Async/Await. For this article I have completed the last and final refactor by loading JGProgressHUD using Dependencies.swift as a SwiftPackage.

Looking back 1 year, the project started with these dependencies:

In the last article I removed the Moya dependency, thus leaving only JGProgressHUD as a Swift Package dependency.

Compiling the project using the tuist 1.x approach the frameworks listed in the Pokedex project look like this:

Note that when tuist focus is run, all of the modules are compiled and loaded as binary frameworks.

On updating the project to using the 2.x approach to dependencies and running cache warm the frameworks now look like this:

There’s a nice consistency with this in that all the dependencies, and modules are all binary linked frameworks. The last and (entirely optional) change is to switch over to using XCFrameworks.

Taking into consideration the changes in the project, I defined these 6 cases:

1: Project as a basic Xcode project before using Tuist.

2: Project converted to Tuist with Haneke as an external source lib, Moya dependency — Tuist 1.x

3: Project with all code moved to modules, JGProgressHUD + Moya Dependency. Tuist 1.x

4: Project with all code moved to modules, JGProgressHUD as Dependency. Tuist 1.x

5: Project with all code moved to modules, JGProgressHUD as Dependency. Tuist 2.x — caching modules as frameworks.

6: Project with all code moved to modules, JGProgressHUD as XCFramework Dependency . Tuist 2.x — caching modules as XCFrameworks.

I repeated each case 5 times and calculated the average. The results are in the graph below. The final build time average for the project loading XCFrameworks was a mere 1.7 seconds, whereas the initial average time was measured at 7.8 seconds.

Comparison of build times for the 6 cases with the minimum of 1.05 seconds in red.

The final comparison I did was running a clean compile of a new clean slate tuist template project. This had an average build time of 1.05 seconds and is shown as a red line. What is clear here is that there is fraction of a second difference between compiling the Pokedex project and an empty one, thus showing the true potential of tuist as a build system.

Conclusion

The path taken to arrive that this point may have been a long one, but now the project compile time is as close to minimum as it can get. Even though the Pokedex project is a trivial example, you should be able to see a clear advantage on what impact this would have on the build times of larger projects.

Although faster build times is great for a single developer, large companies rely on continuous build and integration systems. These can often run for over an hour leading to endless frustration for the development teams. In the next article I will focus on exactly this — let the tuist magic take on the CI.

The changes done in the Pokedex project for this article are available here.

If you liked this article, please follow to get more updates in this series.

Update for Tuist 3.x

In 3.x the generate and focus commands were merged and they unfortunately choose generate as the single replacement, whereas I feel focus has more meaning here. All the rest stays the same.

--

--

Ronan O Ciosoig

iOS software architect eDreams ODIGEO and electronic music fan