iOS Dev Tools

Generating Xcodeproj’s with Tuist

Get rid of unreadable xcodeproj files and say hello to readable project configuration files

Anurag Ajwani
Better Programming
Published in
8 min readMar 16, 2022

--

Photo by Hanna Morris on Unsplash

Have you ever created pull requests(PR)/merge requests(MR) with changes to the xcodeproj file of your iOS app project?

It is likely that a team of 2 more iOS devs to make changes to the xcodeproj files simultaneously.

For example, both may create new files. In such a case, you may create conflicting changes as both of you modify the same file. Additionally, xcodeproj files are not readable. Reviewing PRs with changes in the xcodeproj files is hard to follow and understand.

Git conflicts in xcodeproj after file creations in separate branches
The conflicting lines

Is there any way we can get rid of xcodeproj files? Unfortunately, there is no easy way to get rid of these xcodeproj files for iOS app development.

However, we can easily generate xcodeproj files from more readable configuration files using Tuist.

Project.swift example

Tuist allows you to store your project configuration in a Swift in a file named Project.swift. Tuist then generates the xcodeproj files based on the Swift code in Project.swift.

This is similar to Swift Package Manager’s Package.swift.

In this post, I will show you how to adopt a sample project to use Tuist. Then I will show how to add Swift Packages to an iOS app target using Tuist. Next, I’ll show you how to cache modules using Tuist. Finally I’ll cover how to change build settings.

I have used Swift 5.6 and Xcode 13.3 for this article.

How to convert an iOS app project to use Tuist

In this section, we’ll start by installing Tuist.

We’ll then download an already existing app.

Finally, create an Tuist project configuration file, remove the existing xcodeproj file and generate the xcodeproj file using Tuist.

Here are the steps we are going to take:

  1. Install Tuist
  2. Download the starter pack
  3. Converting the project to use Tuist

Let’s get started!

1. Install Tuist

There is only way to install Tuist (at the time of writing). Open terminal and run the following command:

curl -Ls https://install.tuist.io | bash
Tuist installation

2. Download the starter pack

Let’s download an already existing iOS app project. Open a terminal and run the following commands:

cd $HOME
curl https://github.com/anuragajwani/tuist-tut/archive/starter.zip -o starter.zip -L -s
unzip -q starter.zip
cd tuist-tut-starter
open -a Xcode SaladMaker.xcodeproj

Run the app to see it in action.

3. Converting the project to use Tuist

In order for us to generate the xcodeproj files we must declare the project structure, it’s targets and configuration in file named Project.swift. First let’s take a look at the project targets and its configuration within the current xcodeproj:

Xcode project structure

The SaladMaker project is comprised by 2 targets:

  1. SaladMaker iOS app
  2. SayHelloKit framework

The source code for SaladMaker sits inside a directory named SaladMaker. The source code for SayHelloKit sits inside a directory named SayHelloKit. Both of these targets support iOS 14 and above. SayHelloKit framework is consumed by the SaladMaker app.

Next let’s create the Project.swift file. In terminal run the following command:

cat > Project.swift <<-EOF
import ProjectDescription
let project = Project(
name: "SaladMaker",
organizationName: "com.anuragajwani",
targets: [
Target(
name: "SaladMaker",
platform: .iOS,
product: .app,
bundleId: "com.anuragajwani.SaladMaker",
deploymentTarget: .iOS(targetVersion: "14.0", devices: .iphone),
infoPlist: "SaladMaker/Info.plist",
sources: ["SaladMaker/**"],
resources: ["SaladMaker/Assets.xcassets/"],
dependencies: [.target(name: "SayHelloKit")]
),
Target(
name: "SayHelloKit",
platform: .iOS,
product: .framework,
bundleId: "com.anuragajwani.SayHelloKit",
deploymentTarget: .iOS(targetVersion: "14.0", devices: .iphone),
sources: ["SayHelloKit/**"],
settings: .settings(base: ["GENERATE_INFOPLIST_FILE": "YES"])
)
]
)
EOF

Above we have create a project named SaladMaker with 2 targets; the SaladMaker iOS app and the SayHelloKit framework. We have specified each:

  • target name
  • bundle identifier
  • the platform it serves
  • the product type
  • where its source code lies
  • where its assets lies (images, text files and other non-code files)

Note we have also declared a build setting for the framework target. Don’t worry about this, we’ll come back to this later in the “How to change build settings” section of this post. Additionally we have declared that the SaladMaker app depends on SayHelloKit:

dependencies: [.target(name: "SayHelloKit")]

Next, let’s delete SaladMaker.xcodeproj and tell Tuist to generate the xcodeproj files for the project. Run the following command:

rm -rf SaladMaker.xcodeproj && tuist generate

Tuist will automatically open Xcode for you. Run the app and it should run just as it did before.

That’s it we are now generating unreadable xcodeproj over 500 lines from 31 lines of Swift code!

Comparing Project.swift vs Xcodeproj files

How to add Swift Packages when using Tuist

Most applications use open source libraries to aid or speed development. There are multiple ways of importing open source libraries. One of the most popular methods is to use a dependency manager. One of the most popular dependencies managers used in iOS development is Swift Package Manager.

In this section, I will show you how to import a popular open-source library used in iOS development: Alamofire.

In terminal run the following commands at the root of the project:

mkdir Tuist && cd Tuist
cat > Dependencies.swift <<-EOF
import ProjectDescription
let dependencies = Dependencies(
swiftPackageManager: [
.remote(url: "https://github.com/Alamofire/Alamofire", requirement: .upToNextMajor(from: "5.0.0")),
],
platforms: [.iOS]
)
EOF

In order for us to use third party external dependencies in our project using Tuist we must declare these dependencies in file named Dependencies.swift under a directory named Tuist at the root of the project.

Note the dependencies declared in Dependencies.swift must support Swift Package Manager.

We can now tell Tuist to fetch these external dependencies by running the following command at the root of the project:

tuist fetch
Fetching external dependencies

We haven’t yet told Tuist that we want to use Alamofire in the SaladMaker app. Let’s do that next. Open Project.swift and change the SaladMaker dependencies to the following:

dependencies: [
.target(name: "SayHelloKit"),
.external(name: "Alamofire")
]

Next let’s tell Tuist to re-generate xcodeproj files. Run the following command in terminal:

tuist generate

We are now ready to consume Alamofire within the SaladMaker app:

Linked frameworks and libraries of SaladMaker

How to cache modules

When developing iOS apps Xcode will compile all of the source code as part of the build process including its third party dependencies. In the SaladMaker app for example we started using Alamofire. It is unlikely that we will ever need to change Alamofire code. Yet Xcode will keep building Alamofire over and over again many times through the development process.

Additionally we may work on certain features that don’t require working on our SayHelloKit module. Again Xcode will build this over and over again many times.

What if we could it build it once and reuse the built or compiled form from that point onwards for our builds during development? In other words: can we cache modules?

In this section we’ll look at:

  1. How to cache external third party dependencies
  2. How to cache project modules

1. How to cache external third party dependencies

We can cache Alamofire and other third party dependencies (declared in Dependencies.swift) and use a compiled form of these during the build process of our app during development. To do so run the following command:

tuist cache warm --dependencies-only
Caching external dependencies

Tuist will build and cache Alamofire. Next let’s we need to tell Tuist to re-generate the project and it will automatically use the cached Alamofire. Run the following command:

tuist generate
Using cached external dependencies

2. How to cache project modules

We can also cache our own modules. If we know we won’t be making any changes to certain modules it is worth to cache these too. This can help to speed the development process of the feature we are currently building. To cache SayHelloKit run the following command:

tuist cache warm SayHelloKit
Caching SayHelloKit

If we want to only need to work on the SaladMaker target then we must tell Tuist that we only want to develop that target:

tuist generate SaladMaker

Tuist will then replace its dependencies with cached versions of such dependencies.

Using cached SayHelloKit

How to change build settings

By default, Tuist will set the xcodeproj default build settings or not specify it at all and allow Xcode to set the default.

However, in some cases, we might want to tweak build settings based on our app-specific needs. Tuist allows us to specify any build settings that you want to set for a target. Let’s say we want to disable bitcode for our SaladMaker app. Open Project.swift and at the end of the SaladMaker app target declaration add the following line:

    ...
dependencies: [
.target(name: "SayHelloKit"),
.external(name: "Alamofire")
],
settings: .settings(base: ["ENABLE_BITCODE": "NO"])
)

Once again let’s tell Tuist to re-generate the xcodeproj files:

tuist generate

See the changes in action:

Disabled Bitcode on SaladMaker

Summary

In this post we learnt:

  • How to generate Xcodeprojs using Tuist
  • How to consume Swift Packages when using Tuist
  • How to change build settings when using Tuist
  • How to add build phases when using Tuist
  • How to cache modules using Tuist

There is a lot you can configure using Tuist. Make sure to checkout the specs and documentation.

Final Thoughts

You can find the source code in this post in my Github repo tuist-tut:

A few weeks ago I covered how to generate Xcodeproj’s using Xcodegen. Xcodegen declares projects in YML formats. Tuist declares projects in Swift. Both can leverage Swift Packages. However, Tuist does not add Swift Packages to project in a conventional manner which is then managed by Xcode. Tuist builds dependencies graphs so it can then add features on top such as module caching.

If you’re only looking to generate Xcodeproj files then it’s up to your preference of format (YML vs Swift). Personally, I prefer Xcodegen’s project.yml as in my opinion it is more readable than Tuist’s Project.swift. However Tuist has more to offer.

Want to Connect?For more on iOS development follow me on Twitter or Medium.

--

--

Senior iOS Engineer at Travelperk. 7+ years experience with iOS and Swift. Blogging my knowledge and experience.