Skip to main content

Overview

Dependencies in Tuist can be internal (between your own targets) or external (third-party packages and frameworks). Tuist provides a robust dependency management system that validates your dependency graph and prevents common issues.

Why Tuist’s Approach is Different

Unlike Xcode projects where dependency graphs can be error-prone and implicit, Tuist makes dependencies explicit and static. This enables:
  • Validation: Automatic detection of cycles and invalid dependencies
  • Optimization: Binary caching and selective testing
  • Consistency: Guaranteed correct linking and embedding
  • Simplicity: Focus on what depends on what, not implementation details
Tuist automatically handles complex details like transitive dynamic dependencies, static XCFramework processing, and proper linking configurations.

Internal Dependencies

Internal dependencies are connections between targets in your project.

Dependency Types

When defining a target, use the dependencies parameter with these types:
.target(
    name: "App",
    dependencies: [
        .target(name: "FeatureA")  // Same project
    ]
)

Conditional Dependencies

Link dependencies only for specific platforms:
Project.swift
.target(
    name: "App",
    dependencies: [
        .target(name: "CoreFeature"),
        .target(name: "iOSOnlyFeature", condition: .when([.ios])),
        .target(name: "macOSOnlyFeature", condition: .when([.macos]))
    ]
)

Dependency Graph Best Practices

Layered Architecture

Organize dependencies in clear layers:
App Layer
 depends on
Feature Layer
 depends on
Core Layer
 depends on
External Dependencies
Avoid circular dependencies. Tuist validates the dependency graph and will error if cycles are detected.

Visualize Your Graph

Use Tuist’s graph command to understand dependencies:
# Generate visual graph
tuist graph

# Export to specific format
tuist graph --format png
tuist graph --format json

External Dependencies

Tuist supports multiple ways to integrate external dependencies. Swift Packages are the recommended approach for external dependencies.

XcodeProj-Based Integration

Tuist’s default integration method provides more control and enables caching:
1
Step 1: Create Package Manifest
2
Create Tuist/Package.swift or Package.swift at the project root:
3
// swift-tools-version: 5.9
import PackageDescription

#if TUIST
    import ProjectDescription

    let packageSettings = PackageSettings(
        productTypes: [
            "Alamofire": .framework,  // Override default (.staticFramework)
        ],
        baseSettings: .settings(configurations: [
            .debug(name: "Debug"),
            .release(name: "Release")
        ])
    )
#endif

let package = Package(
    name: "MyApp",
    dependencies: [
        .package(url: "https://github.com/Alamofire/Alamofire", from: "5.0.0"),
        .package(url: "https://github.com/onevcat/Kingfisher", .upToNextMajor(from: "7.12.0")),
    ]
)
4
If your project uses custom build configurations (not standard Debug/Release), specify them in baseSettings to ensure dependencies build correctly.
5
Step 2: Install Dependencies
6
Resolve and fetch dependencies:
7
tuist install
8
This downloads packages to Tuist/Dependencies/.
9
Like CocoaPods, dependency resolution is a separate command. This gives you control over when dependencies update and ensures Xcode opens ready to compile.
10
Step 3: Reference in Targets
11
Use .external() to reference packages:
12
import ProjectDescription

let project = Project(
    name: "App",
    targets: [
        .target(
            name: "App",
            destinations: [.iPhone],
            product: .app,
            bundleId: "dev.tuist.app",
            sources: ["Sources/**"],
            dependencies: [
                .external(name: "Alamofire"),
                .external(name: "Kingfisher")
            ]
        )
    ]
)

Xcode’s Default Integration

For simpler projects or when you need Xcode’s native SPM integration:
Project.swift
let project = Project(
    name: "MyProject",
    packages: [
        .remote(
            url: "https://github.com/krzyzanowskim/CryptoSwift",
            requirement: .exact("1.8.0")
        )
    ],
    targets: [
        .target(
            name: "MyTarget",
            dependencies: [
                .package(product: "CryptoSwift", type: .runtime)
            ]
        )
    ]
)
Dependency types:
  • .runtime - Standard dependency
  • .macro - Swift macros
  • .plugin - Build tool plugins
SPM build tool plugins must use Xcode’s default integration, even when using XcodeProj-based integration for other dependencies.

Build Tool Plugin Example

Project.swift
import ProjectDescription

let project = Project(
    name: "Framework",
    packages: [
        .remote(
            url: "https://github.com/SimplyDanny/SwiftLintPlugins",
            requirement: .upToNextMajor(from: "0.56.1")
        )
    ],
    targets: [
        .target(
            name: "Framework",
            dependencies: [
                .package(product: "SwiftLintBuildToolPlugin", type: .plugin)
            ]
        )
    ]
)

Binary Targets

Include binary frameworks directly in Package.swift:
Tuist/Package.swift
let package = Package(
    name: "MyApp",
    dependencies: [],
    targets: [
        .binaryTarget(
            name: "Sentry",
            url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.40.1/Sentry.xcframework.zip",
            checksum: "db928e6fdc30de1aa97200576d86d467880df710cf5eeb76af23997968d7b2c7"
        )
    ]
)
Reference like any other external dependency:
dependencies: [
    .external(name: "Sentry")
]

Carthage

Integrate Carthage dependencies as frameworks:
1
Step 1: Run Carthage
2
carthage update --platform iOS
3
Step 2: Reference Frameworks
4
.target(
    name: "App",
    dependencies: [
        .xcframework(path: "Carthage/Build/MyFramework.xcframework")
    ]
)
5
Step 3: Create Build Script
6
#!/usr/bin/env bash

carthage update
tuist generate
Ensure Carthage dependencies are present before running xcodebuild or tuist test.

CocoaPods

Integrate CocoaPods after generating the project:
1
Step 1: Create Build Script
2
#!/usr/bin/env bash

tuist generate
pod install
3
Step 2: Use Generated Workspace
4
Open the .xcworkspace file created by CocoaPods, not the Tuist-generated workspace.
CocoaPods dependencies are incompatible with Tuist’s build, test, binary caching, and selective testing features.

Static vs Dynamic Linking

The choice between static and dynamic linking significantly impacts app size and boot time.

General Guidelines

  • Release builds: Link as much as possible statically for fast boot times
  • Debug builds: Link as much as possible dynamically for fast iteration

Configuring Linking Type

Use environment variables to change linking at generation time:
Project.swift
import ProjectDescription

func productType() -> Product {
    if case let .string(linking) = Environment.linking {
        return linking == "static" ? .staticFramework : .framework
    } else {
        return .framework
    }
}

let project = Project(
    name: "MyFeature",
    targets: [
        .target(
            name: "MyFeature",
            product: productType(),
            // ...
        )
    ]
)
Generate with different linking:
# Dynamic linking
tuist generate

# Static linking
LINKING=static tuist generate

Special Scenarios

Apps with Extensions

Shared code between app and extensions should be dynamic to avoid duplication:
Project.swift
.target(name: "SharedKit", product: .framework),  // Dynamic
.target(name: "App", dependencies: [.target(name: "SharedKit")]),
.target(name: "ShareExtension", dependencies: [.target(name: "SharedKit")])

Static Side Effects

Tuist warns about “static side effects” - when a static target is linked transitively through dynamic targets. This can increase binary size or cause runtime crashes.

Troubleshooting

Objective-C Dependencies

Objective-C dependencies may require the -ObjC linker flag:
Project.swift
.target(
    name: "App",
    settings: .settings(
        base: ["OTHER_LDFLAGS": "$(inherited) -ObjC"]
    ),
    dependencies: [
        .external(name: "ObjectiveCPackage")
    ]
)

Firebase and Google Libraries

Add -ObjC Flag

Project.swift
.target(
    name: "App",
    settings: .settings(
        base: ["OTHER_LDFLAGS": "$(inherited) -ObjC"]
    ),
    dependencies: [
        .external(name: "FirebaseAnalytics")
    ]
)

Configure FBLPromises

Set FBLPromises to dynamic framework:
Tuist/Package.swift
let packageSettings = PackageSettings(
    productTypes: [
        "FBLPromises": .framework
    ]
)

The Composable Architecture

Link all TCA dependencies dynamically:
Tuist/Package.swift
#if TUIST
import ProjectDescription

let packageSettings = PackageSettings(
    productTypes: [
        "ComposableArchitecture": .framework,
        "Dependencies": .framework,
        "CasePaths": .framework,
        // ... all other TCA packages
    ],
    targetSettings: [
        "ComposableArchitecture": .settings(base: [
            "OTHER_SWIFT_FLAGS": ["-module-alias", "Sharing=SwiftSharing"]
        ]),
        "Sharing": .settings(base: [
            "PRODUCT_NAME": "SwiftSharing",
            "OTHER_SWIFT_FLAGS": ["-module-alias", "Sharing=SwiftSharing"]
        ])
    ]
)
#endif
With this configuration, import SwiftSharing instead of Sharing.

Transitive Static Dependencies

When a dynamic framework depends on static ones, use internal import (Swift 6+):
internal import StaticModule
For older Swift versions, use @_implementationOnly:
@_implementationOnly import StaticModule

Dependencies Not Resolving

1
Step 1: Clean Dependencies
2
rm -rf Tuist/Dependencies
3
Step 2: Reinstall
4
tuist install
5
Step 3: Force Resolved Versions (CI)
6
On CI, use pinned versions:
7
tuist install --force-resolved-versions

Best Practices

On CI: Force Resolved Versions

Ensure deterministic builds:
CI Script
tuist install --force-resolved-versions
tuist generate
xcodebuild build -workspace App.xcworkspace -scheme App

Keep Dependencies Updated

Regularly update and test:
# Update Package.resolved
tuist install

# Review changes
git diff Tuist/Package.resolved

# Test with updated dependencies
tuist test

Version Pinning Strategy

Tuist/Package.swift
let package = Package(
    name: "MyApp",
    dependencies: [
        // Exact version for critical dependencies
        .package(url: "https://github.com/realm/realm-swift", exact: "10.45.0"),
        
        // Up to next major for stable packages
        .package(url: "https://github.com/Alamofire/Alamofire", .upToNextMajor(from: "5.8.0")),
        
        // Branch for internal packages
        .package(url: "https://github.com/myorg/shared-kit", branch: "main")
    ]
)

Document Special Configurations

Create a DEPENDENCIES.md file:
DEPENDENCIES.md
# Dependencies

## Special Configurations

### Firebase
- Requires `-ObjC` linker flag
- `FBLPromises` must be dynamic framework
- See: Tuist/Package.swift

### The Composable Architecture
- All packages linked dynamically
- Import `SwiftSharing` instead of `Sharing`
- See: Tuist/Package.swift

Next Steps

Optimize Builds

Enable binary caching for faster builds

Project Structure

Learn how to organize your dependency graph

Set Up CI/CD

Configure CI for dependency management

Migrate from Xcode

Migrate existing dependencies to Tuist

Build docs developers (and LLMs) love