Project Generation with Tuist
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?
- Mergeable configs: Swift files merge cleanly
- Reusable templates: Define patterns once, use everywhere
- Consistent settings: No more "works on my machine"
- Type safety: Catch config errors at generation time
Real-World Success Stories
Major companies have seen dramatic improvements:
| Company | Scale | Build Time Improvement |
|---|---|---|
| Trendyol | 170+ developers | 65% faster (30 min → 10 min) |
| Lapse | 220 modules | 75% faster (53 min → 13 min) |
| Back Market | Large team | Up 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.xcworkspaceMulti-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 generateTuist 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 project2. Using @_exported
Re-exporting modules breaks Tuist's caching:
// Bad - breaks caching
@_exported import Foundation
// Good - explicit imports
import Foundation3. 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:
- Install Tuist:
curl -Ls https://install.tuist.io | bash - Create scaffolding: Add
Tuist.swiftandProject.swift - Migrate one target: Start with your app target
- Add tests: Migrate test targets next
- Extract patterns: Create helpers as patterns emerge
- Enable caching: Connect Tuist Cloud for team benefits
Commit your Project.swift and helpers, but add *.xcodeproj and *.xcworkspace to .gitignore.