Log in
product

Optimizing compilation and test runs with Xcode projects

Slow workspaces and long compilation times can hinder developer productivity. Learn how Tuist optimizes Xcode projects to improve performance and accelerate feature delivery.

Xcode projects and workspaces can take a long time to compile in clean environments. The compilation time typically grows with the project, with periodic performance improvements due to hardware upgrades—a costly endeavor for organizations.

Some developers normalize this as an inherent challenge of Apple native development. Others introduce dynamic runtimes like React Native, which can hot-reload code and skip most compilation cycles. Some organizations even consider absorbing the cost of replacing Xcode’s build system.

For years, we have invested in solving this challenge using the most native and cost-effective approach possible through Xcode primitives. We believe that unproductive development environments can negatively impact organizational productivity and the quality of shipped applications. Our goal is to help organizations prevent such inefficiencies.

In this blog post, I’ll share how Tuist addresses these challenges and the potential impact on your projects. Let’s begin by discussing project graphs.

Your Project is a Graph

Your project forms a compilation graph that the editor uses to provide a coding experience and that the build system uses to generate build products. Traditionally, this graph was statically codified in .pbxproj and peripheral files like .xcconfig or test plans. The build could be dynamically configured during operations like build or test.

This landscape changed with the introduction of Swift Package Manager (SPM). Suddenly, projects gained references to another graph—the package graph—which was dynamically generated by SPM and reconciled at build-time with closed-source code.

How can we optimize this? Simply put, we need to skip compilation steps in the graph. However, Xcode’s build system doesn’t allow direct manipulation of internal graph processing, unlike build systems like Gradle.

You can add tasks to the graph via build phases or SPM plugins, but what’s truly desired is a function that takes a graph as input and returns an optimized graph output. The image below captures this concept:

If optimizations were this straightforward, Apple would have implemented it already. To understand why it hasn’t happened, we need to discuss hermeticity.

Hermeticity in Xcode Projects

Bazel effectively demonstrates the importance of hermeticity in build systems. In essence, hermeticity means a build system should produce identical output given the same input, regardless of the execution environment. This enables artifact caching and sharing across different machines—a critical feature in distributed systems.

However, Xcode’s build system lacks hermeticity. Examples that break hermeticity include:

  • Implicit dependencies relying on heuristics rather than explicit declarations
  • Shared caches like derived data that can cause varying build behaviors
  • Dependencies on external states like environment variables, system files, or user-specific configurations
  • Non-hermetic scripts dependent on external commands with unpinned versions

Hermeticity isn’t just about build optimization—it’s crucial for making incremental builds more deterministic and improving features like SwiftUI previews. If the editor cannot determine the relationship between a change and the binary graph, previews simply won’t work.

Optimizing Xcode projects would require Xcode to evolve its graph toward encouraging hermeticity and discouraging implicit configurations. While Explicit Modules represent a step in this direction, significant progress remains ahead.

Encouraging Explicitness and Mutating the Graph

Tuist introduces a Swift DSL for declaring project graphs. When users define their projects, they’re essentially mapping out the module graph. The tuist generate command transforms this graph into an Xcode workspace and projects.

This subtle capability is key to project optimization. By generating Xcode workspaces and projects, we can optimize them during the generation phase.

While we can’t completely solve hermeticity, we can encourage it through our APIs, particularly at the dependency level. By designing APIs that promote explicit dependency declarations, we can mitigate non-hermetic behaviors.

Our approach offers several advantages:

  • Graph mutation using native Xcode project and target primitives
  • A language that discourages non-hermetic configurations
  • Utilizing XCFrameworks as compilation units

Caching and Selective Test Execution

By constructing a Merkle Tree that changes when a target changes, we can create unique identifiers for each target. This enables powerful workflows:

# Cache all cacheable targets tuist cache # Generate projects reusing cached binaries tuist generate tuist build tuist test

Tuist also introduces selective test execution. By persisting tests at the target level with associated hashes, the system can determine whether tests need re-execution.

Accelerating Feature Delivery

The combination of selective test execution and caching can yield significant CI improvements—up to 70% and beyond. Developers can optimize their Xcode projects without costly build system replacements or platform abstractions.

By declaring projects using a Swift DSL, teams can achieve optimizations, cleaner project structures, and more reliable Xcode performance. The impact on team productivity and business value delivery can be substantial.

Interested in learning more? Schedule a call with the Tuist team for a comprehensive walkthrough.

You might also like

1.14.0 Spezi, a release packed with improvements
In this blog post we present the improvements and bug fixes that we included in the version of Tuist 1.14.0
From a URL click to a running app preview: Introducing the Tuist macOS app
We've released a Tuist macOS app as the next step in making sharing your apps a joyful experience.
Interview with Angry Nerds - Project description helpers are a game changer for modular apps
In this blog post we interview Marcel from Angry Nerds, a custom software development company based in Wrocław, Poland. Marcel talks about a wide range of topics which includes their workflows, preferred code patterns and architecture, and their testing strategy.

Supercharge your app development

Get started