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.

Supercharge your Swift app development

Get started

You might also like

Define your watchOS apps and extensions easily with Tuist 0.19.0
Until today, defining watchOS apps and extensions in Tuist was not possible. The good news is that from Tuist 0.19.0 that's no longer true because it extends its beautifully simplified abstractions to watchOS. On top of that, we also shipped support for enabling test coverage in the schemes, and defining the deployment targets in targets. We also took the opportunity to iron out some bugs that had been reported by users.
Tuist 0.16.0 allows users to link system libraries and frameworks
From the just released 0.16.0 version of Tuist, users will be able to define dependencies with system libraries and frameworks from their targets. Moreover, we added support for customizing the list of input and output files in their target action, and generation of targets with no build settings at all. This version also ships with minor improvements and bug fixes that had been reported by users.
Enabling Tuist Cache: Enhancing the Developer Experience at Trendyol
Trendyol reduced build times by 65% with Tuist, cutting CI builds to 10 minutes and local UI test setups to 30 seconds. Their self-hosted Tuist instance ensures secure, fast performance, streamlining workflows for 170+ developers.