2026-07-06 6 min read Software Engineer

Tuist Module Cache for Modular and Agentic Workflows

Tuist module cache is great. But treating it as one magic switch is where the pain starts.

In a large modular iOS project, you do not have one workflow. You have several:

  • sometimes you want to run the app and review what a coding agent changed in Xcode,
  • sometimes you use static linking but need SwiftUI Previews for one module,
  • sometimes you are the coding agent and you do not need Xcode at all, just a fast CLI feedback loop.

Those are different jobs. One generated workspace will not be good at all of them.

The setup I like is boring:

  • all-possible - cache everything Tuist can safely replace with binaries,
  • only-external - cache third-party dependencies only,
  • a few mise tasks so I do not have to remember long commands.

No custom build system. No clever wrapper around a wrapper. Just enough command surface for the actual workflows.

Tuist Setup

The project-specific details do not matter much. The important part is that generation can switch three things from the command line:

  • cache profile: aggressive allPossible vs onlyExternal,
  • configuration: usually Debug,
  • linking: static by default, dynamic for SwiftUI Previews.

For previews I pass TUIST_LINKING=dynamic into Tuist manifests and switch module products from static frameworks to dynamic frameworks. That’s it. It should be a switch, not a subsystem.

1. Reviewing Agent Work

The problem: a coding agent changed a few files. I want to run the app, click through the result, and see exactly those changed modules in Xcode.

I do not want the full workspace. I also do not want the changed module hidden behind cache.

Command:

mise run tuist:generate:diff

Task:

[tasks."tuist:generate:diff"]
description = "Generate the fast Xcode workspace with changed modules from source and the rest cached."
depends = ["tuist:install"]
run = '''
set -e
: "${TUIST_GENERATE_CACHE_PROFILE:=all-possible}"
: "${TUIST_GENERATE_CONFIGURATION:=Debug}"
TUIST_GENERATE_TARGETS="$(scripts/tuist-generate-diff-targets.py)"
tuist generate $TUIST_GENERATE_TARGETS --no-open --cache-profile "$TUIST_GENERATE_CACHE_PROFILE" --configuration "$TUIST_GENERATE_CONFIGURATION"
'''

The helper script does the boring work:

  • reads staged, unstaged, and untracked files from git diff,
  • maps paths like Modules/Features/Home/... to Tuist targets,
  • always includes the main app target so the app can run,
  • lets Tuist expand the dependency graph.

I could publish this script, but I do not think that is the useful part anymore. In the age of AI coding, it is usually better to ask your agent to generate this small mapper for your project structure: your module paths, your target naming rules, your test target conventions, your edge cases.

So when Home changes, I do not try to hand-write the entire target list. I pass the app and the changed modules. Tuist adds whatever the graph requires: dependencies, test targets, and dependent modules that need to be built from source. Everything else can come from cache.

The local dependent modules are useful during review too. Sometimes the changed code calls into another app module and I want to jump straight to that file in Xcode. Third-party dependencies can stay binary; local code is often worth keeping reachable.

That’s the whole trick.

2. SwiftUI Previews with Static Linking

Static linking is good for normal builds. SwiftUI Previews are less happy with it. For preview work, I generate a dynamic workspace for the module I am touching.

Not the whole app. Just the module.

Command:

mise run tuist:generate:preview Home

Task:

[tasks."tuist:generate:preview"]
description = "Generate a dynamic preview workspace for explicit module targets."
depends = ["tuist:install"]
run = '''
set -e
if [ "$#" -eq 0 ] && [ -z "${TUIST_GENERATE_TARGETS:-}" ]; then
  echo "usage: mise run tuist:generate:preview <target> [target...]" >&2
  exit 2
fi

targets=("$@")
if [ "$#" -eq 0 ]; then
  read -r -a targets <<< "$TUIST_GENERATE_TARGETS"
fi

: "${TUIST_GENERATE_CACHE_PROFILE:=all-possible}"
: "${TUIST_GENERATE_CONFIGURATION:=Debug}"

TUIST_LINKING=dynamic tuist generate "${targets[@]}" --no-open --cache-profile "$TUIST_GENERATE_CACHE_PROFILE" --configuration "$TUIST_GENERATE_CONFIGURATION"
'''

Why allPossible? Because the target passed to tuist generate is focused and stays source-based. The rest can be cached.

3. Coding Agent Feedback Loop

A coding agent does not need Xcode. It does not need source files visible in the project navigator. It needs to generate, build, test, and get feedback quickly.

For that, the aggressive app workspace is the default:

mise run tuist:generate:app

Task:

[tasks."tuist:generate:app"]
description = "Generate the fast Xcode workspace using the full Tuist module cache."
depends = ["tuist:install"]
run = '''
set -e
: "${TUIST_GENERATE_CACHE_PROFILE:=all-possible}"
: "${TUIST_GENERATE_CONFIGURATION:=Debug}"
tuist generate App --no-open --cache-profile "$TUIST_GENERATE_CACHE_PROFILE" --configuration "$TUIST_GENERATE_CONFIGURATION"
'''

This is the “give me the fastest useful feedback” mode. If the agent is building and testing through CLI, keeping every local module source-based buys nothing.

Benchmarks

Environment: Tuist 4.200.5, Xcode 26.5.

Each variant ran 3 times with a fresh DerivedData. I did not benchmark tuist generate alone because that number is mostly a distraction. The useful number is generate + clean build.

Variant What it builds Average Range Workspace
app app target, everything else cached as much as possible 13.790 s 13.641-13.938 s 1 project
diff app + modules from the diff + graph required by Tuist 22.410 s 20.763-25.092 s 4 projects
all-modules all local modules from source 65.293 s 61.996-69.962 s 37 projects

The difference is not subtle. generate:diff was 42.883 s faster than generating all local modules from source. That is the point: for review, I want the changed code visible and buildable, not every module in the repository.

generate:app is even faster, but it solves a different problem. It is great for normal CLI feedback. It is worse for reviewing agent work because Xcode will not show the modules the agent changed.

For coding agents, I still default to allPossible. They do not use Xcode, so keeping local modules source-based is just ceremony.

The Whole Setup

The command set is small:

# fast app workspace
mise run tuist:generate:app

# review current git diff
mise run tuist:generate:diff

# SwiftUI Preview for one module
mise run tuist:generate:preview Home

# baseline/debug: all local modules from source, external deps cached
mise run tuist:generate:all-modules

# warm the default cache
mise run tuist:cache:warm

That covers the workflows I actually use day to day.

The important part is not the exact task names. It is having separate commands for separate jobs: app feedback, agent review, preview work, and the full source workspace when you really need it.