Home iOS & Swift Tutorials

Tuist Tutorial for Xcode

Learn how to use Tuist to create and manage complex Xcode projects and workspaces on-the-fly.

5/5 2 Ratings

Version

  • Swift 5, iOS 14, Xcode 12

The almighty project file and its big brother, the workspace, are among the most significant pain points you’ll deal with in iOS development. Tuist is a tool that aims to make dealing with Xcode projects easier by taking the complexity out of thinking about your project structure. It embraces the Configuration by Convention philosophy and lets you focus on your project’s content rather than its organization.

In this Tuist tutorial, you’ll learn how to replace the traditional Xcode project workflow by using Tuist. You’ll start with an already functioning project, an app that displays top-rated movies, and update it to use Tuist. You’ll then take advantage of Tuist’s features to:

  • Remove the project and workspace files. Instead, you’ll generate your project any time you need to.
  • Add a unit test target.
  • Handle the Swift Package Manager dependencies.
  • Add new features that depend on the Tuist workflow.
Note: This intermediate-level tutorial assumes you’re comfortable building an iOS app using Xcode and writing Swift. The example also uses SwiftUI.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial. The starter project uses The Movie Database as a back end to display a list of top-rated movies.

Take a look through the project. You’ll see a standard setup with a network class that performs an API request with your credentials, then fetches and transforms the response before delivering it to a List component in the SwiftUI ContentView.

To use the starter project, you’ll need to sign up for an account to get an API key. Follow these steps to register and get your API key set up in the sample app:

  1. Sign up for a free account at the TMDB website.
  2. Go to the API Settings section.
  3. Under the Request an API Key section, click where it says click here.
  4. On the next screen, choose to register a Developer API key.
  5. Assuming you’ve agreed to the terms of use, you’ll then need to enter some information about yourself and your app. It doesn’t matter what you enter here. For Developer API keys, you’ll get access to an API key immediately.
  6. You’ll end up back in the API Settings section, which will now show an API key. Copy the value from API Key (v3 auth).
  7. In the starter project, open Network.swift from the Project/MovieInfo/Source/Network group.
  8. At the top of the file, replace the <YOUR_API_KEY_HERE> value of apiKey with your API key.
  9. In the target settings for MovieInfo, replace -YOUR-BUNDLE-ID-HERE- with a bundle ID of your choice.

Finally, build and run. You’ll see a list of the 20 highest-rated movies:


starter project run

Installing Tuist

Your next step is to install the Tuist tooling. Open Terminal and run the following command:

bash <(curl -Ls https://install.tuist.io)

You'll see:

==> Downloading tuistenv...
==> Unzipping tuistenv...
==> Installing tuistenv...
Password:
==> tuistenv installed. Try running 'tuist'
==> Check out the documentation at https://tuist.io/docs

This installs the tuist command.

Now that you've set everything up, it's time to dive into Tuist!

Getting Started With Tuist

Your first goal is to convert this project to use Tuist. From this point on, you won't rely on keeping the project and workspace files at the root of your project directory anymore. You'll shift your thinking; Tuist will only generate these files when you need them.

Understanding the Tuist Manifest File

The Tuist manifest is a file called Project.swift that tells Tuist how to create your Xcode project. You'll interact with Tuist from the command line, and you'll build your Tuist manifest in Xcode. Tuist generates an environment for you to work on your manifest file, then launches Xcode to give you code completion and everything you're used to in the Xcode environment.

Before you start, delete the existing project and workspace files. From here on out, you won't need to keep them around since you'll generate them when you need them!

Close the workspace in Xcode, then delete MovieInfo.xcodeproj and MovieInfo.xcworkspace.

Setting up the Project File

Next, create an empty Tuist manifest file. In Terminal, navigate to the root directory of the starter project and enter the following:

touch Project.swift

This will create an empty manifest. Next, enter this:

tuist edit

The first time you run the command, you might see some messages in the terminal about downloading and updating Tuist. After that, you'll see the following output:

Generating project Manifests
Opening Xcode to edit the project. Press CTRL + C once you are done editing

Xcode will open with a generated environment where you can begin editing the project file. Select Project.swift in Project navigator to start.

Tuist open in Xcode

Next, add the following to the top of Project.swift:

import ProjectDescription

This will give you access to the Tuist project DSL. You'll use this to build out your project with some helpful code completion.

Defining Your Project

Your first step is to set up your main project. Add this code to Project.swift under import:

// 1
let project = Project(
  // 2
  name: "MovieInfo",
  // 3
  organizationName: "<YOUR_ORG_NAME_HERE>",
  // 4
  settings: nil,
  // 5
  targets: [])

Here's what's going on in the code above:

  1. You create project, which represents your base project and workspace container. You can name it anything you want, but Tuist recommends project.
  2. name represents the name of the project and workspace files.
  3. organizationName appears in the copyright notice added at the top of each new file. Replace <YOUR_ORG_NAME_HERE> with your organization name.
  4. settings allows you to specify some other project settings. You'll set this in a later section.
  5. targets represents the list of targets associated with the project. You'll fill this in next.
Note: You can build the Tuist manifest the same way as you would any Xcode project: with Command-B. You get the same type of safety here as anywhere else with Swift, and the compiler will tell you if you did something wrong!

Defining Your First Target

Add the following to the empty targets array:

// 1
Target(
  // 2
  name: "MovieInfo",
  // 3
  platform: .iOS,
  // 4
  product: .app,
  // 5
  bundleId: "<YOUR_BUNDLE_ID_HERE>",
  // 6
  infoPlist: "MovieInfo/Info.plist",
  // 7
  sources: ["MovieInfo/Source/**"],
  // 8
  resources: ["MovieInfo/Resources/**"],
  // 9
  dependencies: [],
  // 10
  settings: nil)

This does the following:

  1. Defines a Target for the targets array.
  2. Specifies the app's name.
  3. Sets the platform to iOS.
  4. Defines the output product as an app.
  5. Sets the bundle ID of the target. Replace <YOUR_BUNDLE_ID_HERE> with your preferred bundle ID.
  6. Locates Info.plist relative to Project.swift.
  7. Uses a wildcard pattern to specify all source files in the Source directory. Any directories become Xcode groups after project generation.
  8. Uses a wildcard pattern to specify all source files in the Resources directory. These directories also become Xcode groups after project generation.
  9. Specifies that there are no dependencies, which are external frameworks that require linking. You'll add some later in the tutorial.
  10. This is used to specify other settings to the target. You'll also add some later in the tutorial.
Note: There are several more optional properties available for Target. Tuist generates sensible defaults for these, and you don't need to customize them, in this case. However, for more complex projects, there are several useful options worth checking out. For example, you can define run script and build phases, customize build schemes, link to Xcode template files to customize file generation and much more. Check out the Project.swift docs for much more info.

Your Project.swift is now ready to be used to generate your project.

Generating Your First Project

OK, the time has come to use Tuist to generate your first project and workspace! Open Terminal again. Press Control-C or Command-. to stop the current editing session. Xcode will show you a message indicating the workspace no longer exists:

Xcode Close Workspace dialog

Click Close.

Before generating the project, you'll use Tuist's lint command to check the validity of your project manifest.

From Terminal, run the following:

tuist lint project

This command will lint the manifest file in the current directory.

You'll see output similar to this:

Loading the dependency graph
Loading project at /Users/ski081/Desktop/WiP
Running linters
Linting the environment
Linting the loaded dependency graph
No linting issues found

Next, in Terminal, enter the following:

tuist generate

You'll get output similar to the following:

Generating workspace MovieInfo.xcworkspace
Generating project MovieInfo
Project generated.
Total time taken: 0.579s

Now, run the following in Terminal to see the directory's new content:

ls -al

You'll see something like this:

drwxr-xr-x@ 10 ski081  staff   320 May 30 22:01 .
drwx------@ 26 ski081  staff   832 May 30 21:51 ..
-rw-r--r--@  1 ski081  staff  6148 May 29 08:50 .DS_Store
drwxr-xr-x@  4 ski081  staff   128 May 30 08:23 AdditionalTargetFiles
drwxr-xr-x   3 ski081  staff    96 May 30 22:01 Derived
drwxr-xr-x@  8 ski081  staff   256 May 30 21:50 MovieInfo
drwxr-xr-x@  6 ski081  staff   192 May 30 22:01 MovieInfo.xcodeproj
drwxr-xr-x@  6 ski081  staff   192 May 30 22:01 MovieInfo.xcworkspace
-rw-r--r--@  1 ski081  staff   409 May 30 22:00 Project.swift
drwxr-xr-x@  5 ski081  staff   160 May 30 08:24 config

Congratulations! You generated your first project and workspace. :]

Finally, enter the following to open the workspace:

xed .
Note: xed . opens a workspace in the current directory, if one exists. If not, it will open an Xcode project. So, in either case, it opens the "correct" file.

Build and run. The app will behave exactly as before, but there's a huge difference: You've removed the dependency on the project file and can now generate it whenever you want.

Introducing Focus Mode

Before you start adding additional features and projects through Tuist, there's another interesting feature to review: focus mode. Focus lets you open a single target and its dependencies. This lets you ignore any projects or dependencies you don't want to use at the time.

Try this out on your newly generated project setup. From Terminal, enter the following:

tuist focus MovieInfo

This tells Tuist to generate and open only the assets you need to work with MovieInfo. It will also remove unneeded assets from the Xcode cache and replace them with pre-compiled assets to ensure Xcode doesn't try to index them.

You'll see a message like this:

Generating workspace MovieInfo.xcworkspace
Generating project MovieInfo
Deleted persisted 1622124912.TuistAnalytics.45978508-3C7B-4A87-B3DD-1B5FD4476A55.json

Xcode will open with your target and all dependencies initialized. This isn't useful with a small project like MovieInfo, but you can see how it would benefit larger projects with multiple sub-projects and dependencies.

Adding Support for Settings Files

Next, you'll generate your project and target settings from external .xcconfig files. This is a best practice to give you more control over how Tuist builds the project.

This practice also isolates you from any changes you need to make to the project and target settings in the future. If you needed to change a build setting without an external configuration, Tuist would override it each time it regenerated the file. You'd then need to remember to make that change again in the project or target settings.

By pointing Tuist to an external source, you can make changes there and any new project generation commands will use them.

Tuist uses Settings to represent configurations that you can use in project and target objects. You'll use it to set up external configuration sources.

Begin another editing session from Terminal:

tuist edit

Open Project.swift and add the following just after the import:

// 1
let projectSettings = Settings(
  // 2
  debug: Configuration(xcconfig: Path("config/MovieInfoProject.xcconfig")),
  release: Configuration(xcconfig: Path("config/MovieInfoProject.xcconfig")),
  defaultSettings: .none)

This does the following:

  1. Declares a Settings object.
  2. Specifies .xcconfig files to use for debug and release configurations. These are already in the starter project, in the config folder.

Next, in the initialization of project, replace the settings parameter with your new object:

settings: projectSettings,

Next, you'll apply settings to your target. Right under projectSettings, create targetSettings:

let targetSettings = Settings(
  debug: Configuration(xcconfig: Path("config/MovieInfoTarget.xcconfig")),
  release: Configuration(xcconfig: Path("config/MovieInfoTarget.xcconfig")),
  defaultSettings: .none)

Now update the settings parameter in the initialization of the Target object, to the following:

settings: targetSettings)

Save Project.swift and exit with Control-C in Terminal. Now, generate your project again with:

tuist generate

Open the workspace. Build and run. The app looks the same as before, but now your build settings live outside of the project so you can apply the same settings every time you generate the project with Tuist.

Setting up External Dependencies

Your next goal is to set up external framework dependencies in the Tuist manifest file. This lets you automatically set up and link any framework code you already have on disk to your project. No more dragging and dropping and fussing with Xcode build settings!

Imagine you've decided to move the networking functionality out of the iOS codebase so you can share it with multiple targets, possibly for a future macOS or watchOS app. The easiest way to do this is to extract the network layer and turn it into a dynamic framework in your project.

Moving Files Into Place

First, you'll need to move the relevant files into a separate location. This lets you reference them easily from the Tuist manifest file.

The starter project has a directory named AdditionalTargetFiles. Move NetworkKit from inside this directory to the root of the project. It should sit alongside the MovieInfo directory and Project.swift.

Once you finish, your directory structure should look like this:

NetworkKit file structure

Move the Network directory at MovieInfo/Source/Network and all its contents to the trash.

Generating the Framework From Tuist

In Terminal, switch to the NetworkKit directory and run the edit command:

cd NetworkKit
tuist edit

As before, this opens a project in Xcode. Select Project.swift. You'll see a blank file with an import at the top.

Now, enter the following:

let projectSettings = Settings(
  debug: Configuration(xcconfig: Path("config/NetworkKitProject.xcconfig")),
  release: Configuration(xcconfig: Path("config/NetworkKitProject.xcconfig")),
  defaultSettings: .none)

let targetSettings = Settings(
  debug: Configuration(xcconfig: Path("config/NetworkKitTarget.xcconfig")),
  release: Configuration(xcconfig: Path("config/NetworkKitTarget.xcconfig")),
  defaultSettings: .none)

let project = Project(
  name: "NetworkKit",
  organizationName: "Ray Wenderlich",
  settings: projectSettings,
  targets: [
    Target(
      name: "NetworkKit",
      platform: .iOS,
      product: .framework,
      bundleId: "<YOUR_BUNDLE_ID_HERE>",
      infoPlist: "NetworkKit/Info.plist",
      sources: ["NetworkKit/Source/**"],
      settings: targetSettings)
  ])

Don't forget to replace <YOUR_BUNDLE_ID_HERE> with your own values. The bundle ID must be different from the bundle ID you're using for the MovieInfo app.

The code above is similar to your main project definition, with one key difference: Notice that product in the Target initializer declares a framework instead of an app product. This generates a dynamic iOS framework.

Save this file and enter Control-C in Terminal to end the editing session.

Linking the Framework in MovieInfo

At this point, you need to let Tuist know about the new framework so it can generate and link it. Change your directory to go back to the root of the project, then run the edit command from Terminal again:

cd ..
tuist edit

When Xcode opens, select Project.swift.

Now, in dependencies, add a dependency to the array:

.project(
  target: "NetworkKit",
  path: .relativeToManifest("NetworkKit"))

This tells Tuist to generate and link the NetworkKit framework. It also passes the relative path of the Tuist project file for the framework. With this, Tuist can generate both your main project and the framework project and link them. Cool! :]

Generating and Updating the Codebase

OK, it's showtime! From Terminal, press Control-C to exit Tuist, then generate your project again:

tuist generate

Now, open the MovieInfo workspace. You'll see two projects, MovieInfo and NetworkKit:

Xcode with NetworkKit integrated

Since you removed the networking code from the MovieInfo target, you need to let your view model know about NetworkKit. Import this framework at the top of MovieListViewModel.swift:

import NetworkKit

Since NetworkKit has a new copy of Network.swift, it doesn't have your API key yet. Open Network.swift and replace <YOUR_API_KEY_HERE> with your API key.

Select the MovieInfo scheme. Build and run. The app will look the same as before but you're now using an independent networking framework, completely configured by Tuist. :]

Adding a Unit Test Target

Next, you'll add some unit tests and the appropriate target for them. Of course, you'll use Tuist to do this!

Start by moving the MovieInfoTests directory from AdditionalTargetFiles into the root project directory. If you peek inside, you'll see one test suite in Source and an Info.plist inside Resources. This is enough to define your test target.

Enter edit mode again with Tuist:

tuist edit

Add a definition for the test target to targets with the following code:

Target(
  name: "MovieInfoTests",
  platform: .iOS,
  product: .unitTests,
  bundleId: "<YOUR_BUNDLE_ID_HERE>",
  infoPlist: "MovieInfoTests/Resources/Info.plist",
  sources: ["MovieInfoTests/Source/**"],
  dependencies: [
    .target(name: "MovieInfo")
  ])

This adds a unit test target to MovieInfo.

You'll notice the product type is .unitTests and it has a dependency on MovieInfo. Make sure you replace the bundleId with your own bundle ID, which is different from the ones used for the MovieInfo and NetworkKit targets.

Next, it's time to regenerate your project. Press Control-C to end editing and run the generate command again:

tuist generate

You'll see the following output:

Generating workspace MovieInfo.xcworkspace
Generating project MovieInfo
Generating project NetworkKit
Project generated.
Total time taken: 0.150s

Open the workspace. You'll see the MovieInfoTests target under MovieInfo:

MovieInfo project editor showing MovieInfo and MovieInfoTests targets

Press Command-U to run the tests. They'll pass.

Next, you'll use Swift Package Manager to add a feature to the app that requires a third-party library. You'll use Tuist to manage this!

Adding Poster Images

Your final goal is to add a new feature to the app: poster images for the movies in the list. You'll use the third-party library FetchImage to easily lazy-load the images based on poster paths you'll get from the MovieDB API.

Adding a Swift Package

First, you'll add a dependency on FetchImage. You'll need to add two declarations to Project.swift: a package declaration and a new target dependency.

Adding the Package Definition

Open Tuist in edit mode by running:

tuist edit

Next, open Project.swift. In the initialization of Project, add the following argument after organizationName:

// 1
packages: [
  // 2
  Package.remote(
    // 3
    url: "https://github.com/kean/FetchImage.git",
    // 4
    requirement: .upToNextMajor(
      from: Version(0, 4, 0)))
],

The code above does the following:

  1. Declares a packages array argument. This is optional and wasn't included in the original setup because you didn't need it until now.
  2. Declares a remote Package.
  3. Defines the remote URL for the package definition.
  4. Defines version requirements. For this package, you're using the Swift Package Manager default: up to next major version, starting at the current version of FetchImage.

Adding the Package Dependency

Still in Project.swift, add the package as a dependency to the target. Inside the MovieInfo target declaration, add the following to the end of the dependencies array:

, .package(product: "FetchImage")

This lets Tuist know MovieInfo has a dependency on FetchImage. Save Project.swift.

Now, generate your project again. In Terminal, press Control-C to end the Tuist editing session. Enter the generate command:

tuist generate

You'll see output like the following:

Generating workspace MovieInfo.xcworkspace
Generating project MovieInfo
Generating project NetworkKit
Resolving package dependencies using xcodebuild
2021-05-27 21:40:21.126 xcodebuild[13105:459506] [MT] DVTPlugInManager: Required plug-in compatibility UUID F56A1938-53DE-493D-9D64-87EE6C415E4D for GraphQL.ideplugin (com.apollographql.xcode.graphql) not present

Project generated.
Total time taken: 7.779s
Note: The DVTPlugInManager error seems to be a bug in Tuist. If it shows up for you, it shows up any time you try to use Swift Package Manager support. However, it doesn't affect the functionality of the workspace or project. And you may not see it at all.

Open the workspace and you'll see the FetchImage is fetched and ready to use!

FetchImage integrated into Xcode

Note the Nuke package in the list. Nuke is a dependency of FetchImage.

Implementing Poster Images in the List

OK, now that you've initialized the package, you'll use it to implement image fetching in the list.

Setting up an ImageView

Your next step is to add a reusable image view that uses FetchImage to lazily load images. ImageView is already in the starter project.

To do so, follow these steps:

  1. Move ImageView.swift from AdditionalTargetFiles to MovieInfo/Source/Views. It should sit alongside ContentView.swift.
  2. Drag ImageView.swift into the Views group in MovieInfo/Source/Views in the Xcode project.

Integrating the ImageView

Your next step is to use ImageView in ContentView.

Open ContentView.swift. Inside List, add the following above the Text view:

// 1
if let url = movie.posterURL {
  // 2
  ImageView(url: url)
    .frame(width: 92, height: 138)
}

This performs the following:

  1. Checks if there is a URL for the movie. This is an optional property from the API, so it might be nil.
  2. If a poster URL is present, use ImageView and pass it for loading.

Build and run. You'll now see the lazy-loaded poster images!

Movie Info displaying posters of movies

Congratulations! You've built up an entire project structure in Tuist, which gives you a portable setup with a ton of functionality.

Where to Go From Here?

Download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

This tutorial only scratches the surface of what Tuist can do. Additionally, you can use it to set up framework templates, custom build phases, use CocoaPods and Carthage, lint projects and much more.

Check out the Tuist docs to extend your knowledge of Tuist and continue down the path to Xcode project file independence!

We hope that you've enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!

Average Rating

5/5

Add a rating for this content

2 ratings

More like this

Contributors

Comments