Migrate an Xcode project
Unless you create a new project using Tuist, in which case you get everything configured automatically, you'll have to define your Xcode projects using Tuist's primitives. How tedious this process is, depends on how complex your projects are.
As you probably know, Xcode projects can become messy and complex over time: groups that don't match the directory structure, files that are shared across targets, or file references that point to nonexisting files (to mention some). All that accumulated complexity makes it hard for us to provide a command that reliably migrates project.
Moreover, manual migration is an excellent exercise to clean up and simplify your projects. Not only the developers in your project will be thankful for that, but Xcode, who will be faster processing and indexing them. Once you have fully adopted Tuist, it will make sure that projects are consistently defined and that they remain simple.
In the aim of easing that work, we are giving you some guidelines based on the feedback that we have received from the users.
Create project scaffold
First of all, create a scaffold for your project with the following Tuist files:
import ProjectDescription
let tuist = Tuist()
import ProjectDescription
import ProjectDescription
let project = Project(
name: "MyApp-Tuist",
targets: [
/** Targets will go here **/
]
)
// swift-tools-version: 5.9
import PackageDescription
#if TUIST
import ProjectDescription
let packageSettings = PackageSettings(
// Customize the product types for specific package product
// Default is .staticFramework
// productTypes: ["Alamofire": .framework,]
productTypes: [:]
)
#endif
let package = Package(
name: "MyApp",
dependencies: [
// Add your own dependencies here:
// .package(url: "https://github.com/Alamofire/Alamofire", from: "5.0.0"),
// You can read more about dependencies here: https://docs.tuist.io/documentation/tuist/dependencies
]
)
Project.swift
is the manifest file where you'll define your project, and Package.swift
is the manifest file where you'll define your dependencies. The Tuist.swift
file is where you can define project-scoped Tuist settings for your project.
PROJECT NAME WITH -TUIST SUFFIX
To prevent conflicts with the existing Xcode project, we recommend adding the -Tuist
suffix to the project name. You can drop it once you've fully migrated your project to Tuist.
Build and test the Tuist project in CI
To ensure the migration of each change is valid, we recommend extending your continuous integration to build and test the project generated by Tuist from your manifest file:
tuist install
tuist generate
tuist build -- ...{xcodebuild flags} # or tuist test
Extract the project build settings into .xcconfig
files
Extract the build settings from the project into an .xcconfig
file to make the project leaner and easier to migrate. You can use the following command to extract the build settings from the project into an .xcconfig
file:
mkdir -p xcconfigs/
tuist migration settings-to-xcconfig -p MyApp.xcodeproj -x xcconfigs/MyApp-Project.xcconfig
Then update your Project.swift
file to point to the .xcconfig
file you've just created:
import ProjectDescription
let project = Project(
name: "MyApp",
settings: .settings(configurations: [
.debug(name: "Debug", xcconfig: "./xcconfigs/MyApp-Project.xcconfig"),
.debug(name: "Release", xcconfig: "./xcconfigs/MyApp-Project.xcconfig"),
]),
targets: [
/** Targets will go here **/
]
)
Then extend your continuous integration pipeline to run the following command to ensure that changes to build settings are made directly to the .xcconfig
files:
tuist migration check-empty-settings -p Project.xcodeproj
Extract package dependencies
Extract all your project's dependencies into the Tuist/Package.swift
file:
// swift-tools-version: 5.9
import PackageDescription
#if TUIST
import ProjectDescription
let packageSettings = PackageSettings(
// Customize the product types for specific package product
// Default is .staticFramework
// productTypes: ["Alamofire": .framework,]
productTypes: [:]
)
#endif
let package = Package(
name: "MyApp",
dependencies: [
// Add your own dependencies here:
// .package(url: "https://github.com/Alamofire/Alamofire", from: "5.0.0"),
// You can read more about dependencies here: https://docs.tuist.io/documentation/tuist/dependencies
.package(url: "https://github.com/onevcat/Kingfisher", .upToNextMajor(from: "7.12.0"))
]
)
PRODUCT TYPES
You can override the product type for a specific package by adding it to the productTypes
dictionary in the PackageSettings
struct. By default, Tuist assumes that all packages are static frameworks.
Determine the migration order
We recommend migrating the targets from the one that is the most dependent upon to the least. You can use the following command to list the targets of a project, sorted by the number of dependencies:
tuist migration list-targets -p Project.xcodeproj
Start migrating the targets from the top of the list, as they are the ones that are the most depended upon.
Migrate targets
Migrate the targets one by one. We recommend doing a pull request for each target to ensure that the changes are reviewed and tested before merging them.
Extract the target build settings into .xcconfig
files
Like you did with the project build settings, extract the target build settings into an .xcconfig
file to make the target leaner and easier to migrate. You can use the following command to extract the build settings from the target into an .xcconfig
file:
tuist migration settings-to-xcconfig -p MyApp.xcodeproj -t TargetX -x xcconfigs/TargetX.xcconfig
Define the target in the Project.swift
file
Define the target in Project.targets
:
import ProjectDescription
let project = Project(
name: "MyApp",
settings: .settings(configurations: [
.debug(name: "Debug", xcconfig: "./xcconfigs/Project.xcconfig"),
.debug(name: "Release", xcconfig: "./xcconfigs/Project.xcconfig"),
]),
targets: [
.target(
name: "TargetX",
destinations: .iOS,
product: .framework, // [!code ++] // or .staticFramework, .staticLibrary...
bundleId: "io.tuist.targetX",
sources: ["Sources/TargetX/**"],
dependencies: [
/** Dependencies go here **/
/** .external(name: "Kingfisher") **/
/** .target(name: "OtherProjectTarget") **/
],
settings: .settings(configurations: [
.debug(name: "Debug", xcconfig: "./xcconfigs/TargetX.xcconfig"),
.debug(name: "Release", xcconfig: "./xcconfigs/TargetX.xcconfig"),
])
),
]
)
TEST TARGETS
If the target has an associated test target, you should define it in the Project.swift
file as well repeating the same steps.
Validate the target migration
Run tuist build
and tuist test
to ensure the project builds and tests pass. Additionally, you can use xcdiff to compare the generated Xcode project with the existing one to ensure that the changes are correct.
Repeat
Repeat until all the targets are fully migrated. Once you are done, we recommend updating your CI and CD pipelines to build and test the project using tuist build
and tuist test
commands to benefit from the speed and reliability that Tuist provides.
Troubleshooting
Compilation errors due to missing files.
If the files associated to your Xcode project targets were not all contained in a file-system directory representing the target, you might end up with a project that doesn't compile. Make sure the list of files after generating the project with Tuist matches the list of files in the Xcode project, and take the opportunity to align the file structure with the target structure.