Skip to content

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:

js
import ProjectDescription

let tuist = Tuist()
js
import ProjectDescription

let project = Project(
    name: "MyApp-Tuist",
    targets: [
        /** Targets will go here **/
    ]
)
js
// 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:

bash
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:

bash
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:

swift
import ProjectDescription

let project = Project(
    name: "MyApp",
    settings: .settings(configurations: [
        .debug(name: "Debug", xcconfig: "./xcconfigs/MyApp-Project.xcconfig"), 
        .release(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:

bash
tuist migration check-empty-settings -p Project.xcodeproj

Extract package dependencies

Extract all your project's dependencies into the Tuist/Package.swift file:

swift
// 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:

bash
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:

bash
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:

swift
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.

Released under the MIT License.