name: tuist description: Manages Tuist configuration. Use when adding xcframeworks, configuring dependencies, or modifying Project.swift.
Tuist
Skill for managing Tuist configuration in the project.
MCP Server (Required)
IMPORTANT: This project has a Tuist MCP server installed. Always use the MCP tools for Tuist operations instead of running bash commands directly.
Available MCP Resources
The Tuist MCP server provides project graph information:
URI: tuist:///{project-path}
Resource: {AppName} graph
Type: application/json
Use ReadMcpResourceTool to read the project graph and understand:
- Project structure and targets
- Dependencies between modules
- Build configurations
Usage
// Read project graph
ReadMcpResourceTool(
server: "tuist",
uri: "tuist:///{project-path}"
)
Why use MCP?
- Provides structured JSON output of project configuration
- Enables understanding of module dependencies
- Integrates directly with Claude Code workflow
Note: Use
ListMcpResourcesToolto see all available Tuist resources.
Project Options
The root Project.swift delegates to mainApp.project (defined in ModuleKit/App.swift). The project structure adapts based on the active module strategy set in ModuleFactory.swift.
// Project.swift
let project = mainApp.project
// App.swift — generates the project with all module targets and schemes
Project(
name: name,
options: .options(
automaticSchemesOptions: .disabled, // Manual scheme generation
developmentRegion: "en",
disableBundleAccessors: true, // BundleAccessorGenerator handles this
disableSynthesizedResourceAccessors: true // No TuistAssets+*.swift
),
packages: packages, // Aggregated from modules (empty for FrameworkModule)
settings: .settings(
base: baseSettings.merging([...]) { _, new in new },
configurations: BuildConfiguration.all
),
targets: [appTarget, uiTestsTarget] + moduleTargets, // App + UI tests + module framework targets
schemes: allSchemes + [uiTestsScheme] + moduleSchemes // Environment schemes + tests + per-module
)
// Workspace.swift — coverage mode adapts to active strategy
let workspace = Workspace(
name: mainApp.name,
projects: ["."],
generationOptions: .options(
autogeneratedWorkspaceSchemes: .enabled(
codeCoverageMode: mainApp.coverageMode, // .targets() for Framework, .relevant for SPM
testingOptions: []
)
)
)
Module Test Scheme
The Challenge (Dev) scheme aggregates all module test targets via ModuleAggregation, which dispatches on the active strategy:
- Framework: Uses
.targets(...)with aggregatedtestableTargetsandcodeCoverageTargetsfrom each module — no test plan file is generated. - SPM: Generates a
.xctestplanviaTestPlanGeneratorand uses.testPlans(...), which is the only mechanism to aggregate test targets across SPM local packages.
Focus mode: When TUIST_FOCUS_MODULES env var is set (via generate.sh --focus), ModuleAggregation filters testable and coverage targets to only include focused modules and their transitive dependents. Each FrameworkModule target is tagged with metadata: .metadata(tags: ["module:<ShortName>"]) for Tuist's tag-based generation filtering. The expansion is computed in generate.sh using tuist graph --format json to build the reverse dependency graph.
The test command is the same for both:
xcodebuild test \
-workspace Challenge.xcworkspace \
-scheme "Challenge (Dev)" \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro,OS=26.1'
Tests run via xcodebuild test (not tuist test) because Tuist can't resolve SPM package test targets when using .testPlans().
Why disable synthesizers?
| Option | Effect | Reason |
|---|---|---|
disableBundleAccessors |
No Tuist Bundle+*.swift |
BundleAccessorGenerator creates custom Bundle.module accessors |
disableSynthesizedResourceAccessors |
No TuistAssets+*.swift |
Not using generated asset accessors |
Localization Configuration
The app must declare supported languages in CFBundleLocalizations for iOS to load localizations from embedded frameworks:
// Project.swift
let appInfoPlist: [String: Plist.Value] = [
"CFBundleLocalizations": ["en", "es"],
// ... other settings ...
]
| Key | Value | Description |
|---|---|---|
CFBundleLocalizations |
["en", "es"] |
Languages the app supports |
developmentRegion |
"en" |
Default/fallback language (in project options) |
Important: Without
CFBundleLocalizations, iOS ignores localizations in embedded frameworks (likeChallengeResources) and always shows the development language.
See /resources skill for adding new languages and managing localized strings.
URL Schemes (Deep Links)
To enable the app to receive external URLs (from Safari, other apps, push notifications), configure CFBundleURLTypes in the app's Info.plist:
// Project.swift
let appInfoPlist: [String: Plist.Value] = [
"UILaunchStoryboardName": "LaunchScreen",
// ... other settings ...
"CFBundleURLTypes": [
[
"CFBundleURLSchemes": ["challenge"],
"CFBundleURLName": "com.app.Challenge",
],
],
]
Configuration:
| Key | Value | Description |
|---|---|---|
CFBundleURLSchemes |
["challenge"] |
URL scheme the app responds to |
CFBundleURLName |
"com.app.Challenge" |
Unique identifier for this URL type |
Usage:
After configuration, the app can receive URLs like:
challenge://character/listchallenge://character/detail/42
Handle incoming URLs in RootContainerView with .onOpenURL:
.onOpenURL { url in
appContainer.handle(url: url, navigator: navigationCoordinator)
}
See /navigator skill for deep link implementation details.
Derived Folder
Tuist generates files in the Derived/ folder:
Derived/
└── InfoPlists/
├── {AppName}-Info.plist
└── {AppName}UITests-Info.plist
Contents: Only Info.plist files for the app and UI tests targets. Module Info.plists are managed by their respective strategy (Tuist for Framework, SPM for SPM packages).
Git: The Derived/ folder is in .gitignore.
Adding an XCFramework as a Dependency
1. XCFramework Location
Place the .xcframework file in the Tuist/Dependencies/ directory:
{AppName}/
├── Tuist/
│ ├── Dependencies/
│ │ └── FrameworkName.xcframework
│ └── ProjectDescriptionHelpers/
├── Libraries/
├── App/
└── Project.swift
Note: The
Tuist/Dependencies/directory is ignored by git. Do not commit xcframeworks to the repository.
2. Create the XCFrameworks Helper
If it doesn't exist, create the file Tuist/ProjectDescriptionHelpers/Dependencies.swift:
import ProjectDescription
public enum XCFrameworks {
public static let frameworkName: TargetDependency = .xcframework(path: "Tuist/Dependencies/FrameworkName.xcframework")
}
3. Add the Dependency to a Target
In Project.swift, add the dependency to the target that needs it:
.target(
name: appName,
destinations: [.iPhone, .iPad],
product: .app,
bundleId: "com.app.\(appName)",
deploymentTargets: developmentTarget,
sources: ["App/Sources/**"],
resources: ["App/Sources/Resources/**"],
dependencies: [
XCFrameworks.frameworkName,
]
)
4. For Internal Frameworks
If the xcframework is a dependency of an internal framework in Libraries/, update Target+Framework.swift:
public extension Target {
static func createFramework(
name: String,
destinations: ProjectDescription.Destinations = [.iPhone, .iPad],
dependencies: [TargetDependency] = []
) -> Self {
let targetName = "\(appName)\(name)"
return .target(
name: targetName,
destinations: destinations,
product: .framework,
bundleId: "${PRODUCT_BUNDLE_IDENTIFIER}.\(targetName)",
sources: ["Libraries/\(name)/**"],
dependencies: dependencies
)
}
}
Then in Project.swift:
.createFramework(
name: "Features/UserFeature",
dependencies: [
XCFrameworks.frameworkName,
]
)
5. Regenerate the Project
tuist generate