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

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

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

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

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 .

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

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

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.

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.

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

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

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
- Tuist
Official documentation
- Tuist Videos
Tuist Author presentation videos