🔀 How to untangle and manage build distribution — Webinar, May 16th — Register
🔀 How to untangle and manage build distribution — Webinar, May 16th — Register

Getting started with Tuist for Xcode project generation and modularization on iOS

We here at Runway recently began sponsoring the Tuist project. Tuist is a popular Xcode project generation tool which allows you to express your Xcode project — targets, dependencies, and more — purely in Swift, giving you a lot more flexibility when working with Xcode project files. We covered Tuist a little bit in a prior article about the benefits of Xcode project generation when working on iOS apps. In this article, we’ll take a closer look at Tuist, including getting started on adopting it in your project and implementing a module structure.

Modules are great because they provide a lot of flexibility in how you architect your application. Building modules out in Tuist has a lot of benefits — it takes care of the little annoying details, like applying standard build settings and resolving dependencies, so you only have to worry about your module’s core code.

The first steps in getting ready to adopt Tuist hold from our previous post. If you haven't done those yet then we recommend you pause here and run through the Preparing for Project Generation section before diving further into project generation.

Structure of a modular Tuist project

We'll take the sample project generated by <code>tuist init<code> as the jumping off point. Running that command generates the following contents:

 
Plugins
Project.swift
Targets
Tuist
  |- Config.swift
  |- ProjectDescriptionHelpers


We can ignore the <code>Plugins<code> directory for our purposes and look at the other 3. Let's look at each one.

Project.swift

This is our first introduction to a Tuist manifest. These files are all written in Swift, and at the top we have 2 interesting imports:

 
import ProjectDescription
import ProjectDescriptionHelpers

If you've worked with Swift Package Manager (SPM) before then this may feel comfortable to you. Tuist's APIs have been inspired largely by SPM. They provide the <code>ProjectDescription<code> library to create the projects, targets, schemes, and everything that we need to declare our projects.

Below that is <code>ProjectDescriptionHelpers<code>. This is a target where we get to put _our own code_ to suit our needs when building out our Xcode projects. This is where the bulk of our time will be spent in the rest of this article, because we will use it to build our module system out.

Targets directory

The <code>Targets<code> directory is going to be renamed to <code>Modules<code> but is where our source code will live. Inside of this directory will be directories for each module we want to make. We'll have directories for <code>App<code>, <code>Models<code>, and <code>UI<code> (each being a module of their own).

Tuist/Config.swift

Inside of the Tuist directory lives a special file called <code>Config.swift<code>. This lets us configure parts of the Xcode project such as the organization name and language region for Xcode but also there are options for Tuist itself. Tuist has built out features like Tuist Cloud and Plugins that can enable Tuist to do even more work on your behalf.

Full documentation of Config.swift can be found here.

Tuist/ProjectDescriptionHelpers directory

Lastly comes the <code>ProjectDescriptionHelpers<code> directory inside of the Tuist directory. This is where we get to write our Tuist code (all in Swift!). Let’s move in there and start building out the module system.

The source code for our modularized Tuist example can be found in this GitHub repo. This includes all of the Tuist code to create the module APIs.

Defining a module

For our purposes a module will be a subdirectory of the top level <code>Modules<code> directory. Each module could contain the following structure:

 
README.md
Sources
Tests *
Resources *
TestResources *
* Optional

The only 2 required items here are Sources and a README. Everything else can be optional (even though tests are encouraged they may not always be needed). Moving to code there are 2 parts to declaring a module in Tuist: first is the module's name, and then declaring the module itself:


extension Module {
   public static var models: Module {
       Module(name: .models)
   }
}
extension ModuleName {
   public static var models: ModuleName = "Models"
}

This snippet is defining a module named <code>Models<code>. The reason we define the module's name as a type is because Tuist will use strings to link dependencies together. Defining the type (which only has one property — <code>name<code> — on it and is also <code>ExpressibleByStringLiteral<code>) lets us do things in a more type-safe way. Taking that module name, we create the module with it. That's all there is to it.

What's really great about Tuist is that it's written in Swift. This allows the module system to also be written in Swift, with all the niceties that come with it such as default arguments. Hidden from above is the module's configuration which really is the source of power in this system. The <code>Module.Config<code> type allows for setting dependencies, declaring test targets (complete with a testing config), specifying whether the module should have a resource bundle generated or not, and even changing the generated product from a static library (the default) to a dynamic library or even another product type.

To get a sense of how this works with an app, let's look at the app's module definition:

 
extension Module {
   public static var sampleApp: Module {
       .init(name: .sampleApp, config: appConfig)
   }
}
extension ModuleName {
   public static var sampleApp: ModuleName = "App"
}
private let appConfig = Module.Config(
   dependencies: [
       .module(.models),
       .module(.ui),
   ],
   product: .custom(.app),
   testConfig: Module.TestConfig(hasResources: false)
)

Here we have the <code>App<code> module, which declares dependencies on 2 other modules (Models and UI) and also has unit tests associated with it. All of this lets us create our project in the Project.swift file like so:


let project = Project(
   modules: [
       .sampleApp,
       .models,
       .ui
   ]
)

Under the hood, generating the project will result in modules resolving dependencies correctly so there aren't any duplicate symbol errors (which can happen with static linking if you're not careful) and an Xcode project that is ready to go and easy to reason about.

Extending Tuist

There are lots of Tuist types being utilized in this approach. Tuist provides types such as <code>Path<code> — which we use to determine where files and directories should be placed in the file system — and <code>TargetScript<code> — which lets us write scripts that become build phases. These types are incredibly powerful and we can harness them for our modules and their configurations.

A great place to see what Tuist provides is their API documentation which covers everything in the <code>ProjectDescription<code> target.

Advanced Tuist

As your project grows you may also be able to take advantage of Tuist's focus mode as part of the <code>generate<code> command to build a project with only the targets you need. <code>tuist generate Models<code> with our project would give us only the <code>Models<code> target and nothing else. In big projects this is really powerful — Tuist will give us a project with only the specific targets included. Their dependencies are compiled during project generation so projects build way faster than if you have to build every target's dependencies each time.

Another common use case is in leveraging Tuist during continuous integration (CI). CI providers will need to run an Xcode project, so having a step to generate the project as a prerequisite to running any project commands is vital. It's helpful to have a script which can generate the project for you (perhaps a command in a <code>Makefile<code>) so CI and your developers all run the same command, unifying all project generation steps under a single command. This has proven incredibly helpful:

 
makefile
PHONY: project
project:
   # any pre-generation steps you want
   tuist generate
   # any post-generation steps you want

With this Make command, you can run <code>make project<code> in a terminal or build it into a CI script. If you need any pre-or-post generation steps to happen then there's no different command to program or memorize.

Go forth and build

Much like SwiftUI where we can describe our view hierarchies declaratively, Tuist has brought that same philosophy to our Xcode projects. We can now replace project files — with their difficult to read and fragile nature — with Swift files that are easily reasoned about and are much easier to scale.

Because Tuist manifests are all in Swift, it’s straightforward to establish a robust module system — an added benefit that can supercharge your project even more. Modules can be customized just as needed and the system takes care of the rest, letting your developers do the thing they want to do most: develop apps.


And while you're developing your apps, why not read the next post in this series on how to build a Tuist plugin and publish it using SPM and version control.

Don’t have a CI/CD pipeline for your mobile app yet? Struggling with a flaky one?
Try Runway Quickstart CI/CD to quickly autogenerate an end-to-end workflow for major CI/CD providers.
Try our free tool ->
Sign up for the Flight Deck — our monthly newsletter.
We'll share our perspectives on the mobile landscape, peeks into how other mobile teams and developers get things done, technical guides to optimizing your app for performance, and more. (See a recent issue here)
The App Store Connect API is very powerful, but it can quickly become a time sink.
Runway offers a lot of the functionality you might be looking for — and more — outofthebox and maintenancefree.
Learn more
App Development

Release better with Runway.

Runway integrates with all the tools you’re already using to level-up your release coordination and automation, from kickoff to release to rollout. No more cat-herding, spreadsheets, or steady drip of manual busywork.

Release better with Runway.

Runway integrates with all the tools you’re already using to level-up your release coordination and automation, from kickoff to release to rollout. No more cat-herding, spreadsheets, or steady drip of manual busywork.

Don’t have a CI/CD pipeline for your mobile app yet? Struggling with a flaky one?

Try Runway Quickstart CI/CD to quickly autogenerate an end-to-end workflow for major CI/CD providers.

Looking for a better way to distribute all your different flavors of builds, from one-offs to nightlies to RCs?

Give Build Distro a try! Sign up for Runway and see it in action for yourself.

Release better with Runway.

What if you could get the functionality you're looking for, without needing to use the ASC API at all? Runway offers you this — and more — right out-of-the-box, with no maintenance required.