Using Swift Scripts with Xcode

Learn how to run Swift scripts as part of the Xcode build phase, giving you control to configure or validate your app while building your project. By Ehab Amer.

5 (10) · 1 Review

Download materials
Save for later
Share

Building apps with Swift is a lot of fun — and it’s also fun to use it in the build process itself.

Xcode lets you run your own scripts as part of the build phases, but instead of limiting yourself to shell scripts only, you can leverage your knowledge and expertise in Swift and do more with less effort.

In this tutorial, you’ll learn how to create build scripts in Swift and create operations for your build process. You’ll create four different scripts that cover a wide variety of operations you can perform on your projects. For example:

  • Creating a basic script and executing it as you build the project
  • Configuring input and output files for scripts
  • Reading the project’s settings and adding custom values yourself
  • Altering project resources during build time
  • Executing command-line operations from your code
  • Loading remote data and interrupting the build process

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial. Unzip it and open HelloXcode.xcodeproj in the starter folder.

Hello Xcode Project navigator showing the project files and some helper scripts.

The app itself doesn’t do much. Build and run. You’ll see a view controller with some text:

The main page of the Hello Xcode app.

For this tutorial, you’ll work in the Scripts folder.

Writing Hello Xcode

Create a new Swift file under Scripts and name it HelloXcode.swift. Add the following code:

import Foundation

@main
enum MyScript {
  static func main() {
    print("Hello Xcode")
  }
}

The code above prints “Hello Xcode” in the console.

Then, open the terminal, navigate to the Scripts folder and execute this command:

xcrun swiftc -parse-as-library HelloXcode.swift
Note: To navigate to a path on the terminal, use the command cd followed by the path you want to reach. For example cd /Users/your_user_name/SwiftBuildPhase/Starter/Scripts/

This will compile your Swift file and create an executable binary with the same name.

Scripts folder showing HelloXcode.swift file and its compiled counterpart.

Next, run this command:

./HelloXcode

This will print Hello Xcode in your terminal window.

You just created a very basic application that does nothing except print the text Hello Xcode. You compiled this application and executed it. If you double-click the compiled file, it’ll open a new terminal window with some more messages before and after Hello Xcode.

........Scripts/HelloXcode ; exit;
Hello Xcode
logout
Saving session...
...copying shared history...
...saving history...truncating history files...
...completed.

[Process completed]

xcrun can take several parameters one of them is -parse-as-librarywhich specifies to treat this file as a library otherwise, it’ll complain about the @main attribute.

You can also specify what kind of SDK you want to compile the code against. Ideally, since you’re going to execute them from Xcode, it makes sense to use the macOS SDK. You can also rename the output file to something specific using the -o attribute.

Your build command should be:

xcrun --sdk macosx swiftc -parse-as-library HelloXcode.swift -o CompiledScript

This command compiles HelloXcode.swift using the macOS SDK. The output file will be named CompiledScript.

Scripts folder showing HelloXcode.swift file and its compiled counterpart renamed to CompiledScript.

Understanding Build Phases

Compiling your Swift file and executing it from Xcode is as easy as doing it from the terminal. All you need to do is define a New Run Script Phase and add the commands you executed on the terminal into it.

From the Project navigator, select the project file. Then in Build Phases add a new phase and select New Run Script Phase from the drop-down menu.

Adding a New Run Script Phase for the project

The Build Phases tab is the central point for Xcode’s build operation. When you build any project, Xcode does a number of steps in order:

  1. Identifies the dependencies and compiles them.
  2. Compiles the source files.
  3. Links the compiled files with their compiled dependencies.
  4. Copies resources to the bundle.

However, you may want to add your own operations at specific moments. For this tutorial, you’ll add new operations to the beginning — so when you add a new run script phase, drag it to the top of the list.

A gif showing the addition of a run script phase and dragging it to the top

Going back to the new phase you added a moment ago, delete the commented line and add these commands:

xcrun --sdk macosx swiftc -parse-as-library Scripts/HelloXcode.swift \
  -o CompiledScript
./CompiledScript

The difference between this and what you did before on the terminal is that Xcode executes these scripts on the path of the project file — that’s why adding Scripts/ is important.

Build the project to try out the new script and build phase you just added, then open the build log when it finishes. You’ll see the message you printed logged directly in Xcode’s log.

Hello Xcode message in Xcode's build log

Exploring Input and Output Files

Xcode’s run phase allows you to specify files as configuration instead of having them explicit in the script. Think of it as sending a file as a parameter to a function. This way, your scripts can be more dynamic and portable to use.

In the run phase you added, add to the Input Files list $(SRCROOT)/Scripts/HelloXcode.swift and update the script to:

xcrun --sdk macosx swiftc -parse-as-library $SCRIPT_INPUT_FILE_0 \
  -o CompiledScript
./CompiledScript

The script after using input files

This doesn’t cause any changes to the execution of the scripts, but it makes Xcode more aware of the files you will change or use in your scripts. It will validate the existence of the input files and will make sure that files that are an output of a script then an input of another are used in the correct order.

Note: Items added to the Input Files list are referenced as $SCRIPT_INPUT_FILE_0, $SCRIPT_INPUT_FILE_1, … etc. Items in the Output Files list are $SCRIPT_OUTPUT_FILE_0, $SCRIPT_OUTPUT_FILE_1, … etc.

Accessing Environment Variables

Sending file paths as parameters can be useful, but it’s not enough. Sometimes you’ll need to read information related to the project itself — like, is this a release or debug build? What is the version of the project? Or the name of the project?

When Xcode executes any run script phase, it shares all its build settings through environment variables. Think of environment variables as global variables. You can still read them from your Swift code. In HelloXcode.swift, add the following in main():

if let value = ProcessInfo.processInfo.environment["PRODUCT_NAME"] {
  print("Product Name is: \(value)")
}

if let value = ProcessInfo.processInfo.environment["CONFIGURATION"] {
  print("Configuration is: \(value)")
}

The code above reads the values for the environment variables “PRODUCT_NAME” and “CONFIGURATION” and prints them to Xcode’s log.

Build and open the build log. You’ll see the two values printed in the log:

Two new messages are showing in the build log.

When you open the details of the run script phase from the log, you’ll see all the build settings exported. You can read any of those values the same way you did with PRODUCT_NAME and CONFIGURATION. You can also read more to understand what each stands for in Apple’s Build Settings Reference.

Some of the exported Build Settings values in the log.

In some cases, you’ll want to add your own custom settings for the project. This won’t make any difference for Xcode or its build process, but it could make a huge difference for your own custom scripts. Select the project file and go to Build Settings. Click on the + at the top, select Add User-Defined Setting and name the setting CUSTOM_VALUE. Enter This is a test value! as its value.

Adding a new user-defined build setting

To read the new value you just added, add the following to HelloXcode.swift:

if let value = ProcessInfo.processInfo.environment["CUSTOM_VALUE"] {
  print("Custom Value is: \(value)")
}

Build and you’ll see the new value you wrote printed in the log.