Back to blog

Project Generation with Tuist

·4 min read

Xcode projects are notoriously hard to merge. .xcodeproj files are XML that git can't resolve sensibly. Tuist solves this by generating projects from Swift code.

Why Tuist?

  1. Mergeable configs: Swift files merge cleanly
  2. Reusable templates: Define patterns once, use everywhere
  3. Consistent settings: No more "works on my machine"
  4. Type safety: Catch config errors at generation time

Real-World Success Stories

Major companies have seen dramatic improvements:

CompanyScaleBuild Time Improvement
Trendyol170+ developers65% faster (30 min → 10 min)
Lapse220 modules75% faster (53 min → 13 min)
Back MarketLarge teamUp to 90% with full cache

These aren't synthetic benchmarks—they're production CI pipelines.

Project Structure

MyApp/
├── Project.swift          # Main project definition
├── Tuist/
│   ├── Package.swift      # Dependencies
│   ├── Tuist.swift        # Tuist configuration
│   └── ProjectDescriptionHelpers/
│       └── Project+Templates.swift
├── MyApp/
│   └── Sources/
└── MyAppTests/
    └── Sources/

Basic Project.swift

import ProjectDescription
 
let project = Project(
    name: "MyApp",
    organizationName: "Windybank",
    targets: [
        .target(
            name: "MyApp",
            destinations: [.iPhone, .iPad],
            product: .app,
            bundleId: "net.windybank.MyApp",
            deploymentTargets: .iOS("17.0"),
            infoPlist: .extendingDefault(with: [
                "UILaunchStoryboardName": "LaunchScreen",
            ]),
            sources: ["MyApp/Sources/**"],
            resources: ["MyApp/Resources/**"],
            dependencies: [
                .external(name: "Alamofire"),
            ]
        ),
        .target(
            name: "MyAppTests",
            destinations: [.iPhone, .iPad],
            product: .unitTests,
            bundleId: "net.windybank.MyApp.tests",
            sources: ["MyAppTests/**"],
            dependencies: [
                .target(name: "MyApp"),
            ]
        ),
    ]
)

Adding Dependencies

Define in Tuist/Package.swift:

// Tuist/Package.swift
import PackageDescription
 
let package = Package(
    name: "Dependencies",
    dependencies: [
        .package(url: "https://github.com/Alamofire/Alamofire", from: "5.11.0"),
        .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.17.0"),
    ]
)

Tuist resolves SPM packages once, then caches them. Much faster than Xcode's resolution.

Reusable Templates

Create helpers in ProjectDescriptionHelpers/:

// Project+Templates.swift
import ProjectDescription
 
extension Project {
    public static func app(
        name: String,
        bundleIdPrefix: String = "net.windybank",
        destinations: Destinations = [.iPhone, .iPad],
        dependencies: [TargetDependency] = []
    ) -> Project {
        Project(
            name: name,
            organizationName: "Windybank",
            settings: .settings(base: baseSettings),
            targets: [
                .appTarget(name: name, bundleIdPrefix: bundleIdPrefix,
                          destinations: destinations, dependencies: dependencies),
                .testTarget(name: "\(name)Tests", bundleIdPrefix: bundleIdPrefix,
                           destinations: destinations, appName: name),
            ],
            schemes: [
                .appScheme(name: name),
            ]
        )
    }
}
 
private let baseSettings: SettingsDictionary = [
    "SWIFT_VERSION": "6.0",
    "SWIFT_STRICT_CONCURRENCY": "complete",
]

Now Project.swift is just:

import ProjectDescription
import ProjectDescriptionHelpers
 
let project = Project.app(
    name: "MyApp",
    dependencies: [
        .external(name: "Alamofire"),
    ]
)

Generating the Project

# Install Tuist
curl -Ls https://install.tuist.io | bash
 
# Generate Xcode project
tuist generate
 
# Open in Xcode
open MyApp.xcworkspace

Multi-Platform Support

.target(
    name: "MyApp",
    destinations: [.iPhone, .iPad, .mac, .appleTV],
    // ...
)

Tuist handles the conditional compilation automatically.

Schemes with Coverage

extension Scheme {
    public static func appScheme(name: String) -> Scheme {
        Scheme(
            name: name,
            shared: true,
            buildAction: .buildAction(targets: [.target(name)]),
            testAction: .targets(
                [.target("\(name)Tests")],
                configuration: .debug,
                options: .options(coverage: true)
            ),
            runAction: .runAction(configuration: .debug)
        )
    }
}

Tuist Cloud

Tuist Cloud provides remote caching for teams:

  • Up to 90% cache efficiency on clean builds
  • Shared artifacts: One developer builds, everyone benefits
  • Insights dashboard: Track build times and cache hit rates
# Enable caching
tuist cache warm
 
# Generate with cached dependencies
tuist generate

Tuist 4.0 (2024) added Swift Macro caching and improved multi-platform support.

Benefits I've Seen

  • Zero merge conflicts on project files
  • Consistent settings across 5 iOS projects
  • Faster CI with cached dependencies
  • Type-safe configuration catches errors early

Tuist-generated projects shouldn't be committed. Add *.xcodeproj and *.xcworkspace to .gitignore.

Common Mistakes

1. Forgetting tuist install

Always run tuist install before tuist generate. Missing this causes cryptic build failures:

tuist install  # Resolve dependencies first
tuist generate # Then generate project

2. Using @_exported

Re-exporting modules breaks Tuist's caching:

// Bad - breaks caching
@_exported import Foundation
 
// Good - explicit imports
import Foundation

3. Complex File Structures

Deep, nested directories slow project generation. Keep source organization simple.

4. CocoaPods Compatibility

Tuist doesn't support CocoaPods. Migrate dependencies to Swift packages or Carthage before adopting Tuist.

Getting Started

Migration takes 1-3 months for complex projects. Start incrementally:

  1. Install Tuist: curl -Ls https://install.tuist.io | bash
  2. Create scaffolding: Add Tuist.swift and Project.swift
  3. Migrate one target: Start with your app target
  4. Add tests: Migrate test targets next
  5. Extract patterns: Create helpers as patterns emerge
  6. Enable caching: Connect Tuist Cloud for team benefits

Commit your Project.swift and helpers, but add *.xcodeproj and *.xcworkspace to .gitignore.

Share:

Related Posts

How to use SwiftData safely in Swift 6 with strict concurrency using the DataThespian wrapper.

Quick tips for setting up comprehensive snapshot testing across devices and color schemes.

How to use the Page Object pattern to write UI tests that don't break with every UI change.