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:
We can ignore the <code>Plugins<code> directory for our purposes and look at the other 3. Let's look at each one.
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:
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.
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).
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.
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:
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:
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:
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:
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.
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.
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:
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.