Scaling iOS Application Development with Tuist

May 26, 2026

Originally published on the Delivery Hero Tech Blog.

Over time, codebases tend to increase in size. Engineers can't help but create code, product managers are creating features, releases are being shipped: the product matures, but never stops evolving, adding more and more sub-features and capabilities. In the era of AI, new code is generated even faster, accelerating iteration but also amplifying complexity, dependency growth, and architectural entropy. In mobile engineering, where in most setups the application code must be compiled to run, this creates a challenge with the time it takes to build the application.

With the modern tools at hand, the Delivery Hero Logistics team managed to significantly decrease the project compilation time, while enabling state-of-the-art modularization. How? Continue reading.

Where do we stand?

Our team works on the application for riders, who play a key role in our ecosystem by helping us provide the best possible delivery experience for customers across the Delivery Hero Group brands.

iOS Rider app went from 25 modules in 2022 to roughly 115 today.

Modules over time

Before Tuist, our local clean builds were taking 2.5 minutes, and incremental ones 0.5 minutes. On the CI, a full run of unit tests took around 30 minutes. This already includes selective testing configured via XcodeSelectiveTesting.

Build performance has a strong impact on productivity, as engineers have to wait for the build to complete in order to test their changes and unit tests must pass for PR to become mergeable.

Possible solutions

We are not the first team to face this challenge. There are several established approaches to improving build times and scaling large mobile codebases:

  • Bazel / Buck
  • Tuist
  • Custom solutions on top of Xcode

Swift Package Manager was already in use across the project for both external dependencies and local packages. The remaining challenge was therefore not dependency management, but how to scale project structure, build performance, and modularization on top of Xcode.

Different solutions come with different trade-offs in terms of performance, flexibility, maintenance cost, and team adoption.

Bazel or Buck

Build systems like Bazel or Buck are powerful tools designed for large-scale, multi-language codebases. They offer advanced features such as aggressive caching, highly parallelized builds, and hermetic execution, which can lead to significant build time improvements, especially in CI environments.

However, adopting Bazel or Buck typically requires diverging from Xcode's native build system. This introduces additional complexity, including maintaining custom build rules (often via open-source Bazel rules), bridging gaps with Apple tooling, and onboarding the team to a fundamentally different build model. Additionally, it's not always immediately possible to benefit from improvements and features the Apple team is adding to Xcode and its build system. For our team, this would have been a large technical and organizational shift, with a high upfront investment and long-term maintenance cost.

Additional downside of this approach is the cognitive overhead introduced by adopting a configuration language that differs from the one the team uses daily. For example, Bazel module definitions are written in Starlark, which requires developers to learn new syntax, tooling conventions, and debugging patterns. In contrast, Tuist project definitions are written in Swift allowing engineers to leverage existing knowledge, tooling, and language features. This reduces context switching, improves readability of project configuration, and lowers the barrier for contributors to understand and evolve the build setup alongside the product code.

Custom solution

We already had experience with a custom approach to improve build times, particularly around external dependencies. In this setup, prebuilt xcframeworks were stored directly in the repository to avoid recompilation and reduce build times.

While this approach delivered short-term gains, it proved to be inflexible over time. Maintaining binary artifacts, keeping them in sync with source changes, handling multiple architectures, and updating them reliably became increasingly costly. As the codebase and team scaled, this solution required continuous manual effort and did not provide a sustainable foundation for long-term modularization.

Why Tuist

Tuist offered a middle ground between the power of custom build systems and the familiarity of Xcode. It allowed us to keep Xcode as the underlying build system, while moving project configuration and structure into code.

Key reasons we chose Tuist include:

  • Code-defined project structure: Project configuration lives in Swift code, making it explicit, reviewable, and easier to evolve over time.
  • Scalable modularization: Defining modules, targets, and dependencies becomes systematic, enabling consistent architectural boundaries across the codebase.
  • Faster builds through better structure: By enforcing smaller, well-defined modules, Tuist enables more effective incremental builds and reduces unnecessary recompilation.
  • First-class integration with Swift Package Manager: Tuist works seamlessly with both external and local Swift packages, allowing us to build on our existing dependency setup.
  • Improved CI performance and reproducibility: Generated projects are deterministic, reducing CI flakiness and enabling more predictable build behavior.

By adopting Tuist, we managed to reduce CI build and unit test execution time by 2.5x, while simultaneously simplifying project maintenance and enabling a more scalable modular architecture.

What Tuist actually is?

This is the question I've had to answer multiple times, especially outside the mobile engineering circle.

When explaining Tuist to leadership, I avoided framing it as "yet another build tool." Instead, I described it as infrastructure for managing complexity. Tuist does not replace Xcode or Swift Package Manager; it sits on top of them and turns project configuration into code.

In simple terms: Tuist lets us describe how the app is structured, how modules depend on each other, and how everything is built, using Swift code instead of manually maintained Xcode project files. Xcode projects are then generated deterministically from that code. This makes the setup reproducible, reviewable, and easier to evolve as the codebase grows.

Working with Tuist

Tuist is not just a local project generator. It is complemented by an online service that enables features critical for scaling build performance across a team and CI.

One of the key capabilities is binary caching. Tuist computes a hash for each module based on its sources, build settings, and dependencies. If the hash hasn't changed, the compiled artifact can be reused instead of rebuilt. This applies both locally and in CI, drastically reducing redundant work.

Another important feature is selective testing. Since Tuist understands the full dependency graph, it can determine which tests are actually affected by a given change. Instead of running the entire test suite on every commit, CI can execute only the relevant subset, further reducing feedback time.

Because the project structure and dependencies are defined in code, Tuist also makes the dependency graph explicit and machine-readable. This enables better tooling, easier reasoning about architectural boundaries, and opens the door for automation that would be difficult or impossible with opaque Xcode project files.

Tuist Capabilities Deep Dive

Declarative, code-defined project generation

At its core, Tuist lets you define your entire project structure in Swift using manifest files such as Workspace.swift and Project.swift. From that definition, Tuist can automatically generate consistent, conflict-free Xcode project and workspace files.

This means:

  • Project configurations are reviewable and versionable as code.
  • You dramatically reduce merge conflicts common with manually edited Xcode files.
  • You can express modular boundaries, targets, and schemes in a declarative way.
  • The generated workspace reflects exactly what the team intended: no hidden Xcode state.

This code-defined approach also enables workflows like generating only a subset of modules for focused development (e.g., using selective generation flags) and makes the project structure readable by humans and tooling alike.

Other productivity and workflow enhancements

Beyond generation and caching, Tuist provides additional features and tooling that help at scale:

  • Selective testing: By understanding your dependency graph, Tuist can determine which tests are affected by a change and run only those — shortening test feedback loops.
  • Project insights and analytics: Tuist's tooling can expose metrics about build performance and project health, helping teams identify bottlenecks before they impact delivery.
  • Package registry acceleration: Tuist offers tooling around Swift Package Manager resolution, turning what can be minutes of package resolution into seconds, which improves onboarding and CI performance.
  • Tuist simplified our strategy for white-label app–specific features. It allowed us to integrate third-party SDKs for individual white-label apps, and even use different versions of the same SDK across multiple apps — a process that was much complex and harder to manage.

Together, these features allow teams to maintain Xcode-native workflows while still benefiting from advanced build-system hygiene, scalable modular architecture, and faster feedback — without the learning curve or infrastructure cost of alternatives like Bazel or Buck.

Selective Testing Deep Dive

Selective testing is a powerful mechanism that allows reducing the number of tests executed based on heuristics applied to the changeset.

XcodeSelectiveTesting, developed by Michael Gerasymenko and available as a free and open-source project on GitHub, uses a multi-step approach:

  • Change Detection: It utilizes git diff to analyze the source code repository and identify the exact changeset.
  • Impact Analysis: It then reads the project structure to determine which targets are affected by the changes, either directly or indirectly due to dependencies.

Tuist implements an advanced caching mechanism by hashing all the files belonging to a specific target. This hash uniquely identifies the state of that target. When tests are planned, Tuist checks this hash against a remote cache to determine if the tests for this exact target state have been executed successfully before.

If a successful execution is found, the test target creation and subsequent execution are skipped, leading to significant time savings in the CI/CD pipeline and local development.

Results and caveats

Results

Tuist is a paid tool, and that is an important consideration. However, the cost is significantly outweighed by the savings in CI resources alone. By reducing build and test times by 2.5x, we were able to lower CI usage and shorten feedback loops, resulting in a clear net gain.

Beyond CI, developers benefit from faster local builds and more predictable behavior. Engineers can iterate on features with less waiting, which compounds over time and directly improves team productivity.

Our local clean builds went from 2.5 minutes to 0.5 minutes (5x improvement), and incremental builds from 30 seconds to 10–15 seconds (2x improvement).

Architecture

An additional, often underestimated benefit is architectural clarity. With the dependency graph defined in code, it becomes much easier to reason about module boundaries and enforce architectural decisions. This structure is also friendly to modern AI-assisted tooling, which can navigate, analyze, and reason about the project more effectively than with manually maintained project files.

That said, Tuist is not free of trade-offs. It introduces an additional layer of tooling and requires discipline in maintaining the project definition. Like any infrastructure choice, it pays off most when the team commits to using it consistently.

Challenges and trade-offs

While the results have been significant, adopting Tuist was not without challenges. Like any infrastructure change, it introduced complexity during migration and surfaced edge cases we had to learn how to handle.

Build setting edge cases and test-only frameworks

One of the more subtle challenges involved test-only framework targets.

Some internal modules were intended to be used exclusively by test targets. These frameworks need to import XCTest, which requires enabling:

ENABLE_TESTING_SEARCH_PATHS = YES

This allows the test bundle to link correctly against XCTest. However, this setting must be applied carefully. If such a module is accidentally imported by a production target, the application will fail to link because production binaries cannot depend on test frameworks.

These kinds of issues are not unique to Tuist, but when modularizing aggressively and defining targets in code, such constraints become more visible and must be handled deliberately.

Migration cost and temporary dual setup

Migrating an existing, mature project to Tuist comes with a real cost. A time investment must be done in order to complete the migration. Duration of the migration would depend on the size of the project and the team's size. In our experience, it was possible to migrate base configuration in one to four weeks for a monorepo setup. AI tools like Codex or Claude Code are able to assist with the migration significantly.

Additionally, for a period of time, the team had to support both configurations:

  • the original, manually maintained Xcode setup, and
  • the new Tuist-generated structure.

Maintaining two parallel configurations introduced overhead in CI, onboarding, and day-to-day development. This transitional phase required careful coordination and discipline to avoid divergence between setups.

As with most architectural migrations, the long-term gains only materialize after this temporary investment.

Debugging limitations with cached binaries

Binary caching significantly improves build times, but it changes certain debugging behaviors.

When a module is fetched from cache as a precompiled binary:

  • The debugger may occasionally struggle to infer types correctly.
  • Printing properties or inspecting complex objects can sometimes fail.
  • Stepping through implementation details is limited if source code is not available in the generated project.

Additionally, if a module is fully cached and not generated as source in the local workspace, its source files are not directly visible in Xcode. While this is expected behavior for binary artifacts, it can temporarily slow down debugging workflows when deeper inspection is needed.

In practice, these situations can be resolved by disabling cache for specific modules during development or forcing a local rebuild. However, it is an important trade-off to acknowledge: faster builds sometimes come at the cost of deeper runtime introspection.

Perspective

None of these challenges were blockers, but they required awareness and team alignment. Tuist is not "magic build speed for free." It is infrastructure, and infrastructure demands understanding.

That said, once the migration stabilized, the productivity gains outweighed the temporary friction.

Conclusion

As mobile codebases grow, build performance and architectural clarity become first-class concerns. Incremental optimizations and ad-hoc solutions can help temporarily, but they rarely scale with the product and the team.

By adopting Tuist, we were able to significantly reduce build times, improve CI efficiency, and establish a scalable, code-defined project structure without abandoning Xcode or our existing Swift Package Manager setup. For us, Tuist proved to be a pragmatic and sustainable investment in developer productivity and long-term maintainability.

Treat for the curious

For teams integrating Tuist, I strongly recommend adding the following command to your PR workflows:

tuist inspect implicit-imports

Why?

In day-to-day development, it's easy to add a new import statement to a file and forget to declare the corresponding dependency in the Tuist manifest.

Xcode will not necessarily complain. As long as the referenced framework exists somewhere in the search path, the import may compile successfully. From Xcode's perspective, everything looks fine.

However, Tuist relies on an explicitly declared dependency graph. If a dependency is not declared in the manifest:

  • Tuist does not "see" the relationship.
  • Selective testing may skip tests that should actually run.
  • Binary caching may produce incorrect reuse decisions.
  • The dependency graph becomes subtly inconsistent.

The implicit-imports inspection command detects exactly this scenario. It verifies that all imports are backed by explicitly declared dependencies and fails fast when the graph and the source code diverge.

In other words: it protects the integrity of your modular architecture.

When working with build graph–aware tooling like Tuist, correctness of the dependency graph is not optional, it is foundational. Adding this lint step to your PR pipeline is a small investment that prevents subtle, hard-to-debug issues later.