Skip to main content

Overview

A well-structured Tuist project improves maintainability, reduces build times, and makes collaboration easier. This guide covers best practices for organizing your projects, targets, and dependencies.

Understanding Tuist Directory Structure

Standard Project Layout

A typical Tuist project follows this structure:
MyProject/
├── Tuist/
   ├── ProjectDescriptionHelpers/
   └── Project+Templates.swift
   └── Package.swift
├── Tuist.swift
├── Workspace.swift
├── Projects/
   ├── App/
   ├── Project.swift
   ├── Sources/
   ├── Resources/
   └── Tests/
   └── Features/
       ├── FeatureA/
   └── Project.swift
       └── FeatureB/
           └── Project.swift
└── xcconfigs/
    ├── Project.xcconfig
    └── Targets/

Key Directories Explained

Tuist Directory

The Tuist/ directory serves two critical purposes:
  1. Signals the project root: Allows running Tuist commands from any subdirectory
  2. Contains shared configuration:
    • ProjectDescriptionHelpers/: Reusable Swift code for manifest files
    • Package.swift: External dependency definitions
Place the Tuist/ directory at your repository root. This allows you to run Tuist commands from anywhere within the project.

Root Directory Files

Tuist.swift: Project-wide configuration
Tuist.swift
import ProjectDescription

let tuist = Tuist(
    fullHandle: "your-org/your-project",
    project: .tuist(
        generationOptions: .options(
            enableCaching: true,
            disableSandbox: false
        )
    )
)
Workspace.swift: Groups multiple projects (optional)
Workspace.swift
import ProjectDescription

let workspace = Workspace(
    name: "MyWorkspace",
    projects: [
        "Projects/**"
    ]
)
Project.swift: Defines individual project targets
Project.swift
import ProjectDescription

let project = Project(
    name: "MyApp",
    targets: [
        // Target definitions
    ]
)
Xcode workspaces in Tuist are optional. Tuist auto-generates a workspace containing your project and its dependencies. Only use Workspace.swift if you need custom workspace configuration.

Organization Strategies

Single Project Structure

Best for small to medium apps with straightforward requirements:
MyApp/
├── Tuist/
   └── Package.swift
├── Tuist.swift
├── Project.swift
├── Sources/
   ├── App/
   ├── Features/
   └── Core/
├── Resources/
└── Tests/
When to use:
  • Single application target
  • Limited feature complexity
  • Small team (1-5 developers)

Multi-Project Structure

Best for larger applications with distinct feature modules:
MyApp/
├── Tuist/
├── Tuist.swift
├── Workspace.swift
├── Projects/
   ├── App/
   └── Project.swift
   ├── FeatureA/
   └── Project.swift
   ├── FeatureB/
   └── Project.swift
   └── Core/
       └── Project.swift
└── xcconfigs/
When to use:
  • Multiple feature teams
  • Need for clear module boundaries
  • Desire for parallel development
  • Teams of 5+ developers

Micro-Framework Architecture

Best for large-scale apps with strict separation of concerns:
MyApp/
├── Tuist/
├── Workspace.swift
├── Projects/
   ├── App/
   ├── Features/
   ├── Authentication/
   ├── Project.swift
   ├── Sources/
   ├── Tests/
   └── Example/
   ├── Dashboard/
   └── Settings/
   ├── Core/
   ├── Networking/
   ├── Database/
   └── UI/
   └── Platform/
       ├── Analytics/
       └── Logging/
When to use:
  • Very large applications
  • Multiple teams working independently
  • Need for module reusability
  • Desire for selective compilation and testing

Target Organization Best Practices

Target Types

Organize targets by their purpose:
.target(
    name: "MyApp",
    destinations: [.iPhone, .iPad],
    product: .app,
    bundleId: "com.example.myapp",
    sources: ["Sources/**"],
    resources: ["Resources/**"],
    dependencies: [
        .target(name: "FeatureA"),
        .target(name: "Core")
    ]
)

Source File Organization

Tuist 4.62.0+ supports buildable folders (Xcode 16+), which automatically sync with the file system:
Project.swift
let target = Target(
    name: "App",
    buildableFolders: [
        "App/Sources",
        "App/Resources"
    ]
)
Benefits:
  • No regeneration needed when adding/removing files
  • AI-friendly (coding assistants can modify files freely)
  • Eliminates merge conflicts in project files
  • Simpler configuration
Buildable folders are recommended for all new projects. They provide a better developer experience and work seamlessly with AI coding tools.

Using Wildcard Patterns (Traditional)

For older Xcode versions or more control:
Project.swift
let target = Target(
    name: "App",
    sources: ["App/Sources/**"],
    resources: ["App/Resources/**"]
)

Dependency Organization

Layered Architecture

Organize dependencies in layers:
// App layer - depends on features
App  Features

// Feature layer - depends on core
Features  Core

// Core layer - depends on external packages
Core  External Dependencies
Avoid circular dependencies. Tuist validates your dependency graph and will error on cycles.

Example Layered Structure

Project.swift
import ProjectDescription
import ProjectDescriptionHelpers

let project = Project(
    name: "MyApp",
    targets: [
        // App Target - Top Layer
        .target(
            name: "MyApp",
            product: .app,
            dependencies: [
                .target(name: "FeatureAuthentication"),
                .target(name: "FeatureDashboard")
            ]
        ),
        // Feature Targets - Middle Layer
        .target(
            name: "FeatureAuthentication",
            product: .framework,
            dependencies: [
                .target(name: "CoreNetworking"),
                .target(name: "CoreUI")
            ]
        ),
        // Core Targets - Bottom Layer
        .target(
            name: "CoreNetworking",
            product: .framework,
            dependencies: [
                .external(name: "Alamofire")
            ]
        ),
        .target(
            name: "CoreUI",
            product: .framework,
            dependencies: []
        )
    ]
)

Build Settings Organization

Using XCConfig Files

Extract build settings to xcconfig files for better maintainability:
xcconfigs/
├── Project.xcconfig              # Project-level settings
├── Targets/
   ├── App.xcconfig             # App target settings
   ├── FeatureA.xcconfig        # Feature settings
   └── Tests.xcconfig           # Test settings
└── Shared/
    ├── Debug.xcconfig           # Debug configuration
    └── Release.xcconfig         # Release configuration
// Project-level settings
IPHONEOS_DEPLOYMENT_TARGET = 15.0
SWIFT_VERSION = 5.9
MARKETING_VERSION = 1.0.0
Reference in your manifest:
Project.swift
let project = Project(
    name: "MyApp",
    settings: .settings(configurations: [
        .debug(name: "Debug", xcconfig: "./xcconfigs/Shared/Debug.xcconfig"),
        .release(name: "Release", xcconfig: "./xcconfigs/Shared/Release.xcconfig")
    ]),
    targets: [
        .target(
            name: "MyApp",
            settings: .settings(configurations: [
                .debug(name: "Debug", xcconfig: "./xcconfigs/Targets/App.xcconfig"),
                .release(name: "Release", xcconfig: "./xcconfigs/Targets/App.xcconfig")
            ])
        )
    ]
)

Using ProjectDescriptionHelpers

Create reusable code to reduce duplication across manifests:
Tuist/ProjectDescriptionHelpers/Project+Templates.swift
import ProjectDescription

public extension Project {
    static func feature(
        name: String,
        dependencies: [TargetDependency] = []
    ) -> Project {
        return Project(
            name: name,
            targets: [
                .target(
                    name: name,
                    destinations: [.iPhone, .iPad],
                    product: .framework,
                    bundleId: "com.example.\(name.lowercased())",
                    sources: ["Sources/**"],
                    resources: ["Resources/**"],
                    dependencies: dependencies
                ),
                .target(
                    name: "\(name)Tests",
                    destinations: [.iPhone],
                    product: .unitTests,
                    bundleId: "com.example.\(name.lowercased()).tests",
                    sources: ["Tests/**"],
                    dependencies: [
                        .target(name: name),
                        .xctest
                    ]
                )
            ]
        )
    }
}
Use in your manifests:
Projects/FeatureA/Project.swift
import ProjectDescription
import ProjectDescriptionHelpers

let project = Project.feature(
    name: "FeatureA",
    dependencies: [
        .target(name: "Core")
    ]
)

Best Practices

Avoid Complex Build Configurations

Stick to standard Debug and Release configurations:
Avoid using build configurations to model remote environments (e.g., Debug-Production, Release-Staging). This leads to complexity and potential inconsistencies.
Instead:
  • Use environment variables at runtime for different environments
  • Use compiler directives to conditionally compile code
  • Keep configurations simple and consistent
App Configuration
// Good: Runtime environment switching
let environment: Environment = {
    #if DEBUG
    return ProcessInfo.processInfo.environment["API_ENV"] == "production" 
        ? .production 
        : .development
    #else
    return .production
    #endif
}()

Validate Your Graph

Regularly check your dependency graph:
# Visualize the graph
tuist graph

# Check for issues
tuist generate

Keep Projects Focused

Each project should have a single, clear responsibility:
  • App: Composition and app-specific code
  • Features: User-facing functionality
  • Core: Shared utilities and infrastructure
  • Platform: Third-party integrations

Troubleshooting

Project Generation Failures

If tuist generate fails:
  1. Check for circular dependencies: tuist graph
  2. Validate manifest syntax
  3. Ensure all referenced files exist
  4. Check that target names are unique

Slow Build Times

If builds are slow:
  1. Break large targets into smaller modules
  2. Use static linking in release builds
  3. Enable binary caching (see Build Optimization)
  4. Review dependency graph depth

Merge Conflicts

If you still experience merge conflicts:
  1. Use buildable folders instead of explicit file lists
  2. Keep manifest files simple and focused
  3. Use ProjectDescriptionHelpers for shared logic
  4. Have each team work in separate feature directories

Next Steps

Manage Dependencies

Learn how to handle external and internal dependencies

Optimize Builds

Speed up compilation with caching and optimization techniques

Set Up CI/CD

Configure continuous integration for your structured project

Migrate from Xcode

Migrate an existing project to this structure