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 Dependency
Project Dependency
Framework Dependency
Library Dependency
XCFramework Dependency
SDK Dependency
XCTest Dependency
. target (
name : "App" ,
dependencies : [
. target ( name : "FeatureA" ) // Same project
]
)
Conditional Dependencies
Link dependencies only for specific platforms:
. 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 (Recommended)
Swift Packages are the recommended approach for external dependencies.
XcodeProj-Based Integration
Tuist’s default integration method provides more control and enables caching:
Step 1: Create Package Manifest
Create Tuist/Package.swift or Package.swift at the project root:
// 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" )),
]
)
If your project uses custom build configurations (not standard Debug/Release), specify them in baseSettings to ensure dependencies build correctly.
Step 2: Install Dependencies
Resolve and fetch dependencies:
This downloads packages to Tuist/Dependencies/.
Like CocoaPods, dependency resolution is a separate command. This gives you control over when dependencies update and ensures Xcode opens ready to compile.
Step 3: Reference in Targets
Use .external() to reference packages:
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:
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.
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:
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:
carthage update --platform iOS
Step 2: Reference Frameworks
. target (
name : "App" ,
dependencies : [
. xcframework ( path : "Carthage/Build/MyFramework.xcframework" )
]
)
Step 3: Create Build Script
#!/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:
Step 1: Create Build Script
#!/usr/bin/env bash
tuist generate
pod install
Step 2: Use Generated Workspace
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:
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:
. 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:
. target (
name : "App" ,
settings : . settings (
base : [ "OTHER_LDFLAGS" : "$(inherited) -ObjC" ]
),
dependencies : [
. external ( name : "ObjectiveCPackage" )
]
)
Firebase and Google Libraries
Add -ObjC Flag
. target (
name : "App" ,
settings : . settings (
base : [ "OTHER_LDFLAGS" : "$(inherited) -ObjC" ]
),
dependencies : [
. external ( name : "FirebaseAnalytics" )
]
)
Set FBLPromises to dynamic framework:
let packageSettings = PackageSettings (
productTypes : [
"FBLPromises" : . framework
]
)
The Composable Architecture
Link all TCA dependencies dynamically:
# 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
Step 1: Clean Dependencies
rm -rf Tuist/Dependencies
Step 3: Force Resolved Versions (CI)
On CI, use pinned versions:
tuist install --force-resolved-versions
Best Practices
On CI: Force Resolved Versions
Ensure deterministic builds:
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
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
## 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