# Git Worktrees + Tuist Binary Cache for iOS projects

---

Imagine, you're halfway through a feature. Your tech lead drops an urgent bug fix on you. You stash your changes, switch branches, rebuild, fix the bug, switch back, restore stash  -  and Xcode decides it needs to reindex everything. Or your current ticket is in progress and an AI agent is thinking way too long, so you feel like you have time to solve another ticket. Sound familiar?

There's a Git feature many iOS developers have never touched that solves this completely: git worktrees. You keep your main project open on one branch while a second folder runs a different branch entirely  -  no stashing, no rebuild from scratch, no lost context.

This guide shows you how to use worktrees in your project and avoid the traps that catch people first time. After that I will show how we can use Tuist to cut new worktree build times for iOS projects. Let's start!

You can find the video version of this article [here](https://youtu.be/TOCkMgV4S0E).

## What's a worktree, exactly?

Think of your project folder as a desk. A worktree is a second desk in the same office  -  a different surface to work on, but connected to the same Git history. You can have different branches open at each desk simultaneously.

**Technically:** a git worktree is a separate working directory linked to the same repository. Each worktree can have a different branch checked out at the same time. Your original project folder is itself a worktree - the "default" one.

## The trade-offs upfront

Before you set one up, here's what you're getting into:

**What you gain** 👍:

- **Work on two branches simultaneously**  -  true parallelism, useful if you're running AI agents on separate tasks
- **No context switching overhead**  -  each worktree in it's own folder and stays on its own branch

**What it costs you** 👎:

- **Cold build time** when a worktree is new  -  no compiled artifacts, Xcode starts fresh
- **Disk space**  -  every worktree is a full working copy of your repository

The cold build time is the main pain point. Later this article will cover how to address it.

---

## How derived data works with worktrees

You might wonder: does each worktree build everything from scratch, or can they share a build cache?

**The short answer:** each worktree gets its own derived data folder automatically, and you don't need to configure anything.

Xcode identifies projects by a hash of their path. Since each worktree sits at a different path on disk, each one gets a separate folder under `~/Library/Developer/Xcode/DerivedData/`. There's no collision, no conflict, just independent build state per worktree.

## Setting one up

Two ways to create a worktree, depending on whether the branch already exists:

```bash
# Attach an existing branch to a new worktree folder
git worktree add ../ProjectName-wt1 branch-wt1-fix-bug

# Create a new branch and attach it at the same time
git worktree add -b branch-wt1-fix-bug ../ProjectName-wt1 source_branch
```

(Or you can just prompt your agent to start a new branch in a new worktree).

The path (`../ProjectName-wt1`) is where the new folder for the worktree gets created. Placing it outside your main repo directory is the standard convention  -  it keeps things clean and avoids confusing Xcode.

To check what worktrees you have and clean up when you're done:

```bash
git worktree list
git worktree remove ../ProjectName-wt1
```

---

## One important constraint

Git won't let two worktrees check out the same branch at the same time. If you try, you'll see:

```bash
git switch branch-wt1-fix-bug
# fatal: 'branch-wt1-fix-bug' is already used by worktree at '../path_to_your_project/ProjectName-wt1'
```

This is why it's worth naming your branches with a `wt1` prefix:

`branch-wt1-fix-bug`

It makes clear that the branch was created for worktree 1. When you see it, you immediately know where it lives. Beyond the branch name, the folder itself usually makes it obvious which worktree you're in.

---

## How many worktrees should you have?

For most setups, **two should be enough**: your main project folder acts as the first, plus one worktree to parallelize against.

```plaintext
ProjectName/         ← main, always on staging
ProjectName-wt1/     ← worktree 1, switch branches as needed
```

Rather than creating a new worktree per task, try to treat `wt1` as a reusable second desk. When a task is done, you just switch it to the next branch:

```bash
cd ../ProjectName-wt1
git switch some-other-branch
```

Reusing worktrees lowers the cold build time for each new session.

Add more worktrees if your Mac has enough storage and you can comfortably keep track of what's where.

---

## A few things to watch

**Use different simulators per worktree.**

If both worktrees target the same simulator, one run can kill the other mid-session. Create dedicated simulator presets with clear names  -  for example, "`iPhone 17 Pro 26.4 wt1`".

**Check your version manager.**

If you use mise, upgrade to v2026.7.5 or higher  -  it introduced worktree trust sharing so you don't get re-prompted for each new worktree.

---

## SPM and Worktrees

While worktrees and SPM can work together on small projects, package resolution becomes a serious bottleneck as a project scales. There are four main reasons why SPM and worktrees struggle together:

**Global Cache Contention**

Think of the SPM package cache like a single key to a shared cabinet  -  only one person can use it at a time. When fetching packages, SPM uses a lock-protected global cache (~/Library/Caches/org.swift.swiftpm/) to store the raw repositories. If two worktrees attempt to resolve packages simultaneously, one takes the lock and forces the other to wait, sometimes leading to a resolution error or an infinite resolving state.

**Local Resolution Overhead**

Even after packages are cached globally, each worktree must individually calculate its own dependency graph and copy the correct source files into its local build environment. The infamous Xcode "Resolving Package Graph" spinner takes time, and that adds up across multiple worktrees.

**Lack of Binary Caching**

SPM doesn't offer binary caching out of the box, so every worktree must rebuild all dependencies from source. Repeatedly compiling non-trivial packages from scratch in each worktree creates a massive bottleneck.

**Fragile Xcode Integration**

SPM's integration with Xcode is not stable enough at scale in practice. Package resolution often fails silently, gets stuck, or produces inconsistent results and errors depending on timing. Developers end up relying on manual workarounds  -  resetting package caches, cleaning the build folder, restarting Xcode, rebooting the Mac, buying a new machine…

Together, these 4 reasons are why my team moved from SPM to Tuist.

---

## Xcode 26 Compilation Cache

Xcode 26 introduced content-addressable compilation caching via `COMPILATION_CACHE_ENABLE_CACHING`, and moved the store to a machine-wide pool instead of siloing it per-project inside DerivedData. Think of it like a fingerprint check rather than an address check  -  Xcode stores compiled results by the content of the source file, not by where it lives on disk. Two worktrees that compile the same unmodified file can share the same cached result.

`COMPILATION_CACHE_ENABLE_CACHING = YES`

**A machine-wide cache pool**

You can view and manage the pool via `Xcode → Settings → Locations → Compilation Cache`.

Because this pool is shared across your entire machine, different Git worktrees can theoretically benefit from each other's cached compilations. But there's a catch.

**The limitation: absolute paths**

While worktrees share the global cache, you won't get a 100% cache hit rate. Many generated artifacts  -  like bridging headers  -  still embed absolute paths.

Because `Project-wt1/` and `Project-wt2/` exist at different paths on your disk, the compiler treats those absolute paths as distinct, leading to partial cache misses. This is a known issue that Apple is addressing for future versions. Until it's resolved, worktrees at different paths will always have some cache misses.

**Real-world experiment**

To measure the actual impact, I enabled the compilation cache on a large iOS Xcode project containing hundreds of modules. My goal was to see how much time the cache would save when starting a brand-new worktree.

The result: the difference in build time was minimal, close to zero.

**Why some projects won't see big wins**

Even if the Swift compilation step becomes faster, overall builds can still feel slow. Compilation is only one piece of a larger pipeline. If your build times aren't dropping, you're likely hitting non-compiler bottlenecks:

- **Asset Catalog Processing:** compiling and optimizing heavy design assets
- **Large Copy Phases:** moving thousands of files into the final app bundle
- **Heavy Script Phases:** blocking the pipeline with SwiftLint, code generation, or crash-reporting scripts
- **Linking and Embedding:** assembling the final executable and embedding dynamic frameworks

The Xcode 26 compilation cache is a great architectural step forward, but until the absolute path issue is fixed and non-compiler bottlenecks are optimized, you may not see dramatic time savings on fresh worktrees just yet.

---

## Tuist Binary Cache

Tuist has a built-in solution that helps with worktrees: **binary caching**.

Think of it like a frozen meal: Tuist pre-cooks your dependencies once and stores them as precompiled binaries. Every worktree reheats the same meal instead of cooking from scratch.

There are two options: `.allPossible` and `.onlyExternal`.

By running `tuist cache warm`, Tuist  -  depending on the setting  -  either builds every module in your project or only third-party dependencies, and stores them as precompiled binaries. The key is that all your worktrees share this cache. So when you spin up a new worktree and run tuist generate, it pulls the cached binaries instead of compiling everything from source.

Tuist stores the cache at: `~/.cache/tuist/Binaries`

That's global per machine, all worktrees share it automatically with no extra config needed.

## Benchmark

I measured the impact on the same iOS project with hundreds of modules. This is how long it takes to setup the new workflow and build the project depending on the chosen caching strategy:

![](https://cdn.hashnode.com/uploads/covers/63177bc3961d2507e75fbf92/77fcc9a3-6b17-49c2-aa93-5b665355ba0c.png align="center")

Or, in more visual way:

![](https://cdn.hashnode.com/uploads/covers/63177bc3961d2507e75fbf92/2f9879d2-1219-4da1-9e75-bfbc1923eb61.png align="center")

It's not free  -  `tuist cache warm` on a cold cache took around 600s to run. Overall time including cache warming is bigger with caching strategies. But you generally need to warm cache only once and it's shared between worktrees after that. So if you rebuild the project often during working on your tickets, it's a huge improvement.

You pay upfront to get the speed boost on every subsequent build.

**Worth noting:** the gains depend heavily on what you change in the project. If your changes affect the dependency graph heavily, cache invalidation spreads and results will be worse. This is one more reason to invest in a well-modularized architecture  -  cleaner module dependency graph organization means fewer modules get invalidated when something changes.

---

## Wrapping up

Worktrees are a great way to get true branch parallelism on git repository work, especially useful if you're starting to pair them with AI coding agents working on separate tasks. The new worktree cold build time is the only real friction, and the reusable-worktree approach keeps that to a one-time cost per worktree. Tuist binary caching can cut that further, but it has its own upfront cost.

Thank you for reading this!
