Alternative to .xcodeproj Chaos: Intro to Tuist for iOS Newbies

Tuist is a powerful yet often underutilized tool that can greatly simplify project setup, modularization, and CI workflows. For iOS development beginners, it offers a valuable opportunity to overcome the complexity and fragility of Xcode project files—especially when aiming to build scalable architectures.

This post serves as a step-by-step guide to help developers get started with Tuist from scratch. While Tuist truly shines in large, complex projects involving multiple developers, this guide is tailored for individuals working on personal apps. The goal is to introduce a different, more structured way of managing project configurations—and to offer a glimpse into how larger, scalable projects are typically handled.

Install Tuist

I have installed Tuist by using homebrew, just typing floowing two commands:

$ brew tap tuist/tuist
$ brew install --formula tuist
intallTuist

That is not the only way. To learn more, please refer to the Install Tuist section in the official documentation.

Create HelloTuist project

Navigate to the parent folder where you want to create your iOS app project. The Tuist initialization command will create a new folder containing the necessary project files.

To initialize a project for the first time, simply run:

$ tuist init
tuistinit

You will be asked a few questions, such as whether the project is being created from scratch or if you’re migrating from an existing .xcodeproj or workspace, which platform you’re targeting, and whether you’re implementing a server.

Since this is an introduction to Tuist, we will choose «Create a generated project.»

Let’s take a look at the command that was generated.

Key Point for understanding this technology, it’s important to know that we’ll be working with two projects.

The first one, which we’ll refer to in this post as the Tuist project, is the Xcode project responsible for managing the iOS project configuration—this includes target settings, build configurations, library dependencies, and so on.

The second one is the application project, which is the actual codebase of your app. However, note that this project scaffolder is regenerated each time certain configuration settings change.

I understand this might sound complicated at first, but it’s a great way to separate application logic from project configuration.

Lets take a look at the generaded Tuist project by typing following command:

$ tuist edit

XCode will be opened up showing to you Tuist project, with some source code, this Swift project configuration source code will be the responsilbe for generating the project scaffoling for your application (or library).

tuistproj

This Swift code uses the Tuist project description framework to define a project named «HelloTuist» with two main targets: an iOS app (HelloTuist) and a set of unit tests (HelloTuistTests). The main app target is configured as an iOS application with a unique bundle identifier, sources and resources from specified directories, and a custom launch screen setup in its Info.plist. The test target is set up for iOS unit testing, uses default Info.plist settings, includes test source files, and depends on the main app target for its operation.

Lets continue with the workflow, next step is generating application project by typing:

$ tuist generate --no-open
tuistgenerate

By using this command, we have created the final .xcodeproj application project. The --no-open optional parameter was specified to prevent Xcode from opening the project automatically. We will open the project manually from the command line.

$ xed .
xcode

The default project folder structure is different from what we’re accustomed to when coming from classical Xcode project creation. However, it’s something we can get used to, and it’s also configurable through the Tuist project.

Deploying the app on the simulator is useful just to verify that everything works as expect

Simulator Screenshot - iPhone 16 Pro - 2025-05-31 at 16.55.34

From now on, focus only on your Swift application’s source code files. It’s important to remember that the .xcodeproj file is no longer part of your base code — it is autogenerated. Whenever you need to change project configurations, edit the Tuist project and run tuist generate.

Setup application version

The application version and build number are two parameters—MARKETING_VERSION and CURRENT_PROJECT_VERSION—managed in the project’s build settings. To set these values, open the Tuist project using the command line.

$ tuist edit

And set following changes (On Tuist prject):

public extension Project {
    static let settings: Settings = {
        let baseConfiguration: SettingsDictionary = [
            "MARKETING_VERSION": "1.2.3",
            "CURRENT_PROJECT_VERSION": "42"
        ]
        let releaseConfiguration = baseConfiguration
        return Settings.settings(base: baseConfiguration, debug: baseConfiguration, release: releaseConfiguration)
    }()
}

public extension Target {
    static let settings: Settings = {
        let baseConfiguration: SettingsDictionary = [:]
        var releaseConfig = baseConfiguration
        return Settings.settings(base: baseConfiguration, debug: baseConfiguration, release: releaseConfig)
    }()
}

We aim to have a unique version value across all targets. To achieve this, we’ve set the version value at the project level and assigned an empty dictionary ([:]) in the target settings to inherit values from the project settings.

Finally, configure the settings for both the project and target structures:

let project = Project(
    name: "HelloTuist",
    settings: Project.settings,
    targets: [
        .target(
            name: "HelloTuist",
            destinations: .iOS,
            product: .app,
            bundleId: "io.tuist.HelloTuist",
            infoPlist: .extendingDefault(with: [
                "CFBundleDisplayName": "Tuist App",
                "CFBundleShortVersionString": "$(MARKETING_VERSION)",
                "CFBundleVersion": "$(CURRENT_PROJECT_VERSION)"
            ]),
            sources: ["HelloTuist/Sources/**"],
            resources: ["HelloTuist/Resources/**"],
            dependencies: [],
            settings: Target.settings
        ),
        .target(
            name: "HelloTuistTests",
            destinations: .iOS,
            product: .unitTests,
            bundleId: "io.tuist.HelloTuistTests",
            infoPlist: .default,
            sources: ["HelloTuist/Tests/**"],
            resources: [],
            dependencies: [.target(name: "HelloTuist")]
        ),
    ]
)

The settings parameter is defined both at the project and target levels. Additionally, MARKETING_VERSION is linked to CFBundleShortVersionString in the Info.plist. As a result, the app will retrieve the version value from CFBundleShortVersionString.

Once the Tuist project is set up, the application project should be regenerated.

$ tuist generate --no-open
rege

And open application project adding following changes con the view:

struct ContentView: View {
    var appVersion: String {
        let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "?"
        let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "?"
        return "Version \(version) (\(build))"
    }
    
    var body: some View {
        NavigationView {
            List {
                Section(header: Text("Information")) {
                    HStack {
                        Label("App versusion", systemImage: "number")
                        Spacer()
                        Text(appVersion)
                            .foregroundColor(.secondary)
                    }
                }
            }
            .navigationTitle("Hello Tuist!")
            .navigationBarTitleDisplayMode(.inline)
        }
    }
}

Deploy app for checking that version is properly presented.

Simulator Screenshot - iPhone 16 Pro - 2025-05-31 at 17.34.41

As you have observed, the operations of setting the app version in the project configuration and presenting the version are decoupled. Additionally, changes made to the project are now easier to review.

changes

Adding library dependencies

Another common project configuration is the addition of third-party libraries. I’m neither for nor against them—this is a religious debate I prefer not to engage in.

For demonstration purposes, we will integrate the Kingfisher library to fetch a remote image from the internet and display it in the application’s view.

Again, open back Tuist project by typing ‘tuist edit’ and set the library url on ‘Tuist/Package.swift’ file:

let package = Package(
    name: "tuistHello",
    dependencies: [
        // Add your own dependencies here:
        .package(url: "https://github.com/onevcat/Kingfisher", .upToNextMajor(from: "8.3.2")),
        // You can read more about dependencies here: https://docs.tuist.io/documentation/tuist/dependencies
    ]
)

Also set dependencies attribute array:

let project = Project(
    name: "HelloTuist",
    settings: Project.settings,
    targets: [
        .target(
            name: "HelloTuist",
            destinations: .iOS,
            product: .app,
            bundleId: "io.tuist.HelloTuist",
            infoPlist: .extendingDefault(with: [
                "CFBundleDisplayName": "Tuist App",
                "CFBundleShortVersionString": "$(MARKETING_VERSION)",
                "CFBundleVersion": "$(CURRENT_PROJECT_VERSION)"
            ]),
            sources: ["HelloTuist/Sources/**"],
            resources: ["HelloTuist/Resources/**"],
            dependencies: [
                .external(name: "Kingfisher")
            ],
            settings: Target.settings
        ),
        .target(
            name: "HelloTuistTests",
            destinations: .iOS,
            product: .unitTests,
            bundleId: "io.tuist.HelloTuistTests",
            infoPlist: .default,
            sources: ["HelloTuist/Tests/**"],
            resources: [],
            dependencies: [.target(name: "HelloTuist")]
        ),
    ]
)

Following workflow, re-generate application project by typing ‘tuist generate’.

tuistgenfa

… but this time something is going wrong. And is because when an external library is bein integrated iun a Tuist project we have to install it first. By typing following command app is downloaded and installed:

$ tuist install

After ‘tuist install’ execute ‘tuist generate –no-open’ for finally generating application project. Add following content to view:

import Kingfisher
import SwiftUI

struct ContentView: View {
   ...
    var body: some View {
        NavigationView {
            List {
                Section(header: Text("Information")) {
                    HStack {
                        Label("App versusion", systemImage: "number")
                        Spacer()
                        Text(appVersion)
                            .foregroundColor(.secondary)
                    }
                }
                KFImage(URL(string: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQyfYoLcb2WNoStJH01TT2TLAf_JbD_FhIJng&s")!)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width: 300, height: 300)
                    .cornerRadius(12)
                    .shadow(radius: 5)
                    .padding()
            }
            .navigationTitle("Hello Tuist!")
            .navigationBarTitleDisplayMode(.inline)
        }
    }
}

Finally deploy app for checking that image is properly fetched:

Simulator Screenshot - iPhone 16 Pro - 2025-05-31 at 18.16.20

Finally deploy app for checking that image is properly fetched.

Conclusions

The first time I heard about this technology, I was immediately drawn to it. However, I must confess that I struggled at first to understand the workflow. That’s exactly why I decided to write this post.

While Tuist might be overkill for small projects, it becomes essential for large ones—especially when you consider the number of lines of code and developers involved. After all, every big project started out small.

Another major advantage is that changes to the project setup are decoupled from changes to the application itself. This makes them easier to review—much like comparing SwiftUI code to .xib files or storyboards.

Who knows? Maybe Apple will one day release its own version of Tuist, just like it did with Combine and Swift Package Manager (SPM).

You can find source code used for writing this post in following repository

References

Copyright © 2024-2025 JaviOS. All rights reserved