iOS Dev Tools
Generating Xcodeproj’s with Tuist
Get rid of unreadable xcodeproj files and say hello to readable project configuration files
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.
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.
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:
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
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:
The SaladMaker
project is comprised by 2 targets:
SaladMaker
iOS appSayHelloKit
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 ProjectDescriptionlet 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!
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 ProjectDescriptionlet 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
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:
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:
- How to cache external third party dependencies
- 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
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
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
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.
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:
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.